beecork 1.4.11 → 1.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/channels/admin.d.ts +10 -0
  2. package/dist/channels/admin.js +20 -0
  3. package/dist/channels/command-handler.d.ts +2 -10
  4. package/dist/channels/command-handler.js +47 -73
  5. package/dist/channels/discord.d.ts +1 -3
  6. package/dist/channels/discord.js +28 -28
  7. package/dist/channels/loader.js +0 -1
  8. package/dist/channels/send-helpers.d.ts +19 -0
  9. package/dist/channels/send-helpers.js +21 -0
  10. package/dist/channels/telegram.d.ts +1 -9
  11. package/dist/channels/telegram.js +46 -71
  12. package/dist/channels/types.d.ts +2 -10
  13. package/dist/channels/voice-state.d.ts +29 -0
  14. package/dist/channels/voice-state.js +43 -0
  15. package/dist/channels/webhook.d.ts +1 -1
  16. package/dist/channels/webhook.js +68 -24
  17. package/dist/channels/whatsapp.d.ts +1 -3
  18. package/dist/channels/whatsapp.js +79 -74
  19. package/dist/cli/doctor.js +5 -2
  20. package/dist/cli/handoff.js +6 -6
  21. package/dist/config.d.ts +5 -1
  22. package/dist/config.js +17 -14
  23. package/dist/daemon.js +29 -17
  24. package/dist/dashboard/html.js +20 -8
  25. package/dist/dashboard/routes.d.ts +17 -0
  26. package/dist/dashboard/routes.js +559 -0
  27. package/dist/dashboard/server.js +33 -488
  28. package/dist/db/index.js +16 -2
  29. package/dist/db/migrations.js +44 -8
  30. package/dist/mcp/handlers.d.ts +37 -0
  31. package/dist/mcp/handlers.js +451 -0
  32. package/dist/mcp/server.js +25 -849
  33. package/dist/mcp/tool-definitions.d.ts +1225 -0
  34. package/dist/mcp/tool-definitions.js +364 -0
  35. package/dist/media/index.d.ts +2 -7
  36. package/dist/media/index.js +1 -1
  37. package/dist/observability/analytics.d.ts +1 -1
  38. package/dist/observability/analytics.js +6 -3
  39. package/dist/projects/index.d.ts +3 -2
  40. package/dist/projects/index.js +2 -2
  41. package/dist/projects/manager.d.ts +1 -3
  42. package/dist/projects/manager.js +26 -25
  43. package/dist/projects/router.d.ts +10 -0
  44. package/dist/projects/router.js +28 -0
  45. package/dist/session/manager.d.ts +4 -0
  46. package/dist/session/manager.js +48 -42
  47. package/dist/session/subprocess.d.ts +1 -0
  48. package/dist/session/subprocess.js +21 -0
  49. package/dist/session/tab-store.d.ts +28 -0
  50. package/dist/session/tab-store.js +77 -0
  51. package/dist/tasks/scheduler.d.ts +6 -0
  52. package/dist/tasks/scheduler.js +52 -13
  53. package/dist/tasks/store.js +6 -6
  54. package/dist/timeline/query.js +6 -2
  55. package/dist/types.d.ts +15 -0
  56. package/dist/util/paths.d.ts +1 -0
  57. package/dist/util/paths.js +4 -1
  58. package/dist/util/rate-limiter.js +8 -0
  59. package/dist/util/text.d.ts +21 -1
  60. package/dist/util/text.js +25 -1
  61. package/dist/watchers/scheduler.js +2 -3
  62. package/package.json +1 -1
  63. package/dist/users/index.d.ts +0 -2
  64. package/dist/users/index.js +0 -1
  65. package/dist/users/service.d.ts +0 -17
  66. package/dist/users/service.js +0 -46
@@ -2,20 +2,20 @@
2
2
  import { Server } from '@modelcontextprotocol/sdk/server/index.js';
3
3
  import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js';
4
4
  import { CallToolRequestSchema, ListToolsRequestSchema, } from '@modelcontextprotocol/sdk/types.js';
5
- import { v4 as uuidv4 } from 'uuid';
6
5
  import Database from 'better-sqlite3';
7
6
  import fs from 'node:fs';
8
- import path from 'node:path';
9
- import os from 'node:os';
7
+ import { getDbPath, getCronReloadSignalPath, getWatcherReloadSignalPath, } from '../util/paths.js';
8
+ import { VERSION } from '../version.js';
9
+ import { TOOL_DEFINITIONS } from './tool-definitions.js';
10
+ import { HANDLERS, fail } from './handlers.js';
10
11
  // MCP server runs as a child of `claude`, not the Beecork daemon.
11
12
  // It communicates with the daemon via shared SQLite + signal files.
12
- function ok(text) { return { content: [{ type: 'text', text }] }; }
13
- function fail(text) { return { content: [{ type: 'text', text }], isError: true }; }
14
- const BEECORK_HOME = process.env.BEECORK_HOME || path.join(os.homedir(), '.beecork');
15
- const DB_PATH = path.join(BEECORK_HOME, 'memory.db');
16
- const CRON_RELOAD_SIGNAL = path.join(BEECORK_HOME, '.cron-reload');
17
- const WATCHER_RELOAD_SIGNAL = path.join(BEECORK_HOME, '.watcher-reload');
18
- // Cached singleton connection (lives for the MCP server's lifetime)
13
+ // Path helpers from util/paths.ts so daemon + MCP child always agree on locations,
14
+ // including when BEECORK_HOME env is set for isolation/testing.
15
+ const DB_PATH = getDbPath();
16
+ const CRON_RELOAD_SIGNAL = getCronReloadSignalPath();
17
+ const WATCHER_RELOAD_SIGNAL = getWatcherReloadSignalPath();
18
+ // Cached singleton connection (lives for the MCP server's lifetime).
19
19
  let cachedDb = null;
20
20
  function getDb() {
21
21
  if (cachedDb)
@@ -27,867 +27,43 @@ function getDb() {
27
27
  cachedDb = db;
28
28
  return db;
29
29
  }
30
- // Clean up on process exit
31
30
  process.on('exit', () => { cachedDb?.close(); });
32
- // Cached media generators (lazy singleton, like cachedDb)
31
+ // Cached media generators (lazy singleton).
33
32
  let cachedGenerators = null;
34
33
  async function getGenerators() {
35
34
  if (!cachedGenerators) {
36
- const config = getConfig();
35
+ const { getConfig } = await import('../config.js');
37
36
  const { initMediaGenerators } = await import('../media/index.js');
38
- cachedGenerators = initMediaGenerators(config.mediaGenerators);
37
+ cachedGenerators = initMediaGenerators(getConfig().mediaGenerators);
39
38
  }
40
39
  return cachedGenerators;
41
40
  }
42
- const MAX_CONTENT_LENGTH = 10240; // 10KB
43
- const MAX_NAME_LENGTH = 256;
44
- const VALID_SCHEDULE_TYPES = ['at', 'every', 'cron'];
45
- // Tab name validation is centralized in validateTabName() from config.ts
46
41
  function signalCronReload() {
47
42
  fs.writeFileSync(CRON_RELOAD_SIGNAL, String(Date.now()));
48
43
  }
49
44
  function signalWatcherReload() {
50
45
  fs.writeFileSync(WATCHER_RELOAD_SIGNAL, String(Date.now()));
51
46
  }
52
- import { VERSION } from '../version.js';
53
- import { getConfig, validateTabName } from '../config.js';
54
- import { createTabRecord } from '../db/index.js';
55
- async function handleMediaGeneration(db, mediaType, args) {
56
- const { prompt, style, duration, provider } = args;
57
- if (!prompt)
58
- return fail('Missing prompt');
59
- const generators = await getGenerators();
60
- const gen = provider
61
- ? generators.find(g => g.id === provider)
62
- : generators.find(g => g.supportedTypes.includes(mediaType));
63
- if (!gen)
64
- return fail(`No ${mediaType} generator configured. Run: beecork media`);
65
- try {
66
- const result = await gen.generate(mediaType, prompt, { style, duration });
67
- 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');
68
- return ok(`Generated ${mediaType}: ${result.filePath}`);
69
- }
70
- catch (err) {
71
- return fail(`${mediaType} generation failed: ${err instanceof Error ? err.message : String(err)}`);
72
- }
73
- }
74
47
  const server = new Server({ name: 'beecork', version: VERSION }, { capabilities: { tools: {} } });
75
48
  server.setRequestHandler(ListToolsRequestSchema, async () => ({
76
- tools: [
77
- {
78
- name: 'beecork_remember',
79
- 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.',
80
- inputSchema: {
81
- type: 'object',
82
- properties: {
83
- content: { type: 'string', description: 'The fact or information to remember' },
84
- 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)' },
85
- category: { type: 'string', description: 'For global scope: people, preferences, routines, or general' },
86
- },
87
- required: ['content'],
88
- },
89
- },
90
- {
91
- name: 'beecork_task_create',
92
- description: 'Schedule a task that will run automatically. The task sends a message to a Beecork tab at the scheduled time.',
93
- inputSchema: {
94
- type: 'object',
95
- properties: {
96
- name: { type: 'string', description: 'Human-readable name for the task' },
97
- scheduleType: {
98
- type: 'string',
99
- enum: ['at', 'every', 'cron'],
100
- description: '"at" = one-time ISO datetime, "every" = interval like "30m"/"2h"/"1d", "cron" = cron expression like "0 9 * * 1"',
101
- },
102
- schedule: { type: 'string', description: 'The schedule value (ISO datetime, interval, or cron expression)' },
103
- message: { type: 'string', description: 'The prompt/message to send when the task fires' },
104
- tabName: { type: 'string', description: 'Which tab to send the message to (default: "default")' },
105
- },
106
- required: ['name', 'scheduleType', 'schedule', 'message'],
107
- },
108
- },
109
- {
110
- name: 'beecork_task_list',
111
- description: 'List all scheduled tasks.',
112
- inputSchema: {
113
- type: 'object',
114
- properties: {},
115
- },
116
- },
117
- {
118
- name: 'beecork_task_delete',
119
- description: 'Delete a scheduled task by ID.',
120
- inputSchema: {
121
- type: 'object',
122
- properties: {
123
- id: { type: 'string', description: 'The ID of the task to delete' },
124
- },
125
- required: ['id'],
126
- },
127
- },
128
- // Backward-compatible aliases
129
- {
130
- name: 'beecork_cron_create',
131
- description: '[Alias for beecork_task_create] Schedule a task that will run automatically.',
132
- inputSchema: {
133
- type: 'object',
134
- properties: {
135
- name: { type: 'string', description: 'Human-readable name for the job' },
136
- scheduleType: { type: 'string', enum: ['at', 'every', 'cron'] },
137
- schedule: { type: 'string' },
138
- message: { type: 'string' },
139
- tabName: { type: 'string' },
140
- },
141
- required: ['name', 'scheduleType', 'schedule', 'message'],
142
- },
143
- },
144
- {
145
- name: 'beecork_cron_list',
146
- description: '[Alias for beecork_task_list] List all scheduled tasks.',
147
- inputSchema: { type: 'object', properties: {} },
148
- },
149
- {
150
- name: 'beecork_cron_delete',
151
- description: '[Alias for beecork_task_delete] Delete a scheduled task by ID.',
152
- inputSchema: {
153
- type: 'object',
154
- properties: { id: { type: 'string' } },
155
- required: ['id'],
156
- },
157
- },
158
- // Watcher tools
159
- {
160
- name: 'beecork_watch_create',
161
- description: 'Create a watcher that periodically runs a check command and triggers an action when a condition is met.',
162
- inputSchema: {
163
- type: 'object',
164
- properties: {
165
- name: { type: 'string', description: 'Human-readable name for the watcher' },
166
- description: { type: 'string', description: 'What to watch (natural language)' },
167
- checkCommand: { type: 'string', description: 'Shell command to run for checking' },
168
- condition: { type: 'string', description: 'When to trigger: "contains X", "not contains X", "> N", "< N", "any", "error"' },
169
- action: { type: 'string', enum: ['notify', 'fix', 'delegate'], description: 'What to do when triggered (default: notify)' },
170
- actionDetails: { type: 'string', description: 'For fix: command to run. For delegate: tab name + message.' },
171
- schedule: { type: 'string', description: 'How often: "every 5m", "every 1h", or cron expression' },
172
- },
173
- required: ['name', 'checkCommand', 'condition', 'schedule'],
174
- },
175
- },
176
- {
177
- name: 'beecork_watch_list',
178
- description: 'List all watchers with their status.',
179
- inputSchema: { type: 'object', properties: {} },
180
- },
181
- {
182
- name: 'beecork_watch_delete',
183
- description: 'Delete a watcher by ID.',
184
- inputSchema: {
185
- type: 'object',
186
- properties: {
187
- id: { type: 'string', description: 'The ID of the watcher to delete' },
188
- },
189
- required: ['id'],
190
- },
191
- },
192
- {
193
- name: 'beecork_tab_create',
194
- description: 'Create a new virtual tab for a separate task context.',
195
- inputSchema: {
196
- type: 'object',
197
- properties: {
198
- name: { type: 'string', description: 'Name for the new tab' },
199
- workingDir: { type: 'string', description: 'Working directory for the tab (default: ~/)' },
200
- template: { type: 'string', description: 'Name of a tab template to apply' },
201
- systemPrompt: { type: 'string', description: 'Custom system prompt for this tab' },
202
- },
203
- required: ['name'],
204
- },
205
- },
206
- {
207
- name: 'beecork_tab_list',
208
- description: 'List all virtual tabs and their current status.',
209
- inputSchema: {
210
- type: 'object',
211
- properties: {},
212
- },
213
- },
214
- {
215
- name: 'beecork_send_message',
216
- description: 'Send a message to another tab. The message will be processed as if a user sent it.',
217
- inputSchema: {
218
- type: 'object',
219
- properties: {
220
- tabName: { type: 'string', description: 'Name of the tab to send the message to' },
221
- message: { type: 'string', description: 'The message/prompt to send' },
222
- },
223
- required: ['tabName', 'message'],
224
- },
225
- },
226
- {
227
- name: 'beecork_recall',
228
- description: 'Search long-term memory for relevant facts, decisions, or outcomes from past sessions.',
229
- inputSchema: {
230
- type: 'object',
231
- properties: {
232
- query: { type: 'string', description: 'Search term to find relevant memories' },
233
- limit: { type: 'number', description: 'Max results to return (default: 10)' },
234
- },
235
- required: ['query'],
236
- },
237
- },
238
- {
239
- name: 'beecork_notify',
240
- description: 'Send a notification to the user mid-task without ending the session. Use for progress updates, questions, or intermediate results.',
241
- inputSchema: {
242
- type: 'object',
243
- properties: {
244
- message: { type: 'string', description: 'The notification message to send' },
245
- urgent: { type: 'boolean', description: 'If true, sends with higher priority (default: false)' },
246
- provider: { type: 'string', description: 'Optional: send via specific provider (pushover, ntfy, webhook-notify). Omit to broadcast to all.' },
247
- },
248
- required: ['message'],
249
- },
250
- },
251
- {
252
- name: 'beecork_status',
253
- description: 'Get current Beecork system status: active tabs, cron jobs, uptime.',
254
- inputSchema: {
255
- type: 'object',
256
- properties: {},
257
- },
258
- },
259
- {
260
- name: 'beecork_send_media',
261
- description: 'Send a media file (image, document, etc.) to the user via the active channel. The file must exist on disk.',
262
- inputSchema: {
263
- type: 'object',
264
- properties: {
265
- filePath: { type: 'string', description: 'Absolute path to the file to send' },
266
- caption: { type: 'string', description: 'Optional caption for the media' },
267
- tabName: { type: 'string', description: 'Tab name to determine which channel/peer to send to (optional, defaults to current)' },
268
- },
269
- required: ['filePath'],
270
- },
271
- },
272
- {
273
- name: 'beecork_channels',
274
- description: 'List active channels and their capabilities',
275
- inputSchema: { type: 'object', properties: {} },
276
- },
277
- {
278
- name: 'beecork_cost',
279
- description: 'Show cost tracking: spend per tab, today, and rolling 30 days',
280
- inputSchema: {
281
- type: 'object',
282
- properties: {
283
- tabName: { type: 'string', description: 'Optional: show cost for a specific tab only' },
284
- },
285
- },
286
- },
287
- {
288
- name: 'beecork_failed_deliveries',
289
- description: 'Show messages that failed to deliver after retries',
290
- inputSchema: { type: 'object', properties: {} },
291
- },
292
- {
293
- name: 'beecork_activity',
294
- description: 'Show activity summary for the last N hours',
295
- inputSchema: {
296
- type: 'object',
297
- properties: {
298
- hours: { type: 'number', description: 'Number of hours to look back (default 24)' },
299
- },
300
- },
301
- },
302
- {
303
- name: 'beecork_export_data',
304
- description: 'Export cost and activity data as JSON',
305
- inputSchema: {
306
- type: 'object',
307
- properties: {
308
- type: { type: 'string', enum: ['costs', 'messages', 'crons'], description: 'Data type to export' },
309
- days: { type: 'number', description: 'Number of days to export (default 30)' },
310
- },
311
- required: ['type'],
312
- },
313
- },
314
- {
315
- name: 'beecork_handoff',
316
- description: 'Get session handoff info for a tab — session ID, working dir, and recent context for resuming in terminal',
317
- inputSchema: {
318
- type: 'object',
319
- properties: {
320
- tabName: { type: 'string', description: 'Tab name to export' },
321
- },
322
- required: ['tabName'],
323
- },
324
- },
325
- {
326
- name: 'beecork_delegate',
327
- description: 'Delegate a task to another tab. The target tab runs independently and the result is automatically sent back to the source tab when complete. Use this for tasks that need their own working directory or context.',
328
- inputSchema: {
329
- type: 'object',
330
- properties: {
331
- tabName: { type: 'string', description: 'Target tab name (created if it does not exist)' },
332
- message: { type: 'string', description: 'The task to delegate' },
333
- returnToTab: { type: 'string', description: 'Tab to send results back to (defaults to current tab)' },
334
- },
335
- required: ['tabName', 'message'],
336
- },
337
- },
338
- {
339
- name: 'beecork_delegation_status',
340
- description: 'Check status of delegated tasks',
341
- inputSchema: {
342
- type: 'object',
343
- properties: {
344
- tabName: { type: 'string', description: 'Filter by source tab (optional)' },
345
- },
346
- },
347
- },
348
- {
349
- name: 'beecork_project_create',
350
- description: 'Register a new folder in the workspace',
351
- inputSchema: {
352
- type: 'object',
353
- properties: {
354
- name: { type: 'string', description: 'Folder name' },
355
- path: { type: 'string', description: 'Optional: custom path. Defaults to workspace root.' },
356
- },
357
- required: ['name'],
358
- },
359
- },
360
- {
361
- name: 'beecork_project_list',
362
- description: 'List all known folders and categories',
363
- inputSchema: { type: 'object', properties: {} },
364
- },
365
- {
366
- name: 'beecork_close_tab',
367
- description: 'Permanently close a tab — deletes all history and session. Cannot be undone.',
368
- inputSchema: {
369
- type: 'object',
370
- properties: {
371
- tabName: { type: 'string', description: 'Tab to permanently close' },
372
- },
373
- required: ['tabName'],
374
- },
375
- },
376
- {
377
- name: 'beecork_generate_image',
378
- description: 'Generate an image from a text prompt using the configured image provider (DALL-E, Stable Diffusion, etc.)',
379
- inputSchema: {
380
- type: 'object',
381
- properties: {
382
- prompt: { type: 'string', description: 'Image description' },
383
- style: { type: 'string', description: 'Optional style (e.g., "hd", "vivid", "natural")' },
384
- provider: { type: 'string', description: 'Optional: specific provider to use' },
385
- },
386
- required: ['prompt'],
387
- },
388
- },
389
- {
390
- name: 'beecork_generate_video',
391
- description: 'Generate a video from a text prompt using the configured video provider (Runway, Veo, Kling)',
392
- inputSchema: {
393
- type: 'object',
394
- properties: {
395
- prompt: { type: 'string', description: 'Video description' },
396
- duration: { type: 'number', description: 'Duration in seconds (default 5)' },
397
- provider: { type: 'string', description: 'Optional: specific provider' },
398
- },
399
- required: ['prompt'],
400
- },
401
- },
402
- {
403
- name: 'beecork_generate_audio',
404
- description: 'Generate audio (music or sound effects) from a text prompt',
405
- inputSchema: {
406
- type: 'object',
407
- properties: {
408
- prompt: { type: 'string', description: 'Audio description' },
409
- type: { type: 'string', enum: ['music', 'sfx'], description: 'Music or sound effect' },
410
- style: { type: 'string', description: 'Optional: music genre or style' },
411
- provider: { type: 'string', description: 'Optional: specific provider' },
412
- },
413
- required: ['prompt'],
414
- },
415
- },
416
- {
417
- name: 'beecork_media_providers',
418
- description: 'List configured media generation providers and their capabilities',
419
- inputSchema: { type: 'object', properties: {} },
420
- },
421
- {
422
- name: 'beecork_knowledge',
423
- description: 'List all knowledge Beecork has about the current context (global + folder + tab)',
424
- inputSchema: {
425
- type: 'object',
426
- properties: {
427
- scope: { type: 'string', enum: ['global', 'project', 'tab', 'all'], description: 'Which layer to show: global, project (folder), tab, or all (default: all)' },
428
- },
429
- },
430
- },
431
- {
432
- name: 'beecork_capabilities',
433
- description: 'List available and enabled capability packs (email, calendar, github, etc.)',
434
- inputSchema: { type: 'object', properties: {} },
435
- },
436
- {
437
- name: 'beecork_history',
438
- description: 'Show activity timeline — what Beecork has been doing',
439
- inputSchema: {
440
- type: 'object',
441
- properties: {
442
- date: { type: 'string', description: 'Date filter (YYYY-MM-DD). Default: today' },
443
- tabName: { type: 'string', description: 'Filter by tab name' },
444
- limit: { type: 'number', description: 'Max events (default 50)' },
445
- },
446
- },
447
- },
448
- {
449
- name: 'beecork_replay',
450
- description: 'Re-run a past task by its event ID',
451
- inputSchema: {
452
- type: 'object',
453
- properties: {
454
- eventId: { type: 'string', description: 'Activity event ID to replay' },
455
- },
456
- required: ['eventId'],
457
- },
458
- },
459
- {
460
- name: 'beecork_store_search',
461
- description: 'Search the Beecork store for community packages (capabilities, media generators, channels)',
462
- inputSchema: {
463
- type: 'object',
464
- properties: {
465
- query: { type: 'string', description: 'Search query' },
466
- },
467
- required: ['query'],
468
- },
469
- },
470
- ],
49
+ tools: TOOL_DEFINITIONS,
471
50
  }));
472
51
  server.setRequestHandler(CallToolRequestSchema, async (request) => {
473
52
  const { name, arguments: args } = request.params;
53
+ const handler = HANDLERS[name];
54
+ if (!handler)
55
+ return fail(`Unknown tool: ${name}`);
474
56
  try {
475
- const db = getDb();
476
- switch (name) {
477
- case 'beecork_remember': {
478
- const { content, scope, category } = args;
479
- if (!content || content.length > MAX_CONTENT_LENGTH) {
480
- return fail(`Content is required and must be under ${MAX_CONTENT_LENGTH} characters.`);
481
- }
482
- if (scope && scope !== 'tab' && scope !== 'auto') {
483
- const { addKnowledge } = await import('../knowledge/index.js');
484
- // Determine tab info for project scope
485
- const currentTab = db.prepare("SELECT working_dir FROM tabs ORDER BY last_activity_at DESC LIMIT 1").get();
486
- addKnowledge(content, scope, { category, projectPath: currentTab?.working_dir, tabName: undefined });
487
- return ok(`Remembered (${scope}): ${content.slice(0, 100)}`);
488
- }
489
- // Default: existing tab memory behavior
490
- const fullContent = category ? `[${category}] ${content}` : content;
491
- // Dedup: skip insert if an identical fact already exists
492
- const existing = db.prepare('SELECT id FROM memories WHERE content = ? AND tab_name IS NULL LIMIT 1').get(fullContent);
493
- if (existing) {
494
- return ok(`Already remembered: "${fullContent}"`);
495
- }
496
- db.prepare('INSERT INTO memories (content, source) VALUES (?, ?)').run(fullContent, 'tool');
497
- return ok(`Remembered: "${fullContent}"`);
498
- }
499
- case 'beecork_task_create':
500
- case 'beecork_cron_create': {
501
- const { name: jobName, scheduleType, schedule, message, tabName } = args;
502
- if (!jobName || jobName.length > MAX_NAME_LENGTH) {
503
- return fail(`Task name is required and must be under ${MAX_NAME_LENGTH} characters.`);
504
- }
505
- if (!VALID_SCHEDULE_TYPES.includes(scheduleType)) {
506
- return fail(`Invalid scheduleType "${scheduleType}". Must be one of: ${VALID_SCHEDULE_TYPES.join(', ')}`);
507
- }
508
- if (!message || message.length > MAX_CONTENT_LENGTH) {
509
- return fail(`Message is required and must be under ${MAX_CONTENT_LENGTH} characters.`);
510
- }
511
- const id = uuidv4();
512
- const tab = tabName || 'default';
513
- if (tab !== 'default') {
514
- const tabError = validateTabName(tab);
515
- if (tabError) {
516
- return fail(tabError);
517
- }
518
- }
519
- db.prepare(`INSERT INTO tasks (id, name, schedule_type, schedule, tab_name, message, enabled, user_id, created_at)
520
- VALUES (?, ?, ?, ?, ?, ?, 1, 'local', ?)`).run(id, jobName, scheduleType, schedule, tab, message, new Date().toISOString());
521
- signalCronReload();
522
- return ok(`Task created: "${jobName}" (${scheduleType}: ${schedule}) -> tab:${tab}\nID: ${id}`);
523
- }
524
- case 'beecork_task_list':
525
- case 'beecork_cron_list': {
526
- const jobs = db.prepare('SELECT * FROM tasks WHERE user_id = ? ORDER BY created_at').all('local');
527
- if (jobs.length === 0) {
528
- return ok('No tasks scheduled.');
529
- }
530
- const lines = jobs.map(j => `- ${j.name} [${j.enabled ? 'enabled' : 'disabled'}] (${j.schedule_type}: ${j.schedule}) -> tab:${j.tab_name} (ID: ${j.id})`);
531
- return ok(lines.join('\n'));
532
- }
533
- case 'beecork_task_delete':
534
- case 'beecork_cron_delete': {
535
- const { id } = args;
536
- const result = db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
537
- if (result.changes === 0) {
538
- return ok(`No task found with ID: ${id}`);
539
- }
540
- signalCronReload();
541
- return ok(`Deleted task: ${id}`);
542
- }
543
- case 'beecork_watch_create': {
544
- const { name: watchName, description: watchDesc, checkCommand, condition, action, actionDetails, schedule: watchSchedule } = args;
545
- if (!watchName || watchName.length > MAX_NAME_LENGTH) {
546
- return fail(`Watcher name is required and must be under ${MAX_NAME_LENGTH} characters.`);
547
- }
548
- if (!checkCommand)
549
- return fail('checkCommand is required.');
550
- if (!condition)
551
- return fail('condition is required.');
552
- if (!watchSchedule)
553
- return fail('schedule is required.');
554
- const watchId = uuidv4();
555
- db.prepare(`INSERT INTO watchers (id, name, description, check_command, condition, action, action_details, schedule)
556
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(watchId, watchName, watchDesc || null, checkCommand, condition, action || 'notify', actionDetails || null, watchSchedule);
557
- signalWatcherReload();
558
- return ok(`Watcher created: "${watchName}" (${watchSchedule})\nID: ${watchId}`);
559
- }
560
- case 'beecork_watch_list': {
561
- const watchers = db.prepare('SELECT * FROM watchers ORDER BY created_at').all();
562
- if (watchers.length === 0) {
563
- return ok('No watchers configured.');
564
- }
565
- const watchLines = watchers.map(w => `- ${w.name} [${w.enabled ? 'enabled' : 'disabled'}] ${w.schedule} | action: ${w.action} | triggers: ${w.trigger_count} (ID: ${w.id})`);
566
- return ok(watchLines.join('\n'));
567
- }
568
- case 'beecork_watch_delete': {
569
- const { id: watchDelId } = args;
570
- const watchDelResult = db.prepare('DELETE FROM watchers WHERE id = ?').run(watchDelId);
571
- if (watchDelResult.changes === 0) {
572
- return ok(`No watcher found with ID: ${watchDelId}`);
573
- }
574
- signalWatcherReload();
575
- return ok(`Deleted watcher: ${watchDelId}`);
576
- }
577
- case 'beecork_tab_create': {
578
- const { name: tabName, workingDir, template: templateName, systemPrompt } = args;
579
- if (!tabName) {
580
- return fail('Tab name is required.');
581
- }
582
- const tabCreateError = validateTabName(tabName);
583
- if (tabCreateError) {
584
- return fail(tabCreateError);
585
- }
586
- // Apply template if specified
587
- const config = getConfig();
588
- const template = templateName ? config.tabTemplates?.[templateName] : undefined;
589
- if (templateName && !template) {
590
- return fail(`Template "${templateName}" not found. Available: ${Object.keys(config.tabTemplates || {}).join(', ') || 'none'}`);
591
- }
592
- // Explicit args take precedence over template values
593
- let dir = workingDir || template?.workingDir || os.homedir();
594
- dir = dir.startsWith('~') ? dir.replace('~', os.homedir()) : dir;
595
- dir = path.resolve(dir);
596
- if (!fs.existsSync(dir) || !fs.statSync(dir).isDirectory()) {
597
- return fail(`Working directory does not exist or is not a directory: ${dir}`);
598
- }
599
- const tabSystemPrompt = systemPrompt || template?.systemPrompt || null;
600
- const result = createTabRecord(db, { name: tabName, workingDir: dir, systemPrompt: tabSystemPrompt });
601
- if (!result.created) {
602
- return ok(`Tab "${tabName}" already exists.`);
603
- }
604
- const parts = [`Created tab: "${tabName}" (working dir: ${dir})`];
605
- if (tabSystemPrompt)
606
- parts.push(`System prompt: "${tabSystemPrompt.slice(0, 100)}${tabSystemPrompt.length > 100 ? '...' : ''}"`);
607
- if (templateName)
608
- parts.push(`Template: ${templateName}`);
609
- return ok(parts.join('\n'));
610
- }
611
- case 'beecork_tab_list': {
612
- const tabs = db.prepare('SELECT name, status, working_dir, last_activity_at FROM tabs ORDER BY last_activity_at DESC').all();
613
- if (tabs.length === 0) {
614
- return ok('No tabs.');
615
- }
616
- const lines = tabs.map(t => `- ${t.name} [${t.status}] dir:${t.working_dir} last:${t.last_activity_at}`);
617
- return ok(lines.join('\n'));
618
- }
619
- case 'beecork_send_message': {
620
- const { tabName, message } = args;
621
- if (!tabName || !message) {
622
- return fail('Both tabName and message are required.');
623
- }
624
- if (tabName !== 'default') {
625
- const sendTabError = validateTabName(tabName);
626
- if (sendTabError) {
627
- return fail(sendTabError);
628
- }
629
- }
630
- if (message.length > MAX_CONTENT_LENGTH) {
631
- return fail(`Message must be under ${MAX_CONTENT_LENGTH} characters.`);
632
- }
633
- db.prepare('INSERT INTO pending_messages (tab_name, message) VALUES (?, ?)').run(tabName, message);
634
- return ok(`Message queued for tab "${tabName}".`);
635
- }
636
- case 'beecork_recall': {
637
- const { query, limit } = args;
638
- const maxResults = Math.min(limit ?? 10, 50);
639
- const memories = db.prepare('SELECT content, tab_name, source, created_at FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT ?').all(`%${query}%`, maxResults);
640
- // Also search knowledge files
641
- const { searchKnowledge } = await import('../knowledge/index.js');
642
- const knowledgeResults = searchKnowledge(query);
643
- // Merge and return
644
- const allResults = [
645
- ...knowledgeResults.map(k => k.content),
646
- ...memories.map(m => m.content),
647
- ];
648
- if (allResults.length === 0) {
649
- return ok(`No relevant knowledge found matching "${query}".`);
650
- }
651
- return ok(allResults.join('\n---\n'));
652
- }
653
- case 'beecork_notify': {
654
- const { message, urgent } = args;
655
- if (!message) {
656
- return fail('Message is required.');
657
- }
658
- const prefix = urgent ? '🚨 ' : '';
659
- db.prepare("INSERT INTO pending_messages (tab_name, message, type) VALUES ('_notify', ?, 'notification')").run(prefix + message);
660
- return ok(`Notification sent to user.`);
661
- }
662
- case 'beecork_status': {
663
- const tabCount = db.prepare('SELECT COUNT(*) as c FROM tabs').get().c;
664
- const activeTabs = db.prepare("SELECT COUNT(*) as c FROM tabs WHERE status = 'running'").get().c;
665
- const taskCount = db.prepare('SELECT COUNT(*) as c FROM tasks WHERE enabled = 1').get().c;
666
- const watcherCount = db.prepare('SELECT COUNT(*) as c FROM watchers WHERE enabled = 1').get().c;
667
- const memoryCount = db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
668
- const lines = [
669
- `Tabs: ${tabCount} total, ${activeTabs} running`,
670
- `Tasks: ${taskCount} active`,
671
- `Watchers: ${watcherCount} active`,
672
- `Memories: ${memoryCount} stored`,
673
- ];
674
- return ok(lines.join('\n'));
675
- }
676
- case 'beecork_send_media': {
677
- const { filePath, caption, tabName } = args;
678
- if (!fs.existsSync(filePath)) {
679
- return fail(`File not found: ${filePath}`);
680
- }
681
- // Store as pending message with media flag
682
- const tab = tabName || 'default';
683
- db.prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run(tab, JSON.stringify({ type: 'media', filePath, caption }), 'media');
684
- return ok(`Media queued for sending: ${filePath}`);
685
- }
686
- case 'beecork_channels': {
687
- // Read channel info from config to show configured channels
688
- const config = getConfig();
689
- const channels = [];
690
- if (config.telegram?.token) {
691
- channels.push({ id: 'telegram', name: 'Telegram', streaming: true, media: true });
692
- }
693
- if (config.whatsapp?.enabled) {
694
- channels.push({ id: 'whatsapp', name: 'WhatsApp', streaming: false, media: true });
695
- }
696
- if (config.webhook?.enabled) {
697
- channels.push({ id: 'webhook', name: 'Webhook', streaming: false, media: false });
698
- }
699
- if (config.discord?.token) {
700
- channels.push({ id: 'discord', name: 'Discord', streaming: false, media: true });
701
- }
702
- return ok(JSON.stringify(channels, null, 2));
703
- }
704
- case 'beecork_cost': {
705
- const { tabName } = (args || {});
706
- const { getCostSummary, formatCostSummary } = await import('../observability/analytics.js');
707
- const summary = getCostSummary();
708
- if (tabName) {
709
- const tab = summary.perTab.find(t => t.name === tabName);
710
- if (!tab)
711
- return fail(`Tab "${tabName}" not found`);
712
- return ok(`Tab "${tabName}": $${tab.cost.toFixed(4)} (${tab.messages} messages)`);
713
- }
714
- return ok(formatCostSummary(summary));
715
- }
716
- case 'beecork_failed_deliveries': {
717
- const failed = 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();
718
- if (failed.length === 0) {
719
- return ok('No failed deliveries.');
720
- }
721
- const failedLines = failed.map((f) => `[${f.created_at}] tab:${f.tab_name} retries:${f.retry_count}\n ${f.content.slice(0, 200)}`);
722
- return ok(failedLines.join('\n\n'));
723
- }
724
- case 'beecork_activity': {
725
- const hours = args?.hours || 24;
726
- const { getActivitySummary, formatActivitySummary } = await import('../observability/analytics.js');
727
- return ok(formatActivitySummary(getActivitySummary(hours)));
728
- }
729
- case 'beecork_export_data': {
730
- const { type: dataType, days = 30 } = args;
731
- const since = new Date(Date.now() - days * 86400000).toISOString();
732
- let data;
733
- switch (dataType) {
734
- case 'costs':
735
- data = 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);
736
- break;
737
- case 'messages':
738
- data = 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);
739
- break;
740
- case 'crons':
741
- data = db.prepare("SELECT * FROM tasks ORDER BY created_at").all();
742
- break;
743
- default:
744
- return fail('Invalid type. Use: costs, messages, or crons');
745
- }
746
- return ok(JSON.stringify(data, null, 2));
747
- }
748
- case 'beecork_handoff': {
749
- const { tabName } = args;
750
- const tab = db.prepare('SELECT * FROM tabs WHERE name = ?').get(tabName);
751
- if (!tab)
752
- return fail(`Tab "${tabName}" not found`);
753
- const messages = db.prepare('SELECT role, content FROM messages WHERE tab_id = ? ORDER BY created_at DESC LIMIT 5').all(tab.id);
754
- const info = {
755
- sessionId: tab.session_id,
756
- workingDir: tab.working_dir,
757
- status: tab.status,
758
- resumeCommand: `beecork attach ${tabName}`,
759
- manualCommand: `cd ${tab.working_dir} && claude --session-id ${tab.session_id} --resume`,
760
- recentMessages: messages.reverse().map((m) => ({ role: m.role, preview: m.content.slice(0, 200) })),
761
- };
762
- return ok(JSON.stringify(info, null, 2));
763
- }
764
- case 'beecork_delegate': {
765
- const { tabName, message, returnToTab } = args;
766
- try {
767
- const { createDelegation } = await import('../delegation/manager.js');
768
- const delegation = createDelegation(returnToTab || 'default', tabName, message, returnToTab);
769
- // Queue the message for the target tab
770
- db.prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run(tabName, message, 'delegation');
771
- return ok(`Delegated to tab "${tabName}". Result will be sent back to "${delegation.returnToTab}" when complete.\n\nDelegation ID: ${delegation.id}`);
772
- }
773
- catch (err) {
774
- return fail(`Delegation failed: ${err instanceof Error ? err.message : String(err)}`);
775
- }
776
- }
777
- case 'beecork_delegation_status': {
778
- const { tabName } = (args || {});
779
- const { getPendingDelegations } = await import('../delegation/manager.js');
780
- const delegations = getPendingDelegations(tabName);
781
- if (delegations.length === 0) {
782
- return ok('No pending delegations.');
783
- }
784
- const lines = delegations.map(d => `${d.sourceTab} → ${d.targetTab} [${d.status}] (depth ${d.depth})\n "${d.message.slice(0, 100)}"`);
785
- return ok(lines.join('\n\n'));
786
- }
787
- case 'beecork_project_create': {
788
- const { name, path: customPath } = args;
789
- const { createProject } = await import('../projects/index.js');
790
- const project = createProject(name, customPath);
791
- return ok(`Folder "${name}" registered at ${project.path}`);
792
- }
793
- case 'beecork_project_list': {
794
- const { listProjects } = await import('../projects/index.js');
795
- const projects = listProjects();
796
- if (projects.length === 0)
797
- return ok('No folders discovered. Create one with beecork_project_create.');
798
- const lines = projects.map(p => `${p.type === 'category' ? '📁' : '📦'} ${p.name} — ${p.path}`);
799
- return ok(lines.join('\n'));
800
- }
801
- case 'beecork_close_tab': {
802
- const { tabName } = args;
803
- // Mark tab as stopped so daemon's recovery loop cleans up the subprocess
804
- db.prepare("UPDATE tabs SET status = 'stopped', pid = NULL WHERE name = ? AND status = 'running'").run(tabName);
805
- const { closeTab } = await import('../projects/index.js');
806
- const closed = closeTab(tabName);
807
- return closed ? ok(`Tab "${tabName}" permanently closed.`) : fail(`Tab "${tabName}" not found.`);
808
- }
809
- case 'beecork_generate_image': return handleMediaGeneration(db, 'image', args || {});
810
- case 'beecork_generate_video': return handleMediaGeneration(db, 'video', args || {});
811
- case 'beecork_generate_audio': return handleMediaGeneration(db, 'audio', args || {});
812
- case 'beecork_media_providers': {
813
- const generators = await getGenerators();
814
- if (generators.length === 0) {
815
- return ok('No media generators configured. Add mediaGenerators to config.json.');
816
- }
817
- const lines = generators.map(g => `- ${g.name} (${g.id}): ${g.supportedTypes.join(', ')}`);
818
- return ok(lines.join('\n'));
819
- }
820
- case 'beecork_knowledge': {
821
- const { scope: knowledgeScope } = (args || {});
822
- const { getGlobalKnowledge, getProjectKnowledge, getTabKnowledge, getAllKnowledge, formatKnowledgeForContext } = await import('../knowledge/index.js');
823
- let entries;
824
- if (knowledgeScope === 'global') {
825
- entries = getGlobalKnowledge();
826
- }
827
- else if (knowledgeScope === 'project') {
828
- const currentTab = db.prepare("SELECT working_dir FROM tabs ORDER BY last_activity_at DESC LIMIT 1").get();
829
- entries = currentTab ? getProjectKnowledge(currentTab.working_dir) : [];
830
- }
831
- else if (knowledgeScope === 'tab') {
832
- const currentTab = db.prepare("SELECT name FROM tabs ORDER BY last_activity_at DESC LIMIT 1").get();
833
- entries = currentTab ? getTabKnowledge(currentTab.name) : [];
834
- }
835
- else {
836
- entries = getAllKnowledge();
837
- }
838
- if (entries.length === 0) {
839
- return ok('No knowledge stored yet.');
840
- }
841
- return ok(formatKnowledgeForContext(entries));
842
- }
843
- case 'beecork_capabilities': {
844
- const { getAvailablePacks, isEnabled } = await import('../capabilities/index.js');
845
- const packs = getAvailablePacks();
846
- const capLines = packs.map(p => {
847
- const status = isEnabled(p.id) ? '✓ enabled' : '○ available';
848
- return `${status} | ${p.id} — ${p.name}: ${p.description}`;
849
- });
850
- return ok(capLines.join('\n'));
851
- }
852
- case 'beecork_history': {
853
- const { date, tabName, limit } = (args || {});
854
- const { getTimeline, formatTimeline } = await import('../timeline/index.js');
855
- const events = getTimeline({ date: date || new Date().toISOString().slice(0, 10), tabName, limit });
856
- return ok(formatTimeline(events));
857
- }
858
- case 'beecork_replay': {
859
- const { eventId } = args;
860
- const { getReplayInfo } = await import('../timeline/index.js');
861
- const info = getReplayInfo(eventId);
862
- if (!info)
863
- return fail('Event not found or not replayable.');
864
- db.prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run(info.tabName, info.message, 'replay');
865
- return ok(`Replaying in tab "${info.tabName}": ${info.message.slice(0, 200)}`);
866
- }
867
- case 'beecork_store_search': {
868
- const { query } = args;
869
- try {
870
- const response = await fetch(`https://registry.npmjs.org/-/v1/search?text=beecork+${encodeURIComponent(query)}&size=10`, { signal: AbortSignal.timeout(10000) });
871
- if (!response.ok)
872
- return fail('npm registry search failed');
873
- const data = await response.json();
874
- const packages = data.objects?.filter((o) => o.package.name.startsWith('beecork-')) || [];
875
- if (packages.length === 0)
876
- return ok(`No beecork packages found for "${query}". You can create one with: beecork channel create <name> or beecork media create <name>`);
877
- const lines = packages.map((o) => `${o.package.name}@${o.package.version} — ${o.package.description || 'No description'}`);
878
- return ok(`${packages.length} package(s):\n${lines.join('\n')}\n\nInstall: npm install -g <package-name>`);
879
- }
880
- catch {
881
- return fail('Failed to search npm registry');
882
- }
883
- }
884
- default:
885
- return fail(`Unknown tool: ${name}`);
886
- }
57
+ const ctx = {
58
+ db: getDb(),
59
+ signalCronReload,
60
+ signalWatcherReload,
61
+ getGenerators,
62
+ };
63
+ return await handler(ctx, args);
887
64
  }
888
65
  catch (err) {
889
- const errMsg = err instanceof Error ? err.message : String(err);
890
- return fail(`Beecork error: ${errMsg}`);
66
+ return fail(`Beecork error: ${err instanceof Error ? err.message : String(err)}`);
891
67
  }
892
68
  });
893
69
  async function main() {