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
@@ -1,52 +1,40 @@
1
1
  import http from 'node:http';
2
2
  import crypto from 'node:crypto';
3
- import { execFile, exec } from 'node:child_process';
4
- import { promisify } from 'node:util';
3
+ import { execFile } from 'node:child_process';
5
4
  import { platform } from 'node:os';
6
- import Database from 'better-sqlite3';
7
- import { getDbPath } from '../util/paths.js';
8
5
  import { getDashboardHtml } from './html.js';
9
- import { VERSION } from '../version.js';
10
- import { getDaemonPid } from '../cli/helpers.js';
11
- import { validateTabName } from '../config.js';
12
- import { createTabRecord } from '../db/index.js';
13
- let cachedDashDb = null;
14
- function getDashDb() {
15
- if (!cachedDashDb) {
16
- cachedDashDb = new Database(getDbPath(), { readonly: true });
17
- cachedDashDb.pragma('journal_mode = WAL');
6
+ import { logger } from '../util/logger.js';
7
+ import { dispatch, json } from './routes.js';
8
+ function safeEqualToken(provided, expected) {
9
+ if (!provided)
10
+ return false;
11
+ const a = Buffer.from(provided);
12
+ const b = Buffer.from(expected);
13
+ if (a.length !== b.length)
14
+ return false;
15
+ try {
16
+ return crypto.timingSafeEqual(a, b);
18
17
  }
19
- return cachedDashDb;
20
- }
21
- let cachedWriteDb = null;
22
- function getWriteDb() {
23
- if (!cachedWriteDb) {
24
- cachedWriteDb = new Database(getDbPath());
25
- cachedWriteDb.pragma('journal_mode = WAL');
18
+ catch {
19
+ return false;
26
20
  }
27
- return cachedWriteDb;
28
- }
29
- function withWriteDb(fn) {
30
- return fn(getWriteDb());
31
- }
32
- function json(res, data, status = 200) {
33
- res.writeHead(status, { 'Content-Type': 'application/json' });
34
- res.end(JSON.stringify(data));
35
21
  }
36
22
  function openBrowser(url) {
37
23
  const p = platform();
38
- if (p === 'darwin') {
24
+ if (p === 'darwin')
39
25
  execFile('open', [url]);
40
- }
41
- else if (p === 'win32') {
26
+ else if (p === 'win32')
42
27
  execFile('cmd', ['/c', 'start', url]);
43
- }
44
- else {
28
+ else
45
29
  execFile('xdg-open', [url]);
46
- }
47
30
  }
31
+ const SECURITY_HEADERS = {
32
+ 'X-Frame-Options': 'DENY',
33
+ 'X-Content-Type-Options': 'nosniff',
34
+ 'Referrer-Policy': 'no-referrer',
35
+ };
48
36
  export function startDashboardServer(port = 0) {
49
- // Generate auth token at server start
37
+ // Generate auth token at server start (24 random bytes → 192-bit base64url).
50
38
  const authToken = crypto.randomBytes(24).toString('base64url');
51
39
  const server = http.createServer(async (req, res) => {
52
40
  const url = new URL(req.url || '/', `http://localhost`);
@@ -55,487 +43,48 @@ export function startDashboardServer(port = 0) {
55
43
  if (path === '/' || path === '/index.html') {
56
44
  const token = url.searchParams.get('token');
57
45
  if (!token) {
58
- // Redirect to add token
59
46
  res.writeHead(302, { Location: `/?token=${authToken}` });
60
47
  res.end();
61
48
  return;
62
49
  }
63
- if (token !== authToken) {
64
- res.writeHead(403, { 'Content-Type': 'text/plain' });
50
+ if (!safeEqualToken(token, authToken)) {
51
+ res.writeHead(403, { 'Content-Type': 'text/plain', ...SECURITY_HEADERS });
65
52
  res.end('Forbidden');
66
53
  return;
67
54
  }
68
55
  res.writeHead(200, {
69
56
  'Content-Type': 'text/html',
70
- 'Referrer-Policy': 'no-referrer',
57
+ ...SECURITY_HEADERS,
71
58
  'Set-Cookie': `beecork_dash=${authToken}; HttpOnly; SameSite=Strict; Path=/`,
72
59
  });
73
60
  res.end(getDashboardHtml(authToken));
74
61
  return;
75
62
  }
76
- // Auth check for API routes
63
+ // Auth check for API routes (constant-time compare)
77
64
  if (path.startsWith('/api/')) {
78
65
  const authHeader = req.headers.authorization;
79
66
  const queryToken = url.searchParams.get('token');
80
- const cookieToken = req.headers.cookie?.split(';').map(c => c.trim()).find(c => c.startsWith('beecork_dash='))?.split('=')[1];
67
+ const cookieToken = req.headers.cookie
68
+ ?.split(';')
69
+ .map((c) => c.trim())
70
+ .find((c) => c.startsWith('beecork_dash='))
71
+ ?.split('=')[1];
81
72
  const providedToken = authHeader?.replace('Bearer ', '') || queryToken || cookieToken;
82
- if (providedToken !== authToken) {
73
+ if (!safeEqualToken(providedToken, authToken)) {
83
74
  json(res, { error: 'Unauthorized' }, 401);
84
75
  return;
85
76
  }
86
77
  }
87
- // API routes
88
78
  try {
89
- // SSE endpoint for real-time updates
90
- if (path === '/api/events') {
91
- res.writeHead(200, {
92
- 'Content-Type': 'text/event-stream',
93
- 'Cache-Control': 'no-cache',
94
- 'Connection': 'keep-alive',
95
- });
96
- res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
97
- const interval = setInterval(() => {
98
- try {
99
- const db = getDashDb();
100
- const tabs = db.prepare('SELECT name, status, last_activity_at FROM tabs ORDER BY last_activity_at DESC').all();
101
- const activeCount = tabs.filter((t) => t.status === 'running').length;
102
- res.write(`data: ${JSON.stringify({ type: 'update', tabs, activeTabs: activeCount })}\n\n`);
103
- }
104
- catch { }
105
- }, 2000);
106
- req.on('close', () => {
107
- clearInterval(interval);
108
- });
109
- return;
110
- }
111
- // POST: Send message to tab
112
- if (path.match(/^\/api\/tabs\/[^/]+\/send$/) && req.method === 'POST') {
113
- let body = '';
114
- for await (const chunk of req) {
115
- body += chunk;
116
- if (body.length > 1_000_000) {
117
- json(res, { error: 'Payload too large' }, 413);
118
- return;
119
- }
120
- }
121
- let parsed;
122
- try {
123
- parsed = JSON.parse(body);
124
- }
125
- catch {
126
- json(res, { error: 'Invalid JSON' }, 400);
127
- return;
128
- }
129
- const { message } = parsed;
130
- if (!message) {
131
- json(res, { error: 'Missing message' }, 400);
132
- return;
133
- }
134
- const tabName = decodeURIComponent(path.split('/')[3]);
135
- const tabErr = validateTabName(tabName);
136
- if (tabErr) {
137
- json(res, { error: tabErr }, 400);
138
- return;
139
- }
140
- withWriteDb(db => db.prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run(tabName, message, 'user'));
141
- json(res, { success: true, tab: tabName });
142
- return;
143
- }
144
- // POST: Create tab
145
- if (path === '/api/tabs' && req.method === 'POST') {
146
- let body = '';
147
- for await (const chunk of req) {
148
- body += chunk;
149
- if (body.length > 1_000_000) {
150
- json(res, { error: 'Payload too large' }, 413);
151
- return;
152
- }
153
- }
154
- let parsedTab;
155
- try {
156
- parsedTab = JSON.parse(body);
157
- }
158
- catch {
159
- json(res, { error: 'Invalid JSON' }, 400);
160
- return;
161
- }
162
- const { name, workingDir, systemPrompt } = parsedTab;
163
- if (!name) {
164
- json(res, { error: 'Missing tab name' }, 400);
165
- return;
166
- }
167
- const nameErr = validateTabName(name);
168
- if (nameErr) {
169
- json(res, { error: nameErr }, 400);
170
- return;
171
- }
172
- withWriteDb(db => createTabRecord(db, { name, workingDir, systemPrompt }));
173
- json(res, { success: true, name });
174
- return;
175
- }
176
- // DELETE: Delete tab
177
- if (path.match(/^\/api\/tabs\/[^/]+$/) && req.method === 'DELETE') {
178
- const tabName = decodeURIComponent(path.split('/')[3]);
179
- withWriteDb(db => {
180
- const tab = db.prepare('SELECT id FROM tabs WHERE name = ?').get(tabName);
181
- if (tab) {
182
- db.prepare('DELETE FROM messages WHERE tab_id = ?').run(tab.id);
183
- db.prepare('DELETE FROM tabs WHERE id = ?').run(tab.id);
184
- }
185
- });
186
- json(res, { success: true });
187
- return;
188
- }
189
- // POST: Create task (supports both /api/tasks and /api/crons)
190
- if ((path === '/api/tasks' || path === '/api/crons') && req.method === 'POST') {
191
- let body = '';
192
- for await (const chunk of req) {
193
- body += chunk;
194
- if (body.length > 1_000_000) {
195
- json(res, { error: 'Payload too large' }, 413);
196
- return;
197
- }
198
- }
199
- let parsedCron;
200
- try {
201
- parsedCron = JSON.parse(body);
202
- }
203
- catch {
204
- json(res, { error: 'Invalid JSON' }, 400);
205
- return;
206
- }
207
- const { name, scheduleType, schedule, tabName, message } = parsedCron;
208
- if (!name || !schedule || !message) {
209
- json(res, { error: 'Missing required fields' }, 400);
210
- return;
211
- }
212
- const effectiveTab = tabName || 'default';
213
- const cronTabErr = validateTabName(effectiveTab);
214
- if (cronTabErr) {
215
- json(res, { error: cronTabErr }, 400);
216
- return;
217
- }
218
- const id = crypto.randomUUID();
219
- withWriteDb(db => db.prepare('INSERT INTO tasks (id, name, schedule_type, schedule, tab_name, message, enabled) VALUES (?, ?, ?, ?, ?, ?, 1)').run(id, name, scheduleType || 'every', schedule, effectiveTab, message));
220
- json(res, { success: true, id });
221
- return;
222
- }
223
- // DELETE: Delete task (supports both /api/tasks and /api/crons)
224
- if ((path.match(/^\/api\/tasks\/[^/]+$/) || path.match(/^\/api\/crons\/[^/]+$/)) && req.method === 'DELETE') {
225
- const taskId = decodeURIComponent(path.split('/')[3]);
226
- withWriteDb(db => db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId));
227
- json(res, { success: true });
228
- return;
229
- }
230
- // GET: List watchers
231
- if (path === '/api/watchers' && req.method === 'GET') {
232
- const watchers = getDashDb().prepare('SELECT * FROM watchers ORDER BY created_at').all();
233
- json(res, watchers);
234
- return;
235
- }
236
- // DELETE: Delete watcher
237
- if (path.match(/^\/api\/watchers\/[^/]+$/) && req.method === 'DELETE') {
238
- const watcherId = decodeURIComponent(path.split('/')[3]);
239
- withWriteDb(db => db.prepare('DELETE FROM watchers WHERE id = ?').run(watcherId));
240
- json(res, { success: true });
241
- return;
242
- }
243
- // POST: Create memory
244
- if (path === '/api/memories' && req.method === 'POST') {
245
- let body = '';
246
- for await (const chunk of req) {
247
- body += chunk;
248
- if (body.length > 1_000_000) {
249
- json(res, { error: 'Payload too large' }, 413);
250
- return;
251
- }
252
- }
253
- let parsedMemory;
254
- try {
255
- parsedMemory = JSON.parse(body);
256
- }
257
- catch {
258
- json(res, { error: 'Invalid JSON' }, 400);
259
- return;
260
- }
261
- const { content, tabName } = parsedMemory;
262
- if (!content) {
263
- json(res, { error: 'Missing content' }, 400);
264
- return;
265
- }
266
- withWriteDb(db => db.prepare('INSERT INTO memories (content, tab_name, source) VALUES (?, ?, ?)').run(content, tabName || null, 'tool'));
267
- json(res, { success: true });
268
- return;
269
- }
270
- // DELETE: Delete memory
271
- if (path.match(/^\/api\/memories\/\d+$/) && req.method === 'DELETE') {
272
- const memoryId = path.split('/')[3];
273
- withWriteDb(db => db.prepare('DELETE FROM memories WHERE id = ?').run(memoryId));
274
- json(res, { success: true });
275
- return;
276
- }
277
- if (path === '/api/media/config') {
278
- const { getConfig } = await import('../config.js');
279
- const config = getConfig();
280
- const generators = config.mediaGenerators || [];
281
- const info = generators.map((g) => ({
282
- provider: g.provider,
283
- model: g.model,
284
- configured: !!g.apiKey,
285
- }));
286
- json(res, { generators: info });
287
- return;
288
- }
289
- if (path === '/api/channels/config') {
290
- const { getConfig } = await import('../config.js');
291
- const config = getConfig();
292
- const channels = {
293
- telegram: { configured: !!config.telegram?.token, botUsername: null },
294
- discord: { configured: !!config.discord?.token },
295
- whatsapp: { configured: !!config.whatsapp?.enabled },
296
- webhook: { configured: !!config.webhook?.enabled, port: config.webhook?.port },
297
- };
298
- json(res, channels);
299
- return;
300
- }
301
- if (path === '/api/computer-use' && req.method === 'POST') {
302
- let body = '';
303
- for await (const chunk of req) {
304
- body += chunk;
305
- if (body.length > 1_000_000) {
306
- json(res, { error: 'Payload too large' }, 413);
307
- return;
308
- }
309
- }
310
- let parsedCU;
311
- try {
312
- parsedCU = JSON.parse(body);
313
- }
314
- catch {
315
- json(res, { error: 'Invalid JSON' }, 400);
316
- return;
317
- }
318
- const { enabled } = parsedCU;
319
- const { getConfig, saveConfig } = await import('../config.js');
320
- const config = getConfig();
321
- config.claudeCode.computerUse = !!enabled;
322
- saveConfig(config);
323
- json(res, { enabled: !!enabled, message: 'Restart daemon to apply.' });
324
- return;
325
- }
326
- if (path === '/api/computer-use') {
327
- const { getConfig } = await import('../config.js');
328
- const config = getConfig();
329
- json(res, { enabled: !!config.claudeCode.computerUse });
330
- return;
331
- }
332
- if (path === '/api/timeline') {
333
- const { getTimeline } = await import('../timeline/index.js');
334
- const date = url.searchParams.get('date') || new Date().toISOString().slice(0, 10);
335
- const limit = parseInt(url.searchParams.get('limit') || '50');
336
- const events = getTimeline({ date, limit });
337
- json(res, { events });
338
- return;
339
- }
340
- const db = getDashDb();
341
- if (path === '/api/status') {
342
- const pid = getDaemonPid();
343
- const tabCount = db.prepare('SELECT COUNT(*) as c FROM tabs').get().c;
344
- const activeCount = db.prepare("SELECT COUNT(*) as c FROM tabs WHERE status = 'running'").get().c;
345
- const cronCount = db.prepare("SELECT COUNT(*) as c FROM tasks WHERE enabled = 1").get().c;
346
- const memoryCount = db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
347
- json(res, { version: VERSION, daemonPid: pid, tabs: tabCount, activeTabs: activeCount, cronJobs: cronCount, memories: memoryCount });
348
- return;
349
- }
350
- if (path === '/api/tabs' && req.method === 'GET') {
351
- const tabs = db.prepare(`
352
- SELECT t.*,
353
- (SELECT COUNT(*) FROM messages WHERE tab_id = t.id) as message_count,
354
- (SELECT COALESCE(SUM(cost_usd), 0) FROM messages WHERE tab_id = t.id) as total_cost
355
- FROM tabs t ORDER BY t.last_activity_at DESC
356
- `).all();
357
- json(res, tabs);
358
- return;
359
- }
360
- const tabMsgMatch = path.match(/^\/api\/tabs\/([^/]+)\/messages$/);
361
- if (tabMsgMatch) {
362
- const tabName = decodeURIComponent(tabMsgMatch[1]);
363
- const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 200);
364
- const offset = parseInt(url.searchParams.get('offset') || '0');
365
- const tab = db.prepare('SELECT id FROM tabs WHERE name = ?').get(tabName);
366
- if (!tab) {
367
- json(res, { error: 'Tab not found' }, 404);
368
- return;
369
- }
370
- 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(tab.id, limit, offset);
371
- const total = db.prepare('SELECT COUNT(*) as c FROM messages WHERE tab_id = ?').get(tab.id).c;
372
- json(res, { messages: messages.reverse(), total, limit, offset });
373
- return;
374
- }
375
- if (path === '/api/memories' && req.method === 'GET') {
376
- const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 200);
377
- const offset = parseInt(url.searchParams.get('offset') || '0');
378
- const q = url.searchParams.get('q') || '';
379
- let memories, total;
380
- if (q) {
381
- 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);
382
- total = db.prepare('SELECT COUNT(*) as c FROM memories WHERE content LIKE ?').get(`%${q}%`).c;
383
- }
384
- else {
385
- memories = db.prepare('SELECT id, content, tab_name, source, created_at FROM memories ORDER BY created_at DESC LIMIT ? OFFSET ?').all(limit, offset);
386
- total = db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
387
- }
388
- json(res, { memories, total, limit, offset });
389
- return;
390
- }
391
- if ((path === '/api/tasks' || path === '/api/crons') && req.method === 'GET') {
392
- const crons = db.prepare('SELECT * FROM tasks ORDER BY created_at').all();
393
- json(res, crons);
394
- return;
395
- }
396
- if (path === '/api/costs') {
397
- const costs = db.prepare(`
398
- SELECT date(created_at) as day,
399
- SUM(cost_usd) as total_cost,
400
- COUNT(*) as message_count
401
- FROM messages
402
- WHERE role = 'assistant' AND cost_usd > 0
403
- AND created_at > datetime('now', '-30 days')
404
- GROUP BY date(created_at)
405
- ORDER BY day
406
- `).all();
407
- json(res, costs);
408
- return;
409
- }
410
- // GET /api/update/status — check versions for beecork, claude code, and other packages
411
- if (path === '/api/update/status') {
412
- const execAsync = promisify(exec);
413
- async function checkPackage(name, installCmd) {
414
- const pkg = { name };
415
- try {
416
- const { stdout } = await execAsync(`${installCmd || name} --version`, { timeout: 10000 });
417
- pkg.installed = stdout.trim().replace(/^v/, '');
418
- }
419
- catch {
420
- pkg.installed = null;
421
- }
422
- try {
423
- const { stdout } = await execAsync(`npm view ${name} version`, { timeout: 10000 });
424
- pkg.latest = stdout.trim();
425
- }
426
- catch {
427
- pkg.latest = null;
428
- }
429
- pkg.updateAvailable = !!(pkg.installed && pkg.latest && pkg.installed !== pkg.latest);
430
- return pkg;
431
- }
432
- const packages = await Promise.all([
433
- (async () => {
434
- const p = await checkPackage('beecork');
435
- p.installed = VERSION; // use our known version, more reliable
436
- p.updateAvailable = !!(p.latest && p.installed !== p.latest);
437
- return p;
438
- })(),
439
- (async () => {
440
- const p = { name: '@anthropic-ai/claude-code' };
441
- try {
442
- const { stdout } = await execAsync('claude --version', { timeout: 10000 });
443
- p.installed = stdout.trim().replace(/^.*?(\d+\.\d+\.\d+).*$/, '$1');
444
- }
445
- catch {
446
- p.installed = null;
447
- }
448
- try {
449
- const { stdout } = await execAsync('npm view @anthropic-ai/claude-code version', { timeout: 10000 });
450
- p.latest = stdout.trim();
451
- }
452
- catch {
453
- p.latest = null;
454
- }
455
- p.updateAvailable = !!(p.installed && p.latest && p.installed !== p.latest);
456
- return p;
457
- })(),
458
- ]);
459
- json(res, { packages });
460
- return;
461
- }
462
- // POST /api/update/:package — update a specific package
463
- if (path.match(/^\/api\/update\/[^/]+$/) && req.method === 'POST') {
464
- const pkgName = decodeURIComponent(path.split('/')[3]);
465
- const execAsync = promisify(exec);
466
- const allowedPackages = {
467
- 'beecork': 'npm install -g beecork@latest',
468
- '@anthropic-ai/claude-code': 'npm install -g @anthropic-ai/claude-code@latest',
469
- };
470
- const cmd = allowedPackages[pkgName];
471
- if (!cmd) {
472
- json(res, { error: `Package "${pkgName}" is not in the allowed update list.` }, 400);
473
- return;
474
- }
475
- try {
476
- const { stdout } = await execAsync(cmd, { timeout: 120000 });
477
- json(res, { success: true, package: pkgName, output: stdout.trim() });
478
- }
479
- catch (err) {
480
- json(res, { error: err instanceof Error ? err.message : String(err) }, 500);
481
- }
482
- return;
483
- }
484
- // GET /api/capabilities — list all packs + enabled status
485
- if (path === '/api/capabilities') {
486
- const { getAvailablePacks, isEnabled } = await import('../capabilities/index.js');
487
- const packs = getAvailablePacks().map(p => ({
488
- ...p,
489
- enabled: isEnabled(p.id),
490
- // Don't expose API keys in the response
491
- mcpServer: { package: p.mcpServer.package },
492
- }));
493
- json(res, { packs });
494
- return;
495
- }
496
- // POST /api/capabilities/:id/enable — enable a pack
497
- if (path.match(/^\/api\/capabilities\/[^/]+\/enable$/) && req.method === 'POST') {
498
- const packId = path.split('/')[3];
499
- let body = '';
500
- for await (const chunk of req) {
501
- body += chunk;
502
- if (body.length > 1_000_000) {
503
- json(res, { error: 'Payload too large' }, 413);
504
- return;
505
- }
506
- }
507
- let parsedCap;
508
- try {
509
- parsedCap = JSON.parse(body);
510
- }
511
- catch {
512
- json(res, { error: 'Invalid JSON' }, 400);
513
- return;
514
- }
515
- const { apiKey } = parsedCap;
516
- const { enablePack } = await import('../capabilities/index.js');
517
- try {
518
- enablePack(packId, apiKey);
519
- json(res, { success: true, message: 'Restart daemon to activate.' });
520
- }
521
- catch (err) {
522
- json(res, { error: err instanceof Error ? err.message : String(err) }, 400);
523
- }
524
- return;
525
- }
526
- // POST /api/capabilities/:id/disable — disable a pack
527
- if (path.match(/^\/api\/capabilities\/[^/]+\/disable$/) && req.method === 'POST') {
528
- const packId = path.split('/')[3];
529
- const { disablePack } = await import('../capabilities/index.js');
530
- disablePack(packId);
531
- json(res, { success: true });
79
+ const route = dispatch(req.method || 'GET', path);
80
+ if (!route) {
81
+ json(res, { error: 'Not found' }, 404);
532
82
  return;
533
83
  }
534
- // 404
535
- json(res, { error: 'Not found' }, 404);
84
+ await route.handler({ req, res, url, path });
536
85
  }
537
86
  catch (err) {
538
- console.error('Dashboard error:', err);
87
+ logger.error('Dashboard error:', err);
539
88
  json(res, { error: 'Internal server error' }, 500);
540
89
  }
541
90
  });
@@ -0,0 +1,29 @@
1
+ /**
2
+ * Single source of truth for opening the Beecork SQLite database.
3
+ *
4
+ * Previously three sites each opened the DB with their own pragma setup:
5
+ * - daemon side (db/index.ts) — applied migrations + WAL checkpoint interval
6
+ * - MCP side (mcp/server.ts) — read-only-ish singleton, no migrations
7
+ * - doctor (cli/doctor.ts) — ad-hoc read-only handle
8
+ *
9
+ * The pragmas matched today but the duplication was silent-drift bait. This
10
+ * helper centralizes pragma setup so future tweaks land in one place.
11
+ */
12
+ import Database from 'better-sqlite3';
13
+ export interface OpenDbOptions {
14
+ /** Open the file read-only (used by `beecork doctor` for status snapshots). */
15
+ readonly?: boolean;
16
+ /**
17
+ * If true, restrict file mode to 0o600 after open. Daemon side wants this;
18
+ * read-only sidecar handles don't need to retouch perms.
19
+ */
20
+ enforcePerms?: boolean;
21
+ }
22
+ /**
23
+ * Open a Beecork SQLite database with consistent pragmas.
24
+ *
25
+ * Migrations are NOT applied here — the daemon's `getDb()` runs migrations
26
+ * separately, so this helper can be reused by the MCP child + doctor without
27
+ * accidentally double-applying migrations.
28
+ */
29
+ export declare function openDb(dbPath: string, opts?: OpenDbOptions): Database.Database;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Single source of truth for opening the Beecork SQLite database.
3
+ *
4
+ * Previously three sites each opened the DB with their own pragma setup:
5
+ * - daemon side (db/index.ts) — applied migrations + WAL checkpoint interval
6
+ * - MCP side (mcp/server.ts) — read-only-ish singleton, no migrations
7
+ * - doctor (cli/doctor.ts) — ad-hoc read-only handle
8
+ *
9
+ * The pragmas matched today but the duplication was silent-drift bait. This
10
+ * helper centralizes pragma setup so future tweaks land in one place.
11
+ */
12
+ import Database from 'better-sqlite3';
13
+ import fs from 'node:fs';
14
+ /**
15
+ * Open a Beecork SQLite database with consistent pragmas.
16
+ *
17
+ * Migrations are NOT applied here — the daemon's `getDb()` runs migrations
18
+ * separately, so this helper can be reused by the MCP child + doctor without
19
+ * accidentally double-applying migrations.
20
+ */
21
+ export function openDb(dbPath, opts = {}) {
22
+ const db = new Database(dbPath, opts.readonly ? { readonly: true } : undefined);
23
+ db.pragma('journal_mode = WAL');
24
+ db.pragma('foreign_keys = ON');
25
+ db.pragma('busy_timeout = 5000');
26
+ if (opts.enforcePerms && !opts.readonly) {
27
+ for (const p of [dbPath, dbPath + '-wal', dbPath + '-shm']) {
28
+ try {
29
+ fs.chmodSync(p, 0o600);
30
+ }
31
+ catch {
32
+ /* sidecar may not exist yet */
33
+ }
34
+ }
35
+ }
36
+ return db;
37
+ }