Initial commit: Hermes Chat App with Svelte 5 + shadcn-svelte

This commit is contained in:
2026-05-22 04:25:20 +02:00
commit e0697c9522
82 changed files with 3349 additions and 0 deletions

View File

@@ -0,0 +1,98 @@
import { json } from '@sveltejs/kit';
import { spawn } from 'child_process';
import type { RequestEvent } from '@sveltejs/kit';
export async function POST({ request }: RequestEvent) {
try {
const { messages, config } = await request.json();
const lastMsg = messages?.[messages.length - 1]?.content || '';
const modelFlag = config?.model ? ['-m', config.model] : [];
const providerFlag = config?.provider ? ['--provider', config.provider] : [];
const hermes = spawn('hermes', [
'chat',
'-q', lastMsg,
...modelFlag,
...providerFlag,
'-Q',
'--source', 'web-ui'
], {
timeout: 60000,
env: { ...process.env }
});
const stream = new ReadableStream({
async start(controller) {
const decoder = new TextDecoder();
let buffer = '';
const processChunk = (chunk: Buffer) => {
buffer += decoder.decode(chunk, { stream: true });
while (buffer.includes('\n')) {
const idx = buffer.indexOf('\n');
const line = buffer.slice(0, idx);
buffer = buffer.slice(idx + 1);
if (line.trim()) {
const payload = JSON.stringify({
choices: [{ delta: { content: line + '\n' } }]
});
controller.enqueue(new TextEncoder().encode(`data: ${payload}\n\n`));
}
}
};
hermes.stdout?.on('data', processChunk);
hermes.stderr?.on('data', (data: Buffer) => {
const text = decoder.decode(data, { stream: true });
// Only send non-empty stderr lines (warnings, etc.)
if (text.trim()) {
const payload = JSON.stringify({
choices: [{ delta: { content: `*${text.trim()}* ` } }]
});
controller.enqueue(new TextEncoder().encode(`data: ${payload}\n\n`));
}
});
hermes.on('close', (code) => {
// Flush remaining buffer
if (buffer.trim()) {
const payload = JSON.stringify({
choices: [{ delta: { content: buffer } }]
});
controller.enqueue(new TextEncoder().encode(`data: ${payload}\n\n`));
}
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
controller.close();
});
hermes.on('error', (err) => {
controller.enqueue(
new TextEncoder().encode(
`data: ${JSON.stringify({ error: err.message })}\n\n`
)
);
controller.enqueue(new TextEncoder().encode('data: [DONE]\n\n'));
controller.close();
});
}
});
return new Response(stream, {
headers: {
'Content-Type': 'text/event-stream',
'Cache-Control': 'no-cache',
Connection: 'keep-alive'
}
});
} catch (err) {
return json(
{ error: err instanceof Error ? err.message : 'Internal server error' },
{ status: 500 }
);
}
}

View File

@@ -0,0 +1,29 @@
import { json } from '@sveltejs/kit';
import { execSync } from 'child_process';
export async function GET() {
try {
// Try to get models from Hermes config
const output = execSync('hermes config get model 2>/dev/null', {
timeout: 5000,
encoding: 'utf-8'
});
const lines = output.split('\n').filter((l) => l.trim());
return json(lines);
} catch {
// Return sensible defaults
return json([
'openai/gpt-4o',
'openai/gpt-4o-mini',
'anthropic/claude-sonnet-4',
'anthropic/claude-haiku-3.5',
'google/gemini-3.5-flash',
'deepseek/deepseek-v4-flash',
'deepseek/deepseek-v4-pro',
'mistral/mistral-small-3.1',
'meta-llama/llama-4-maverick',
'qwen/qwen-3-235b-a22b'
]);
}
}

View File

@@ -0,0 +1,22 @@
import { json } from '@sveltejs/kit';
import { execSync } from 'child_process';
export async function GET() {
try {
const output = execSync(
'hermes sessions list --limit 20 --json 2>/dev/null || echo "[]"',
{ timeout: 5000, encoding: 'utf-8' }
);
const parsed = JSON.parse(output);
return json(
parsed.map((s: any) => ({
id: s.id || s.session_id || '',
title: s.title || 'Untitled',
preview: s.preview || (s.first_message || '').slice(0, 80),
timestamp: s.timestamp || s.created_at || Date.now()
}))
);
} catch {
return json([]);
}
}

View File

@@ -0,0 +1,35 @@
import { json } from '@sveltejs/kit';
import { execSync } from 'child_process';
export async function GET() {
try {
const version = execSync('hermes --version 2>/dev/null || echo "not found"', {
timeout: 5000,
encoding: 'utf-8'
}).trim();
const modelOutput = execSync(
'hermes config get model.default 2>/dev/null || echo "not set"',
{ timeout: 5000, encoding: 'utf-8' }
).trim();
const providerOutput = execSync(
'hermes config get model.provider 2>/dev/null || echo "openrouter"',
{ timeout: 5000, encoding: 'utf-8' }
).trim();
return json({
available: version !== 'not found',
model: modelOutput === 'not set' ? '' : modelOutput,
provider: providerOutput,
version
});
} catch {
return json({
available: false,
model: '',
provider: 'openrouter',
version: 'not found'
});
}
}