hermes-web-ui 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (72) hide show
  1. package/README.md +134 -408
  2. package/bin/hermes-web-ui.mjs +111 -97
  3. package/dist/assets/ChatView-BCcUaLWg.css +1 -0
  4. package/dist/assets/ChatView-gu2Y-jpZ.js +16 -0
  5. package/dist/assets/Input-yD_s5hp8.js +234 -0
  6. package/dist/assets/JobsView-C1YU-sgU.js +123 -0
  7. package/dist/assets/LogsView-BN_TkDPi.css +1 -0
  8. package/dist/assets/LogsView-BSNn4Rmi.js +1 -0
  9. package/dist/assets/MarkdownRenderer-CrMwC_5d.css +1 -0
  10. package/dist/assets/MarkdownRenderer-DNP-kPA8.js +23 -0
  11. package/dist/assets/MemoryView-BsGBmN7h.js +5 -0
  12. package/dist/assets/MemoryView-CK0PemlP.css +1 -0
  13. package/dist/assets/Modal-CSHJclVU.js +232 -0
  14. package/dist/assets/Popover-blRIsd-8.js +117 -0
  15. package/dist/assets/Scrollbar-CYtpM7mc.js +77 -0
  16. package/dist/assets/Select-DTG-wtu5.js +454 -0
  17. package/dist/assets/SettingsView-BzBMKaLz.css +1 -0
  18. package/dist/assets/SettingsView-CppuSCZA.js +1129 -0
  19. package/dist/assets/SkillsView-86Z-HE_X.css +1 -0
  20. package/dist/assets/SkillsView-CciuH5wp.js +1 -0
  21. package/dist/assets/Spin-DWreY5m0.js +43 -0
  22. package/dist/assets/Suffix-B828Aenx.js +25 -0
  23. package/dist/assets/Tooltip-BCHslENT.js +1 -0
  24. package/dist/assets/Warning-Ba-wYRRB.js +1 -0
  25. package/dist/assets/_plugin-vue_export-helper-CV9qMihF.js +47 -0
  26. package/dist/assets/app-4gFjNUoo.js +1 -0
  27. package/dist/assets/chat-P2QaPvpZ.js +6 -0
  28. package/dist/assets/context-B74FvHiJ.js +109 -0
  29. package/dist/assets/index-Cx8mFF2D.js +307 -0
  30. package/dist/assets/index-DwVgwUIX.css +1 -0
  31. package/dist/assets/jobs-DVxci1LY.js +1 -0
  32. package/dist/assets/keysOf-Dvq9k1rv.js +1 -0
  33. package/dist/assets/omit-QZj0aLfU.js +1 -0
  34. package/dist/assets/pinia-tE0RcsDr.js +1 -0
  35. package/dist/assets/runtime-core.esm-bundler-yNW65ghW.js +1 -0
  36. package/dist/assets/skills-DgbQAgoB.js +1 -0
  37. package/dist/assets/use-message-Dz_GbH5h.js +1 -0
  38. package/dist/index.html +19 -6
  39. package/dist/server/config.d.ts +7 -0
  40. package/dist/server/config.js +12 -0
  41. package/dist/server/index.d.ts +1 -0
  42. package/dist/server/index.js +156 -0
  43. package/dist/server/routes/filesystem.d.ts +2 -0
  44. package/dist/server/routes/filesystem.js +427 -0
  45. package/dist/server/routes/logs.d.ts +2 -0
  46. package/dist/server/routes/logs.js +87 -0
  47. package/dist/server/routes/proxy-handler.d.ts +2 -0
  48. package/dist/server/routes/proxy-handler.js +82 -0
  49. package/dist/server/routes/proxy.d.ts +2 -0
  50. package/dist/server/routes/proxy.js +12 -0
  51. package/dist/server/routes/sessions.d.ts +2 -0
  52. package/dist/server/routes/sessions.js +69 -0
  53. package/dist/server/routes/upload.d.ts +2 -0
  54. package/dist/server/routes/upload.js +50 -0
  55. package/dist/server/routes/webhook.d.ts +2 -0
  56. package/dist/server/routes/webhook.js +33 -0
  57. package/dist/server/services/hermes-cli.d.ts +50 -0
  58. package/dist/server/services/hermes-cli.js +185 -0
  59. package/dist/server/services/hermes.d.ts +40 -0
  60. package/dist/server/services/hermes.js +118 -0
  61. package/package.json +19 -4
  62. package/dist/assets/ChatView-DrzZz5ex.css +0 -1
  63. package/dist/assets/ChatView-WnNeYrS7.js +0 -38
  64. package/dist/assets/JobsView-BF9wWdwU.js +0 -831
  65. package/dist/assets/Modal-CQVLL_Oc.js +0 -276
  66. package/dist/assets/_plugin-vue_export-helper-D5N3Hfca.js +0 -231
  67. package/dist/assets/chat-640V6r6N.js +0 -1
  68. package/dist/assets/index-4iFlrAYJ.js +0 -307
  69. package/dist/assets/index-CMJKLUKk.css +0 -1
  70. package/dist/assets/jobs-Cuol6Mqb.js +0 -1
  71. package/dist/assets/use-message-B8ClBznx.js +0 -117
  72. /package/dist/assets/{client-BxIiwrqC.js → client-BKiVmpru.js} +0 -0
@@ -0,0 +1,427 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.fsRoutes = void 0;
7
+ const router_1 = __importDefault(require("@koa/router"));
8
+ const promises_1 = require("fs/promises");
9
+ const path_1 = require("path");
10
+ const os_1 = require("os");
11
+ const authPath = (0, path_1.resolve)((0, os_1.homedir)(), '.hermes', 'auth.json');
12
+ async function loadAuthJson() {
13
+ try {
14
+ const raw = await (0, promises_1.readFile)(authPath, 'utf-8');
15
+ return JSON.parse(raw);
16
+ }
17
+ catch {
18
+ return null;
19
+ }
20
+ }
21
+ async function fetchProviderModels(baseUrl, apiKey) {
22
+ try {
23
+ const url = baseUrl.replace(/\/+$/, '') + '/models';
24
+ const res = await fetch(url, {
25
+ headers: { Authorization: `Bearer ${apiKey}` },
26
+ signal: AbortSignal.timeout(8000),
27
+ });
28
+ if (!res.ok) {
29
+ console.error(`[available-models] ${baseUrl} returned ${res.status}`);
30
+ return [];
31
+ }
32
+ const data = await res.json();
33
+ if (!Array.isArray(data.data)) {
34
+ console.error(`[available-models] ${baseUrl} returned unexpected format`);
35
+ return [];
36
+ }
37
+ return data.data.map(m => m.id).sort();
38
+ }
39
+ catch (err) {
40
+ console.error(`[available-models] ${baseUrl} failed: ${err.message}`);
41
+ return [];
42
+ }
43
+ }
44
+ exports.fsRoutes = new router_1.default();
45
+ const hermesDir = (0, path_1.resolve)((0, os_1.homedir)(), '.hermes');
46
+ // --- Helpers ---
47
+ function extractDescription(content) {
48
+ // SKILL.md format: YAML frontmatter between --- delimiters, then markdown body
49
+ // Extract first non-empty, non-frontmatter, non-heading line as description
50
+ const lines = content.split('\n');
51
+ let inFrontmatter = false;
52
+ let bodyStarted = false;
53
+ for (const line of lines) {
54
+ if (!bodyStarted && line.trim() === '---') {
55
+ if (!inFrontmatter) {
56
+ inFrontmatter = true;
57
+ continue;
58
+ }
59
+ else {
60
+ inFrontmatter = false;
61
+ bodyStarted = true;
62
+ continue;
63
+ }
64
+ }
65
+ if (inFrontmatter)
66
+ continue;
67
+ if (line.trim() === '')
68
+ continue;
69
+ if (line.startsWith('#'))
70
+ continue;
71
+ // Return first meaningful line, truncated
72
+ return line.trim().slice(0, 80);
73
+ }
74
+ return '';
75
+ }
76
+ async function safeReadFile(filePath) {
77
+ try {
78
+ return await (0, promises_1.readFile)(filePath, 'utf-8');
79
+ }
80
+ catch {
81
+ return null;
82
+ }
83
+ }
84
+ async function safeStat(filePath) {
85
+ try {
86
+ const s = await (0, promises_1.stat)(filePath);
87
+ return { mtime: Math.round(s.mtimeMs) };
88
+ }
89
+ catch {
90
+ return null;
91
+ }
92
+ }
93
+ // --- Skills Routes ---
94
+ // List all skills grouped by category
95
+ exports.fsRoutes.get('/api/skills', async (ctx) => {
96
+ const skillsDir = (0, path_1.join)(hermesDir, 'skills');
97
+ try {
98
+ const entries = await (0, promises_1.readdir)(skillsDir, { withFileTypes: true });
99
+ const categories = [];
100
+ for (const entry of entries) {
101
+ if (!entry.isDirectory() || entry.name.startsWith('.'))
102
+ continue;
103
+ const catDir = (0, path_1.join)(skillsDir, entry.name);
104
+ const catDesc = await safeReadFile((0, path_1.join)(catDir, 'DESCRIPTION.md'));
105
+ const catDescription = catDesc ? catDesc.trim().split('\n')[0].replace(/^#+\s*/, '').slice(0, 100) : '';
106
+ const skillEntries = await (0, promises_1.readdir)(catDir, { withFileTypes: true });
107
+ const skills = [];
108
+ for (const se of skillEntries) {
109
+ if (!se.isDirectory())
110
+ continue;
111
+ const skillMd = await safeReadFile((0, path_1.join)(catDir, se.name, 'SKILL.md'));
112
+ if (skillMd) {
113
+ skills.push({
114
+ name: se.name,
115
+ description: extractDescription(skillMd),
116
+ });
117
+ }
118
+ }
119
+ if (skills.length > 0) {
120
+ categories.push({ name: entry.name, description: catDescription, skills });
121
+ }
122
+ }
123
+ // Sort categories alphabetically
124
+ categories.sort((a, b) => a.name.localeCompare(b.name));
125
+ for (const cat of categories) {
126
+ cat.skills.sort((a, b) => a.name.localeCompare(b.name));
127
+ }
128
+ ctx.body = { categories };
129
+ }
130
+ catch (err) {
131
+ ctx.status = 500;
132
+ ctx.body = { error: `Failed to read skills directory: ${err.message}` };
133
+ }
134
+ });
135
+ // List files in a skill directory (for references/templates/scripts)
136
+ // Must be registered before the wildcard route
137
+ async function listFilesRecursive(dir, prefix) {
138
+ const result = [];
139
+ let entries;
140
+ try {
141
+ entries = await (0, promises_1.readdir)(dir, { withFileTypes: true });
142
+ }
143
+ catch {
144
+ return result;
145
+ }
146
+ for (const entry of entries) {
147
+ const relPath = prefix ? `${prefix}/${entry.name}` : entry.name;
148
+ if (entry.isDirectory()) {
149
+ result.push(...await listFilesRecursive((0, path_1.join)(dir, entry.name), relPath));
150
+ }
151
+ else {
152
+ result.push({ path: relPath, name: entry.name });
153
+ }
154
+ }
155
+ return result;
156
+ }
157
+ exports.fsRoutes.get('/api/skills/:category/:skill/files', async (ctx) => {
158
+ const { category, skill } = ctx.params;
159
+ const skillDir = (0, path_1.join)(hermesDir, 'skills', category, skill);
160
+ try {
161
+ const allFiles = await listFilesRecursive(skillDir, '');
162
+ const files = allFiles.filter(f => f.path !== 'SKILL.md');
163
+ ctx.body = { files };
164
+ }
165
+ catch (err) {
166
+ ctx.status = 500;
167
+ ctx.body = { error: err.message };
168
+ }
169
+ });
170
+ // Read a specific file under skills/
171
+ exports.fsRoutes.get('/api/skills/:path(.+)', async (ctx) => {
172
+ const filePath = ctx.params.path;
173
+ const fullPath = (0, path_1.resolve)((0, path_1.join)(hermesDir, 'skills', filePath));
174
+ // Security: ensure path stays within skills directory
175
+ if (!fullPath.startsWith((0, path_1.join)(hermesDir, 'skills'))) {
176
+ ctx.status = 403;
177
+ ctx.body = { error: 'Access denied' };
178
+ return;
179
+ }
180
+ const content = await safeReadFile(fullPath);
181
+ if (content === null) {
182
+ ctx.status = 404;
183
+ ctx.body = { error: 'File not found' };
184
+ return;
185
+ }
186
+ ctx.body = { content };
187
+ });
188
+ // --- Memory Routes ---
189
+ // Read MEMORY.md and USER.md
190
+ exports.fsRoutes.get('/api/memory', async (ctx) => {
191
+ const memoryPath = (0, path_1.join)(hermesDir, 'memories', 'MEMORY.md');
192
+ const userPath = (0, path_1.join)(hermesDir, 'memories', 'USER.md');
193
+ const [memory, user, memoryStat, userStat] = await Promise.all([
194
+ safeReadFile(memoryPath),
195
+ safeReadFile(userPath),
196
+ safeStat(memoryPath),
197
+ safeStat(userPath),
198
+ ]);
199
+ ctx.body = {
200
+ memory: memory || '',
201
+ user: user || '',
202
+ memory_mtime: memoryStat?.mtime || null,
203
+ user_mtime: userStat?.mtime || null,
204
+ };
205
+ });
206
+ // Write MEMORY.md or USER.md
207
+ exports.fsRoutes.post('/api/memory', async (ctx) => {
208
+ const { section, content } = ctx.request.body;
209
+ if (!section || !content) {
210
+ ctx.status = 400;
211
+ ctx.body = { error: 'Missing section or content' };
212
+ return;
213
+ }
214
+ if (section !== 'memory' && section !== 'user') {
215
+ ctx.status = 400;
216
+ ctx.body = { error: 'Section must be "memory" or "user"' };
217
+ return;
218
+ }
219
+ const fileName = section === 'memory' ? 'MEMORY.md' : 'USER.md';
220
+ const filePath = (0, path_1.join)(hermesDir, 'memories', fileName);
221
+ try {
222
+ await (0, promises_1.writeFile)(filePath, content, 'utf-8');
223
+ ctx.body = { success: true };
224
+ }
225
+ catch (err) {
226
+ ctx.status = 500;
227
+ ctx.body = { error: err.message };
228
+ }
229
+ });
230
+ // --- Config Model Routes ---
231
+ const configPath = (0, path_1.resolve)((0, os_1.homedir)(), '.hermes/config.yaml');
232
+ // Build model list from user's actual config.yaml configuration
233
+ // Only shows models the user has explicitly configured, not entire provider catalogs
234
+ function buildModelGroups(yaml) {
235
+ let defaultModel = '';
236
+ let defaultProvider = '';
237
+ const groups = [];
238
+ const allModelIds = new Set();
239
+ // 1. Extract current model from `model:` section
240
+ const defaultMatch = yaml.match(/^model:\s*\n\s+default:\s*(.+)/m);
241
+ if (defaultMatch)
242
+ defaultModel = defaultMatch[1].trim();
243
+ const providerMatch = yaml.match(/^model:\s*\n(?:.*\n)*?\s+provider:\s*(.+)/m);
244
+ if (providerMatch)
245
+ defaultProvider = providerMatch[1].trim();
246
+ // 2. Extract providers: section (user-defined endpoints)
247
+ const providersSection = yaml.match(/^providers:\s*\n((?: .+\n(?: .+\n)*)*)/m);
248
+ if (providersSection) {
249
+ const entries = providersSection[1].match(/^ (\S+):\s*\n((?: .+\n)*)/gm);
250
+ if (entries) {
251
+ for (const entry of entries) {
252
+ const nameMatch = entry.match(/^ (\S+):/);
253
+ const baseUrlMatch = entry.match(/base_url:\s*(.+)/);
254
+ const name = nameMatch?.[1]?.trim();
255
+ if (name) {
256
+ // Provider entry itself — mark as available but don't add model yet
257
+ // (it's an endpoint the user can switch to, models are fetched at runtime)
258
+ }
259
+ }
260
+ }
261
+ }
262
+ // 3. Extract custom_providers: section
263
+ const customSection = yaml.match(/^custom_providers:\s*\n((?:\s*- .+\n(?: .+\n)*)*)/m);
264
+ if (customSection) {
265
+ const entryBlocks = customSection[1].match(/\s*- name:\s*(.+)\n((?: .+\n)*)/g);
266
+ if (entryBlocks) {
267
+ const customModels = [];
268
+ for (const block of entryBlocks) {
269
+ const cName = block.match(/name:\s*(.+)/)?.[1]?.trim();
270
+ const cModel = block.match(/model:\s*(.+)/)?.[1]?.trim();
271
+ if (cName && cModel) {
272
+ customModels.push({ id: cModel, label: `${cName}: ${cModel}` });
273
+ allModelIds.add(cModel);
274
+ }
275
+ }
276
+ if (customModels.length > 0) {
277
+ groups.push({ provider: 'Custom', models: customModels });
278
+ }
279
+ }
280
+ }
281
+ // 4. Add current default model (if not already in custom_providers)
282
+ if (defaultModel && !allModelIds.has(defaultModel)) {
283
+ groups.unshift({ provider: 'Current', models: [{ id: defaultModel, label: defaultModel }] });
284
+ }
285
+ return { default: defaultModel, groups };
286
+ }
287
+ // GET /api/available-models — fetch models from all credential pool endpoints
288
+ exports.fsRoutes.get('/api/available-models', async (ctx) => {
289
+ try {
290
+ const auth = await loadAuthJson();
291
+ const pool = auth?.credential_pool || {};
292
+ // Read current default model from config.yaml
293
+ const yaml = await safeReadFile(configPath) || '';
294
+ const defaultMatch = yaml.match(/^model:\s*\n\s+default:\s*(.+)/m);
295
+ const currentDefault = defaultMatch?.[1]?.trim() || '';
296
+ // Collect unique endpoints from credential pool
297
+ const endpoints = [];
298
+ const seenUrls = new Set();
299
+ for (const [providerKey, entries] of Object.entries(pool)) {
300
+ if (!Array.isArray(entries) || entries.length === 0)
301
+ continue;
302
+ const entry = entries.find(e => e.last_status !== 'exhausted') || entries[0];
303
+ if (!entry?.base_url || !entry?.access_token)
304
+ continue;
305
+ const baseUrl = entry.base_url.replace(/\/+$/, '');
306
+ if (seenUrls.has(baseUrl))
307
+ continue;
308
+ seenUrls.add(baseUrl);
309
+ endpoints.push({
310
+ key: providerKey,
311
+ label: providerKey.replace(/^custom:/, '') || entry.label || baseUrl,
312
+ base_url: baseUrl,
313
+ token: entry.access_token,
314
+ });
315
+ }
316
+ // Fetch all provider models in parallel
317
+ const results = await Promise.allSettled(endpoints.map(async (ep) => {
318
+ const models = await fetchProviderModels(ep.base_url, ep.token);
319
+ return { ...ep, models };
320
+ }));
321
+ const groups = [];
322
+ for (const result of results) {
323
+ if (result.status === 'fulfilled' && result.value.models.length > 0) {
324
+ const { key, label, base_url, models } = result.value;
325
+ groups.push({ provider: key, label, base_url, models });
326
+ }
327
+ else if (result.status === 'rejected') {
328
+ console.error(`[available-models] Failed: ${result.reason?.message || result.reason}`);
329
+ }
330
+ }
331
+ // Fallback: if no providers returned models, fall back to config.yaml parsing
332
+ if (groups.length === 0) {
333
+ const fallback = buildModelGroups(yaml);
334
+ ctx.body = fallback;
335
+ return;
336
+ }
337
+ ctx.body = { default: currentDefault, groups };
338
+ }
339
+ catch (err) {
340
+ ctx.status = 500;
341
+ ctx.body = { error: err.message };
342
+ }
343
+ });
344
+ // GET /api/config/models
345
+ exports.fsRoutes.get('/api/config/models', async (ctx) => {
346
+ try {
347
+ const yaml = await safeReadFile(configPath);
348
+ ctx.body = yaml ? buildModelGroups(yaml) : { default: '', groups: [] };
349
+ }
350
+ catch (err) {
351
+ ctx.status = 500;
352
+ ctx.body = { error: err.message };
353
+ }
354
+ });
355
+ // PUT /api/config/model
356
+ exports.fsRoutes.put('/api/config/model', async (ctx) => {
357
+ const { default: defaultModel, provider: reqProvider } = ctx.request.body;
358
+ if (!defaultModel) {
359
+ ctx.status = 400;
360
+ ctx.body = { error: 'Missing default model' };
361
+ return;
362
+ }
363
+ try {
364
+ await (0, promises_1.copyFile)(configPath, configPath + '.bak');
365
+ let yaml = await safeReadFile(configPath) || '';
366
+ // Rebuild the model: block
367
+ const modelBlockMatch = yaml.match(/^(model:\s*\n(?: .+\n)*)/m);
368
+ if (modelBlockMatch) {
369
+ const lines = [`model:`, ` default: ${defaultModel}`];
370
+ if (reqProvider) {
371
+ // Provider from credential pool key (e.g. "zai" or "custom:subrouter.ai")
372
+ // Hermes resolves base_url/api_key from auth.json automatically
373
+ lines.push(` provider: ${reqProvider}`);
374
+ }
375
+ yaml = yaml.replace(modelBlockMatch[1], lines.join('\n') + '\n');
376
+ }
377
+ await (0, promises_1.writeFile)(configPath, yaml, 'utf-8');
378
+ ctx.body = { success: true };
379
+ }
380
+ catch (err) {
381
+ ctx.status = 500;
382
+ ctx.body = { error: err.message };
383
+ }
384
+ });
385
+ // POST /api/config/providers
386
+ exports.fsRoutes.post('/api/config/providers', async (ctx) => {
387
+ const { name, base_url, api_key, model } = ctx.request.body;
388
+ if (!name || !base_url || !model) {
389
+ ctx.status = 400;
390
+ ctx.body = { error: 'Missing name, base_url, or model' };
391
+ return;
392
+ }
393
+ try {
394
+ await (0, promises_1.copyFile)(configPath, configPath + '.bak');
395
+ let yaml = await safeReadFile(configPath) || '';
396
+ const newEntry = `- name: ${name}\n base_url: ${base_url}\n api_key: ${api_key || ''}\n model: ${model}\n`;
397
+ if (/^custom_providers:/m.test(yaml)) {
398
+ yaml = yaml.replace(/^(custom_providers:)/m, `$1\n${newEntry}`);
399
+ }
400
+ else {
401
+ yaml = yaml.trimEnd() + `\n\ncustom_providers:\n${newEntry}\n`;
402
+ }
403
+ await (0, promises_1.writeFile)(configPath, yaml, 'utf-8');
404
+ ctx.body = { success: true };
405
+ }
406
+ catch (err) {
407
+ ctx.status = 500;
408
+ ctx.body = { error: err.message };
409
+ }
410
+ });
411
+ // DELETE /api/config/providers/:name
412
+ exports.fsRoutes.delete('/api/config/providers/:name', async (ctx) => {
413
+ const name = ctx.params.name;
414
+ try {
415
+ await (0, promises_1.copyFile)(configPath, configPath + '.bak');
416
+ let yaml = await safeReadFile(configPath) || '';
417
+ const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
418
+ const blockRegex = new RegExp(` - name:\\s*${escaped}\\s*\\n(?: .+\\n)*`, 'g');
419
+ yaml = yaml.replace(blockRegex, '');
420
+ await (0, promises_1.writeFile)(configPath, yaml, 'utf-8');
421
+ ctx.body = { success: true };
422
+ }
423
+ catch (err) {
424
+ ctx.status = 500;
425
+ ctx.body = { error: err.message };
426
+ }
427
+ });
@@ -0,0 +1,2 @@
1
+ import Router from '@koa/router';
2
+ export declare const logRoutes: Router<import("koa").DefaultState, import("koa").DefaultContext>;
@@ -0,0 +1,87 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ exports.logRoutes = void 0;
40
+ const router_1 = __importDefault(require("@koa/router"));
41
+ const hermesCli = __importStar(require("../services/hermes-cli"));
42
+ exports.logRoutes = new router_1.default();
43
+ // List available log files
44
+ exports.logRoutes.get('/api/logs', async (ctx) => {
45
+ const files = await hermesCli.listLogFiles();
46
+ ctx.body = { files };
47
+ });
48
+ // Parse a single log line into structured entry
49
+ function parseLine(line) {
50
+ // Match: 2026-04-11 20:16:16,289 INFO aiohttp.access: message
51
+ const match = line.match(/^(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})\s+(DEBUG|INFO|WARNING|ERROR|CRITICAL)\s+(\S+?):\s(.*)$/);
52
+ if (match) {
53
+ return {
54
+ timestamp: match[1],
55
+ level: match[2],
56
+ logger: match[3],
57
+ message: match[4],
58
+ raw: line,
59
+ };
60
+ }
61
+ // Unparseable line (e.g. traceback continuation)
62
+ return null;
63
+ }
64
+ // Read log lines (parsed)
65
+ exports.logRoutes.get('/api/logs/:name', async (ctx) => {
66
+ const logName = ctx.params.name;
67
+ const lines = ctx.query.lines ? parseInt(ctx.query.lines, 10) : 100;
68
+ const level = ctx.query.level || undefined;
69
+ const session = ctx.query.session || undefined;
70
+ const since = ctx.query.since || undefined;
71
+ try {
72
+ const content = await hermesCli.readLogs(logName, lines, level, session, since);
73
+ const rawLines = content.split('\n');
74
+ const entries = [];
75
+ for (const line of rawLines) {
76
+ // Skip header lines like "--- ~/.hermes/logs/agent.log (last 100) ---"
77
+ if (line.startsWith('---') || line.trim() === '')
78
+ continue;
79
+ entries.push(parseLine(line));
80
+ }
81
+ ctx.body = { entries };
82
+ }
83
+ catch (err) {
84
+ ctx.status = 500;
85
+ ctx.body = { error: err.message };
86
+ }
87
+ });
@@ -0,0 +1,2 @@
1
+ import type { Context } from 'koa';
2
+ export declare function proxy(ctx: Context): Promise<void>;
@@ -0,0 +1,82 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.proxy = proxy;
4
+ const config_1 = require("../config");
5
+ async function proxy(ctx) {
6
+ const upstream = config_1.config.upstream.replace(/\/$/, '');
7
+ const url = `${upstream}${ctx.path}${ctx.search || ''}`;
8
+ console.log(`[PROXY] ${ctx.method} ${ctx.path} -> ${url}`);
9
+ // Build headers — forward most, strip browser-specific ones
10
+ const headers = {};
11
+ for (const [key, value] of Object.entries(ctx.headers)) {
12
+ if (value == null)
13
+ continue;
14
+ const lower = key.toLowerCase();
15
+ if (lower === 'host') {
16
+ headers['host'] = new URL(upstream).host;
17
+ }
18
+ else if (lower !== 'origin' && lower !== 'referer' && lower !== 'connection') {
19
+ const v = Array.isArray(value) ? value[0] : value;
20
+ if (v)
21
+ headers[key] = v;
22
+ }
23
+ }
24
+ // Add SSE-friendly headers
25
+ if (ctx.path.match(/\/events$/)) {
26
+ headers['x-accel-buffering'] = 'no';
27
+ headers['cache-control'] = 'no-cache';
28
+ }
29
+ try {
30
+ // Build request body from raw body
31
+ let body;
32
+ if (ctx.req.method !== 'GET' && ctx.req.method !== 'HEAD') {
33
+ body = ctx.request.rawBody;
34
+ }
35
+ const res = await fetch(url, {
36
+ method: ctx.req.method,
37
+ headers,
38
+ body,
39
+ });
40
+ // Set response headers
41
+ const resHeaders = {};
42
+ res.headers.forEach((value, key) => {
43
+ const lower = key.toLowerCase();
44
+ if (lower !== 'transfer-encoding' && lower !== 'connection') {
45
+ resHeaders[key] = value;
46
+ }
47
+ });
48
+ if (ctx.path.match(/\/events$/)) {
49
+ resHeaders['x-accel-buffering'] = 'no';
50
+ resHeaders['cache-control'] = 'no-cache';
51
+ }
52
+ ctx.status = res.status;
53
+ ctx.set(resHeaders);
54
+ // Stream response body
55
+ if (res.body) {
56
+ const reader = res.body.getReader();
57
+ const pump = async () => {
58
+ while (true) {
59
+ const { done, value } = await reader.read();
60
+ if (done)
61
+ break;
62
+ ctx.res.write(value);
63
+ }
64
+ ctx.res.end();
65
+ };
66
+ await pump();
67
+ }
68
+ else {
69
+ ctx.res.end();
70
+ }
71
+ }
72
+ catch (err) {
73
+ if (!ctx.res.headersSent) {
74
+ ctx.status = 502;
75
+ ctx.set('Content-Type', 'application/json');
76
+ ctx.body = { error: { message: `Proxy error: ${err.message}` } };
77
+ }
78
+ else {
79
+ ctx.res.end();
80
+ }
81
+ }
82
+ }
@@ -0,0 +1,2 @@
1
+ import Router from '@koa/router';
2
+ export declare const proxyRoutes: Router<import("koa").DefaultState, import("koa").DefaultContext>;
@@ -0,0 +1,12 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.proxyRoutes = void 0;
7
+ const router_1 = __importDefault(require("@koa/router"));
8
+ const proxy_handler_1 = require("./proxy-handler");
9
+ exports.proxyRoutes = new router_1.default();
10
+ // Proxy all /api/*, /v1/* to upstream Hermes API
11
+ exports.proxyRoutes.all('/api/(.*)', proxy_handler_1.proxy);
12
+ exports.proxyRoutes.all('/v1/(.*)', proxy_handler_1.proxy);
@@ -0,0 +1,2 @@
1
+ import Router from '@koa/router';
2
+ export declare const sessionRoutes: Router<import("koa").DefaultState, import("koa").DefaultContext>;