beecork 1.4.10 → 1.5.0

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 (66) hide show
  1. package/dist/channels/admin.d.ts +10 -0
  2. package/dist/channels/admin.js +20 -0
  3. package/dist/channels/command-handler.d.ts +2 -10
  4. package/dist/channels/command-handler.js +47 -73
  5. package/dist/channels/discord.d.ts +1 -3
  6. package/dist/channels/discord.js +28 -28
  7. package/dist/channels/loader.js +0 -1
  8. package/dist/channels/send-helpers.d.ts +19 -0
  9. package/dist/channels/send-helpers.js +21 -0
  10. package/dist/channels/telegram.d.ts +1 -9
  11. package/dist/channels/telegram.js +46 -71
  12. package/dist/channels/types.d.ts +2 -10
  13. package/dist/channels/voice-state.d.ts +29 -0
  14. package/dist/channels/voice-state.js +43 -0
  15. package/dist/channels/webhook.d.ts +1 -1
  16. package/dist/channels/webhook.js +68 -24
  17. package/dist/channels/whatsapp.d.ts +1 -3
  18. package/dist/channels/whatsapp.js +79 -74
  19. package/dist/cli/doctor.js +5 -2
  20. package/dist/cli/handoff.js +6 -6
  21. package/dist/config.d.ts +5 -1
  22. package/dist/config.js +17 -14
  23. package/dist/daemon.js +29 -17
  24. package/dist/dashboard/html.js +20 -8
  25. package/dist/dashboard/routes.d.ts +17 -0
  26. package/dist/dashboard/routes.js +559 -0
  27. package/dist/dashboard/server.js +33 -488
  28. package/dist/db/index.js +16 -2
  29. package/dist/db/migrations.js +44 -8
  30. package/dist/mcp/handlers.d.ts +37 -0
  31. package/dist/mcp/handlers.js +451 -0
  32. package/dist/mcp/server.js +25 -849
  33. package/dist/mcp/tool-definitions.d.ts +1225 -0
  34. package/dist/mcp/tool-definitions.js +364 -0
  35. package/dist/media/index.d.ts +2 -7
  36. package/dist/media/index.js +1 -1
  37. package/dist/observability/analytics.d.ts +7 -1
  38. package/dist/observability/analytics.js +25 -7
  39. package/dist/projects/index.d.ts +3 -2
  40. package/dist/projects/index.js +2 -2
  41. package/dist/projects/manager.d.ts +1 -3
  42. package/dist/projects/manager.js +26 -25
  43. package/dist/projects/router.d.ts +10 -0
  44. package/dist/projects/router.js +28 -0
  45. package/dist/session/manager.d.ts +4 -0
  46. package/dist/session/manager.js +48 -42
  47. package/dist/session/subprocess.d.ts +1 -0
  48. package/dist/session/subprocess.js +21 -0
  49. package/dist/session/tab-store.d.ts +28 -0
  50. package/dist/session/tab-store.js +77 -0
  51. package/dist/tasks/scheduler.d.ts +6 -0
  52. package/dist/tasks/scheduler.js +52 -13
  53. package/dist/tasks/store.js +6 -6
  54. package/dist/timeline/query.js +6 -2
  55. package/dist/types.d.ts +15 -0
  56. package/dist/util/paths.d.ts +1 -0
  57. package/dist/util/paths.js +4 -1
  58. package/dist/util/rate-limiter.js +8 -0
  59. package/dist/util/text.d.ts +21 -1
  60. package/dist/util/text.js +25 -1
  61. package/dist/watchers/scheduler.js +2 -3
  62. package/package.json +1 -1
  63. package/dist/users/index.d.ts +0 -2
  64. package/dist/users/index.js +0 -1
  65. package/dist/users/service.d.ts +0 -17
  66. package/dist/users/service.js +0 -46
@@ -0,0 +1,559 @@
1
+ // Dashboard route handlers. Each entry is keyed by `<METHOD> <pathPattern>` where
2
+ // pathPattern is a regex string (or exact path). The dispatcher in server.ts
3
+ // chooses the first matching entry and invokes its handler.
4
+ import crypto from 'node:crypto';
5
+ import { exec } from 'node:child_process';
6
+ import { promisify } from 'node:util';
7
+ import { getDb } from '../db/index.js';
8
+ import { logger } from '../util/logger.js';
9
+ import { validateTabName, validateTabNameOrDefault } from '../config.js';
10
+ import { createTabRecord } from '../db/index.js';
11
+ import { VERSION } from '../version.js';
12
+ import { getDaemonPid } from '../cli/helpers.js';
13
+ import { MESSAGE_LIMITS } from '../util/text.js';
14
+ import { TabStore } from '../session/tab-store.js';
15
+ const execAsync = promisify(exec);
16
+ function parseIntParam(value, def, max) {
17
+ if (value === null)
18
+ return def;
19
+ const n = parseInt(value, 10);
20
+ if (Number.isNaN(n) || n < 0)
21
+ return def;
22
+ return Math.min(n, max);
23
+ }
24
+ function json(res, data, status = 200) {
25
+ res.writeHead(status, { 'Content-Type': 'application/json' });
26
+ res.end(JSON.stringify(data));
27
+ }
28
+ async function readBody(req, res) {
29
+ let body = '';
30
+ for await (const chunk of req) {
31
+ body += chunk;
32
+ if (body.length > MESSAGE_LIMITS.HTTP_BODY) {
33
+ json(res, { error: 'Payload too large' }, 413);
34
+ req.destroy();
35
+ return null;
36
+ }
37
+ }
38
+ return body;
39
+ }
40
+ function exactPath(p) {
41
+ return path => path === p;
42
+ }
43
+ function regexPath(re) {
44
+ return path => re.test(path);
45
+ }
46
+ export const ROUTES = [
47
+ // SSE — never log a "broken pipe" write as a hard error
48
+ {
49
+ method: 'GET',
50
+ test: exactPath('/api/events'),
51
+ handler: ({ req, res }) => {
52
+ res.writeHead(200, {
53
+ 'Content-Type': 'text/event-stream',
54
+ 'Cache-Control': 'no-cache',
55
+ 'Connection': 'keep-alive',
56
+ });
57
+ res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
58
+ const interval = setInterval(() => {
59
+ if (res.writableEnded)
60
+ return;
61
+ try {
62
+ const tabs = TabStore.listAll().map(t => ({
63
+ name: t.name, status: t.status, last_activity_at: t.lastActivityAt,
64
+ }));
65
+ const activeCount = tabs.filter(t => t.status === 'running').length;
66
+ res.write(`data: ${JSON.stringify({ type: 'update', tabs, activeTabs: activeCount })}\n\n`);
67
+ }
68
+ catch (err) {
69
+ logger.warn('Dashboard SSE tick failed:', err);
70
+ }
71
+ }, 2000);
72
+ req.on('close', () => clearInterval(interval));
73
+ },
74
+ },
75
+ // POST /api/tabs/:name/send
76
+ {
77
+ method: 'POST',
78
+ test: regexPath(/^\/api\/tabs\/[^/]+\/send$/),
79
+ handler: async ({ req, res, path }) => {
80
+ const body = await readBody(req, res);
81
+ if (body === null)
82
+ return;
83
+ let parsed;
84
+ try {
85
+ parsed = JSON.parse(body);
86
+ }
87
+ catch {
88
+ json(res, { error: 'Invalid JSON' }, 400);
89
+ return;
90
+ }
91
+ if (!parsed.message) {
92
+ json(res, { error: 'Missing message' }, 400);
93
+ return;
94
+ }
95
+ const tabName = decodeURIComponent(path.split('/')[3]);
96
+ const err = validateTabNameOrDefault(tabName);
97
+ if (err) {
98
+ json(res, { error: err }, 400);
99
+ return;
100
+ }
101
+ getDb().prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run(tabName, parsed.message, 'user');
102
+ json(res, { success: true, tab: tabName });
103
+ },
104
+ },
105
+ // POST /api/tabs — create
106
+ {
107
+ method: 'POST',
108
+ test: exactPath('/api/tabs'),
109
+ handler: async ({ req, res }) => {
110
+ const body = await readBody(req, res);
111
+ if (body === null)
112
+ return;
113
+ let parsed;
114
+ try {
115
+ parsed = JSON.parse(body);
116
+ }
117
+ catch {
118
+ json(res, { error: 'Invalid JSON' }, 400);
119
+ return;
120
+ }
121
+ if (!parsed.name) {
122
+ json(res, { error: 'Missing tab name' }, 400);
123
+ return;
124
+ }
125
+ const err = validateTabName(parsed.name);
126
+ if (err) {
127
+ json(res, { error: err }, 400);
128
+ return;
129
+ }
130
+ try {
131
+ createTabRecord(getDb(), { name: parsed.name, workingDir: parsed.workingDir, systemPrompt: parsed.systemPrompt });
132
+ json(res, { success: true, name: parsed.name });
133
+ }
134
+ catch (e) {
135
+ json(res, { error: e instanceof Error ? e.message : String(e) }, 400);
136
+ }
137
+ },
138
+ },
139
+ // DELETE /api/tabs/:name
140
+ {
141
+ method: 'DELETE',
142
+ test: regexPath(/^\/api\/tabs\/[^/]+$/),
143
+ handler: ({ res, path }) => {
144
+ const tabName = decodeURIComponent(path.split('/')[3]);
145
+ TabStore.deleteWithMessages(tabName);
146
+ json(res, { success: true });
147
+ },
148
+ },
149
+ // POST /api/tasks or /api/crons
150
+ {
151
+ method: 'POST',
152
+ test: path => path === '/api/tasks' || path === '/api/crons',
153
+ handler: async ({ req, res }) => {
154
+ const body = await readBody(req, res);
155
+ if (body === null)
156
+ return;
157
+ let parsed;
158
+ try {
159
+ parsed = JSON.parse(body);
160
+ }
161
+ catch {
162
+ json(res, { error: 'Invalid JSON' }, 400);
163
+ return;
164
+ }
165
+ if (!parsed.name || !parsed.schedule || !parsed.message) {
166
+ json(res, { error: 'Missing required fields' }, 400);
167
+ return;
168
+ }
169
+ const effectiveTab = parsed.tabName || 'default';
170
+ const tabErr = validateTabNameOrDefault(effectiveTab);
171
+ if (tabErr) {
172
+ json(res, { error: tabErr }, 400);
173
+ return;
174
+ }
175
+ const scheduleType = parsed.scheduleType || 'every';
176
+ const { validateSchedule } = await import('../tasks/scheduler.js');
177
+ const scheduleErr = validateSchedule(scheduleType, parsed.schedule);
178
+ if (scheduleErr) {
179
+ json(res, { error: scheduleErr }, 400);
180
+ return;
181
+ }
182
+ const id = crypto.randomUUID();
183
+ getDb().prepare(`INSERT INTO tasks (id, name, schedule_type, schedule, tab_name, message, payload_type, enabled)
184
+ VALUES (?, ?, ?, ?, ?, ?, 'agentTurn', 1)`).run(id, parsed.name, scheduleType, parsed.schedule, effectiveTab, parsed.message);
185
+ json(res, { success: true, id });
186
+ },
187
+ },
188
+ // DELETE /api/tasks/:id or /api/crons/:id
189
+ {
190
+ method: 'DELETE',
191
+ test: path => /^\/api\/(tasks|crons)\/[^/]+$/.test(path),
192
+ handler: ({ res, path }) => {
193
+ const taskId = decodeURIComponent(path.split('/')[3]);
194
+ getDb().prepare('DELETE FROM tasks WHERE id = ?').run(taskId);
195
+ json(res, { success: true });
196
+ },
197
+ },
198
+ // GET /api/watchers
199
+ {
200
+ method: 'GET',
201
+ test: exactPath('/api/watchers'),
202
+ handler: ({ res }) => {
203
+ const limit = 500;
204
+ const watchers = getDb().prepare('SELECT * FROM watchers ORDER BY created_at LIMIT ?').all(limit);
205
+ json(res, watchers);
206
+ },
207
+ },
208
+ // DELETE /api/watchers/:id
209
+ {
210
+ method: 'DELETE',
211
+ test: regexPath(/^\/api\/watchers\/[^/]+$/),
212
+ handler: ({ res, path }) => {
213
+ const id = decodeURIComponent(path.split('/')[3]);
214
+ getDb().prepare('DELETE FROM watchers WHERE id = ?').run(id);
215
+ json(res, { success: true });
216
+ },
217
+ },
218
+ // POST /api/memories
219
+ {
220
+ method: 'POST',
221
+ test: exactPath('/api/memories'),
222
+ handler: async ({ req, res }) => {
223
+ const body = await readBody(req, res);
224
+ if (body === null)
225
+ return;
226
+ let parsed;
227
+ try {
228
+ parsed = JSON.parse(body);
229
+ }
230
+ catch {
231
+ json(res, { error: 'Invalid JSON' }, 400);
232
+ return;
233
+ }
234
+ if (!parsed.content) {
235
+ json(res, { error: 'Missing content' }, 400);
236
+ return;
237
+ }
238
+ getDb().prepare('INSERT INTO memories (content, tab_name, source) VALUES (?, ?, ?)').run(parsed.content, parsed.tabName || null, 'tool');
239
+ json(res, { success: true });
240
+ },
241
+ },
242
+ // DELETE /api/memories/:id
243
+ {
244
+ method: 'DELETE',
245
+ test: regexPath(/^\/api\/memories\/\d+$/),
246
+ handler: ({ res, path }) => {
247
+ const id = path.split('/')[3];
248
+ getDb().prepare('DELETE FROM memories WHERE id = ?').run(id);
249
+ json(res, { success: true });
250
+ },
251
+ },
252
+ // GET /api/media/config
253
+ {
254
+ method: 'GET',
255
+ test: exactPath('/api/media/config'),
256
+ handler: async ({ res }) => {
257
+ const { getConfig } = await import('../config.js');
258
+ const generators = getConfig().mediaGenerators || [];
259
+ json(res, { generators: generators.map(g => ({ provider: g.provider, model: g.model, configured: !!g.apiKey })) });
260
+ },
261
+ },
262
+ // GET /api/channels/config
263
+ {
264
+ method: 'GET',
265
+ test: exactPath('/api/channels/config'),
266
+ handler: async ({ res }) => {
267
+ const { getConfig } = await import('../config.js');
268
+ const config = getConfig();
269
+ json(res, {
270
+ telegram: { configured: !!config.telegram?.token, botUsername: null },
271
+ discord: { configured: !!config.discord?.token },
272
+ whatsapp: { configured: !!config.whatsapp?.enabled },
273
+ webhook: { configured: !!config.webhook?.enabled, port: config.webhook?.port },
274
+ });
275
+ },
276
+ },
277
+ // POST /api/computer-use
278
+ {
279
+ method: 'POST',
280
+ test: exactPath('/api/computer-use'),
281
+ handler: async ({ req, res }) => {
282
+ const body = await readBody(req, res);
283
+ if (body === null)
284
+ return;
285
+ let parsed;
286
+ try {
287
+ parsed = JSON.parse(body);
288
+ }
289
+ catch {
290
+ json(res, { error: 'Invalid JSON' }, 400);
291
+ return;
292
+ }
293
+ const { getConfig, saveConfig } = await import('../config.js');
294
+ const config = getConfig();
295
+ config.claudeCode.computerUse = !!parsed.enabled;
296
+ saveConfig(config);
297
+ json(res, { enabled: !!parsed.enabled, message: 'Restart daemon to apply.' });
298
+ },
299
+ },
300
+ // GET /api/computer-use
301
+ {
302
+ method: 'GET',
303
+ test: exactPath('/api/computer-use'),
304
+ handler: async ({ res }) => {
305
+ const { getConfig } = await import('../config.js');
306
+ json(res, { enabled: !!getConfig().claudeCode.computerUse });
307
+ },
308
+ },
309
+ // GET /api/timeline
310
+ {
311
+ method: 'GET',
312
+ test: exactPath('/api/timeline'),
313
+ handler: async ({ res, url }) => {
314
+ const { getTimeline } = await import('../timeline/index.js');
315
+ const date = url.searchParams.get('date') || new Date().toISOString().slice(0, 10);
316
+ const limit = parseIntParam(url.searchParams.get('limit'), 50, 200);
317
+ json(res, { events: getTimeline({ date, limit }) });
318
+ },
319
+ },
320
+ // GET /api/status
321
+ {
322
+ method: 'GET',
323
+ test: exactPath('/api/status'),
324
+ handler: ({ res }) => {
325
+ const db = getDb();
326
+ const activeTasks = db.prepare("SELECT COUNT(*) as c FROM tasks WHERE enabled = 1").get().c;
327
+ json(res, {
328
+ version: VERSION,
329
+ daemonPid: getDaemonPid(),
330
+ tabs: TabStore.countAll(),
331
+ activeTabs: TabStore.countRunning(),
332
+ tasks: activeTasks,
333
+ // Legacy alias — HTML reads either key; can be dropped after old dashboards have refreshed.
334
+ cronJobs: activeTasks,
335
+ memories: db.prepare('SELECT COUNT(*) as c FROM memories').get().c,
336
+ });
337
+ },
338
+ },
339
+ // GET /api/tabs
340
+ {
341
+ method: 'GET',
342
+ test: exactPath('/api/tabs'),
343
+ handler: ({ res }) => {
344
+ const tabs = getDb().prepare(`
345
+ SELECT t.*,
346
+ (SELECT COUNT(*) FROM messages WHERE tab_id = t.id) as message_count,
347
+ (SELECT COALESCE(SUM(cost_usd), 0) FROM messages WHERE tab_id = t.id) as total_cost
348
+ FROM tabs t ORDER BY t.last_activity_at DESC
349
+ `).all();
350
+ json(res, tabs);
351
+ },
352
+ },
353
+ // GET /api/tabs/:name/messages
354
+ {
355
+ method: 'GET',
356
+ test: regexPath(/^\/api\/tabs\/[^/]+\/messages$/),
357
+ handler: ({ res, url, path }) => {
358
+ const tabName = decodeURIComponent(path.split('/')[3]);
359
+ const limit = parseIntParam(url.searchParams.get('limit'), 50, 200);
360
+ const offset = parseIntParam(url.searchParams.get('offset'), 0, 100000);
361
+ const tabId = TabStore.getIdByName(tabName);
362
+ if (!tabId) {
363
+ json(res, { error: 'Tab not found' }, 404);
364
+ return;
365
+ }
366
+ const db = getDb();
367
+ const messages = db.prepare('SELECT role, content, cost_usd, tokens_in, tokens_out, created_at FROM messages WHERE tab_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?').all(tabId, limit, offset);
368
+ const total = db.prepare('SELECT COUNT(*) as c FROM messages WHERE tab_id = ?').get(tabId).c;
369
+ json(res, { messages: messages.reverse(), total, limit, offset });
370
+ },
371
+ },
372
+ // GET /api/memories
373
+ {
374
+ method: 'GET',
375
+ test: exactPath('/api/memories'),
376
+ handler: ({ res, url }) => {
377
+ const limit = parseIntParam(url.searchParams.get('limit'), 50, 200);
378
+ const offset = parseIntParam(url.searchParams.get('offset'), 0, 100000);
379
+ const q = url.searchParams.get('q') || '';
380
+ const db = getDb();
381
+ let memories, total;
382
+ if (q) {
383
+ memories = db.prepare('SELECT id, content, tab_name, source, created_at FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT ? OFFSET ?').all(`%${q}%`, limit, offset);
384
+ total = db.prepare('SELECT COUNT(*) as c FROM memories WHERE content LIKE ?').get(`%${q}%`).c;
385
+ }
386
+ else {
387
+ memories = db.prepare('SELECT id, content, tab_name, source, created_at FROM memories ORDER BY created_at DESC LIMIT ? OFFSET ?').all(limit, offset);
388
+ total = db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
389
+ }
390
+ json(res, { memories, total, limit, offset });
391
+ },
392
+ },
393
+ // GET /api/tasks or /api/crons
394
+ {
395
+ method: 'GET',
396
+ test: path => path === '/api/tasks' || path === '/api/crons',
397
+ handler: ({ res, url }) => {
398
+ const limit = parseIntParam(url.searchParams.get('limit'), 100, 500);
399
+ const tasks = getDb().prepare('SELECT * FROM tasks ORDER BY created_at LIMIT ?').all(limit);
400
+ json(res, tasks);
401
+ },
402
+ },
403
+ // GET /api/costs
404
+ {
405
+ method: 'GET',
406
+ test: exactPath('/api/costs'),
407
+ handler: ({ res }) => {
408
+ const costs = getDb().prepare(`
409
+ SELECT date(created_at) as day,
410
+ SUM(cost_usd) as total_cost,
411
+ COUNT(*) as message_count
412
+ FROM messages
413
+ WHERE role = 'assistant' AND cost_usd > 0
414
+ AND created_at > datetime('now', '-30 days')
415
+ GROUP BY date(created_at)
416
+ ORDER BY day
417
+ `).all();
418
+ json(res, costs);
419
+ },
420
+ },
421
+ // GET /api/update/status
422
+ {
423
+ method: 'GET',
424
+ test: exactPath('/api/update/status'),
425
+ handler: async ({ res }) => {
426
+ async function checkPackage(name) {
427
+ const pkg = { name };
428
+ try {
429
+ const { stdout } = await execAsync(`${name} --version`, { timeout: 10000 });
430
+ pkg.installed = stdout.trim().replace(/^v/, '');
431
+ }
432
+ catch {
433
+ pkg.installed = null;
434
+ }
435
+ try {
436
+ const { stdout } = await execAsync(`npm view ${name} version`, { timeout: 10000 });
437
+ pkg.latest = stdout.trim();
438
+ }
439
+ catch {
440
+ pkg.latest = null;
441
+ }
442
+ pkg.updateAvailable = !!(pkg.installed && pkg.latest && pkg.installed !== pkg.latest);
443
+ return pkg;
444
+ }
445
+ const packages = await Promise.all([
446
+ (async () => {
447
+ const p = await checkPackage('beecork');
448
+ p.installed = VERSION;
449
+ p.updateAvailable = !!(p.latest && p.installed !== p.latest);
450
+ return p;
451
+ })(),
452
+ (async () => {
453
+ const p = { name: '@anthropic-ai/claude-code' };
454
+ try {
455
+ const { stdout } = await execAsync('claude --version', { timeout: 10000 });
456
+ p.installed = stdout.trim().replace(/^.*?(\d+\.\d+\.\d+).*$/, '$1');
457
+ }
458
+ catch {
459
+ p.installed = null;
460
+ }
461
+ try {
462
+ const { stdout } = await execAsync('npm view @anthropic-ai/claude-code version', { timeout: 10000 });
463
+ p.latest = stdout.trim();
464
+ }
465
+ catch {
466
+ p.latest = null;
467
+ }
468
+ p.updateAvailable = !!(p.installed && p.latest && p.installed !== p.latest);
469
+ return p;
470
+ })(),
471
+ ]);
472
+ json(res, { packages });
473
+ },
474
+ },
475
+ // POST /api/update/:pkg
476
+ {
477
+ method: 'POST',
478
+ test: regexPath(/^\/api\/update\/[^/]+$/),
479
+ handler: async ({ res, path }) => {
480
+ const pkgName = decodeURIComponent(path.split('/')[3]);
481
+ const allowedPackages = {
482
+ 'beecork': 'npm install -g beecork@latest',
483
+ '@anthropic-ai/claude-code': 'npm install -g @anthropic-ai/claude-code@latest',
484
+ };
485
+ const cmd = allowedPackages[pkgName];
486
+ if (!cmd) {
487
+ json(res, { error: `Package "${pkgName}" is not in the allowed update list.` }, 400);
488
+ return;
489
+ }
490
+ try {
491
+ const { stdout } = await execAsync(cmd, { timeout: 120000 });
492
+ json(res, { success: true, package: pkgName, output: stdout.trim() });
493
+ }
494
+ catch (err) {
495
+ json(res, { error: err instanceof Error ? err.message : String(err) }, 500);
496
+ }
497
+ },
498
+ },
499
+ // GET /api/capabilities
500
+ {
501
+ method: 'GET',
502
+ test: exactPath('/api/capabilities'),
503
+ handler: async ({ res }) => {
504
+ const { getAvailablePacks, isEnabled } = await import('../capabilities/index.js');
505
+ const packs = getAvailablePacks().map(p => ({
506
+ ...p,
507
+ enabled: isEnabled(p.id),
508
+ mcpServer: { package: p.mcpServer.package },
509
+ }));
510
+ json(res, { packs });
511
+ },
512
+ },
513
+ // POST /api/capabilities/:id/enable
514
+ {
515
+ method: 'POST',
516
+ test: regexPath(/^\/api\/capabilities\/[^/]+\/enable$/),
517
+ handler: async ({ req, res, path }) => {
518
+ const packId = path.split('/')[3];
519
+ const body = await readBody(req, res);
520
+ if (body === null)
521
+ return;
522
+ let parsed;
523
+ try {
524
+ parsed = JSON.parse(body);
525
+ }
526
+ catch {
527
+ json(res, { error: 'Invalid JSON' }, 400);
528
+ return;
529
+ }
530
+ const { enablePack } = await import('../capabilities/index.js');
531
+ try {
532
+ enablePack(packId, parsed.apiKey);
533
+ json(res, { success: true, message: 'Restart daemon to activate.' });
534
+ }
535
+ catch (err) {
536
+ json(res, { error: err instanceof Error ? err.message : String(err) }, 400);
537
+ }
538
+ },
539
+ },
540
+ // POST /api/capabilities/:id/disable
541
+ {
542
+ method: 'POST',
543
+ test: regexPath(/^\/api\/capabilities\/[^/]+\/disable$/),
544
+ handler: async ({ res, path }) => {
545
+ const packId = path.split('/')[3];
546
+ const { disablePack } = await import('../capabilities/index.js');
547
+ disablePack(packId);
548
+ json(res, { success: true });
549
+ },
550
+ },
551
+ ];
552
+ export function dispatch(method, path) {
553
+ for (const r of ROUTES) {
554
+ if (r.method === method && r.test(path))
555
+ return r;
556
+ }
557
+ return null;
558
+ }
559
+ export { json };