Initial commit: Hermes Chat App with Svelte 5 + shadcn-svelte
This commit is contained in:
98
src/routes/api/hermes/chat/+server.ts
Normal file
98
src/routes/api/hermes/chat/+server.ts
Normal 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 }
|
||||
);
|
||||
}
|
||||
}
|
||||
29
src/routes/api/hermes/models/+server.ts
Normal file
29
src/routes/api/hermes/models/+server.ts
Normal 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'
|
||||
]);
|
||||
}
|
||||
}
|
||||
22
src/routes/api/hermes/sessions/+server.ts
Normal file
22
src/routes/api/hermes/sessions/+server.ts
Normal 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([]);
|
||||
}
|
||||
}
|
||||
35
src/routes/api/hermes/status/+server.ts
Normal file
35
src/routes/api/hermes/status/+server.ts
Normal 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'
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user