beecork 1.5.0 → 1.7.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 (119) 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/command-handler.js +46 -14
  6. package/dist/channels/discord.d.ts +3 -6
  7. package/dist/channels/discord.js +40 -23
  8. package/dist/channels/index.d.ts +1 -1
  9. package/dist/channels/loader.js +13 -3
  10. package/dist/channels/pipeline.js +14 -5
  11. package/dist/channels/registry.d.ts +17 -1
  12. package/dist/channels/registry.js +33 -4
  13. package/dist/channels/telegram.d.ts +20 -5
  14. package/dist/channels/telegram.js +177 -42
  15. package/dist/channels/types.d.ts +11 -28
  16. package/dist/channels/voice-state.js +3 -1
  17. package/dist/channels/webhook.d.ts +1 -4
  18. package/dist/channels/webhook.js +26 -11
  19. package/dist/channels/whatsapp.d.ts +8 -4
  20. package/dist/channels/whatsapp.js +65 -29
  21. package/dist/cli/capabilities.js +4 -4
  22. package/dist/cli/channel.js +16 -6
  23. package/dist/cli/commands.js +12 -9
  24. package/dist/cli/doctor.js +80 -25
  25. package/dist/cli/handoff.d.ts +7 -14
  26. package/dist/cli/handoff.js +9 -44
  27. package/dist/cli/mcp.js +5 -5
  28. package/dist/cli/media.js +21 -8
  29. package/dist/cli/setup.js +9 -8
  30. package/dist/cli/store.js +29 -12
  31. package/dist/config.js +5 -10
  32. package/dist/daemon.js +88 -38
  33. package/dist/dashboard/html.js +80 -12
  34. package/dist/dashboard/routes.js +143 -79
  35. package/dist/dashboard/server.js +5 -1
  36. package/dist/db/connection.d.ts +29 -0
  37. package/dist/db/connection.js +37 -0
  38. package/dist/db/index.js +30 -12
  39. package/dist/db/migrations.js +84 -28
  40. package/dist/delegation/manager.js +10 -4
  41. package/dist/index.js +39 -59
  42. package/dist/knowledge/manager.js +26 -12
  43. package/dist/mcp/handlers.js +126 -57
  44. package/dist/mcp/server.js +20 -10
  45. package/dist/mcp/tool-definitions.js +68 -20
  46. package/dist/mcp/validate.d.ts +23 -0
  47. package/dist/mcp/validate.js +65 -0
  48. package/dist/media/factory.js +18 -14
  49. package/dist/media/generators/dall-e.js +2 -2
  50. package/dist/media/generators/kling.js +4 -4
  51. package/dist/media/generators/lyria.js +1 -1
  52. package/dist/media/generators/nano-banana.d.ts +1 -1
  53. package/dist/media/generators/nano-banana.js +2 -2
  54. package/dist/media/generators/poll-util.js +4 -4
  55. package/dist/media/generators/recraft.js +3 -3
  56. package/dist/media/generators/runway.js +4 -4
  57. package/dist/media/generators/stable-diffusion.js +2 -2
  58. package/dist/media/generators/veo.js +1 -1
  59. package/dist/media/index.js +1 -1
  60. package/dist/media/store.d.ts +7 -0
  61. package/dist/media/store.js +18 -4
  62. package/dist/media/types.d.ts +22 -0
  63. package/dist/notifications/index.d.ts +2 -4
  64. package/dist/notifications/index.js +6 -19
  65. package/dist/notifications/ntfy.js +3 -3
  66. package/dist/observability/analytics.js +35 -13
  67. package/dist/projects/index.d.ts +1 -1
  68. package/dist/projects/index.js +1 -1
  69. package/dist/projects/manager.d.ts +0 -4
  70. package/dist/projects/manager.js +51 -28
  71. package/dist/projects/router.d.ts +2 -0
  72. package/dist/projects/router.js +70 -45
  73. package/dist/service/install.js +15 -5
  74. package/dist/service/windows.js +1 -1
  75. package/dist/session/budget-guard.d.ts +20 -0
  76. package/dist/session/budget-guard.js +31 -0
  77. package/dist/session/circuit-breaker.d.ts +5 -3
  78. package/dist/session/circuit-breaker.js +45 -20
  79. package/dist/session/context-compactor.d.ts +32 -0
  80. package/dist/session/context-compactor.js +45 -0
  81. package/dist/session/context-monitor.js +2 -2
  82. package/dist/session/handoff.d.ts +21 -0
  83. package/dist/session/handoff.js +50 -0
  84. package/dist/session/manager.d.ts +17 -5
  85. package/dist/session/manager.js +153 -146
  86. package/dist/session/memory-store.d.ts +29 -0
  87. package/dist/session/memory-store.js +45 -0
  88. package/dist/session/message-queue.d.ts +28 -0
  89. package/dist/session/message-queue.js +52 -0
  90. package/dist/session/pending-dispatcher.d.ts +31 -0
  91. package/dist/session/pending-dispatcher.js +120 -0
  92. package/dist/session/pending-store.d.ts +60 -0
  93. package/dist/session/pending-store.js +118 -0
  94. package/dist/session/stale-session.d.ts +31 -0
  95. package/dist/session/stale-session.js +45 -0
  96. package/dist/session/subprocess.d.ts +2 -0
  97. package/dist/session/subprocess.js +33 -11
  98. package/dist/session/tab-store.js +4 -3
  99. package/dist/tasks/scheduler.d.ts +7 -0
  100. package/dist/tasks/scheduler.js +46 -6
  101. package/dist/tasks/store.js +20 -6
  102. package/dist/timeline/logger.js +3 -1
  103. package/dist/timeline/query.js +9 -3
  104. package/dist/types.d.ts +34 -9
  105. package/dist/util/auto-heal.js +15 -5
  106. package/dist/util/install-info.js +3 -1
  107. package/dist/util/logger.d.ts +1 -1
  108. package/dist/util/logger.js +63 -24
  109. package/dist/util/paths.d.ts +1 -0
  110. package/dist/util/paths.js +12 -2
  111. package/dist/util/retry.js +1 -1
  112. package/dist/util/text.js +13 -7
  113. package/dist/voice/index.js +5 -1
  114. package/dist/voice/stt.js +14 -6
  115. package/dist/voice/tts.js +1 -1
  116. package/dist/watchers/scheduler.js +9 -2
  117. package/package.json +18 -13
  118. package/dist/session/tool-classifier.d.ts +0 -4
  119. package/dist/session/tool-classifier.js +0 -56
@@ -10,15 +10,23 @@ import { getConfig, validateTabNameOrDefault } from '../config.js';
10
10
  import { createTabRecord } from '../db/index.js';
11
11
  import { MESSAGE_LIMITS } from '../util/text.js';
12
12
  import { TabStore } from '../session/tab-store.js';
13
- export function ok(text) { return { content: [{ type: 'text', text }] }; }
14
- export function fail(text) { return { content: [{ type: 'text', text }], isError: true }; }
13
+ import { PendingMessageStore } from '../session/pending-store.js';
14
+ import { logActivity } from '../timeline/index.js';
15
+ export function ok(text) {
16
+ return { content: [{ type: 'text', text }] };
17
+ }
18
+ export function fail(text) {
19
+ return { content: [{ type: 'text', text }], isError: true };
20
+ }
15
21
  /**
16
22
  * Convenience wrapper for tools that return structured data — emits pretty-printed
17
23
  * JSON inside the standard `text` content. Use for endpoints whose responses are
18
24
  * meant to be parsed programmatically (channels, handoff, export_data). Use plain
19
25
  * ok() for human-readable lists/summaries.
20
26
  */
21
- export function jsonOk(data) { return ok(JSON.stringify(data, null, 2)); }
27
+ export function jsonOk(data) {
28
+ return ok(JSON.stringify(data, null, 2));
29
+ }
22
30
  const MAX_CONTENT_LENGTH = MESSAGE_LIMITS.MCP_CONTENT;
23
31
  const MAX_NAME_LENGTH = 256;
24
32
  const VALID_SCHEDULE_TYPES = ['at', 'every', 'cron'];
@@ -28,13 +36,14 @@ async function handleMediaGeneration(ctx, mediaType, args) {
28
36
  return fail('Missing prompt');
29
37
  const generators = await ctx.getGenerators();
30
38
  const gen = provider
31
- ? generators.find(g => g.id === provider)
32
- : generators.find(g => g.supportedTypes.includes(mediaType));
39
+ ? generators.find((g) => g.id === provider)
40
+ : generators.find((g) => g.supportedTypes.includes(mediaType));
33
41
  if (!gen)
34
42
  return fail(`No ${mediaType} generator configured. Run: beecork media`);
35
43
  try {
36
44
  const result = await gen.generate(mediaType, prompt, { style, duration });
37
- ctx.db.prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run('default', JSON.stringify({ type: 'media', filePath: result.filePath, caption: prompt.slice(0, 200) }), 'media');
45
+ PendingMessageStore.enqueueMedia('default', { type: 'media', filePath: result.filePath, caption: prompt.slice(0, 200) }, ctx.db);
46
+ logActivity('media_generated', `${mediaType} generated`, { details: result.filePath });
38
47
  return ok(`Generated ${mediaType}: ${result.filePath}`);
39
48
  }
40
49
  catch (err) {
@@ -50,18 +59,25 @@ export const HANDLERS = {
50
59
  if (scope && scope !== 'tab' && scope !== 'auto') {
51
60
  const { addKnowledge } = await import('../knowledge/index.js');
52
61
  const currentTab = TabStore.mostRecent(ctx.db);
53
- addKnowledge(content, scope, { category, projectPath: currentTab?.workingDir, tabName: undefined });
62
+ addKnowledge(content, scope, {
63
+ category,
64
+ projectPath: currentTab?.workingDir,
65
+ tabName: undefined,
66
+ });
54
67
  return ok(`Remembered (${scope}): ${content.slice(0, 100)}`);
55
68
  }
56
69
  const fullContent = category ? `[${category}] ${content}` : content;
57
- const existing = ctx.db.prepare('SELECT id FROM memories WHERE content = ? AND tab_name IS NULL LIMIT 1').get(fullContent);
70
+ const existing = ctx.db
71
+ .prepare('SELECT id FROM memories WHERE content = ? AND tab_name IS NULL LIMIT 1')
72
+ .get(fullContent);
58
73
  if (existing)
59
74
  return ok(`Already remembered: "${fullContent}"`);
60
- ctx.db.prepare('INSERT INTO memories (content, source) VALUES (?, ?)').run(fullContent, 'tool');
75
+ const { MemoryStore } = await import('../session/memory-store.js');
76
+ MemoryStore.add(fullContent, { source: 'tool' }, ctx.db);
61
77
  return ok(`Remembered: "${fullContent}"`);
62
78
  },
63
79
  beecork_task_create: async (ctx, args) => {
64
- const { name: jobName, scheduleType, schedule, message, tabName } = (args || {});
80
+ const { name: jobName, scheduleType, schedule, message, tabName, } = (args || {});
65
81
  if (!jobName || jobName.length > MAX_NAME_LENGTH) {
66
82
  return fail(`Task name is required and must be under ${MAX_NAME_LENGTH} characters.`);
67
83
  }
@@ -81,8 +97,20 @@ export const HANDLERS = {
81
97
  const scheduleErr = validateSchedule(scheduleType, schedule);
82
98
  if (scheduleErr)
83
99
  return fail(scheduleErr);
84
- ctx.db.prepare(`INSERT INTO tasks (id, name, schedule_type, schedule, tab_name, message, payload_type, enabled, created_at)
85
- VALUES (?, ?, ?, ?, ?, ?, 'agentTurn', 1, ?)`).run(id, jobName, scheduleType, schedule, tab, message, new Date().toISOString());
100
+ const { TaskStore } = await import('../tasks/store.js');
101
+ new TaskStore().add({
102
+ id,
103
+ name: jobName,
104
+ scheduleType: scheduleType,
105
+ schedule,
106
+ tabName: tab,
107
+ message,
108
+ payloadType: 'agentTurn',
109
+ enabled: true,
110
+ createdAt: new Date().toISOString(),
111
+ lastRunAt: null,
112
+ nextRunAt: null,
113
+ });
86
114
  try {
87
115
  ctx.signalCronReload();
88
116
  }
@@ -95,7 +123,7 @@ export const HANDLERS = {
95
123
  const jobs = ctx.db.prepare('SELECT * FROM tasks ORDER BY created_at LIMIT 500').all();
96
124
  if (jobs.length === 0)
97
125
  return ok('No tasks scheduled.');
98
- const lines = jobs.map(j => `- ${j.name} [${j.enabled ? 'enabled' : 'disabled'}] (${j.schedule_type}: ${j.schedule}) -> tab:${j.tab_name} (ID: ${j.id})`);
126
+ const lines = jobs.map((j) => `- ${j.name} [${j.enabled ? 'enabled' : 'disabled'}] (${j.schedule_type}: ${j.schedule}) -> tab:${j.tab_name} (ID: ${j.id})`);
99
127
  return ok(lines.join('\n'));
100
128
  },
101
129
  beecork_task_delete: async (ctx, args) => {
@@ -106,7 +134,9 @@ export const HANDLERS = {
106
134
  try {
107
135
  ctx.signalCronReload();
108
136
  }
109
- catch { /* daemon picks up on restart */ }
137
+ catch {
138
+ /* daemon picks up on restart */
139
+ }
110
140
  return ok(`Deleted task: ${id}`);
111
141
  },
112
142
  beecork_watch_create: async (ctx, args) => {
@@ -127,8 +157,10 @@ export const HANDLERS = {
127
157
  if (cronErr && intervalErr)
128
158
  return fail(`Invalid schedule "${watchSchedule}" — must be a cron expression or interval like "5m"/"1h"/"1d".`);
129
159
  const watchId = uuidv4();
130
- ctx.db.prepare(`INSERT INTO watchers (id, name, description, check_command, condition, action, action_details, schedule)
131
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(watchId, watchName, watchDesc || null, checkCommand, condition, action || 'notify', actionDetails || null, watchSchedule);
160
+ ctx.db
161
+ .prepare(`INSERT INTO watchers (id, name, description, check_command, condition, action, action_details, schedule)
162
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?)`)
163
+ .run(watchId, watchName, watchDesc || null, checkCommand, condition, action || 'notify', actionDetails || null, watchSchedule);
132
164
  try {
133
165
  ctx.signalWatcherReload();
134
166
  }
@@ -138,10 +170,12 @@ export const HANDLERS = {
138
170
  return ok(`Watcher created: "${watchName}" (${watchSchedule})\nID: ${watchId}`);
139
171
  },
140
172
  beecork_watch_list: async (ctx) => {
141
- const watchers = ctx.db.prepare('SELECT * FROM watchers ORDER BY created_at LIMIT 500').all();
173
+ const watchers = ctx.db
174
+ .prepare('SELECT * FROM watchers ORDER BY created_at LIMIT 500')
175
+ .all();
142
176
  if (watchers.length === 0)
143
177
  return ok('No watchers configured.');
144
- const watchLines = watchers.map(w => `- ${w.name} [${w.enabled ? 'enabled' : 'disabled'}] ${w.schedule} | action: ${w.action} | triggers: ${w.trigger_count} (ID: ${w.id})`);
178
+ const watchLines = watchers.map((w) => `- ${w.name} [${w.enabled ? 'enabled' : 'disabled'}] ${w.schedule} | action: ${w.action} | triggers: ${w.trigger_count} (ID: ${w.id})`);
145
179
  return ok(watchLines.join('\n'));
146
180
  },
147
181
  beecork_watch_delete: async (ctx, args) => {
@@ -152,11 +186,13 @@ export const HANDLERS = {
152
186
  try {
153
187
  ctx.signalWatcherReload();
154
188
  }
155
- catch { /* daemon picks up on restart */ }
189
+ catch {
190
+ /* daemon picks up on restart */
191
+ }
156
192
  return ok(`Deleted watcher: ${watchDelId}`);
157
193
  },
158
194
  beecork_tab_create: async (ctx, args) => {
159
- const { name: tabName, workingDir, template: templateName, systemPrompt } = (args || {});
195
+ const { name: tabName, workingDir, template: templateName, systemPrompt, } = (args || {});
160
196
  if (!tabName)
161
197
  return fail('Tab name is required.');
162
198
  // tab create does NOT allow "default" — that tab is auto-managed by the daemon.
@@ -174,7 +210,11 @@ export const HANDLERS = {
174
210
  const tabSystemPrompt = systemPrompt || template?.systemPrompt || null;
175
211
  try {
176
212
  // createTabRecord validates existence + isDirectory and throws on failure.
177
- const result = createTabRecord(ctx.db, { name: tabName, workingDir: dir, systemPrompt: tabSystemPrompt });
213
+ const result = createTabRecord(ctx.db, {
214
+ name: tabName,
215
+ workingDir: dir,
216
+ systemPrompt: tabSystemPrompt,
217
+ });
178
218
  if (!result.created)
179
219
  return ok(`Tab "${tabName}" already exists.`);
180
220
  const parts = [`Created tab: "${tabName}" (working dir: ${dir})`];
@@ -192,7 +232,7 @@ export const HANDLERS = {
192
232
  const tabs = TabStore.listAll(ctx.db);
193
233
  if (tabs.length === 0)
194
234
  return ok('No tabs.');
195
- const lines = tabs.map(t => `- ${t.name} [${t.status}] dir:${t.workingDir} last:${t.lastActivityAt}`);
235
+ const lines = tabs.map((t) => `- ${t.name} [${t.status}] dir:${t.workingDir} last:${t.lastActivityAt}`);
196
236
  return ok(lines.join('\n'));
197
237
  },
198
238
  beecork_send_message: async (ctx, args) => {
@@ -205,18 +245,24 @@ export const HANDLERS = {
205
245
  if (message.length > MAX_CONTENT_LENGTH) {
206
246
  return fail(`Message must be under ${MAX_CONTENT_LENGTH} characters.`);
207
247
  }
208
- ctx.db.prepare('INSERT INTO pending_messages (tab_name, message) VALUES (?, ?)').run(tabName, message);
248
+ PendingMessageStore.enqueueUser(tabName, message, ctx.db);
209
249
  return ok(`Message queued for tab "${tabName}".`);
210
250
  },
211
251
  beecork_recall: async (ctx, args) => {
212
252
  const { query, limit } = (args || {});
213
- const maxResults = Math.min(limit ?? 10, 50);
214
- const memories = ctx.db.prepare('SELECT content, tab_name, source, created_at FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT ?').all(`%${query}%`, maxResults);
253
+ if (typeof query !== 'string' || query.length === 0)
254
+ return fail('query is required');
255
+ if (query.length > 256)
256
+ return fail('query is too long (max 256 chars)');
257
+ const maxResults = Math.min(typeof limit === 'number' && limit > 0 ? limit : 10, 50);
258
+ const memories = ctx.db
259
+ .prepare('SELECT content, tab_name, source, created_at FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT ?')
260
+ .all(`%${query}%`, maxResults);
215
261
  const { searchKnowledge } = await import('../knowledge/index.js');
216
262
  const knowledgeResults = searchKnowledge(query);
217
263
  const allResults = [
218
- ...knowledgeResults.map(k => k.content),
219
- ...memories.map(m => m.content),
264
+ ...knowledgeResults.map((k) => k.content),
265
+ ...memories.map((m) => m.content),
220
266
  ];
221
267
  if (allResults.length === 0)
222
268
  return ok(`No relevant knowledge found matching "${query}".`);
@@ -227,7 +273,7 @@ export const HANDLERS = {
227
273
  if (!message)
228
274
  return fail('Message is required.');
229
275
  const prefix = urgent ? '🚨 ' : '';
230
- ctx.db.prepare("INSERT INTO pending_messages (tab_name, message, type) VALUES ('_notify', ?, 'notification')").run(prefix + message);
276
+ PendingMessageStore.enqueueNotification(prefix + message, ctx.db);
231
277
  return ok('Notification sent to user.');
232
278
  },
233
279
  beecork_status: async (ctx) => {
@@ -245,11 +291,21 @@ export const HANDLERS = {
245
291
  },
246
292
  beecork_send_media: async (ctx, args) => {
247
293
  const { filePath, caption, tabName } = (args || {});
248
- if (!fs.existsSync(filePath))
249
- return fail(`File not found: ${filePath}`);
294
+ if (!filePath)
295
+ return fail('filePath is required');
296
+ let resolved;
297
+ try {
298
+ const { assertInsideMediaDir } = await import('../media/store.js');
299
+ resolved = assertInsideMediaDir(filePath);
300
+ }
301
+ catch (err) {
302
+ return fail(err instanceof Error ? err.message : String(err));
303
+ }
304
+ if (!fs.existsSync(resolved))
305
+ return fail(`File not found: ${resolved}`);
250
306
  const tab = tabName || 'default';
251
- ctx.db.prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run(tab, JSON.stringify({ type: 'media', filePath, caption }), 'media');
252
- return ok(`Media queued for sending: ${filePath}`);
307
+ PendingMessageStore.enqueueMedia(tab, { type: 'media', filePath: resolved, caption }, ctx.db);
308
+ return ok(`Media queued for sending: ${resolved}`);
253
309
  },
254
310
  beecork_channels: async () => {
255
311
  const config = getConfig();
@@ -269,7 +325,7 @@ export const HANDLERS = {
269
325
  const { getCostSummary, formatCostSummary } = await import('../observability/analytics.js');
270
326
  const summary = getCostSummary();
271
327
  if (tabName) {
272
- const tab = summary.perTab.find(t => t.name === tabName);
328
+ const tab = summary.perTab.find((t) => t.name === tabName);
273
329
  if (!tab)
274
330
  return fail(`Tab "${tabName}" not found`);
275
331
  return ok(`Tab "${tabName}": $${tab.cost.toFixed(4)} (${tab.messages} messages)`);
@@ -277,10 +333,12 @@ export const HANDLERS = {
277
333
  return ok(formatCostSummary(summary));
278
334
  },
279
335
  beecork_failed_deliveries: async (ctx) => {
280
- const failed = ctx.db.prepare("SELECT m.content, m.created_at, m.retry_count, t.name as tab_name FROM messages m JOIN tabs t ON t.id = m.tab_id WHERE m.delivery_status = 'failed' ORDER BY m.created_at DESC LIMIT 20").all();
336
+ const failed = ctx.db
337
+ .prepare("SELECT m.content, m.created_at, m.retry_count, t.name as tab_name FROM messages m JOIN tabs t ON t.id = m.tab_id WHERE m.delivery_status = 'failed' ORDER BY m.created_at DESC LIMIT 20")
338
+ .all();
281
339
  if (failed.length === 0)
282
340
  return ok('No failed deliveries.');
283
- const lines = failed.map(f => `[${f.created_at}] tab:${f.tab_name} retries:${f.retry_count}\n ${f.content.slice(0, 200)}`);
341
+ const lines = failed.map((f) => `[${f.created_at}] tab:${f.tab_name} retries:${f.retry_count}\n ${f.content.slice(0, 200)}`);
284
342
  return ok(lines.join('\n\n'));
285
343
  },
286
344
  beecork_activity: async (_ctx, args) => {
@@ -294,10 +352,14 @@ export const HANDLERS = {
294
352
  let data;
295
353
  switch (dataType) {
296
354
  case 'costs':
297
- data = ctx.db.prepare("SELECT date(created_at) as day, SUM(cost_usd) as cost, COUNT(*) as messages FROM messages WHERE role = 'assistant' AND created_at > ? GROUP BY date(created_at) ORDER BY day").all(since);
355
+ data = ctx.db
356
+ .prepare("SELECT date(created_at) as day, SUM(cost_usd) as cost, COUNT(*) as messages FROM messages WHERE role = 'assistant' AND created_at > ? GROUP BY date(created_at) ORDER BY day")
357
+ .all(since);
298
358
  break;
299
359
  case 'messages':
300
- data = ctx.db.prepare("SELECT m.role, m.content, m.cost_usd, m.created_at, t.name as tab FROM messages m JOIN tabs t ON t.id = m.tab_id WHERE m.created_at > ? ORDER BY m.created_at DESC LIMIT 500").all(since);
360
+ data = ctx.db
361
+ .prepare('SELECT m.role, m.content, m.cost_usd, m.created_at, t.name as tab FROM messages m JOIN tabs t ON t.id = m.tab_id WHERE m.created_at > ? ORDER BY m.created_at DESC LIMIT 500')
362
+ .all(since);
301
363
  break;
302
364
  case 'crons':
303
365
  data = ctx.db.prepare('SELECT * FROM tasks ORDER BY created_at').all();
@@ -307,19 +369,22 @@ export const HANDLERS = {
307
369
  }
308
370
  return jsonOk(data);
309
371
  },
310
- beecork_handoff: async (ctx, args) => {
372
+ beecork_handoff: async (_ctx, args) => {
311
373
  const { tabName } = (args || {});
312
- const tab = TabStore.findByName(tabName, ctx.db);
313
- if (!tab)
374
+ const { exportTab } = await import('../session/handoff.js');
375
+ const exported = exportTab(tabName);
376
+ if (!exported)
314
377
  return fail(`Tab "${tabName}" not found`);
315
- const messages = ctx.db.prepare('SELECT role, content FROM messages WHERE tab_id = ? ORDER BY created_at DESC LIMIT 5').all(tab.id);
316
378
  const info = {
317
- sessionId: tab.sessionId,
318
- workingDir: tab.workingDir,
319
- status: tab.status,
379
+ sessionId: exported.sessionId,
380
+ workingDir: exported.workingDir,
381
+ status: exported.status,
320
382
  resumeCommand: `beecork attach ${tabName}`,
321
- manualCommand: `cd ${tab.workingDir} && claude --session-id ${tab.sessionId} --resume`,
322
- recentMessages: messages.reverse().map(m => ({ role: m.role, preview: m.content.slice(0, 200) })),
383
+ manualCommand: `cd ${exported.workingDir} && claude --session-id ${exported.sessionId} --resume`,
384
+ recentMessages: exported.recentMessages.map((m) => ({
385
+ role: m.role,
386
+ preview: m.content.slice(0, 200),
387
+ })),
323
388
  };
324
389
  return jsonOk(info);
325
390
  },
@@ -328,7 +393,7 @@ export const HANDLERS = {
328
393
  try {
329
394
  const { createDelegation } = await import('../delegation/manager.js');
330
395
  const delegation = createDelegation(returnToTab || 'default', tabName, message, returnToTab);
331
- ctx.db.prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run(tabName, message, 'delegation');
396
+ PendingMessageStore.enqueueDelegation(tabName, message, ctx.db);
332
397
  return ok(`Delegated to tab "${tabName}". Result will be sent back to "${delegation.returnToTab}" when complete.\n\nDelegation ID: ${delegation.id}`);
333
398
  }
334
399
  catch (err) {
@@ -341,7 +406,7 @@ export const HANDLERS = {
341
406
  const delegations = getPendingDelegations(tabName);
342
407
  if (delegations.length === 0)
343
408
  return ok('No pending delegations.');
344
- const lines = delegations.map(d => `${d.sourceTab} → ${d.targetTab} [${d.status}] (depth ${d.depth})\n "${d.message.slice(0, 100)}"`);
409
+ const lines = delegations.map((d) => `${d.sourceTab} → ${d.targetTab} [${d.status}] (depth ${d.depth})\n "${d.message.slice(0, 100)}"`);
345
410
  return ok(lines.join('\n\n'));
346
411
  },
347
412
  beecork_project_create: async (_ctx, args) => {
@@ -360,7 +425,7 @@ export const HANDLERS = {
360
425
  const projects = listProjects();
361
426
  if (projects.length === 0)
362
427
  return ok('No folders discovered. Create one with beecork_project_create.');
363
- const lines = projects.map(p => `${p.type === 'category' ? '📁' : '📦'} ${p.name} — ${p.path}`);
428
+ const lines = projects.map((p) => `${p.type === 'category' ? '📁' : '📦'} ${p.name} — ${p.path}`);
364
429
  return ok(lines.join('\n'));
365
430
  },
366
431
  beecork_close_tab: async (ctx, args) => {
@@ -380,12 +445,12 @@ export const HANDLERS = {
380
445
  const generators = await ctx.getGenerators();
381
446
  if (generators.length === 0)
382
447
  return ok('No media generators configured. Add mediaGenerators to config.json.');
383
- const lines = generators.map(g => `- ${g.name} (${g.id}): ${g.supportedTypes.join(', ')}`);
448
+ const lines = generators.map((g) => `- ${g.name} (${g.id}): ${g.supportedTypes.join(', ')}`);
384
449
  return ok(lines.join('\n'));
385
450
  },
386
451
  beecork_knowledge: async (ctx, args) => {
387
452
  const { scope: knowledgeScope } = (args || {});
388
- const { getGlobalKnowledge, getProjectKnowledge, getTabKnowledge, getAllKnowledge, formatKnowledgeForContext } = await import('../knowledge/index.js');
453
+ const { getGlobalKnowledge, getProjectKnowledge, getTabKnowledge, getAllKnowledge, formatKnowledgeForContext, } = await import('../knowledge/index.js');
389
454
  let entries;
390
455
  if (knowledgeScope === 'global') {
391
456
  entries = getGlobalKnowledge();
@@ -408,13 +473,17 @@ export const HANDLERS = {
408
473
  beecork_capabilities: async () => {
409
474
  const { getAvailablePacks, isEnabled } = await import('../capabilities/index.js');
410
475
  const packs = getAvailablePacks();
411
- const lines = packs.map(p => `${isEnabled(p.id) ? '✓ enabled' : '○ available'} | ${p.id} — ${p.name}: ${p.description}`);
476
+ const lines = packs.map((p) => `${isEnabled(p.id) ? '✓ enabled' : '○ available'} | ${p.id} — ${p.name}: ${p.description}`);
412
477
  return ok(lines.join('\n'));
413
478
  },
414
479
  beecork_history: async (_ctx, args) => {
415
480
  const { date, tabName, limit } = (args || {});
416
481
  const { getTimeline, formatTimeline } = await import('../timeline/index.js');
417
- const events = getTimeline({ date: date || new Date().toISOString().slice(0, 10), tabName, limit });
482
+ const events = getTimeline({
483
+ date: date || new Date().toISOString().slice(0, 10),
484
+ tabName,
485
+ limit,
486
+ });
418
487
  return ok(formatTimeline(events));
419
488
  },
420
489
  beecork_replay: async (ctx, args) => {
@@ -423,7 +492,7 @@ export const HANDLERS = {
423
492
  const info = getReplayInfo(eventId);
424
493
  if (!info)
425
494
  return fail('Event not found or not replayable.');
426
- ctx.db.prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run(info.tabName, info.message, 'replay');
495
+ PendingMessageStore.enqueueReplay(info.tabName, info.message, ctx.db);
427
496
  return ok(`Replaying in tab "${info.tabName}": ${info.message.slice(0, 200)}`);
428
497
  },
429
498
  beecork_store_search: async (_ctx, args) => {
@@ -432,11 +501,11 @@ export const HANDLERS = {
432
501
  const response = await fetch(`https://registry.npmjs.org/-/v1/search?text=beecork+${encodeURIComponent(query)}&size=10`, { signal: AbortSignal.timeout(10000) });
433
502
  if (!response.ok)
434
503
  return fail('npm registry search failed');
435
- const data = await response.json();
436
- const packages = (data.objects || []).filter(o => o.package.name.startsWith('beecork-'));
504
+ const data = (await response.json());
505
+ const packages = (data.objects || []).filter((o) => o.package.name.startsWith('beecork-'));
437
506
  if (packages.length === 0)
438
507
  return ok(`No beecork packages found for "${query}". You can create one with: beecork channel create <name> or beecork media create <name>`);
439
- const lines = packages.map(o => `${o.package.name}@${o.package.version} — ${o.package.description || 'No description'}`);
508
+ const lines = packages.map((o) => `${o.package.name}@${o.package.version} — ${o.package.description || 'No description'}`);
440
509
  return ok(`${packages.length} package(s):\n${lines.join('\n')}\n\nInstall: npm install -g <package-name>`);
441
510
  }
442
511
  catch {
@@ -1,13 +1,14 @@
1
1
  #!/usr/bin/env node
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
- import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
- import Database from 'better-sqlite3';
4
+ import { CallToolRequestSchema, ListToolsRequestSchema } from '@modelcontextprotocol/sdk/types.js';
6
5
  import fs from 'node:fs';
7
- import { getDbPath, getCronReloadSignalPath, getWatcherReloadSignalPath, } from '../util/paths.js';
6
+ import { getDbPath, getCronReloadSignalPath, getWatcherReloadSignalPath } from '../util/paths.js';
7
+ import { openDb } from '../db/connection.js';
8
8
  import { VERSION } from '../version.js';
9
9
  import { TOOL_DEFINITIONS } from './tool-definitions.js';
10
10
  import { HANDLERS, fail } from './handlers.js';
11
+ import { validateToolArgs } from './validate.js';
11
12
  // MCP server runs as a child of `claude`, not the Beecork daemon.
12
13
  // It communicates with the daemon via shared SQLite + signal files.
13
14
  // Path helpers from util/paths.ts so daemon + MCP child always agree on locations,
@@ -16,18 +17,17 @@ const DB_PATH = getDbPath();
16
17
  const CRON_RELOAD_SIGNAL = getCronReloadSignalPath();
17
18
  const WATCHER_RELOAD_SIGNAL = getWatcherReloadSignalPath();
18
19
  // Cached singleton connection (lives for the MCP server's lifetime).
20
+ // Pragma setup lives in openDb() so daemon + MCP child + doctor can't drift.
19
21
  let cachedDb = null;
20
22
  function getDb() {
21
23
  if (cachedDb)
22
24
  return cachedDb;
23
- const db = new Database(DB_PATH);
24
- db.pragma('journal_mode = WAL');
25
- db.pragma('foreign_keys = ON');
26
- db.pragma('busy_timeout = 5000');
27
- cachedDb = db;
28
- return db;
25
+ cachedDb = openDb(DB_PATH);
26
+ return cachedDb;
29
27
  }
30
- process.on('exit', () => { cachedDb?.close(); });
28
+ process.on('exit', () => {
29
+ cachedDb?.close();
30
+ });
31
31
  // Cached media generators (lazy singleton).
32
32
  let cachedGenerators = null;
33
33
  async function getGenerators() {
@@ -48,11 +48,21 @@ const server = new Server({ name: 'beecork', version: VERSION }, { capabilities:
48
48
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
49
49
  tools: TOOL_DEFINITIONS,
50
50
  }));
51
+ // Pre-build a Map for O(1) tool-definition lookup. A typical agent turn fires
52
+ // 10+ tool calls and each one previously walked the 30-entry array.
53
+ const TOOL_DEFINITION_BY_NAME = new Map(TOOL_DEFINITIONS.map((t) => [t.name, t]));
51
54
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
52
55
  const { name, arguments: args } = request.params;
53
56
  const handler = HANDLERS[name];
54
57
  if (!handler)
55
58
  return fail(`Unknown tool: ${name}`);
59
+ // Validate against the declared inputSchema before dispatch. Without this,
60
+ // handlers had ad-hoc checks for some fields and silently passed-through bad
61
+ // inputs (NaN limits, unrecognized enum values) for others.
62
+ const def = TOOL_DEFINITION_BY_NAME.get(name);
63
+ const validationError = validateToolArgs(def?.inputSchema, args);
64
+ if (validationError)
65
+ return fail(validationError);
56
66
  try {
57
67
  const ctx = {
58
68
  db: getDb(),
@@ -4,13 +4,20 @@
4
4
  export const TOOL_DEFINITIONS = [
5
5
  {
6
6
  name: 'beecork_remember',
7
- description: 'Store a fact in Beecork\'s long-term memory. Use this for preferences, decisions, server addresses, outcomes, or anything the user might want recalled in future sessions.',
7
+ description: "Store a fact in Beecork's long-term memory. Use this for preferences, decisions, server addresses, outcomes, or anything the user might want recalled in future sessions.",
8
8
  inputSchema: {
9
9
  type: 'object',
10
10
  properties: {
11
11
  content: { type: 'string', description: 'The fact or information to remember' },
12
- scope: { type: 'string', enum: ['global', 'project', 'tab', 'auto'], description: 'Where to store: global (about the user), project (about this folder), tab (about this conversation), or auto (Claude decides)' },
13
- category: { type: 'string', description: 'For global scope: people, preferences, routines, or general' },
12
+ scope: {
13
+ type: 'string',
14
+ enum: ['global', 'project', 'tab', 'auto'],
15
+ description: 'Where to store: global (about the user), project (about this folder), tab (about this conversation), or auto (Claude decides)',
16
+ },
17
+ category: {
18
+ type: 'string',
19
+ description: 'For global scope: people, preferences, routines, or general',
20
+ },
14
21
  },
15
22
  required: ['content'],
16
23
  },
@@ -27,9 +34,15 @@ export const TOOL_DEFINITIONS = [
27
34
  enum: ['at', 'every', 'cron'],
28
35
  description: '"at" = one-time ISO datetime, "every" = interval like "30m"/"2h"/"1d", "cron" = cron expression like "0 9 * * 1"',
29
36
  },
30
- schedule: { type: 'string', description: 'The schedule value (ISO datetime, interval, or cron expression)' },
37
+ schedule: {
38
+ type: 'string',
39
+ description: 'The schedule value (ISO datetime, interval, or cron expression)',
40
+ },
31
41
  message: { type: 'string', description: 'The prompt/message to send when the task fires' },
32
- tabName: { type: 'string', description: 'Which tab to send the message to (default: "default")' },
42
+ tabName: {
43
+ type: 'string',
44
+ description: 'Which tab to send the message to (default: "default")',
45
+ },
33
46
  },
34
47
  required: ['name', 'scheduleType', 'schedule', 'message'],
35
48
  },
@@ -86,10 +99,23 @@ export const TOOL_DEFINITIONS = [
86
99
  name: { type: 'string', description: 'Human-readable name for the watcher' },
87
100
  description: { type: 'string', description: 'What to watch (natural language)' },
88
101
  checkCommand: { type: 'string', description: 'Shell command to run for checking' },
89
- condition: { type: 'string', description: 'When to trigger: "contains X", "not contains X", "> N", "< N", "any", "error"' },
90
- action: { type: 'string', enum: ['notify', 'fix', 'delegate'], description: 'What to do when triggered (default: notify)' },
91
- actionDetails: { type: 'string', description: 'For fix: command to run. For delegate: tab name + message.' },
92
- schedule: { type: 'string', description: 'How often to check: cron expression or interval like "5m", "1h"' },
102
+ condition: {
103
+ type: 'string',
104
+ description: 'When to trigger: "contains X", "not contains X", "> N", "< N", "any", "error"',
105
+ },
106
+ action: {
107
+ type: 'string',
108
+ enum: ['notify', 'fix', 'delegate'],
109
+ description: 'What to do when triggered (default: notify)',
110
+ },
111
+ actionDetails: {
112
+ type: 'string',
113
+ description: 'For fix: command to run. For delegate: tab name + message.',
114
+ },
115
+ schedule: {
116
+ type: 'string',
117
+ description: 'How often to check: cron expression or interval like "5m", "1h"',
118
+ },
93
119
  },
94
120
  required: ['name', 'checkCommand', 'condition', 'schedule'],
95
121
  },
@@ -114,9 +140,18 @@ export const TOOL_DEFINITIONS = [
114
140
  inputSchema: {
115
141
  type: 'object',
116
142
  properties: {
117
- name: { type: 'string', description: 'Tab name (alphanumeric + hyphens, max 32 chars). Cannot be "default".' },
118
- workingDir: { type: 'string', description: 'Absolute path or ~/path for the tab\'s working directory' },
119
- template: { type: 'string', description: 'Name of a tab template from config (sets workingDir + systemPrompt)' },
143
+ name: {
144
+ type: 'string',
145
+ description: 'Tab name (alphanumeric + hyphens, max 32 chars). Cannot be "default".',
146
+ },
147
+ workingDir: {
148
+ type: 'string',
149
+ description: "Absolute path or ~/path for the tab's working directory",
150
+ },
151
+ template: {
152
+ type: 'string',
153
+ description: 'Name of a tab template from config (sets workingDir + systemPrompt)',
154
+ },
120
155
  systemPrompt: { type: 'string', description: 'Tab-specific system prompt for Claude' },
121
156
  },
122
157
  required: ['name'],
@@ -129,7 +164,7 @@ export const TOOL_DEFINITIONS = [
129
164
  },
130
165
  {
131
166
  name: 'beecork_send_message',
132
- description: 'Send a message to another Beecork tab. The message will be processed asynchronously by that tab\'s Claude subprocess.',
167
+ description: "Send a message to another Beecork tab. The message will be processed asynchronously by that tab's Claude subprocess.",
133
168
  inputSchema: {
134
169
  type: 'object',
135
170
  properties: {
@@ -141,7 +176,7 @@ export const TOOL_DEFINITIONS = [
141
176
  },
142
177
  {
143
178
  name: 'beecork_recall',
144
- description: 'Search Beecork\'s memory and knowledge files for facts matching a query.',
179
+ description: "Search Beecork's memory and knowledge files for facts matching a query.",
145
180
  inputSchema: {
146
181
  type: 'object',
147
182
  properties: {
@@ -170,13 +205,16 @@ export const TOOL_DEFINITIONS = [
170
205
  },
171
206
  {
172
207
  name: 'beecork_send_media',
173
- description: 'Send a generated or local media file (image/video/audio) through the user\'s channels.',
208
+ description: "Send a generated or local media file (image/video/audio) through the user's channels.",
174
209
  inputSchema: {
175
210
  type: 'object',
176
211
  properties: {
177
212
  filePath: { type: 'string', description: 'Absolute path to the media file' },
178
213
  caption: { type: 'string', description: 'Optional caption' },
179
- tabName: { type: 'string', description: 'Tab to attribute the send to (default: "default")' },
214
+ tabName: {
215
+ type: 'string',
216
+ description: 'Tab to attribute the send to (default: "default")',
217
+ },
180
218
  },
181
219
  required: ['filePath'],
182
220
  },
@@ -213,7 +251,11 @@ export const TOOL_DEFINITIONS = [
213
251
  inputSchema: {
214
252
  type: 'object',
215
253
  properties: {
216
- type: { type: 'string', enum: ['costs', 'messages', 'crons'], description: 'What to export' },
254
+ type: {
255
+ type: 'string',
256
+ enum: ['costs', 'messages', 'crons'],
257
+ description: 'What to export',
258
+ },
217
259
  days: { type: 'number', description: 'Lookback window in days (default 30)' },
218
260
  },
219
261
  required: ['type'],
@@ -221,7 +263,7 @@ export const TOOL_DEFINITIONS = [
221
263
  },
222
264
  {
223
265
  name: 'beecork_handoff',
224
- description: 'Get info to resume a tab\'s session in your terminal.',
266
+ description: "Get info to resume a tab's session in your terminal.",
225
267
  inputSchema: {
226
268
  type: 'object',
227
269
  properties: { tabName: { type: 'string' } },
@@ -236,7 +278,10 @@ export const TOOL_DEFINITIONS = [
236
278
  properties: {
237
279
  tabName: { type: 'string', description: 'Tab to delegate to' },
238
280
  message: { type: 'string', description: 'The task description' },
239
- returnToTab: { type: 'string', description: 'Tab to notify when complete (default: "default")' },
281
+ returnToTab: {
282
+ type: 'string',
283
+ description: 'Tab to notify when complete (default: "default")',
284
+ },
240
285
  },
241
286
  required: ['tabName', 'message'],
242
287
  },
@@ -256,7 +301,10 @@ export const TOOL_DEFINITIONS = [
256
301
  type: 'object',
257
302
  properties: {
258
303
  name: { type: 'string', description: 'Project name (also the folder name)' },
259
- path: { type: 'string', description: 'Optional parent directory (must be under a scan path)' },
304
+ path: {
305
+ type: 'string',
306
+ description: 'Optional parent directory (must be under a scan path)',
307
+ },
260
308
  },
261
309
  required: ['name'],
262
310
  },