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.
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/capabilities/manager.js +13 -9
- package/dist/capabilities/packs.js +3 -1
- package/dist/channels/command-handler.js +46 -14
- package/dist/channels/discord.d.ts +3 -6
- package/dist/channels/discord.js +40 -23
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/loader.js +13 -3
- package/dist/channels/pipeline.js +14 -5
- package/dist/channels/registry.d.ts +17 -1
- package/dist/channels/registry.js +33 -4
- package/dist/channels/telegram.d.ts +20 -5
- package/dist/channels/telegram.js +177 -42
- package/dist/channels/types.d.ts +11 -28
- package/dist/channels/voice-state.js +3 -1
- package/dist/channels/webhook.d.ts +1 -4
- package/dist/channels/webhook.js +26 -11
- package/dist/channels/whatsapp.d.ts +8 -4
- package/dist/channels/whatsapp.js +65 -29
- package/dist/cli/capabilities.js +4 -4
- package/dist/cli/channel.js +16 -6
- package/dist/cli/commands.js +12 -9
- package/dist/cli/doctor.js +80 -25
- package/dist/cli/handoff.d.ts +7 -14
- package/dist/cli/handoff.js +9 -44
- package/dist/cli/mcp.js +5 -5
- package/dist/cli/media.js +21 -8
- package/dist/cli/setup.js +9 -8
- package/dist/cli/store.js +29 -12
- package/dist/config.js +5 -10
- package/dist/daemon.js +88 -38
- package/dist/dashboard/html.js +80 -12
- package/dist/dashboard/routes.js +143 -79
- package/dist/dashboard/server.js +5 -1
- package/dist/db/connection.d.ts +29 -0
- package/dist/db/connection.js +37 -0
- package/dist/db/index.js +30 -12
- package/dist/db/migrations.js +84 -28
- package/dist/delegation/manager.js +10 -4
- package/dist/index.js +39 -59
- package/dist/knowledge/manager.js +26 -12
- package/dist/mcp/handlers.js +126 -57
- package/dist/mcp/server.js +20 -10
- package/dist/mcp/tool-definitions.js +68 -20
- package/dist/mcp/validate.d.ts +23 -0
- package/dist/mcp/validate.js +65 -0
- package/dist/media/factory.js +18 -14
- package/dist/media/generators/dall-e.js +2 -2
- package/dist/media/generators/kling.js +4 -4
- package/dist/media/generators/lyria.js +1 -1
- package/dist/media/generators/nano-banana.d.ts +1 -1
- package/dist/media/generators/nano-banana.js +2 -2
- package/dist/media/generators/poll-util.js +4 -4
- package/dist/media/generators/recraft.js +3 -3
- package/dist/media/generators/runway.js +4 -4
- package/dist/media/generators/stable-diffusion.js +2 -2
- package/dist/media/generators/veo.js +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/store.d.ts +7 -0
- package/dist/media/store.js +18 -4
- package/dist/media/types.d.ts +22 -0
- package/dist/notifications/index.d.ts +2 -4
- package/dist/notifications/index.js +6 -19
- package/dist/notifications/ntfy.js +3 -3
- package/dist/observability/analytics.js +35 -13
- package/dist/projects/index.d.ts +1 -1
- package/dist/projects/index.js +1 -1
- package/dist/projects/manager.d.ts +0 -4
- package/dist/projects/manager.js +51 -28
- package/dist/projects/router.d.ts +2 -0
- package/dist/projects/router.js +70 -45
- package/dist/service/install.js +15 -5
- package/dist/service/windows.js +1 -1
- package/dist/session/budget-guard.d.ts +20 -0
- package/dist/session/budget-guard.js +31 -0
- package/dist/session/circuit-breaker.d.ts +5 -3
- package/dist/session/circuit-breaker.js +45 -20
- package/dist/session/context-compactor.d.ts +32 -0
- package/dist/session/context-compactor.js +45 -0
- package/dist/session/context-monitor.js +2 -2
- package/dist/session/handoff.d.ts +21 -0
- package/dist/session/handoff.js +50 -0
- package/dist/session/manager.d.ts +17 -5
- package/dist/session/manager.js +153 -146
- package/dist/session/memory-store.d.ts +29 -0
- package/dist/session/memory-store.js +45 -0
- package/dist/session/message-queue.d.ts +28 -0
- package/dist/session/message-queue.js +52 -0
- package/dist/session/pending-dispatcher.d.ts +31 -0
- package/dist/session/pending-dispatcher.js +120 -0
- package/dist/session/pending-store.d.ts +60 -0
- package/dist/session/pending-store.js +118 -0
- package/dist/session/stale-session.d.ts +31 -0
- package/dist/session/stale-session.js +45 -0
- package/dist/session/subprocess.d.ts +2 -0
- package/dist/session/subprocess.js +33 -11
- package/dist/session/tab-store.js +4 -3
- package/dist/tasks/scheduler.d.ts +7 -0
- package/dist/tasks/scheduler.js +46 -6
- package/dist/tasks/store.js +20 -6
- package/dist/timeline/logger.js +3 -1
- package/dist/timeline/query.js +9 -3
- package/dist/types.d.ts +34 -9
- package/dist/util/auto-heal.js +15 -5
- package/dist/util/install-info.js +3 -1
- package/dist/util/logger.d.ts +1 -1
- package/dist/util/logger.js +63 -24
- package/dist/util/paths.d.ts +1 -0
- package/dist/util/paths.js +12 -2
- package/dist/util/retry.js +1 -1
- package/dist/util/text.js +13 -7
- package/dist/voice/index.js +5 -1
- package/dist/voice/stt.js +14 -6
- package/dist/voice/tts.js +1 -1
- package/dist/watchers/scheduler.js +9 -2
- package/package.json +18 -13
- package/dist/session/tool-classifier.d.ts +0 -4
- package/dist/session/tool-classifier.js +0 -56
package/dist/mcp/handlers.js
CHANGED
|
@@ -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
|
-
|
|
14
|
-
|
|
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) {
|
|
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
|
-
|
|
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, {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
85
|
-
|
|
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 {
|
|
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
|
|
131
|
-
|
|
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
|
|
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 {
|
|
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, {
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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 (!
|
|
249
|
-
return fail(
|
|
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
|
-
|
|
252
|
-
return ok(`Media queued for sending: ${
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
372
|
+
beecork_handoff: async (_ctx, args) => {
|
|
311
373
|
const { tabName } = (args || {});
|
|
312
|
-
const
|
|
313
|
-
|
|
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:
|
|
318
|
-
workingDir:
|
|
319
|
-
status:
|
|
379
|
+
sessionId: exported.sessionId,
|
|
380
|
+
workingDir: exported.workingDir,
|
|
381
|
+
status: exported.status,
|
|
320
382
|
resumeCommand: `beecork attach ${tabName}`,
|
|
321
|
-
manualCommand: `cd ${
|
|
322
|
-
recentMessages:
|
|
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
|
-
|
|
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({
|
|
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
|
-
|
|
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 {
|
package/dist/mcp/server.js
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
|
|
24
|
-
|
|
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', () => {
|
|
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:
|
|
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: {
|
|
13
|
-
|
|
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: {
|
|
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: {
|
|
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: {
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
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: {
|
|
118
|
-
|
|
119
|
-
|
|
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:
|
|
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:
|
|
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:
|
|
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: {
|
|
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: {
|
|
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:
|
|
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: {
|
|
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: {
|
|
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
|
},
|