beecork 1.4.11 → 1.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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/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 +90 -84
- package/dist/channels/discord.d.ts +4 -9
- package/dist/channels/discord.js +59 -42
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/loader.js +13 -4
- 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/send-helpers.d.ts +19 -0
- package/dist/channels/send-helpers.js +21 -0
- package/dist/channels/telegram.d.ts +21 -14
- package/dist/channels/telegram.js +214 -104
- package/dist/channels/types.d.ts +13 -38
- package/dist/channels/voice-state.d.ts +29 -0
- package/dist/channels/voice-state.js +45 -0
- package/dist/channels/webhook.d.ts +2 -5
- package/dist/channels/webhook.js +88 -29
- package/dist/channels/whatsapp.d.ts +9 -7
- package/dist/channels/whatsapp.js +141 -100
- 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 +85 -27
- 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.d.ts +5 -1
- package/dist/config.js +20 -22
- package/dist/daemon.js +113 -51
- package/dist/dashboard/html.js +100 -20
- package/dist/dashboard/routes.d.ts +17 -0
- package/dist/dashboard/routes.js +623 -0
- package/dist/dashboard/server.js +38 -489
- package/dist/db/connection.d.ts +29 -0
- package/dist/db/connection.js +37 -0
- package/dist/db/index.js +43 -11
- package/dist/db/migrations.js +114 -22
- package/dist/delegation/manager.js +10 -4
- package/dist/index.js +39 -59
- package/dist/knowledge/manager.js +26 -12
- package/dist/mcp/handlers.d.ts +37 -0
- package/dist/mcp/handlers.js +520 -0
- package/dist/mcp/server.js +44 -858
- package/dist/mcp/tool-definitions.d.ts +1225 -0
- package/dist/mcp/tool-definitions.js +412 -0
- 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.d.ts +2 -7
- package/dist/media/index.js +2 -2
- 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.d.ts +1 -1
- package/dist/observability/analytics.js +41 -16
- package/dist/projects/index.d.ts +3 -2
- package/dist/projects/index.js +2 -2
- package/dist/projects/manager.d.ts +1 -7
- package/dist/projects/manager.js +66 -42
- package/dist/projects/router.d.ts +12 -0
- package/dist/projects/router.js +98 -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 +21 -5
- package/dist/session/manager.js +166 -153
- 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 +3 -0
- package/dist/session/subprocess.js +54 -11
- package/dist/session/tab-store.d.ts +28 -0
- package/dist/session/tab-store.js +78 -0
- package/dist/tasks/scheduler.d.ts +13 -0
- package/dist/tasks/scheduler.js +97 -18
- package/dist/tasks/store.js +26 -12
- package/dist/timeline/logger.js +3 -1
- package/dist/timeline/query.js +15 -5
- package/dist/types.d.ts +49 -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 +2 -0
- package/dist/util/paths.js +16 -3
- package/dist/util/rate-limiter.js +8 -0
- package/dist/util/retry.js +1 -1
- package/dist/util/text.d.ts +21 -1
- package/dist/util/text.js +38 -8
- 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 +11 -5
- package/package.json +6 -1
- package/dist/session/tool-classifier.d.ts +0 -4
- package/dist/session/tool-classifier.js +0 -56
- 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
|
@@ -0,0 +1,623 @@
|
|
|
1
|
+
// Dashboard route handlers. Each entry is keyed by `<METHOD> <pathPattern>` where
|
|
2
|
+
// pathPattern is a regex string (or exact path). The dispatcher in server.ts
|
|
3
|
+
// chooses the first matching entry and invokes its handler.
|
|
4
|
+
import crypto from 'node:crypto';
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
8
|
+
import { promisify } from 'node:util';
|
|
9
|
+
import { getDb } from '../db/index.js';
|
|
10
|
+
import { logger } from '../util/logger.js';
|
|
11
|
+
import { validateTabName, validateTabNameOrDefault, getConfig } from '../config.js';
|
|
12
|
+
import { createTabRecord } from '../db/index.js';
|
|
13
|
+
import { VERSION } from '../version.js';
|
|
14
|
+
import { getDaemonPid } from '../cli/helpers.js';
|
|
15
|
+
import { MESSAGE_LIMITS } from '../util/text.js';
|
|
16
|
+
import { TabStore } from '../session/tab-store.js';
|
|
17
|
+
import { PendingMessageStore } from '../session/pending-store.js';
|
|
18
|
+
import { expandHome } from '../util/paths.js';
|
|
19
|
+
const execFileAsync = promisify(execFile);
|
|
20
|
+
/**
|
|
21
|
+
* Check whether a workingDir resolves under an allowed root.
|
|
22
|
+
* Allowed roots: tabs.default.workingDir, projectScanPaths, $HOME.
|
|
23
|
+
* This mirrors the allowlist used by projects/manager.ts:createProject so the
|
|
24
|
+
* dashboard cannot create a tab pointing at /etc or another user's home.
|
|
25
|
+
*/
|
|
26
|
+
function isAllowedWorkingDir(dir) {
|
|
27
|
+
const resolved = path.resolve(expandHome(dir));
|
|
28
|
+
const config = getConfig();
|
|
29
|
+
const home = os.homedir();
|
|
30
|
+
const roots = [config.tabs?.default?.workingDir, ...(config.projectScanPaths ?? []), home]
|
|
31
|
+
.filter((r) => typeof r === 'string' && r.length > 0)
|
|
32
|
+
.map((r) => path.resolve(expandHome(r)));
|
|
33
|
+
return roots.some((root) => resolved === root || resolved.startsWith(root + path.sep));
|
|
34
|
+
}
|
|
35
|
+
const SAFE_NPM_PACKAGE = /^[@a-zA-Z0-9_/.-]+$/;
|
|
36
|
+
function parseIntParam(value, def, max) {
|
|
37
|
+
if (value === null)
|
|
38
|
+
return def;
|
|
39
|
+
const n = parseInt(value, 10);
|
|
40
|
+
if (Number.isNaN(n) || n < 0)
|
|
41
|
+
return def;
|
|
42
|
+
return Math.min(n, max);
|
|
43
|
+
}
|
|
44
|
+
function json(res, data, status = 200) {
|
|
45
|
+
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
46
|
+
res.end(JSON.stringify(data));
|
|
47
|
+
}
|
|
48
|
+
async function readBody(req, res) {
|
|
49
|
+
let body = '';
|
|
50
|
+
for await (const chunk of req) {
|
|
51
|
+
body += chunk;
|
|
52
|
+
if (body.length > MESSAGE_LIMITS.HTTP_BODY) {
|
|
53
|
+
json(res, { error: 'Payload too large' }, 413);
|
|
54
|
+
req.destroy();
|
|
55
|
+
return null;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
return body;
|
|
59
|
+
}
|
|
60
|
+
function exactPath(p) {
|
|
61
|
+
return (path) => path === p;
|
|
62
|
+
}
|
|
63
|
+
function regexPath(re) {
|
|
64
|
+
return (path) => re.test(path);
|
|
65
|
+
}
|
|
66
|
+
export const ROUTES = [
|
|
67
|
+
// SSE — never log a "broken pipe" write as a hard error
|
|
68
|
+
{
|
|
69
|
+
method: 'GET',
|
|
70
|
+
test: exactPath('/api/events'),
|
|
71
|
+
handler: ({ req, res }) => {
|
|
72
|
+
res.writeHead(200, {
|
|
73
|
+
'Content-Type': 'text/event-stream',
|
|
74
|
+
'Cache-Control': 'no-cache',
|
|
75
|
+
Connection: 'keep-alive',
|
|
76
|
+
});
|
|
77
|
+
res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
|
|
78
|
+
// Skip pushes when nothing changed — without this every 2s tick wrote
|
|
79
|
+
// the full tab payload even when the daemon was idle.
|
|
80
|
+
let lastPayload = '';
|
|
81
|
+
const interval = setInterval(() => {
|
|
82
|
+
if (res.writableEnded)
|
|
83
|
+
return;
|
|
84
|
+
try {
|
|
85
|
+
const tabs = TabStore.listAll().map((t) => ({
|
|
86
|
+
name: t.name,
|
|
87
|
+
status: t.status,
|
|
88
|
+
last_activity_at: t.lastActivityAt,
|
|
89
|
+
}));
|
|
90
|
+
const activeCount = tabs.filter((t) => t.status === 'running').length;
|
|
91
|
+
const payload = JSON.stringify({ type: 'update', tabs, activeTabs: activeCount });
|
|
92
|
+
if (payload === lastPayload)
|
|
93
|
+
return;
|
|
94
|
+
lastPayload = payload;
|
|
95
|
+
res.write(`data: ${payload}\n\n`);
|
|
96
|
+
}
|
|
97
|
+
catch (err) {
|
|
98
|
+
logger.warn('Dashboard SSE tick failed:', err);
|
|
99
|
+
}
|
|
100
|
+
}, 2000);
|
|
101
|
+
req.on('close', () => clearInterval(interval));
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
// POST /api/tabs/:name/send
|
|
105
|
+
{
|
|
106
|
+
method: 'POST',
|
|
107
|
+
test: regexPath(/^\/api\/tabs\/[^/]+\/send$/),
|
|
108
|
+
handler: async ({ req, res, path }) => {
|
|
109
|
+
const body = await readBody(req, res);
|
|
110
|
+
if (body === null)
|
|
111
|
+
return;
|
|
112
|
+
let parsed;
|
|
113
|
+
try {
|
|
114
|
+
parsed = JSON.parse(body);
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
json(res, { error: 'Invalid JSON' }, 400);
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
if (!parsed.message) {
|
|
121
|
+
json(res, { error: 'Missing message' }, 400);
|
|
122
|
+
return;
|
|
123
|
+
}
|
|
124
|
+
const tabName = decodeURIComponent(path.split('/')[3]);
|
|
125
|
+
const err = validateTabNameOrDefault(tabName);
|
|
126
|
+
if (err) {
|
|
127
|
+
json(res, { error: err }, 400);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
PendingMessageStore.enqueueUser(tabName, parsed.message, getDb());
|
|
131
|
+
json(res, { success: true, tab: tabName });
|
|
132
|
+
},
|
|
133
|
+
},
|
|
134
|
+
// POST /api/tabs — create
|
|
135
|
+
{
|
|
136
|
+
method: 'POST',
|
|
137
|
+
test: exactPath('/api/tabs'),
|
|
138
|
+
handler: async ({ req, res }) => {
|
|
139
|
+
const body = await readBody(req, res);
|
|
140
|
+
if (body === null)
|
|
141
|
+
return;
|
|
142
|
+
let parsed;
|
|
143
|
+
try {
|
|
144
|
+
parsed = JSON.parse(body);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
json(res, { error: 'Invalid JSON' }, 400);
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
if (!parsed.name) {
|
|
151
|
+
json(res, { error: 'Missing tab name' }, 400);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const err = validateTabName(parsed.name);
|
|
155
|
+
if (err) {
|
|
156
|
+
json(res, { error: err }, 400);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
if (parsed.workingDir && !isAllowedWorkingDir(parsed.workingDir)) {
|
|
160
|
+
json(res, {
|
|
161
|
+
error: 'workingDir must be under the workspace root, a project scan path, or your home directory',
|
|
162
|
+
}, 400);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
createTabRecord(getDb(), {
|
|
167
|
+
name: parsed.name,
|
|
168
|
+
workingDir: parsed.workingDir,
|
|
169
|
+
systemPrompt: parsed.systemPrompt,
|
|
170
|
+
});
|
|
171
|
+
json(res, { success: true, name: parsed.name });
|
|
172
|
+
}
|
|
173
|
+
catch (e) {
|
|
174
|
+
json(res, { error: e instanceof Error ? e.message : String(e) }, 400);
|
|
175
|
+
}
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
// DELETE /api/tabs/:name
|
|
179
|
+
{
|
|
180
|
+
method: 'DELETE',
|
|
181
|
+
test: regexPath(/^\/api\/tabs\/[^/]+$/),
|
|
182
|
+
handler: ({ res, path }) => {
|
|
183
|
+
const tabName = decodeURIComponent(path.split('/')[3]);
|
|
184
|
+
TabStore.deleteWithMessages(tabName);
|
|
185
|
+
json(res, { success: true });
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
// POST /api/tasks or /api/crons
|
|
189
|
+
{
|
|
190
|
+
method: 'POST',
|
|
191
|
+
test: (path) => path === '/api/tasks' || path === '/api/crons',
|
|
192
|
+
handler: async ({ req, res }) => {
|
|
193
|
+
const body = await readBody(req, res);
|
|
194
|
+
if (body === null)
|
|
195
|
+
return;
|
|
196
|
+
let parsed;
|
|
197
|
+
try {
|
|
198
|
+
parsed = JSON.parse(body);
|
|
199
|
+
}
|
|
200
|
+
catch {
|
|
201
|
+
json(res, { error: 'Invalid JSON' }, 400);
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
if (!parsed.name || !parsed.schedule || !parsed.message) {
|
|
205
|
+
json(res, { error: 'Missing required fields' }, 400);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const effectiveTab = parsed.tabName || 'default';
|
|
209
|
+
const tabErr = validateTabNameOrDefault(effectiveTab);
|
|
210
|
+
if (tabErr) {
|
|
211
|
+
json(res, { error: tabErr }, 400);
|
|
212
|
+
return;
|
|
213
|
+
}
|
|
214
|
+
const scheduleType = parsed.scheduleType || 'every';
|
|
215
|
+
const { validateSchedule } = await import('../tasks/scheduler.js');
|
|
216
|
+
const scheduleErr = validateSchedule(scheduleType, parsed.schedule);
|
|
217
|
+
if (scheduleErr) {
|
|
218
|
+
json(res, { error: scheduleErr }, 400);
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
const id = crypto.randomUUID();
|
|
222
|
+
const { TaskStore } = await import('../tasks/store.js');
|
|
223
|
+
new TaskStore().add({
|
|
224
|
+
id,
|
|
225
|
+
name: parsed.name,
|
|
226
|
+
scheduleType: scheduleType,
|
|
227
|
+
schedule: parsed.schedule,
|
|
228
|
+
tabName: effectiveTab,
|
|
229
|
+
message: parsed.message,
|
|
230
|
+
payloadType: 'agentTurn',
|
|
231
|
+
enabled: true,
|
|
232
|
+
createdAt: new Date().toISOString(),
|
|
233
|
+
lastRunAt: null,
|
|
234
|
+
nextRunAt: null,
|
|
235
|
+
});
|
|
236
|
+
json(res, { success: true, id });
|
|
237
|
+
},
|
|
238
|
+
},
|
|
239
|
+
// DELETE /api/tasks/:id or /api/crons/:id
|
|
240
|
+
{
|
|
241
|
+
method: 'DELETE',
|
|
242
|
+
test: (path) => /^\/api\/(tasks|crons)\/[^/]+$/.test(path),
|
|
243
|
+
handler: async ({ res, path }) => {
|
|
244
|
+
const taskId = decodeURIComponent(path.split('/')[3]);
|
|
245
|
+
const { TaskStore } = await import('../tasks/store.js');
|
|
246
|
+
new TaskStore().delete(taskId);
|
|
247
|
+
json(res, { success: true });
|
|
248
|
+
},
|
|
249
|
+
},
|
|
250
|
+
// GET /api/watchers
|
|
251
|
+
{
|
|
252
|
+
method: 'GET',
|
|
253
|
+
test: exactPath('/api/watchers'),
|
|
254
|
+
handler: async ({ res }) => {
|
|
255
|
+
const { WatcherStore } = await import('../watchers/store.js');
|
|
256
|
+
json(res, new WatcherStore().list());
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
// DELETE /api/watchers/:id
|
|
260
|
+
{
|
|
261
|
+
method: 'DELETE',
|
|
262
|
+
test: regexPath(/^\/api\/watchers\/[^/]+$/),
|
|
263
|
+
handler: async ({ res, path }) => {
|
|
264
|
+
const id = decodeURIComponent(path.split('/')[3]);
|
|
265
|
+
const { WatcherStore } = await import('../watchers/store.js');
|
|
266
|
+
new WatcherStore().delete(id);
|
|
267
|
+
json(res, { success: true });
|
|
268
|
+
},
|
|
269
|
+
},
|
|
270
|
+
// POST /api/memories
|
|
271
|
+
{
|
|
272
|
+
method: 'POST',
|
|
273
|
+
test: exactPath('/api/memories'),
|
|
274
|
+
handler: async ({ req, res }) => {
|
|
275
|
+
const body = await readBody(req, res);
|
|
276
|
+
if (body === null)
|
|
277
|
+
return;
|
|
278
|
+
let parsed;
|
|
279
|
+
try {
|
|
280
|
+
parsed = JSON.parse(body);
|
|
281
|
+
}
|
|
282
|
+
catch {
|
|
283
|
+
json(res, { error: 'Invalid JSON' }, 400);
|
|
284
|
+
return;
|
|
285
|
+
}
|
|
286
|
+
if (!parsed.content) {
|
|
287
|
+
json(res, { error: 'Missing content' }, 400);
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
const { MemoryStore } = await import('../session/memory-store.js');
|
|
291
|
+
MemoryStore.add(parsed.content, { tabName: parsed.tabName });
|
|
292
|
+
json(res, { success: true });
|
|
293
|
+
},
|
|
294
|
+
},
|
|
295
|
+
// DELETE /api/memories/:id
|
|
296
|
+
{
|
|
297
|
+
method: 'DELETE',
|
|
298
|
+
test: regexPath(/^\/api\/memories\/\d+$/),
|
|
299
|
+
handler: async ({ res, path }) => {
|
|
300
|
+
const id = path.split('/')[3];
|
|
301
|
+
const { MemoryStore } = await import('../session/memory-store.js');
|
|
302
|
+
MemoryStore.delete(id);
|
|
303
|
+
json(res, { success: true });
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
// GET /api/media/config
|
|
307
|
+
{
|
|
308
|
+
method: 'GET',
|
|
309
|
+
test: exactPath('/api/media/config'),
|
|
310
|
+
handler: async ({ res }) => {
|
|
311
|
+
const { getConfig } = await import('../config.js');
|
|
312
|
+
const generators = getConfig().mediaGenerators || [];
|
|
313
|
+
json(res, {
|
|
314
|
+
generators: generators.map((g) => ({
|
|
315
|
+
provider: g.provider,
|
|
316
|
+
model: g.model,
|
|
317
|
+
configured: !!g.apiKey,
|
|
318
|
+
})),
|
|
319
|
+
});
|
|
320
|
+
},
|
|
321
|
+
},
|
|
322
|
+
// GET /api/channels/config
|
|
323
|
+
{
|
|
324
|
+
method: 'GET',
|
|
325
|
+
test: exactPath('/api/channels/config'),
|
|
326
|
+
handler: async ({ res }) => {
|
|
327
|
+
const { getConfig } = await import('../config.js');
|
|
328
|
+
const config = getConfig();
|
|
329
|
+
json(res, {
|
|
330
|
+
telegram: { configured: !!config.telegram?.token, botUsername: null },
|
|
331
|
+
discord: { configured: !!config.discord?.token },
|
|
332
|
+
whatsapp: { configured: !!config.whatsapp?.enabled },
|
|
333
|
+
webhook: { configured: !!config.webhook?.enabled, port: config.webhook?.port },
|
|
334
|
+
});
|
|
335
|
+
},
|
|
336
|
+
},
|
|
337
|
+
// POST /api/computer-use
|
|
338
|
+
{
|
|
339
|
+
method: 'POST',
|
|
340
|
+
test: exactPath('/api/computer-use'),
|
|
341
|
+
handler: async ({ req, res }) => {
|
|
342
|
+
const body = await readBody(req, res);
|
|
343
|
+
if (body === null)
|
|
344
|
+
return;
|
|
345
|
+
let parsed;
|
|
346
|
+
try {
|
|
347
|
+
parsed = JSON.parse(body);
|
|
348
|
+
}
|
|
349
|
+
catch {
|
|
350
|
+
json(res, { error: 'Invalid JSON' }, 400);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
const { getConfig, saveConfig } = await import('../config.js');
|
|
354
|
+
const config = getConfig();
|
|
355
|
+
config.claudeCode.computerUse = !!parsed.enabled;
|
|
356
|
+
saveConfig(config);
|
|
357
|
+
json(res, { enabled: !!parsed.enabled, message: 'Restart daemon to apply.' });
|
|
358
|
+
},
|
|
359
|
+
},
|
|
360
|
+
// GET /api/computer-use
|
|
361
|
+
{
|
|
362
|
+
method: 'GET',
|
|
363
|
+
test: exactPath('/api/computer-use'),
|
|
364
|
+
handler: async ({ res }) => {
|
|
365
|
+
const { getConfig } = await import('../config.js');
|
|
366
|
+
json(res, { enabled: !!getConfig().claudeCode.computerUse });
|
|
367
|
+
},
|
|
368
|
+
},
|
|
369
|
+
// GET /api/timeline
|
|
370
|
+
{
|
|
371
|
+
method: 'GET',
|
|
372
|
+
test: exactPath('/api/timeline'),
|
|
373
|
+
handler: async ({ res, url }) => {
|
|
374
|
+
const { getTimeline } = await import('../timeline/index.js');
|
|
375
|
+
const date = url.searchParams.get('date') || new Date().toISOString().slice(0, 10);
|
|
376
|
+
const limit = parseIntParam(url.searchParams.get('limit'), 50, 200);
|
|
377
|
+
json(res, { events: getTimeline({ date, limit }) });
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
// GET /api/status
|
|
381
|
+
{
|
|
382
|
+
method: 'GET',
|
|
383
|
+
test: exactPath('/api/status'),
|
|
384
|
+
handler: ({ res }) => {
|
|
385
|
+
const db = getDb();
|
|
386
|
+
const activeTasks = db.prepare('SELECT COUNT(*) as c FROM tasks WHERE enabled = 1').get().c;
|
|
387
|
+
json(res, {
|
|
388
|
+
version: VERSION,
|
|
389
|
+
daemonPid: getDaemonPid(),
|
|
390
|
+
tabs: TabStore.countAll(),
|
|
391
|
+
activeTabs: TabStore.countRunning(),
|
|
392
|
+
tasks: activeTasks,
|
|
393
|
+
// Legacy alias — HTML reads either key; can be dropped after old dashboards have refreshed.
|
|
394
|
+
cronJobs: activeTasks,
|
|
395
|
+
memories: db.prepare('SELECT COUNT(*) as c FROM memories').get().c,
|
|
396
|
+
});
|
|
397
|
+
},
|
|
398
|
+
},
|
|
399
|
+
// GET /api/tabs
|
|
400
|
+
{
|
|
401
|
+
method: 'GET',
|
|
402
|
+
test: exactPath('/api/tabs'),
|
|
403
|
+
handler: ({ res }) => {
|
|
404
|
+
// Explicit column list — do NOT include session_id or system_prompt.
|
|
405
|
+
// session_id is the credential used by `claude --resume`; leaking it via
|
|
406
|
+
// /api/tabs (which any holder of the dashboard token can hit) would let
|
|
407
|
+
// an attacker resume any tab's claude session locally.
|
|
408
|
+
const tabs = getDb()
|
|
409
|
+
.prepare(`
|
|
410
|
+
SELECT t.id, t.name, t.status, t.working_dir, t.last_activity_at, t.created_at, t.pid,
|
|
411
|
+
(SELECT COUNT(*) FROM messages WHERE tab_id = t.id) as message_count,
|
|
412
|
+
(SELECT COALESCE(SUM(cost_usd), 0) FROM messages WHERE tab_id = t.id) as total_cost
|
|
413
|
+
FROM tabs t ORDER BY t.last_activity_at DESC
|
|
414
|
+
`)
|
|
415
|
+
.all();
|
|
416
|
+
json(res, tabs);
|
|
417
|
+
},
|
|
418
|
+
},
|
|
419
|
+
// GET /api/tabs/:name/messages
|
|
420
|
+
{
|
|
421
|
+
method: 'GET',
|
|
422
|
+
test: regexPath(/^\/api\/tabs\/[^/]+\/messages$/),
|
|
423
|
+
handler: ({ res, url, path }) => {
|
|
424
|
+
const tabName = decodeURIComponent(path.split('/')[3]);
|
|
425
|
+
const limit = parseIntParam(url.searchParams.get('limit'), 50, 200);
|
|
426
|
+
const offset = parseIntParam(url.searchParams.get('offset'), 0, 100000);
|
|
427
|
+
const tabId = TabStore.getIdByName(tabName);
|
|
428
|
+
if (!tabId) {
|
|
429
|
+
json(res, { error: 'Tab not found' }, 404);
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
const db = getDb();
|
|
433
|
+
const messages = db
|
|
434
|
+
.prepare('SELECT role, content, cost_usd, tokens_in, tokens_out, created_at FROM messages WHERE tab_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?')
|
|
435
|
+
.all(tabId, limit, offset);
|
|
436
|
+
const total = db.prepare('SELECT COUNT(*) as c FROM messages WHERE tab_id = ?').get(tabId).c;
|
|
437
|
+
json(res, { messages: messages.reverse(), total, limit, offset });
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
// GET /api/memories
|
|
441
|
+
{
|
|
442
|
+
method: 'GET',
|
|
443
|
+
test: exactPath('/api/memories'),
|
|
444
|
+
handler: async ({ res, url }) => {
|
|
445
|
+
const limit = parseIntParam(url.searchParams.get('limit'), 50, 200);
|
|
446
|
+
const offset = parseIntParam(url.searchParams.get('offset'), 0, 100000);
|
|
447
|
+
const q = url.searchParams.get('q') || '';
|
|
448
|
+
const { MemoryStore } = await import('../session/memory-store.js');
|
|
449
|
+
const { memories: rows, total } = MemoryStore.list({ limit, offset, query: q || undefined });
|
|
450
|
+
// Dashboard HTML reads snake_case (tab_name, created_at) — map back here
|
|
451
|
+
// rather than reshape the store's typed return.
|
|
452
|
+
const memories = rows.map((m) => ({
|
|
453
|
+
id: m.id,
|
|
454
|
+
content: m.content,
|
|
455
|
+
tab_name: m.tabName,
|
|
456
|
+
source: m.source,
|
|
457
|
+
created_at: m.createdAt,
|
|
458
|
+
}));
|
|
459
|
+
json(res, { memories, total, limit, offset });
|
|
460
|
+
},
|
|
461
|
+
},
|
|
462
|
+
// GET /api/tasks or /api/crons
|
|
463
|
+
{
|
|
464
|
+
method: 'GET',
|
|
465
|
+
test: (path) => path === '/api/tasks' || path === '/api/crons',
|
|
466
|
+
handler: ({ res, url }) => {
|
|
467
|
+
const limit = parseIntParam(url.searchParams.get('limit'), 100, 500);
|
|
468
|
+
const tasks = getDb().prepare('SELECT * FROM tasks ORDER BY created_at LIMIT ?').all(limit);
|
|
469
|
+
json(res, tasks);
|
|
470
|
+
},
|
|
471
|
+
},
|
|
472
|
+
// GET /api/costs
|
|
473
|
+
{
|
|
474
|
+
method: 'GET',
|
|
475
|
+
test: exactPath('/api/costs'),
|
|
476
|
+
handler: ({ res }) => {
|
|
477
|
+
const costs = getDb()
|
|
478
|
+
.prepare(`
|
|
479
|
+
SELECT date(created_at) as day,
|
|
480
|
+
SUM(cost_usd) as total_cost,
|
|
481
|
+
COUNT(*) as message_count
|
|
482
|
+
FROM messages
|
|
483
|
+
WHERE role = 'assistant' AND cost_usd > 0
|
|
484
|
+
AND created_at > datetime('now', '-30 days')
|
|
485
|
+
GROUP BY date(created_at)
|
|
486
|
+
ORDER BY day
|
|
487
|
+
`)
|
|
488
|
+
.all();
|
|
489
|
+
json(res, costs);
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
// GET /api/update/status
|
|
493
|
+
{
|
|
494
|
+
method: 'GET',
|
|
495
|
+
test: exactPath('/api/update/status'),
|
|
496
|
+
handler: async ({ res }) => {
|
|
497
|
+
async function npmViewLatest(name) {
|
|
498
|
+
try {
|
|
499
|
+
const { stdout } = await execFileAsync('npm', ['view', name, 'version'], {
|
|
500
|
+
timeout: 10000,
|
|
501
|
+
});
|
|
502
|
+
return stdout.trim();
|
|
503
|
+
}
|
|
504
|
+
catch {
|
|
505
|
+
return null;
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
const packages = await Promise.all([
|
|
509
|
+
(async () => {
|
|
510
|
+
const p = {
|
|
511
|
+
name: 'beecork',
|
|
512
|
+
installed: VERSION,
|
|
513
|
+
latest: await npmViewLatest('beecork'),
|
|
514
|
+
};
|
|
515
|
+
p.updateAvailable = !!(p.latest && p.installed !== p.latest);
|
|
516
|
+
return p;
|
|
517
|
+
})(),
|
|
518
|
+
(async () => {
|
|
519
|
+
const p = { name: '@anthropic-ai/claude-code' };
|
|
520
|
+
try {
|
|
521
|
+
const { stdout } = await execFileAsync('claude', ['--version'], { timeout: 10000 });
|
|
522
|
+
p.installed = stdout.trim().replace(/^.*?(\d+\.\d+\.\d+).*$/, '$1');
|
|
523
|
+
}
|
|
524
|
+
catch {
|
|
525
|
+
p.installed = null;
|
|
526
|
+
}
|
|
527
|
+
p.latest = await npmViewLatest('@anthropic-ai/claude-code');
|
|
528
|
+
p.updateAvailable = !!(p.installed && p.latest && p.installed !== p.latest);
|
|
529
|
+
return p;
|
|
530
|
+
})(),
|
|
531
|
+
]);
|
|
532
|
+
json(res, { packages });
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
// POST /api/update/:pkg
|
|
536
|
+
{
|
|
537
|
+
method: 'POST',
|
|
538
|
+
test: regexPath(/^\/api\/update\/[^/]+$/),
|
|
539
|
+
handler: async ({ res, path }) => {
|
|
540
|
+
const pkgName = decodeURIComponent(path.split('/')[3]);
|
|
541
|
+
const allowedPackages = new Set(['beecork', '@anthropic-ai/claude-code']);
|
|
542
|
+
if (!allowedPackages.has(pkgName)) {
|
|
543
|
+
json(res, { error: `Package "${pkgName}" is not in the allowed update list.` }, 400);
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
// Defense-in-depth: even though pkgName is allowlisted, validate against the
|
|
547
|
+
// same regex used elsewhere so a typo in the allowlist can't widen the surface.
|
|
548
|
+
if (!SAFE_NPM_PACKAGE.test(pkgName)) {
|
|
549
|
+
json(res, { error: `Invalid package name: ${pkgName}` }, 400);
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
try {
|
|
553
|
+
const { stdout } = await execFileAsync('npm', ['install', '-g', `${pkgName}@latest`], {
|
|
554
|
+
timeout: 120000,
|
|
555
|
+
});
|
|
556
|
+
json(res, { success: true, package: pkgName, output: stdout.trim() });
|
|
557
|
+
}
|
|
558
|
+
catch (err) {
|
|
559
|
+
json(res, { error: err instanceof Error ? err.message : String(err) }, 500);
|
|
560
|
+
}
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
// GET /api/capabilities
|
|
564
|
+
{
|
|
565
|
+
method: 'GET',
|
|
566
|
+
test: exactPath('/api/capabilities'),
|
|
567
|
+
handler: async ({ res }) => {
|
|
568
|
+
const { getAvailablePacks, isEnabled } = await import('../capabilities/index.js');
|
|
569
|
+
const packs = getAvailablePacks().map((p) => ({
|
|
570
|
+
...p,
|
|
571
|
+
enabled: isEnabled(p.id),
|
|
572
|
+
mcpServer: { package: p.mcpServer.package },
|
|
573
|
+
}));
|
|
574
|
+
json(res, { packs });
|
|
575
|
+
},
|
|
576
|
+
},
|
|
577
|
+
// POST /api/capabilities/:id/enable
|
|
578
|
+
{
|
|
579
|
+
method: 'POST',
|
|
580
|
+
test: regexPath(/^\/api\/capabilities\/[^/]+\/enable$/),
|
|
581
|
+
handler: async ({ req, res, path }) => {
|
|
582
|
+
const packId = path.split('/')[3];
|
|
583
|
+
const body = await readBody(req, res);
|
|
584
|
+
if (body === null)
|
|
585
|
+
return;
|
|
586
|
+
let parsed;
|
|
587
|
+
try {
|
|
588
|
+
parsed = JSON.parse(body);
|
|
589
|
+
}
|
|
590
|
+
catch {
|
|
591
|
+
json(res, { error: 'Invalid JSON' }, 400);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
const { enablePack } = await import('../capabilities/index.js');
|
|
595
|
+
try {
|
|
596
|
+
enablePack(packId, parsed.apiKey);
|
|
597
|
+
json(res, { success: true, message: 'Restart daemon to activate.' });
|
|
598
|
+
}
|
|
599
|
+
catch (err) {
|
|
600
|
+
json(res, { error: err instanceof Error ? err.message : String(err) }, 400);
|
|
601
|
+
}
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
// POST /api/capabilities/:id/disable
|
|
605
|
+
{
|
|
606
|
+
method: 'POST',
|
|
607
|
+
test: regexPath(/^\/api\/capabilities\/[^/]+\/disable$/),
|
|
608
|
+
handler: async ({ res, path }) => {
|
|
609
|
+
const packId = path.split('/')[3];
|
|
610
|
+
const { disablePack } = await import('../capabilities/index.js');
|
|
611
|
+
disablePack(packId);
|
|
612
|
+
json(res, { success: true });
|
|
613
|
+
},
|
|
614
|
+
},
|
|
615
|
+
];
|
|
616
|
+
export function dispatch(method, path) {
|
|
617
|
+
for (const r of ROUTES) {
|
|
618
|
+
if (r.method === method && r.test(path))
|
|
619
|
+
return r;
|
|
620
|
+
}
|
|
621
|
+
return null;
|
|
622
|
+
}
|
|
623
|
+
export { json };
|