Files
hermes-chat-v2/src/routes/api/hermes/sessions/+server.ts
Hermes Agent 930225edb9 Fix: real Hermes session integration, artifact detection, SSE robustness
- Parse plain-text 'hermes sessions list' output for real session IDs
- Pass sessionId to chat API for --resume continuity across page loads
- Filter session_id: lines from SSE content into separate metadata events
- Fix crash: 'Controller is already closed' with proper error handling
- Auto-detect code blocks, HTML, SVG, diff artifacts from responses
- Add diff rendering support in Monaco editor
- Fix status/models API for 'hermes config show' output format
- Load real Hermes sessions in sidebar on mount
- Link local sessions to Hermes session IDs for resumability
2026-05-22 06:14:49 +02:00

86 lines
3.1 KiB
TypeScript

import { json } from '@sveltejs/kit';
import { execSync } from 'child_process';
export async function GET({ url }) {
try {
const limit = url.searchParams.get('limit') || '50';
const output = execSync(`hermes sessions list --limit ${limit} 2>/dev/null`, {
timeout: 5000,
encoding: 'utf-8'
});
const sessions = parseSessions(output);
return json(sessions);
} catch {
return json([]);
}
}
/**
* Parse the plain-text output of `hermes sessions list`.
*
* Expected format (column-aligned with spaces):
* Title Preview Last Active ID
* ──────────────────────────────────────────────────────────────────────────────────────────────────────────────
* — create a way to connect to hermes via 2m ago 20260522_045714_48480d
* Self-Improvement and Token Eff find me good self improvement skills. 45m ago 20260522_045210_2381af
*
* The ID column is always the last column and matches: YYYYMMDD_HHMMSS_[hex]
* The "Last Active" column is second-to-last.
*/
function parseSessions(output: string) {
const lines = output.split('\n').filter((l) => l.trim());
const sessions: { id: string; title: string; preview: string; timestamp: number }[] = [];
for (const line of lines) {
// Skip header and separator lines
if (line.startsWith('Title') || line.startsWith('──')) continue;
// Match the session ID at the end: YYYYMMDD_HHMMSS_[hex]
const idMatch = line.match(/(\d{8}_\d{6}_[0-9a-f]+)\s*$/);
if (!idMatch) continue;
const id = idMatch[1];
const beforeId = line.slice(0, line.lastIndexOf(id)).trimEnd();
// Match the "Last Active" column (relative time like "2m ago", "1h ago", "3d ago")
const timeMatch = beforeId.match(/(\d+[smhdw]\s+ago)\s*$/);
let timestamp = Date.now();
let beforeTime = beforeId;
if (timeMatch) {
const timeStr = timeMatch[1];
beforeTime = beforeId.slice(0, beforeId.lastIndexOf(timeStr)).trimEnd();
timestamp = parseRelativeTime(timeStr);
}
// Everything before the time column is Title + Preview
// Title is typically up to ~32 chars, Preview fills the rest
// We'll split on the first long gap of spaces
const parts = beforeTime.split(/\s{3,}/);
let title = parts[0] || 'Untitled';
let preview = parts.slice(1).join(' ') || '';
// Clean up title (replace dash-only titles)
if (/^[—\-]+$/.test(title)) title = 'Untitled';
sessions.push({ id, title, preview, timestamp });
}
return sessions;
}
function parseRelativeTime(str: string): number {
const now = Date.now();
const match = str.match(/^(\d+)\s*([smhdw])\s+ago$/);
if (!match) return now;
const value = parseInt(match[1]);
const unit = match[2];
const multipliers: Record<string, number> = {
s: 1000,
m: 60 * 1000,
h: 60 * 60 * 1000,
d: 24 * 60 * 60 * 1000,
w: 7 * 24 * 60 * 60 * 1000
};
return now - value * (multipliers[unit] || 0);
}