beecork 1.4.11 → 1.6.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 (138) hide show
  1. package/dist/capabilities/index.d.ts +1 -1
  2. package/dist/capabilities/index.js +1 -1
  3. package/dist/capabilities/manager.js +13 -9
  4. package/dist/capabilities/packs.js +3 -1
  5. package/dist/channels/admin.d.ts +10 -0
  6. package/dist/channels/admin.js +20 -0
  7. package/dist/channels/command-handler.d.ts +2 -10
  8. package/dist/channels/command-handler.js +90 -84
  9. package/dist/channels/discord.d.ts +4 -9
  10. package/dist/channels/discord.js +59 -42
  11. package/dist/channels/index.d.ts +1 -1
  12. package/dist/channels/loader.js +13 -4
  13. package/dist/channels/pipeline.js +14 -5
  14. package/dist/channels/registry.d.ts +17 -1
  15. package/dist/channels/registry.js +33 -4
  16. package/dist/channels/send-helpers.d.ts +19 -0
  17. package/dist/channels/send-helpers.js +21 -0
  18. package/dist/channels/telegram.d.ts +21 -14
  19. package/dist/channels/telegram.js +214 -104
  20. package/dist/channels/types.d.ts +13 -38
  21. package/dist/channels/voice-state.d.ts +29 -0
  22. package/dist/channels/voice-state.js +45 -0
  23. package/dist/channels/webhook.d.ts +2 -5
  24. package/dist/channels/webhook.js +88 -29
  25. package/dist/channels/whatsapp.d.ts +9 -7
  26. package/dist/channels/whatsapp.js +141 -100
  27. package/dist/cli/capabilities.js +4 -4
  28. package/dist/cli/channel.js +16 -6
  29. package/dist/cli/commands.js +12 -9
  30. package/dist/cli/doctor.js +85 -27
  31. package/dist/cli/handoff.d.ts +7 -14
  32. package/dist/cli/handoff.js +9 -44
  33. package/dist/cli/mcp.js +5 -5
  34. package/dist/cli/media.js +21 -8
  35. package/dist/cli/setup.js +9 -8
  36. package/dist/cli/store.js +29 -12
  37. package/dist/config.d.ts +5 -1
  38. package/dist/config.js +20 -22
  39. package/dist/daemon.js +113 -51
  40. package/dist/dashboard/html.js +100 -20
  41. package/dist/dashboard/routes.d.ts +17 -0
  42. package/dist/dashboard/routes.js +623 -0
  43. package/dist/dashboard/server.js +38 -489
  44. package/dist/db/connection.d.ts +29 -0
  45. package/dist/db/connection.js +37 -0
  46. package/dist/db/index.js +43 -11
  47. package/dist/db/migrations.js +114 -22
  48. package/dist/delegation/manager.js +10 -4
  49. package/dist/index.js +39 -59
  50. package/dist/knowledge/manager.js +26 -12
  51. package/dist/mcp/handlers.d.ts +37 -0
  52. package/dist/mcp/handlers.js +520 -0
  53. package/dist/mcp/server.js +44 -858
  54. package/dist/mcp/tool-definitions.d.ts +1225 -0
  55. package/dist/mcp/tool-definitions.js +412 -0
  56. package/dist/mcp/validate.d.ts +23 -0
  57. package/dist/mcp/validate.js +65 -0
  58. package/dist/media/factory.js +18 -14
  59. package/dist/media/generators/dall-e.js +2 -2
  60. package/dist/media/generators/kling.js +4 -4
  61. package/dist/media/generators/lyria.js +1 -1
  62. package/dist/media/generators/nano-banana.d.ts +1 -1
  63. package/dist/media/generators/nano-banana.js +2 -2
  64. package/dist/media/generators/poll-util.js +4 -4
  65. package/dist/media/generators/recraft.js +3 -3
  66. package/dist/media/generators/runway.js +4 -4
  67. package/dist/media/generators/stable-diffusion.js +2 -2
  68. package/dist/media/generators/veo.js +1 -1
  69. package/dist/media/index.d.ts +2 -7
  70. package/dist/media/index.js +2 -2
  71. package/dist/media/store.d.ts +7 -0
  72. package/dist/media/store.js +18 -4
  73. package/dist/media/types.d.ts +22 -0
  74. package/dist/notifications/index.d.ts +2 -4
  75. package/dist/notifications/index.js +6 -19
  76. package/dist/notifications/ntfy.js +3 -3
  77. package/dist/observability/analytics.d.ts +1 -1
  78. package/dist/observability/analytics.js +41 -16
  79. package/dist/projects/index.d.ts +3 -2
  80. package/dist/projects/index.js +2 -2
  81. package/dist/projects/manager.d.ts +1 -7
  82. package/dist/projects/manager.js +66 -42
  83. package/dist/projects/router.d.ts +12 -0
  84. package/dist/projects/router.js +98 -45
  85. package/dist/service/install.js +15 -5
  86. package/dist/service/windows.js +1 -1
  87. package/dist/session/budget-guard.d.ts +20 -0
  88. package/dist/session/budget-guard.js +31 -0
  89. package/dist/session/circuit-breaker.d.ts +5 -3
  90. package/dist/session/circuit-breaker.js +45 -20
  91. package/dist/session/context-compactor.d.ts +32 -0
  92. package/dist/session/context-compactor.js +45 -0
  93. package/dist/session/context-monitor.js +2 -2
  94. package/dist/session/handoff.d.ts +21 -0
  95. package/dist/session/handoff.js +50 -0
  96. package/dist/session/manager.d.ts +21 -5
  97. package/dist/session/manager.js +166 -153
  98. package/dist/session/memory-store.d.ts +29 -0
  99. package/dist/session/memory-store.js +45 -0
  100. package/dist/session/message-queue.d.ts +28 -0
  101. package/dist/session/message-queue.js +52 -0
  102. package/dist/session/pending-dispatcher.d.ts +31 -0
  103. package/dist/session/pending-dispatcher.js +120 -0
  104. package/dist/session/pending-store.d.ts +60 -0
  105. package/dist/session/pending-store.js +118 -0
  106. package/dist/session/stale-session.d.ts +31 -0
  107. package/dist/session/stale-session.js +45 -0
  108. package/dist/session/subprocess.d.ts +3 -0
  109. package/dist/session/subprocess.js +54 -11
  110. package/dist/session/tab-store.d.ts +28 -0
  111. package/dist/session/tab-store.js +78 -0
  112. package/dist/tasks/scheduler.d.ts +13 -0
  113. package/dist/tasks/scheduler.js +97 -18
  114. package/dist/tasks/store.js +26 -12
  115. package/dist/timeline/logger.js +3 -1
  116. package/dist/timeline/query.js +15 -5
  117. package/dist/types.d.ts +49 -9
  118. package/dist/util/auto-heal.js +15 -5
  119. package/dist/util/install-info.js +3 -1
  120. package/dist/util/logger.d.ts +1 -1
  121. package/dist/util/logger.js +63 -24
  122. package/dist/util/paths.d.ts +2 -0
  123. package/dist/util/paths.js +16 -3
  124. package/dist/util/rate-limiter.js +8 -0
  125. package/dist/util/retry.js +1 -1
  126. package/dist/util/text.d.ts +21 -1
  127. package/dist/util/text.js +38 -8
  128. package/dist/voice/index.js +5 -1
  129. package/dist/voice/stt.js +14 -6
  130. package/dist/voice/tts.js +1 -1
  131. package/dist/watchers/scheduler.js +11 -5
  132. package/package.json +6 -1
  133. package/dist/session/tool-classifier.d.ts +0 -4
  134. package/dist/session/tool-classifier.js +0 -56
  135. package/dist/users/index.d.ts +0 -2
  136. package/dist/users/index.js +0 -1
  137. package/dist/users/service.d.ts +0 -17
  138. package/dist/users/service.js +0 -46
@@ -0,0 +1,623 @@
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 os from 'node:os';
6
+ import path from 'node:path';
7
+ import { execFile } from 'node:child_process';
8
+ import { promisify } from 'node:util';
9
+ import { getDb } from '../db/index.js';
10
+ import { logger } from '../util/logger.js';
11
+ import { validateTabName, validateTabNameOrDefault, getConfig } from '../config.js';
12
+ import { createTabRecord } from '../db/index.js';
13
+ import { VERSION } from '../version.js';
14
+ import { getDaemonPid } from '../cli/helpers.js';
15
+ import { MESSAGE_LIMITS } from '../util/text.js';
16
+ import { TabStore } from '../session/tab-store.js';
17
+ import { PendingMessageStore } from '../session/pending-store.js';
18
+ import { expandHome } from '../util/paths.js';
19
+ const execFileAsync = promisify(execFile);
20
+ /**
21
+ * Check whether a workingDir resolves under an allowed root.
22
+ * Allowed roots: tabs.default.workingDir, projectScanPaths, $HOME.
23
+ * This mirrors the allowlist used by projects/manager.ts:createProject so the
24
+ * dashboard cannot create a tab pointing at /etc or another user's home.
25
+ */
26
+ function isAllowedWorkingDir(dir) {
27
+ const resolved = path.resolve(expandHome(dir));
28
+ const config = getConfig();
29
+ const home = os.homedir();
30
+ const roots = [config.tabs?.default?.workingDir, ...(config.projectScanPaths ?? []), home]
31
+ .filter((r) => typeof r === 'string' && r.length > 0)
32
+ .map((r) => path.resolve(expandHome(r)));
33
+ return roots.some((root) => resolved === root || resolved.startsWith(root + path.sep));
34
+ }
35
+ const SAFE_NPM_PACKAGE = /^[@a-zA-Z0-9_/.-]+$/;
36
+ function parseIntParam(value, def, max) {
37
+ if (value === null)
38
+ return def;
39
+ const n = parseInt(value, 10);
40
+ if (Number.isNaN(n) || n < 0)
41
+ return def;
42
+ return Math.min(n, max);
43
+ }
44
+ function json(res, data, status = 200) {
45
+ res.writeHead(status, { 'Content-Type': 'application/json' });
46
+ res.end(JSON.stringify(data));
47
+ }
48
+ async function readBody(req, res) {
49
+ let body = '';
50
+ for await (const chunk of req) {
51
+ body += chunk;
52
+ if (body.length > MESSAGE_LIMITS.HTTP_BODY) {
53
+ json(res, { error: 'Payload too large' }, 413);
54
+ req.destroy();
55
+ return null;
56
+ }
57
+ }
58
+ return body;
59
+ }
60
+ function exactPath(p) {
61
+ return (path) => path === p;
62
+ }
63
+ function regexPath(re) {
64
+ return (path) => re.test(path);
65
+ }
66
+ export const ROUTES = [
67
+ // SSE — never log a "broken pipe" write as a hard error
68
+ {
69
+ method: 'GET',
70
+ test: exactPath('/api/events'),
71
+ handler: ({ req, res }) => {
72
+ res.writeHead(200, {
73
+ 'Content-Type': 'text/event-stream',
74
+ 'Cache-Control': 'no-cache',
75
+ Connection: 'keep-alive',
76
+ });
77
+ res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
78
+ // Skip pushes when nothing changed — without this every 2s tick wrote
79
+ // the full tab payload even when the daemon was idle.
80
+ let lastPayload = '';
81
+ const interval = setInterval(() => {
82
+ if (res.writableEnded)
83
+ return;
84
+ try {
85
+ const tabs = TabStore.listAll().map((t) => ({
86
+ name: t.name,
87
+ status: t.status,
88
+ last_activity_at: t.lastActivityAt,
89
+ }));
90
+ const activeCount = tabs.filter((t) => t.status === 'running').length;
91
+ const payload = JSON.stringify({ type: 'update', tabs, activeTabs: activeCount });
92
+ if (payload === lastPayload)
93
+ return;
94
+ lastPayload = payload;
95
+ res.write(`data: ${payload}\n\n`);
96
+ }
97
+ catch (err) {
98
+ logger.warn('Dashboard SSE tick failed:', err);
99
+ }
100
+ }, 2000);
101
+ req.on('close', () => clearInterval(interval));
102
+ },
103
+ },
104
+ // POST /api/tabs/:name/send
105
+ {
106
+ method: 'POST',
107
+ test: regexPath(/^\/api\/tabs\/[^/]+\/send$/),
108
+ handler: async ({ req, res, path }) => {
109
+ const body = await readBody(req, res);
110
+ if (body === null)
111
+ return;
112
+ let parsed;
113
+ try {
114
+ parsed = JSON.parse(body);
115
+ }
116
+ catch {
117
+ json(res, { error: 'Invalid JSON' }, 400);
118
+ return;
119
+ }
120
+ if (!parsed.message) {
121
+ json(res, { error: 'Missing message' }, 400);
122
+ return;
123
+ }
124
+ const tabName = decodeURIComponent(path.split('/')[3]);
125
+ const err = validateTabNameOrDefault(tabName);
126
+ if (err) {
127
+ json(res, { error: err }, 400);
128
+ return;
129
+ }
130
+ PendingMessageStore.enqueueUser(tabName, parsed.message, getDb());
131
+ json(res, { success: true, tab: tabName });
132
+ },
133
+ },
134
+ // POST /api/tabs — create
135
+ {
136
+ method: 'POST',
137
+ test: exactPath('/api/tabs'),
138
+ handler: async ({ req, res }) => {
139
+ const body = await readBody(req, res);
140
+ if (body === null)
141
+ return;
142
+ let parsed;
143
+ try {
144
+ parsed = JSON.parse(body);
145
+ }
146
+ catch {
147
+ json(res, { error: 'Invalid JSON' }, 400);
148
+ return;
149
+ }
150
+ if (!parsed.name) {
151
+ json(res, { error: 'Missing tab name' }, 400);
152
+ return;
153
+ }
154
+ const err = validateTabName(parsed.name);
155
+ if (err) {
156
+ json(res, { error: err }, 400);
157
+ return;
158
+ }
159
+ if (parsed.workingDir && !isAllowedWorkingDir(parsed.workingDir)) {
160
+ json(res, {
161
+ error: 'workingDir must be under the workspace root, a project scan path, or your home directory',
162
+ }, 400);
163
+ return;
164
+ }
165
+ try {
166
+ createTabRecord(getDb(), {
167
+ name: parsed.name,
168
+ workingDir: parsed.workingDir,
169
+ systemPrompt: parsed.systemPrompt,
170
+ });
171
+ json(res, { success: true, name: parsed.name });
172
+ }
173
+ catch (e) {
174
+ json(res, { error: e instanceof Error ? e.message : String(e) }, 400);
175
+ }
176
+ },
177
+ },
178
+ // DELETE /api/tabs/:name
179
+ {
180
+ method: 'DELETE',
181
+ test: regexPath(/^\/api\/tabs\/[^/]+$/),
182
+ handler: ({ res, path }) => {
183
+ const tabName = decodeURIComponent(path.split('/')[3]);
184
+ TabStore.deleteWithMessages(tabName);
185
+ json(res, { success: true });
186
+ },
187
+ },
188
+ // POST /api/tasks or /api/crons
189
+ {
190
+ method: 'POST',
191
+ test: (path) => path === '/api/tasks' || path === '/api/crons',
192
+ handler: async ({ req, res }) => {
193
+ const body = await readBody(req, res);
194
+ if (body === null)
195
+ return;
196
+ let parsed;
197
+ try {
198
+ parsed = JSON.parse(body);
199
+ }
200
+ catch {
201
+ json(res, { error: 'Invalid JSON' }, 400);
202
+ return;
203
+ }
204
+ if (!parsed.name || !parsed.schedule || !parsed.message) {
205
+ json(res, { error: 'Missing required fields' }, 400);
206
+ return;
207
+ }
208
+ const effectiveTab = parsed.tabName || 'default';
209
+ const tabErr = validateTabNameOrDefault(effectiveTab);
210
+ if (tabErr) {
211
+ json(res, { error: tabErr }, 400);
212
+ return;
213
+ }
214
+ const scheduleType = parsed.scheduleType || 'every';
215
+ const { validateSchedule } = await import('../tasks/scheduler.js');
216
+ const scheduleErr = validateSchedule(scheduleType, parsed.schedule);
217
+ if (scheduleErr) {
218
+ json(res, { error: scheduleErr }, 400);
219
+ return;
220
+ }
221
+ const id = crypto.randomUUID();
222
+ const { TaskStore } = await import('../tasks/store.js');
223
+ new TaskStore().add({
224
+ id,
225
+ name: parsed.name,
226
+ scheduleType: scheduleType,
227
+ schedule: parsed.schedule,
228
+ tabName: effectiveTab,
229
+ message: parsed.message,
230
+ payloadType: 'agentTurn',
231
+ enabled: true,
232
+ createdAt: new Date().toISOString(),
233
+ lastRunAt: null,
234
+ nextRunAt: null,
235
+ });
236
+ json(res, { success: true, id });
237
+ },
238
+ },
239
+ // DELETE /api/tasks/:id or /api/crons/:id
240
+ {
241
+ method: 'DELETE',
242
+ test: (path) => /^\/api\/(tasks|crons)\/[^/]+$/.test(path),
243
+ handler: async ({ res, path }) => {
244
+ const taskId = decodeURIComponent(path.split('/')[3]);
245
+ const { TaskStore } = await import('../tasks/store.js');
246
+ new TaskStore().delete(taskId);
247
+ json(res, { success: true });
248
+ },
249
+ },
250
+ // GET /api/watchers
251
+ {
252
+ method: 'GET',
253
+ test: exactPath('/api/watchers'),
254
+ handler: async ({ res }) => {
255
+ const { WatcherStore } = await import('../watchers/store.js');
256
+ json(res, new WatcherStore().list());
257
+ },
258
+ },
259
+ // DELETE /api/watchers/:id
260
+ {
261
+ method: 'DELETE',
262
+ test: regexPath(/^\/api\/watchers\/[^/]+$/),
263
+ handler: async ({ res, path }) => {
264
+ const id = decodeURIComponent(path.split('/')[3]);
265
+ const { WatcherStore } = await import('../watchers/store.js');
266
+ new WatcherStore().delete(id);
267
+ json(res, { success: true });
268
+ },
269
+ },
270
+ // POST /api/memories
271
+ {
272
+ method: 'POST',
273
+ test: exactPath('/api/memories'),
274
+ handler: async ({ req, res }) => {
275
+ const body = await readBody(req, res);
276
+ if (body === null)
277
+ return;
278
+ let parsed;
279
+ try {
280
+ parsed = JSON.parse(body);
281
+ }
282
+ catch {
283
+ json(res, { error: 'Invalid JSON' }, 400);
284
+ return;
285
+ }
286
+ if (!parsed.content) {
287
+ json(res, { error: 'Missing content' }, 400);
288
+ return;
289
+ }
290
+ const { MemoryStore } = await import('../session/memory-store.js');
291
+ MemoryStore.add(parsed.content, { tabName: parsed.tabName });
292
+ json(res, { success: true });
293
+ },
294
+ },
295
+ // DELETE /api/memories/:id
296
+ {
297
+ method: 'DELETE',
298
+ test: regexPath(/^\/api\/memories\/\d+$/),
299
+ handler: async ({ res, path }) => {
300
+ const id = path.split('/')[3];
301
+ const { MemoryStore } = await import('../session/memory-store.js');
302
+ MemoryStore.delete(id);
303
+ json(res, { success: true });
304
+ },
305
+ },
306
+ // GET /api/media/config
307
+ {
308
+ method: 'GET',
309
+ test: exactPath('/api/media/config'),
310
+ handler: async ({ res }) => {
311
+ const { getConfig } = await import('../config.js');
312
+ const generators = getConfig().mediaGenerators || [];
313
+ json(res, {
314
+ generators: generators.map((g) => ({
315
+ provider: g.provider,
316
+ model: g.model,
317
+ configured: !!g.apiKey,
318
+ })),
319
+ });
320
+ },
321
+ },
322
+ // GET /api/channels/config
323
+ {
324
+ method: 'GET',
325
+ test: exactPath('/api/channels/config'),
326
+ handler: async ({ res }) => {
327
+ const { getConfig } = await import('../config.js');
328
+ const config = getConfig();
329
+ json(res, {
330
+ telegram: { configured: !!config.telegram?.token, botUsername: null },
331
+ discord: { configured: !!config.discord?.token },
332
+ whatsapp: { configured: !!config.whatsapp?.enabled },
333
+ webhook: { configured: !!config.webhook?.enabled, port: config.webhook?.port },
334
+ });
335
+ },
336
+ },
337
+ // POST /api/computer-use
338
+ {
339
+ method: 'POST',
340
+ test: exactPath('/api/computer-use'),
341
+ handler: async ({ req, res }) => {
342
+ const body = await readBody(req, res);
343
+ if (body === null)
344
+ return;
345
+ let parsed;
346
+ try {
347
+ parsed = JSON.parse(body);
348
+ }
349
+ catch {
350
+ json(res, { error: 'Invalid JSON' }, 400);
351
+ return;
352
+ }
353
+ const { getConfig, saveConfig } = await import('../config.js');
354
+ const config = getConfig();
355
+ config.claudeCode.computerUse = !!parsed.enabled;
356
+ saveConfig(config);
357
+ json(res, { enabled: !!parsed.enabled, message: 'Restart daemon to apply.' });
358
+ },
359
+ },
360
+ // GET /api/computer-use
361
+ {
362
+ method: 'GET',
363
+ test: exactPath('/api/computer-use'),
364
+ handler: async ({ res }) => {
365
+ const { getConfig } = await import('../config.js');
366
+ json(res, { enabled: !!getConfig().claudeCode.computerUse });
367
+ },
368
+ },
369
+ // GET /api/timeline
370
+ {
371
+ method: 'GET',
372
+ test: exactPath('/api/timeline'),
373
+ handler: async ({ res, url }) => {
374
+ const { getTimeline } = await import('../timeline/index.js');
375
+ const date = url.searchParams.get('date') || new Date().toISOString().slice(0, 10);
376
+ const limit = parseIntParam(url.searchParams.get('limit'), 50, 200);
377
+ json(res, { events: getTimeline({ date, limit }) });
378
+ },
379
+ },
380
+ // GET /api/status
381
+ {
382
+ method: 'GET',
383
+ test: exactPath('/api/status'),
384
+ handler: ({ res }) => {
385
+ const db = getDb();
386
+ const activeTasks = db.prepare('SELECT COUNT(*) as c FROM tasks WHERE enabled = 1').get().c;
387
+ json(res, {
388
+ version: VERSION,
389
+ daemonPid: getDaemonPid(),
390
+ tabs: TabStore.countAll(),
391
+ activeTabs: TabStore.countRunning(),
392
+ tasks: activeTasks,
393
+ // Legacy alias — HTML reads either key; can be dropped after old dashboards have refreshed.
394
+ cronJobs: activeTasks,
395
+ memories: db.prepare('SELECT COUNT(*) as c FROM memories').get().c,
396
+ });
397
+ },
398
+ },
399
+ // GET /api/tabs
400
+ {
401
+ method: 'GET',
402
+ test: exactPath('/api/tabs'),
403
+ handler: ({ res }) => {
404
+ // Explicit column list — do NOT include session_id or system_prompt.
405
+ // session_id is the credential used by `claude --resume`; leaking it via
406
+ // /api/tabs (which any holder of the dashboard token can hit) would let
407
+ // an attacker resume any tab's claude session locally.
408
+ const tabs = getDb()
409
+ .prepare(`
410
+ SELECT t.id, t.name, t.status, t.working_dir, t.last_activity_at, t.created_at, t.pid,
411
+ (SELECT COUNT(*) FROM messages WHERE tab_id = t.id) as message_count,
412
+ (SELECT COALESCE(SUM(cost_usd), 0) FROM messages WHERE tab_id = t.id) as total_cost
413
+ FROM tabs t ORDER BY t.last_activity_at DESC
414
+ `)
415
+ .all();
416
+ json(res, tabs);
417
+ },
418
+ },
419
+ // GET /api/tabs/:name/messages
420
+ {
421
+ method: 'GET',
422
+ test: regexPath(/^\/api\/tabs\/[^/]+\/messages$/),
423
+ handler: ({ res, url, path }) => {
424
+ const tabName = decodeURIComponent(path.split('/')[3]);
425
+ const limit = parseIntParam(url.searchParams.get('limit'), 50, 200);
426
+ const offset = parseIntParam(url.searchParams.get('offset'), 0, 100000);
427
+ const tabId = TabStore.getIdByName(tabName);
428
+ if (!tabId) {
429
+ json(res, { error: 'Tab not found' }, 404);
430
+ return;
431
+ }
432
+ const db = getDb();
433
+ const messages = db
434
+ .prepare('SELECT role, content, cost_usd, tokens_in, tokens_out, created_at FROM messages WHERE tab_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?')
435
+ .all(tabId, limit, offset);
436
+ const total = db.prepare('SELECT COUNT(*) as c FROM messages WHERE tab_id = ?').get(tabId).c;
437
+ json(res, { messages: messages.reverse(), total, limit, offset });
438
+ },
439
+ },
440
+ // GET /api/memories
441
+ {
442
+ method: 'GET',
443
+ test: exactPath('/api/memories'),
444
+ handler: async ({ res, url }) => {
445
+ const limit = parseIntParam(url.searchParams.get('limit'), 50, 200);
446
+ const offset = parseIntParam(url.searchParams.get('offset'), 0, 100000);
447
+ const q = url.searchParams.get('q') || '';
448
+ const { MemoryStore } = await import('../session/memory-store.js');
449
+ const { memories: rows, total } = MemoryStore.list({ limit, offset, query: q || undefined });
450
+ // Dashboard HTML reads snake_case (tab_name, created_at) — map back here
451
+ // rather than reshape the store's typed return.
452
+ const memories = rows.map((m) => ({
453
+ id: m.id,
454
+ content: m.content,
455
+ tab_name: m.tabName,
456
+ source: m.source,
457
+ created_at: m.createdAt,
458
+ }));
459
+ json(res, { memories, total, limit, offset });
460
+ },
461
+ },
462
+ // GET /api/tasks or /api/crons
463
+ {
464
+ method: 'GET',
465
+ test: (path) => path === '/api/tasks' || path === '/api/crons',
466
+ handler: ({ res, url }) => {
467
+ const limit = parseIntParam(url.searchParams.get('limit'), 100, 500);
468
+ const tasks = getDb().prepare('SELECT * FROM tasks ORDER BY created_at LIMIT ?').all(limit);
469
+ json(res, tasks);
470
+ },
471
+ },
472
+ // GET /api/costs
473
+ {
474
+ method: 'GET',
475
+ test: exactPath('/api/costs'),
476
+ handler: ({ res }) => {
477
+ const costs = getDb()
478
+ .prepare(`
479
+ SELECT date(created_at) as day,
480
+ SUM(cost_usd) as total_cost,
481
+ COUNT(*) as message_count
482
+ FROM messages
483
+ WHERE role = 'assistant' AND cost_usd > 0
484
+ AND created_at > datetime('now', '-30 days')
485
+ GROUP BY date(created_at)
486
+ ORDER BY day
487
+ `)
488
+ .all();
489
+ json(res, costs);
490
+ },
491
+ },
492
+ // GET /api/update/status
493
+ {
494
+ method: 'GET',
495
+ test: exactPath('/api/update/status'),
496
+ handler: async ({ res }) => {
497
+ async function npmViewLatest(name) {
498
+ try {
499
+ const { stdout } = await execFileAsync('npm', ['view', name, 'version'], {
500
+ timeout: 10000,
501
+ });
502
+ return stdout.trim();
503
+ }
504
+ catch {
505
+ return null;
506
+ }
507
+ }
508
+ const packages = await Promise.all([
509
+ (async () => {
510
+ const p = {
511
+ name: 'beecork',
512
+ installed: VERSION,
513
+ latest: await npmViewLatest('beecork'),
514
+ };
515
+ p.updateAvailable = !!(p.latest && p.installed !== p.latest);
516
+ return p;
517
+ })(),
518
+ (async () => {
519
+ const p = { name: '@anthropic-ai/claude-code' };
520
+ try {
521
+ const { stdout } = await execFileAsync('claude', ['--version'], { timeout: 10000 });
522
+ p.installed = stdout.trim().replace(/^.*?(\d+\.\d+\.\d+).*$/, '$1');
523
+ }
524
+ catch {
525
+ p.installed = null;
526
+ }
527
+ p.latest = await npmViewLatest('@anthropic-ai/claude-code');
528
+ p.updateAvailable = !!(p.installed && p.latest && p.installed !== p.latest);
529
+ return p;
530
+ })(),
531
+ ]);
532
+ json(res, { packages });
533
+ },
534
+ },
535
+ // POST /api/update/:pkg
536
+ {
537
+ method: 'POST',
538
+ test: regexPath(/^\/api\/update\/[^/]+$/),
539
+ handler: async ({ res, path }) => {
540
+ const pkgName = decodeURIComponent(path.split('/')[3]);
541
+ const allowedPackages = new Set(['beecork', '@anthropic-ai/claude-code']);
542
+ if (!allowedPackages.has(pkgName)) {
543
+ json(res, { error: `Package "${pkgName}" is not in the allowed update list.` }, 400);
544
+ return;
545
+ }
546
+ // Defense-in-depth: even though pkgName is allowlisted, validate against the
547
+ // same regex used elsewhere so a typo in the allowlist can't widen the surface.
548
+ if (!SAFE_NPM_PACKAGE.test(pkgName)) {
549
+ json(res, { error: `Invalid package name: ${pkgName}` }, 400);
550
+ return;
551
+ }
552
+ try {
553
+ const { stdout } = await execFileAsync('npm', ['install', '-g', `${pkgName}@latest`], {
554
+ timeout: 120000,
555
+ });
556
+ json(res, { success: true, package: pkgName, output: stdout.trim() });
557
+ }
558
+ catch (err) {
559
+ json(res, { error: err instanceof Error ? err.message : String(err) }, 500);
560
+ }
561
+ },
562
+ },
563
+ // GET /api/capabilities
564
+ {
565
+ method: 'GET',
566
+ test: exactPath('/api/capabilities'),
567
+ handler: async ({ res }) => {
568
+ const { getAvailablePacks, isEnabled } = await import('../capabilities/index.js');
569
+ const packs = getAvailablePacks().map((p) => ({
570
+ ...p,
571
+ enabled: isEnabled(p.id),
572
+ mcpServer: { package: p.mcpServer.package },
573
+ }));
574
+ json(res, { packs });
575
+ },
576
+ },
577
+ // POST /api/capabilities/:id/enable
578
+ {
579
+ method: 'POST',
580
+ test: regexPath(/^\/api\/capabilities\/[^/]+\/enable$/),
581
+ handler: async ({ req, res, path }) => {
582
+ const packId = path.split('/')[3];
583
+ const body = await readBody(req, res);
584
+ if (body === null)
585
+ return;
586
+ let parsed;
587
+ try {
588
+ parsed = JSON.parse(body);
589
+ }
590
+ catch {
591
+ json(res, { error: 'Invalid JSON' }, 400);
592
+ return;
593
+ }
594
+ const { enablePack } = await import('../capabilities/index.js');
595
+ try {
596
+ enablePack(packId, parsed.apiKey);
597
+ json(res, { success: true, message: 'Restart daemon to activate.' });
598
+ }
599
+ catch (err) {
600
+ json(res, { error: err instanceof Error ? err.message : String(err) }, 400);
601
+ }
602
+ },
603
+ },
604
+ // POST /api/capabilities/:id/disable
605
+ {
606
+ method: 'POST',
607
+ test: regexPath(/^\/api\/capabilities\/[^/]+\/disable$/),
608
+ handler: async ({ res, path }) => {
609
+ const packId = path.split('/')[3];
610
+ const { disablePack } = await import('../capabilities/index.js');
611
+ disablePack(packId);
612
+ json(res, { success: true });
613
+ },
614
+ },
615
+ ];
616
+ export function dispatch(method, path) {
617
+ for (const r of ROUTES) {
618
+ if (r.method === method && r.test(path))
619
+ return r;
620
+ }
621
+ return null;
622
+ }
623
+ export { json };