beecork 1.4.10 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/channels/admin.d.ts +10 -0
- package/dist/channels/admin.js +20 -0
- package/dist/channels/command-handler.d.ts +2 -10
- package/dist/channels/command-handler.js +47 -73
- package/dist/channels/discord.d.ts +1 -3
- package/dist/channels/discord.js +28 -28
- package/dist/channels/loader.js +0 -1
- package/dist/channels/send-helpers.d.ts +19 -0
- package/dist/channels/send-helpers.js +21 -0
- package/dist/channels/telegram.d.ts +1 -9
- package/dist/channels/telegram.js +46 -71
- package/dist/channels/types.d.ts +2 -10
- package/dist/channels/voice-state.d.ts +29 -0
- package/dist/channels/voice-state.js +43 -0
- package/dist/channels/webhook.d.ts +1 -1
- package/dist/channels/webhook.js +68 -24
- package/dist/channels/whatsapp.d.ts +1 -3
- package/dist/channels/whatsapp.js +79 -74
- package/dist/cli/doctor.js +5 -2
- package/dist/cli/handoff.js +6 -6
- package/dist/config.d.ts +5 -1
- package/dist/config.js +17 -14
- package/dist/daemon.js +29 -17
- package/dist/dashboard/html.js +20 -8
- package/dist/dashboard/routes.d.ts +17 -0
- package/dist/dashboard/routes.js +559 -0
- package/dist/dashboard/server.js +33 -488
- package/dist/db/index.js +16 -2
- package/dist/db/migrations.js +44 -8
- package/dist/mcp/handlers.d.ts +37 -0
- package/dist/mcp/handlers.js +451 -0
- package/dist/mcp/server.js +25 -849
- package/dist/mcp/tool-definitions.d.ts +1225 -0
- package/dist/mcp/tool-definitions.js +364 -0
- package/dist/media/index.d.ts +2 -7
- package/dist/media/index.js +1 -1
- package/dist/observability/analytics.d.ts +7 -1
- package/dist/observability/analytics.js +25 -7
- package/dist/projects/index.d.ts +3 -2
- package/dist/projects/index.js +2 -2
- package/dist/projects/manager.d.ts +1 -3
- package/dist/projects/manager.js +26 -25
- package/dist/projects/router.d.ts +10 -0
- package/dist/projects/router.js +28 -0
- package/dist/session/manager.d.ts +4 -0
- package/dist/session/manager.js +48 -42
- package/dist/session/subprocess.d.ts +1 -0
- package/dist/session/subprocess.js +21 -0
- package/dist/session/tab-store.d.ts +28 -0
- package/dist/session/tab-store.js +77 -0
- package/dist/tasks/scheduler.d.ts +6 -0
- package/dist/tasks/scheduler.js +52 -13
- package/dist/tasks/store.js +6 -6
- package/dist/timeline/query.js +6 -2
- package/dist/types.d.ts +15 -0
- package/dist/util/paths.d.ts +1 -0
- package/dist/util/paths.js +4 -1
- package/dist/util/rate-limiter.js +8 -0
- package/dist/util/text.d.ts +21 -1
- package/dist/util/text.js +25 -1
- package/dist/watchers/scheduler.js +2 -3
- package/package.json +1 -1
- package/dist/users/index.d.ts +0 -2
- package/dist/users/index.js +0 -1
- package/dist/users/service.d.ts +0 -17
- package/dist/users/service.js +0 -46
package/dist/db/migrations.js
CHANGED
|
@@ -146,8 +146,12 @@ const MIGRATIONS = [
|
|
|
146
146
|
up: '',
|
|
147
147
|
},
|
|
148
148
|
{
|
|
149
|
+
// NOTE: this migration is destructive — any user-created project rows from
|
|
150
|
+
// before v13 are lost. discoverProjects() repopulates the table on next
|
|
151
|
+
// daemon start, so in practice only manually-created projects are affected.
|
|
152
|
+
// Future destructive migrations should copy data into the new shape instead.
|
|
149
153
|
version: 13,
|
|
150
|
-
description: 'Recreate projects table with new schema',
|
|
154
|
+
description: 'Recreate projects table with new schema (DESTRUCTIVE — see comment)',
|
|
151
155
|
up: `DROP TABLE IF EXISTS projects;
|
|
152
156
|
CREATE TABLE IF NOT EXISTS projects (
|
|
153
157
|
id TEXT PRIMARY KEY,
|
|
@@ -238,6 +242,29 @@ const MIGRATIONS = [
|
|
|
238
242
|
CREATE INDEX IF NOT EXISTS idx_memories_tab_name ON memories(tab_name, created_at);
|
|
239
243
|
`,
|
|
240
244
|
},
|
|
245
|
+
{
|
|
246
|
+
version: 23,
|
|
247
|
+
description: 'Index routing_preferences.hit_count for fast WHERE/ORDER BY in routing hot path',
|
|
248
|
+
up: 'CREATE INDEX IF NOT EXISTS idx_routing_prefs_hits ON routing_preferences(hit_count DESC)',
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
version: 24,
|
|
252
|
+
description: 'Rip out multi-user scaffolding (never enforced; everything was hardcoded user_id="local")',
|
|
253
|
+
up: `
|
|
254
|
+
DROP TABLE IF EXISTS identities;
|
|
255
|
+
DROP TABLE IF EXISTS users;
|
|
256
|
+
DROP INDEX IF EXISTS idx_cron_jobs_user;
|
|
257
|
+
ALTER TABLE tabs DROP COLUMN user_id;
|
|
258
|
+
ALTER TABLE memories DROP COLUMN user_id;
|
|
259
|
+
ALTER TABLE tasks DROP COLUMN user_id;
|
|
260
|
+
ALTER TABLE pending_messages DROP COLUMN user_id;
|
|
261
|
+
`,
|
|
262
|
+
},
|
|
263
|
+
{
|
|
264
|
+
version: 25,
|
|
265
|
+
description: 'Drop idx_memories_content — leading-wildcard LIKE cannot use a B-tree index',
|
|
266
|
+
up: 'DROP INDEX IF EXISTS idx_memories_content',
|
|
267
|
+
},
|
|
241
268
|
];
|
|
242
269
|
export function runMigrations(db) {
|
|
243
270
|
// Ensure schema_version table exists
|
|
@@ -268,11 +295,20 @@ export function runMigrations(db) {
|
|
|
268
295
|
for (const stmt of statements) {
|
|
269
296
|
try {
|
|
270
297
|
// For ALTER TABLE ADD COLUMN, check if column already exists first
|
|
271
|
-
const
|
|
272
|
-
if (
|
|
273
|
-
const columns = db.pragma(`table_info(${
|
|
274
|
-
if (columns.some(c => c.name ===
|
|
275
|
-
logger.debug(`Migration v${migration.version}: column ${
|
|
298
|
+
const addMatch = stmt.match(/ALTER\s+TABLE\s+(\S+)\s+ADD\s+COLUMN\s+(\S+)/i);
|
|
299
|
+
if (addMatch) {
|
|
300
|
+
const columns = db.pragma(`table_info(${addMatch[1]})`);
|
|
301
|
+
if (columns.some(c => c.name === addMatch[2])) {
|
|
302
|
+
logger.debug(`Migration v${migration.version}: column ${addMatch[1]}.${addMatch[2]} already exists, skipping`);
|
|
303
|
+
continue;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
// For ALTER TABLE DROP COLUMN, skip if column already gone
|
|
307
|
+
const dropMatch = stmt.match(/ALTER\s+TABLE\s+(\S+)\s+DROP\s+COLUMN\s+(\S+)/i);
|
|
308
|
+
if (dropMatch) {
|
|
309
|
+
const columns = db.pragma(`table_info(${dropMatch[1]})`);
|
|
310
|
+
if (!columns.some(c => c.name === dropMatch[2])) {
|
|
311
|
+
logger.debug(`Migration v${migration.version}: column ${dropMatch[1]}.${dropMatch[2]} already absent, skipping`);
|
|
276
312
|
continue;
|
|
277
313
|
}
|
|
278
314
|
}
|
|
@@ -280,8 +316,8 @@ export function runMigrations(db) {
|
|
|
280
316
|
}
|
|
281
317
|
catch (err) {
|
|
282
318
|
const msg = err instanceof Error ? err.message : String(err);
|
|
283
|
-
if (msg.includes('already exists')) {
|
|
284
|
-
logger.debug(`Migration v${migration.version}: object already
|
|
319
|
+
if (msg.includes('already exists') || msg.includes('no such column') || msg.includes('no such table') || msg.includes('no such index')) {
|
|
320
|
+
logger.debug(`Migration v${migration.version}: object already in target state, skipping statement`);
|
|
285
321
|
continue;
|
|
286
322
|
}
|
|
287
323
|
throw err;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
import Database from 'better-sqlite3';
|
|
2
|
+
export declare function ok(text: string): {
|
|
3
|
+
content: {
|
|
4
|
+
type: "text";
|
|
5
|
+
text: string;
|
|
6
|
+
}[];
|
|
7
|
+
};
|
|
8
|
+
export declare function fail(text: string): {
|
|
9
|
+
content: {
|
|
10
|
+
type: "text";
|
|
11
|
+
text: string;
|
|
12
|
+
}[];
|
|
13
|
+
isError: true;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Convenience wrapper for tools that return structured data — emits pretty-printed
|
|
17
|
+
* JSON inside the standard `text` content. Use for endpoints whose responses are
|
|
18
|
+
* meant to be parsed programmatically (channels, handoff, export_data). Use plain
|
|
19
|
+
* ok() for human-readable lists/summaries.
|
|
20
|
+
*/
|
|
21
|
+
export declare function jsonOk(data: unknown): {
|
|
22
|
+
content: {
|
|
23
|
+
type: "text";
|
|
24
|
+
text: string;
|
|
25
|
+
}[];
|
|
26
|
+
};
|
|
27
|
+
export interface ToolContext {
|
|
28
|
+
db: Database.Database;
|
|
29
|
+
signalCronReload(): void;
|
|
30
|
+
signalWatcherReload(): void;
|
|
31
|
+
getGenerators(): Promise<import('../media/types.js').MediaGenerator[]>;
|
|
32
|
+
}
|
|
33
|
+
type ToolResponse = ReturnType<typeof ok> | ReturnType<typeof fail>;
|
|
34
|
+
type ToolArgs = Record<string, unknown> | undefined;
|
|
35
|
+
type Handler = (ctx: ToolContext, args: ToolArgs) => Promise<ToolResponse> | ToolResponse;
|
|
36
|
+
export declare const HANDLERS: Record<string, Handler>;
|
|
37
|
+
export {};
|
|
@@ -0,0 +1,451 @@
|
|
|
1
|
+
// MCP tool handlers. Each handler returns the MCP response shape (or isError).
|
|
2
|
+
// Pulled out of server.ts so the dispatcher there can stay thin and so each
|
|
3
|
+
// handler is independently grep-able / testable.
|
|
4
|
+
import { v4 as uuidv4 } from 'uuid';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import os from 'node:os';
|
|
8
|
+
import { expandHome } from '../util/paths.js';
|
|
9
|
+
import { getConfig, validateTabNameOrDefault } from '../config.js';
|
|
10
|
+
import { createTabRecord } from '../db/index.js';
|
|
11
|
+
import { MESSAGE_LIMITS } from '../util/text.js';
|
|
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 }; }
|
|
15
|
+
/**
|
|
16
|
+
* Convenience wrapper for tools that return structured data — emits pretty-printed
|
|
17
|
+
* JSON inside the standard `text` content. Use for endpoints whose responses are
|
|
18
|
+
* meant to be parsed programmatically (channels, handoff, export_data). Use plain
|
|
19
|
+
* ok() for human-readable lists/summaries.
|
|
20
|
+
*/
|
|
21
|
+
export function jsonOk(data) { return ok(JSON.stringify(data, null, 2)); }
|
|
22
|
+
const MAX_CONTENT_LENGTH = MESSAGE_LIMITS.MCP_CONTENT;
|
|
23
|
+
const MAX_NAME_LENGTH = 256;
|
|
24
|
+
const VALID_SCHEDULE_TYPES = ['at', 'every', 'cron'];
|
|
25
|
+
async function handleMediaGeneration(ctx, mediaType, args) {
|
|
26
|
+
const { prompt, style, duration, provider } = (args || {});
|
|
27
|
+
if (!prompt)
|
|
28
|
+
return fail('Missing prompt');
|
|
29
|
+
const generators = await ctx.getGenerators();
|
|
30
|
+
const gen = provider
|
|
31
|
+
? generators.find(g => g.id === provider)
|
|
32
|
+
: generators.find(g => g.supportedTypes.includes(mediaType));
|
|
33
|
+
if (!gen)
|
|
34
|
+
return fail(`No ${mediaType} generator configured. Run: beecork media`);
|
|
35
|
+
try {
|
|
36
|
+
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');
|
|
38
|
+
return ok(`Generated ${mediaType}: ${result.filePath}`);
|
|
39
|
+
}
|
|
40
|
+
catch (err) {
|
|
41
|
+
return fail(`${mediaType} generation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
export const HANDLERS = {
|
|
45
|
+
beecork_remember: async (ctx, args) => {
|
|
46
|
+
const { content, scope, category } = (args || {});
|
|
47
|
+
if (!content || content.length > MAX_CONTENT_LENGTH) {
|
|
48
|
+
return fail(`Content is required and must be under ${MAX_CONTENT_LENGTH} characters.`);
|
|
49
|
+
}
|
|
50
|
+
if (scope && scope !== 'tab' && scope !== 'auto') {
|
|
51
|
+
const { addKnowledge } = await import('../knowledge/index.js');
|
|
52
|
+
const currentTab = TabStore.mostRecent(ctx.db);
|
|
53
|
+
addKnowledge(content, scope, { category, projectPath: currentTab?.workingDir, tabName: undefined });
|
|
54
|
+
return ok(`Remembered (${scope}): ${content.slice(0, 100)}`);
|
|
55
|
+
}
|
|
56
|
+
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);
|
|
58
|
+
if (existing)
|
|
59
|
+
return ok(`Already remembered: "${fullContent}"`);
|
|
60
|
+
ctx.db.prepare('INSERT INTO memories (content, source) VALUES (?, ?)').run(fullContent, 'tool');
|
|
61
|
+
return ok(`Remembered: "${fullContent}"`);
|
|
62
|
+
},
|
|
63
|
+
beecork_task_create: async (ctx, args) => {
|
|
64
|
+
const { name: jobName, scheduleType, schedule, message, tabName } = (args || {});
|
|
65
|
+
if (!jobName || jobName.length > MAX_NAME_LENGTH) {
|
|
66
|
+
return fail(`Task name is required and must be under ${MAX_NAME_LENGTH} characters.`);
|
|
67
|
+
}
|
|
68
|
+
if (!VALID_SCHEDULE_TYPES.includes(scheduleType)) {
|
|
69
|
+
return fail(`Invalid scheduleType "${scheduleType}". Must be one of: ${VALID_SCHEDULE_TYPES.join(', ')}`);
|
|
70
|
+
}
|
|
71
|
+
if (!message || message.length > MAX_CONTENT_LENGTH) {
|
|
72
|
+
return fail(`Message is required and must be under ${MAX_CONTENT_LENGTH} characters.`);
|
|
73
|
+
}
|
|
74
|
+
const id = uuidv4();
|
|
75
|
+
const tab = tabName || 'default';
|
|
76
|
+
const tabError = validateTabNameOrDefault(tab);
|
|
77
|
+
if (tabError)
|
|
78
|
+
return fail(tabError);
|
|
79
|
+
// Validate schedule expression up-front so misconfigured tasks fail loud instead of silently never firing.
|
|
80
|
+
const { validateSchedule } = await import('../tasks/scheduler.js');
|
|
81
|
+
const scheduleErr = validateSchedule(scheduleType, schedule);
|
|
82
|
+
if (scheduleErr)
|
|
83
|
+
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());
|
|
86
|
+
try {
|
|
87
|
+
ctx.signalCronReload();
|
|
88
|
+
}
|
|
89
|
+
catch (err) {
|
|
90
|
+
return ok(`Task created: "${jobName}" (${scheduleType}: ${schedule}) -> tab:${tab}\nID: ${id}\nWARN: signal-reload failed (${err instanceof Error ? err.message : String(err)}); restart daemon to schedule.`);
|
|
91
|
+
}
|
|
92
|
+
return ok(`Task created: "${jobName}" (${scheduleType}: ${schedule}) -> tab:${tab}\nID: ${id}`);
|
|
93
|
+
},
|
|
94
|
+
beecork_task_list: async (ctx) => {
|
|
95
|
+
const jobs = ctx.db.prepare('SELECT * FROM tasks ORDER BY created_at LIMIT 500').all();
|
|
96
|
+
if (jobs.length === 0)
|
|
97
|
+
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})`);
|
|
99
|
+
return ok(lines.join('\n'));
|
|
100
|
+
},
|
|
101
|
+
beecork_task_delete: async (ctx, args) => {
|
|
102
|
+
const { id } = (args || {});
|
|
103
|
+
const result = ctx.db.prepare('DELETE FROM tasks WHERE id = ?').run(id);
|
|
104
|
+
if (result.changes === 0)
|
|
105
|
+
return ok(`No task found with ID: ${id}`);
|
|
106
|
+
try {
|
|
107
|
+
ctx.signalCronReload();
|
|
108
|
+
}
|
|
109
|
+
catch { /* daemon picks up on restart */ }
|
|
110
|
+
return ok(`Deleted task: ${id}`);
|
|
111
|
+
},
|
|
112
|
+
beecork_watch_create: async (ctx, args) => {
|
|
113
|
+
const { name: watchName, description: watchDesc, checkCommand, condition, action, actionDetails, schedule: watchSchedule, } = (args || {});
|
|
114
|
+
if (!watchName || watchName.length > MAX_NAME_LENGTH) {
|
|
115
|
+
return fail(`Watcher name is required and must be under ${MAX_NAME_LENGTH} characters.`);
|
|
116
|
+
}
|
|
117
|
+
if (!checkCommand)
|
|
118
|
+
return fail('checkCommand is required.');
|
|
119
|
+
if (!condition)
|
|
120
|
+
return fail('condition is required.');
|
|
121
|
+
if (!watchSchedule)
|
|
122
|
+
return fail('schedule is required.');
|
|
123
|
+
// Schedule validation: watchers use "cron" or interval like "5m". Try both.
|
|
124
|
+
const { validateSchedule } = await import('../tasks/scheduler.js');
|
|
125
|
+
const cronErr = validateSchedule('cron', watchSchedule);
|
|
126
|
+
const intervalErr = validateSchedule('every', watchSchedule);
|
|
127
|
+
if (cronErr && intervalErr)
|
|
128
|
+
return fail(`Invalid schedule "${watchSchedule}" — must be a cron expression or interval like "5m"/"1h"/"1d".`);
|
|
129
|
+
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);
|
|
132
|
+
try {
|
|
133
|
+
ctx.signalWatcherReload();
|
|
134
|
+
}
|
|
135
|
+
catch (err) {
|
|
136
|
+
return ok(`Watcher created: "${watchName}" (${watchSchedule})\nID: ${watchId}\nWARN: signal-reload failed (${err instanceof Error ? err.message : String(err)}); restart daemon to schedule.`);
|
|
137
|
+
}
|
|
138
|
+
return ok(`Watcher created: "${watchName}" (${watchSchedule})\nID: ${watchId}`);
|
|
139
|
+
},
|
|
140
|
+
beecork_watch_list: async (ctx) => {
|
|
141
|
+
const watchers = ctx.db.prepare('SELECT * FROM watchers ORDER BY created_at LIMIT 500').all();
|
|
142
|
+
if (watchers.length === 0)
|
|
143
|
+
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})`);
|
|
145
|
+
return ok(watchLines.join('\n'));
|
|
146
|
+
},
|
|
147
|
+
beecork_watch_delete: async (ctx, args) => {
|
|
148
|
+
const { id: watchDelId } = (args || {});
|
|
149
|
+
const watchDelResult = ctx.db.prepare('DELETE FROM watchers WHERE id = ?').run(watchDelId);
|
|
150
|
+
if (watchDelResult.changes === 0)
|
|
151
|
+
return ok(`No watcher found with ID: ${watchDelId}`);
|
|
152
|
+
try {
|
|
153
|
+
ctx.signalWatcherReload();
|
|
154
|
+
}
|
|
155
|
+
catch { /* daemon picks up on restart */ }
|
|
156
|
+
return ok(`Deleted watcher: ${watchDelId}`);
|
|
157
|
+
},
|
|
158
|
+
beecork_tab_create: async (ctx, args) => {
|
|
159
|
+
const { name: tabName, workingDir, template: templateName, systemPrompt } = (args || {});
|
|
160
|
+
if (!tabName)
|
|
161
|
+
return fail('Tab name is required.');
|
|
162
|
+
// tab create does NOT allow "default" — that tab is auto-managed by the daemon.
|
|
163
|
+
const { validateTabName } = await import('../config.js');
|
|
164
|
+
const tabCreateError = validateTabName(tabName);
|
|
165
|
+
if (tabCreateError)
|
|
166
|
+
return fail(tabCreateError);
|
|
167
|
+
const config = getConfig();
|
|
168
|
+
const template = templateName ? config.tabTemplates?.[templateName] : undefined;
|
|
169
|
+
if (templateName && !template) {
|
|
170
|
+
return fail(`Template "${templateName}" not found. Available: ${Object.keys(config.tabTemplates || {}).join(', ') || 'none'}`);
|
|
171
|
+
}
|
|
172
|
+
const dirInput = workingDir || template?.workingDir || os.homedir();
|
|
173
|
+
const dir = path.resolve(expandHome(dirInput));
|
|
174
|
+
const tabSystemPrompt = systemPrompt || template?.systemPrompt || null;
|
|
175
|
+
try {
|
|
176
|
+
// createTabRecord validates existence + isDirectory and throws on failure.
|
|
177
|
+
const result = createTabRecord(ctx.db, { name: tabName, workingDir: dir, systemPrompt: tabSystemPrompt });
|
|
178
|
+
if (!result.created)
|
|
179
|
+
return ok(`Tab "${tabName}" already exists.`);
|
|
180
|
+
const parts = [`Created tab: "${tabName}" (working dir: ${dir})`];
|
|
181
|
+
if (tabSystemPrompt)
|
|
182
|
+
parts.push(`System prompt: "${tabSystemPrompt.slice(0, 100)}${tabSystemPrompt.length > 100 ? '...' : ''}"`);
|
|
183
|
+
if (templateName)
|
|
184
|
+
parts.push(`Template: ${templateName}`);
|
|
185
|
+
return ok(parts.join('\n'));
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
189
|
+
}
|
|
190
|
+
},
|
|
191
|
+
beecork_tab_list: async (ctx) => {
|
|
192
|
+
const tabs = TabStore.listAll(ctx.db);
|
|
193
|
+
if (tabs.length === 0)
|
|
194
|
+
return ok('No tabs.');
|
|
195
|
+
const lines = tabs.map(t => `- ${t.name} [${t.status}] dir:${t.workingDir} last:${t.lastActivityAt}`);
|
|
196
|
+
return ok(lines.join('\n'));
|
|
197
|
+
},
|
|
198
|
+
beecork_send_message: async (ctx, args) => {
|
|
199
|
+
const { tabName, message } = (args || {});
|
|
200
|
+
if (!tabName || !message)
|
|
201
|
+
return fail('Both tabName and message are required.');
|
|
202
|
+
const sendTabError = validateTabNameOrDefault(tabName);
|
|
203
|
+
if (sendTabError)
|
|
204
|
+
return fail(sendTabError);
|
|
205
|
+
if (message.length > MAX_CONTENT_LENGTH) {
|
|
206
|
+
return fail(`Message must be under ${MAX_CONTENT_LENGTH} characters.`);
|
|
207
|
+
}
|
|
208
|
+
ctx.db.prepare('INSERT INTO pending_messages (tab_name, message) VALUES (?, ?)').run(tabName, message);
|
|
209
|
+
return ok(`Message queued for tab "${tabName}".`);
|
|
210
|
+
},
|
|
211
|
+
beecork_recall: async (ctx, args) => {
|
|
212
|
+
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);
|
|
215
|
+
const { searchKnowledge } = await import('../knowledge/index.js');
|
|
216
|
+
const knowledgeResults = searchKnowledge(query);
|
|
217
|
+
const allResults = [
|
|
218
|
+
...knowledgeResults.map(k => k.content),
|
|
219
|
+
...memories.map(m => m.content),
|
|
220
|
+
];
|
|
221
|
+
if (allResults.length === 0)
|
|
222
|
+
return ok(`No relevant knowledge found matching "${query}".`);
|
|
223
|
+
return ok(allResults.join('\n---\n'));
|
|
224
|
+
},
|
|
225
|
+
beecork_notify: async (ctx, args) => {
|
|
226
|
+
const { message, urgent } = (args || {});
|
|
227
|
+
if (!message)
|
|
228
|
+
return fail('Message is required.');
|
|
229
|
+
const prefix = urgent ? '🚨 ' : '';
|
|
230
|
+
ctx.db.prepare("INSERT INTO pending_messages (tab_name, message, type) VALUES ('_notify', ?, 'notification')").run(prefix + message);
|
|
231
|
+
return ok('Notification sent to user.');
|
|
232
|
+
},
|
|
233
|
+
beecork_status: async (ctx) => {
|
|
234
|
+
const tabCount = TabStore.countAll(ctx.db);
|
|
235
|
+
const activeTabs = TabStore.countRunning(ctx.db);
|
|
236
|
+
const taskCount = ctx.db.prepare('SELECT COUNT(*) as c FROM tasks WHERE enabled = 1').get().c;
|
|
237
|
+
const watcherCount = ctx.db.prepare('SELECT COUNT(*) as c FROM watchers WHERE enabled = 1').get().c;
|
|
238
|
+
const memoryCount = ctx.db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
|
|
239
|
+
return ok([
|
|
240
|
+
`Tabs: ${tabCount} total, ${activeTabs} running`,
|
|
241
|
+
`Tasks: ${taskCount} active`,
|
|
242
|
+
`Watchers: ${watcherCount} active`,
|
|
243
|
+
`Memories: ${memoryCount} stored`,
|
|
244
|
+
].join('\n'));
|
|
245
|
+
},
|
|
246
|
+
beecork_send_media: async (ctx, args) => {
|
|
247
|
+
const { filePath, caption, tabName } = (args || {});
|
|
248
|
+
if (!fs.existsSync(filePath))
|
|
249
|
+
return fail(`File not found: ${filePath}`);
|
|
250
|
+
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}`);
|
|
253
|
+
},
|
|
254
|
+
beecork_channels: async () => {
|
|
255
|
+
const config = getConfig();
|
|
256
|
+
const channels = [];
|
|
257
|
+
if (config.telegram?.token)
|
|
258
|
+
channels.push({ id: 'telegram', name: 'Telegram', streaming: true, media: true });
|
|
259
|
+
if (config.whatsapp?.enabled)
|
|
260
|
+
channels.push({ id: 'whatsapp', name: 'WhatsApp', streaming: false, media: true });
|
|
261
|
+
if (config.webhook?.enabled)
|
|
262
|
+
channels.push({ id: 'webhook', name: 'Webhook', streaming: false, media: false });
|
|
263
|
+
if (config.discord?.token)
|
|
264
|
+
channels.push({ id: 'discord', name: 'Discord', streaming: false, media: true });
|
|
265
|
+
return jsonOk(channels);
|
|
266
|
+
},
|
|
267
|
+
beecork_cost: async (_ctx, args) => {
|
|
268
|
+
const { tabName } = (args || {});
|
|
269
|
+
const { getCostSummary, formatCostSummary } = await import('../observability/analytics.js');
|
|
270
|
+
const summary = getCostSummary();
|
|
271
|
+
if (tabName) {
|
|
272
|
+
const tab = summary.perTab.find(t => t.name === tabName);
|
|
273
|
+
if (!tab)
|
|
274
|
+
return fail(`Tab "${tabName}" not found`);
|
|
275
|
+
return ok(`Tab "${tabName}": $${tab.cost.toFixed(4)} (${tab.messages} messages)`);
|
|
276
|
+
}
|
|
277
|
+
return ok(formatCostSummary(summary));
|
|
278
|
+
},
|
|
279
|
+
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();
|
|
281
|
+
if (failed.length === 0)
|
|
282
|
+
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)}`);
|
|
284
|
+
return ok(lines.join('\n\n'));
|
|
285
|
+
},
|
|
286
|
+
beecork_activity: async (_ctx, args) => {
|
|
287
|
+
const hours = (args || {}).hours || 24;
|
|
288
|
+
const { getActivitySummary, formatActivitySummary } = await import('../observability/analytics.js');
|
|
289
|
+
return ok(formatActivitySummary(getActivitySummary(hours)));
|
|
290
|
+
},
|
|
291
|
+
beecork_export_data: async (ctx, args) => {
|
|
292
|
+
const { type: dataType, days = 30 } = (args || {});
|
|
293
|
+
const since = new Date(Date.now() - days * 86400000).toISOString();
|
|
294
|
+
let data;
|
|
295
|
+
switch (dataType) {
|
|
296
|
+
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);
|
|
298
|
+
break;
|
|
299
|
+
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);
|
|
301
|
+
break;
|
|
302
|
+
case 'crons':
|
|
303
|
+
data = ctx.db.prepare('SELECT * FROM tasks ORDER BY created_at').all();
|
|
304
|
+
break;
|
|
305
|
+
default:
|
|
306
|
+
return fail('Invalid type. Use: costs, messages, or crons');
|
|
307
|
+
}
|
|
308
|
+
return jsonOk(data);
|
|
309
|
+
},
|
|
310
|
+
beecork_handoff: async (ctx, args) => {
|
|
311
|
+
const { tabName } = (args || {});
|
|
312
|
+
const tab = TabStore.findByName(tabName, ctx.db);
|
|
313
|
+
if (!tab)
|
|
314
|
+
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
|
+
const info = {
|
|
317
|
+
sessionId: tab.sessionId,
|
|
318
|
+
workingDir: tab.workingDir,
|
|
319
|
+
status: tab.status,
|
|
320
|
+
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) })),
|
|
323
|
+
};
|
|
324
|
+
return jsonOk(info);
|
|
325
|
+
},
|
|
326
|
+
beecork_delegate: async (ctx, args) => {
|
|
327
|
+
const { tabName, message, returnToTab } = (args || {});
|
|
328
|
+
try {
|
|
329
|
+
const { createDelegation } = await import('../delegation/manager.js');
|
|
330
|
+
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');
|
|
332
|
+
return ok(`Delegated to tab "${tabName}". Result will be sent back to "${delegation.returnToTab}" when complete.\n\nDelegation ID: ${delegation.id}`);
|
|
333
|
+
}
|
|
334
|
+
catch (err) {
|
|
335
|
+
return fail(`Delegation failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
336
|
+
}
|
|
337
|
+
},
|
|
338
|
+
beecork_delegation_status: async (_ctx, args) => {
|
|
339
|
+
const { tabName } = (args || {});
|
|
340
|
+
const { getPendingDelegations } = await import('../delegation/manager.js');
|
|
341
|
+
const delegations = getPendingDelegations(tabName);
|
|
342
|
+
if (delegations.length === 0)
|
|
343
|
+
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)}"`);
|
|
345
|
+
return ok(lines.join('\n\n'));
|
|
346
|
+
},
|
|
347
|
+
beecork_project_create: async (_ctx, args) => {
|
|
348
|
+
const { name, path: customPath } = (args || {});
|
|
349
|
+
try {
|
|
350
|
+
const { createProject } = await import('../projects/index.js');
|
|
351
|
+
const project = createProject(name, customPath);
|
|
352
|
+
return ok(`Folder "${name}" registered at ${project.path}`);
|
|
353
|
+
}
|
|
354
|
+
catch (err) {
|
|
355
|
+
return fail(err instanceof Error ? err.message : String(err));
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
beecork_project_list: async () => {
|
|
359
|
+
const { listProjects } = await import('../projects/index.js');
|
|
360
|
+
const projects = listProjects();
|
|
361
|
+
if (projects.length === 0)
|
|
362
|
+
return ok('No folders discovered. Create one with beecork_project_create.');
|
|
363
|
+
const lines = projects.map(p => `${p.type === 'category' ? '📁' : '📦'} ${p.name} — ${p.path}`);
|
|
364
|
+
return ok(lines.join('\n'));
|
|
365
|
+
},
|
|
366
|
+
beecork_close_tab: async (ctx, args) => {
|
|
367
|
+
const { tabName } = (args || {});
|
|
368
|
+
// Mark tab stopped so the daemon's recovery loop cleans up the subprocess.
|
|
369
|
+
// MCP server is a child of `claude` and has no TabManager; we can only mutate the DB.
|
|
370
|
+
TabStore.markRunningAsStopped(tabName, ctx.db);
|
|
371
|
+
const deleted = TabStore.deleteWithMessages(tabName, ctx.db);
|
|
372
|
+
if (!deleted)
|
|
373
|
+
return fail(`Tab "${tabName}" not found.`);
|
|
374
|
+
return ok(`Tab "${tabName}" permanently closed.`);
|
|
375
|
+
},
|
|
376
|
+
beecork_generate_image: (ctx, args) => handleMediaGeneration(ctx, 'image', args),
|
|
377
|
+
beecork_generate_video: (ctx, args) => handleMediaGeneration(ctx, 'video', args),
|
|
378
|
+
beecork_generate_audio: (ctx, args) => handleMediaGeneration(ctx, 'audio', args),
|
|
379
|
+
beecork_media_providers: async (ctx) => {
|
|
380
|
+
const generators = await ctx.getGenerators();
|
|
381
|
+
if (generators.length === 0)
|
|
382
|
+
return ok('No media generators configured. Add mediaGenerators to config.json.');
|
|
383
|
+
const lines = generators.map(g => `- ${g.name} (${g.id}): ${g.supportedTypes.join(', ')}`);
|
|
384
|
+
return ok(lines.join('\n'));
|
|
385
|
+
},
|
|
386
|
+
beecork_knowledge: async (ctx, args) => {
|
|
387
|
+
const { scope: knowledgeScope } = (args || {});
|
|
388
|
+
const { getGlobalKnowledge, getProjectKnowledge, getTabKnowledge, getAllKnowledge, formatKnowledgeForContext } = await import('../knowledge/index.js');
|
|
389
|
+
let entries;
|
|
390
|
+
if (knowledgeScope === 'global') {
|
|
391
|
+
entries = getGlobalKnowledge();
|
|
392
|
+
}
|
|
393
|
+
else if (knowledgeScope === 'project') {
|
|
394
|
+
const currentTab = TabStore.mostRecent(ctx.db);
|
|
395
|
+
entries = currentTab ? getProjectKnowledge(currentTab.workingDir) : [];
|
|
396
|
+
}
|
|
397
|
+
else if (knowledgeScope === 'tab') {
|
|
398
|
+
const currentTab = TabStore.mostRecent(ctx.db);
|
|
399
|
+
entries = currentTab ? getTabKnowledge(currentTab.name) : [];
|
|
400
|
+
}
|
|
401
|
+
else {
|
|
402
|
+
entries = getAllKnowledge();
|
|
403
|
+
}
|
|
404
|
+
if (entries.length === 0)
|
|
405
|
+
return ok('No knowledge stored yet.');
|
|
406
|
+
return ok(formatKnowledgeForContext(entries));
|
|
407
|
+
},
|
|
408
|
+
beecork_capabilities: async () => {
|
|
409
|
+
const { getAvailablePacks, isEnabled } = await import('../capabilities/index.js');
|
|
410
|
+
const packs = getAvailablePacks();
|
|
411
|
+
const lines = packs.map(p => `${isEnabled(p.id) ? '✓ enabled' : '○ available'} | ${p.id} — ${p.name}: ${p.description}`);
|
|
412
|
+
return ok(lines.join('\n'));
|
|
413
|
+
},
|
|
414
|
+
beecork_history: async (_ctx, args) => {
|
|
415
|
+
const { date, tabName, limit } = (args || {});
|
|
416
|
+
const { getTimeline, formatTimeline } = await import('../timeline/index.js');
|
|
417
|
+
const events = getTimeline({ date: date || new Date().toISOString().slice(0, 10), tabName, limit });
|
|
418
|
+
return ok(formatTimeline(events));
|
|
419
|
+
},
|
|
420
|
+
beecork_replay: async (ctx, args) => {
|
|
421
|
+
const { eventId } = (args || {});
|
|
422
|
+
const { getReplayInfo } = await import('../timeline/index.js');
|
|
423
|
+
const info = getReplayInfo(eventId);
|
|
424
|
+
if (!info)
|
|
425
|
+
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');
|
|
427
|
+
return ok(`Replaying in tab "${info.tabName}": ${info.message.slice(0, 200)}`);
|
|
428
|
+
},
|
|
429
|
+
beecork_store_search: async (_ctx, args) => {
|
|
430
|
+
const { query } = (args || {});
|
|
431
|
+
try {
|
|
432
|
+
const response = await fetch(`https://registry.npmjs.org/-/v1/search?text=beecork+${encodeURIComponent(query)}&size=10`, { signal: AbortSignal.timeout(10000) });
|
|
433
|
+
if (!response.ok)
|
|
434
|
+
return fail('npm registry search failed');
|
|
435
|
+
const data = await response.json();
|
|
436
|
+
const packages = (data.objects || []).filter(o => o.package.name.startsWith('beecork-'));
|
|
437
|
+
if (packages.length === 0)
|
|
438
|
+
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'}`);
|
|
440
|
+
return ok(`${packages.length} package(s):\n${lines.join('\n')}\n\nInstall: npm install -g <package-name>`);
|
|
441
|
+
}
|
|
442
|
+
catch {
|
|
443
|
+
return fail('Failed to search npm registry');
|
|
444
|
+
}
|
|
445
|
+
},
|
|
446
|
+
};
|
|
447
|
+
// Aliases retained for backward compatibility with older Claude sessions that cached
|
|
448
|
+
// the cron_* tool names. These dispatch to the same handlers as their task_* counterparts.
|
|
449
|
+
HANDLERS.beecork_cron_create = HANDLERS.beecork_task_create;
|
|
450
|
+
HANDLERS.beecork_cron_list = HANDLERS.beecork_task_list;
|
|
451
|
+
HANDLERS.beecork_cron_delete = HANDLERS.beecork_task_delete;
|