beecork 1.4.11 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +1 -1
- package/dist/observability/analytics.js +6 -3
- 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/dashboard/server.js
CHANGED
|
@@ -1,52 +1,40 @@
|
|
|
1
1
|
import http from 'node:http';
|
|
2
2
|
import crypto from 'node:crypto';
|
|
3
|
-
import { execFile
|
|
4
|
-
import { promisify } from 'node:util';
|
|
3
|
+
import { execFile } from 'node:child_process';
|
|
5
4
|
import { platform } from 'node:os';
|
|
6
|
-
import Database from 'better-sqlite3';
|
|
7
|
-
import { getDbPath } from '../util/paths.js';
|
|
8
5
|
import { getDashboardHtml } from './html.js';
|
|
9
|
-
import {
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
6
|
+
import { logger } from '../util/logger.js';
|
|
7
|
+
import { dispatch, json } from './routes.js';
|
|
8
|
+
function safeEqualToken(provided, expected) {
|
|
9
|
+
if (!provided)
|
|
10
|
+
return false;
|
|
11
|
+
const a = Buffer.from(provided);
|
|
12
|
+
const b = Buffer.from(expected);
|
|
13
|
+
if (a.length !== b.length)
|
|
14
|
+
return false;
|
|
15
|
+
try {
|
|
16
|
+
return crypto.timingSafeEqual(a, b);
|
|
18
17
|
}
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
let cachedWriteDb = null;
|
|
22
|
-
function getWriteDb() {
|
|
23
|
-
if (!cachedWriteDb) {
|
|
24
|
-
cachedWriteDb = new Database(getDbPath());
|
|
25
|
-
cachedWriteDb.pragma('journal_mode = WAL');
|
|
18
|
+
catch {
|
|
19
|
+
return false;
|
|
26
20
|
}
|
|
27
|
-
return cachedWriteDb;
|
|
28
|
-
}
|
|
29
|
-
function withWriteDb(fn) {
|
|
30
|
-
return fn(getWriteDb());
|
|
31
|
-
}
|
|
32
|
-
function json(res, data, status = 200) {
|
|
33
|
-
res.writeHead(status, { 'Content-Type': 'application/json' });
|
|
34
|
-
res.end(JSON.stringify(data));
|
|
35
21
|
}
|
|
36
22
|
function openBrowser(url) {
|
|
37
23
|
const p = platform();
|
|
38
|
-
if (p === 'darwin')
|
|
24
|
+
if (p === 'darwin')
|
|
39
25
|
execFile('open', [url]);
|
|
40
|
-
|
|
41
|
-
else if (p === 'win32') {
|
|
26
|
+
else if (p === 'win32')
|
|
42
27
|
execFile('cmd', ['/c', 'start', url]);
|
|
43
|
-
|
|
44
|
-
else {
|
|
28
|
+
else
|
|
45
29
|
execFile('xdg-open', [url]);
|
|
46
|
-
}
|
|
47
30
|
}
|
|
31
|
+
const SECURITY_HEADERS = {
|
|
32
|
+
'X-Frame-Options': 'DENY',
|
|
33
|
+
'X-Content-Type-Options': 'nosniff',
|
|
34
|
+
'Referrer-Policy': 'no-referrer',
|
|
35
|
+
};
|
|
48
36
|
export function startDashboardServer(port = 0) {
|
|
49
|
-
// Generate auth token at server start
|
|
37
|
+
// Generate auth token at server start (24 random bytes → 192-bit base64url).
|
|
50
38
|
const authToken = crypto.randomBytes(24).toString('base64url');
|
|
51
39
|
const server = http.createServer(async (req, res) => {
|
|
52
40
|
const url = new URL(req.url || '/', `http://localhost`);
|
|
@@ -55,487 +43,44 @@ export function startDashboardServer(port = 0) {
|
|
|
55
43
|
if (path === '/' || path === '/index.html') {
|
|
56
44
|
const token = url.searchParams.get('token');
|
|
57
45
|
if (!token) {
|
|
58
|
-
// Redirect to add token
|
|
59
46
|
res.writeHead(302, { Location: `/?token=${authToken}` });
|
|
60
47
|
res.end();
|
|
61
48
|
return;
|
|
62
49
|
}
|
|
63
|
-
if (token
|
|
64
|
-
res.writeHead(403, { 'Content-Type': 'text/plain' });
|
|
50
|
+
if (!safeEqualToken(token, authToken)) {
|
|
51
|
+
res.writeHead(403, { 'Content-Type': 'text/plain', ...SECURITY_HEADERS });
|
|
65
52
|
res.end('Forbidden');
|
|
66
53
|
return;
|
|
67
54
|
}
|
|
68
55
|
res.writeHead(200, {
|
|
69
56
|
'Content-Type': 'text/html',
|
|
70
|
-
|
|
57
|
+
...SECURITY_HEADERS,
|
|
71
58
|
'Set-Cookie': `beecork_dash=${authToken}; HttpOnly; SameSite=Strict; Path=/`,
|
|
72
59
|
});
|
|
73
60
|
res.end(getDashboardHtml(authToken));
|
|
74
61
|
return;
|
|
75
62
|
}
|
|
76
|
-
// Auth check for API routes
|
|
63
|
+
// Auth check for API routes (constant-time compare)
|
|
77
64
|
if (path.startsWith('/api/')) {
|
|
78
65
|
const authHeader = req.headers.authorization;
|
|
79
66
|
const queryToken = url.searchParams.get('token');
|
|
80
67
|
const cookieToken = req.headers.cookie?.split(';').map(c => c.trim()).find(c => c.startsWith('beecork_dash='))?.split('=')[1];
|
|
81
68
|
const providedToken = authHeader?.replace('Bearer ', '') || queryToken || cookieToken;
|
|
82
|
-
if (providedToken
|
|
69
|
+
if (!safeEqualToken(providedToken, authToken)) {
|
|
83
70
|
json(res, { error: 'Unauthorized' }, 401);
|
|
84
71
|
return;
|
|
85
72
|
}
|
|
86
73
|
}
|
|
87
|
-
// API routes
|
|
88
74
|
try {
|
|
89
|
-
|
|
90
|
-
if (
|
|
91
|
-
res
|
|
92
|
-
'Content-Type': 'text/event-stream',
|
|
93
|
-
'Cache-Control': 'no-cache',
|
|
94
|
-
'Connection': 'keep-alive',
|
|
95
|
-
});
|
|
96
|
-
res.write(`data: ${JSON.stringify({ type: 'connected' })}\n\n`);
|
|
97
|
-
const interval = setInterval(() => {
|
|
98
|
-
try {
|
|
99
|
-
const db = getDashDb();
|
|
100
|
-
const tabs = db.prepare('SELECT name, status, last_activity_at FROM tabs ORDER BY last_activity_at DESC').all();
|
|
101
|
-
const activeCount = tabs.filter((t) => t.status === 'running').length;
|
|
102
|
-
res.write(`data: ${JSON.stringify({ type: 'update', tabs, activeTabs: activeCount })}\n\n`);
|
|
103
|
-
}
|
|
104
|
-
catch { }
|
|
105
|
-
}, 2000);
|
|
106
|
-
req.on('close', () => {
|
|
107
|
-
clearInterval(interval);
|
|
108
|
-
});
|
|
109
|
-
return;
|
|
110
|
-
}
|
|
111
|
-
// POST: Send message to tab
|
|
112
|
-
if (path.match(/^\/api\/tabs\/[^/]+\/send$/) && req.method === 'POST') {
|
|
113
|
-
let body = '';
|
|
114
|
-
for await (const chunk of req) {
|
|
115
|
-
body += chunk;
|
|
116
|
-
if (body.length > 1_000_000) {
|
|
117
|
-
json(res, { error: 'Payload too large' }, 413);
|
|
118
|
-
return;
|
|
119
|
-
}
|
|
120
|
-
}
|
|
121
|
-
let parsed;
|
|
122
|
-
try {
|
|
123
|
-
parsed = JSON.parse(body);
|
|
124
|
-
}
|
|
125
|
-
catch {
|
|
126
|
-
json(res, { error: 'Invalid JSON' }, 400);
|
|
127
|
-
return;
|
|
128
|
-
}
|
|
129
|
-
const { message } = parsed;
|
|
130
|
-
if (!message) {
|
|
131
|
-
json(res, { error: 'Missing message' }, 400);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
|
-
const tabName = decodeURIComponent(path.split('/')[3]);
|
|
135
|
-
const tabErr = validateTabName(tabName);
|
|
136
|
-
if (tabErr) {
|
|
137
|
-
json(res, { error: tabErr }, 400);
|
|
138
|
-
return;
|
|
139
|
-
}
|
|
140
|
-
withWriteDb(db => db.prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run(tabName, message, 'user'));
|
|
141
|
-
json(res, { success: true, tab: tabName });
|
|
142
|
-
return;
|
|
143
|
-
}
|
|
144
|
-
// POST: Create tab
|
|
145
|
-
if (path === '/api/tabs' && req.method === 'POST') {
|
|
146
|
-
let body = '';
|
|
147
|
-
for await (const chunk of req) {
|
|
148
|
-
body += chunk;
|
|
149
|
-
if (body.length > 1_000_000) {
|
|
150
|
-
json(res, { error: 'Payload too large' }, 413);
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
}
|
|
154
|
-
let parsedTab;
|
|
155
|
-
try {
|
|
156
|
-
parsedTab = JSON.parse(body);
|
|
157
|
-
}
|
|
158
|
-
catch {
|
|
159
|
-
json(res, { error: 'Invalid JSON' }, 400);
|
|
160
|
-
return;
|
|
161
|
-
}
|
|
162
|
-
const { name, workingDir, systemPrompt } = parsedTab;
|
|
163
|
-
if (!name) {
|
|
164
|
-
json(res, { error: 'Missing tab name' }, 400);
|
|
165
|
-
return;
|
|
166
|
-
}
|
|
167
|
-
const nameErr = validateTabName(name);
|
|
168
|
-
if (nameErr) {
|
|
169
|
-
json(res, { error: nameErr }, 400);
|
|
170
|
-
return;
|
|
171
|
-
}
|
|
172
|
-
withWriteDb(db => createTabRecord(db, { name, workingDir, systemPrompt }));
|
|
173
|
-
json(res, { success: true, name });
|
|
174
|
-
return;
|
|
175
|
-
}
|
|
176
|
-
// DELETE: Delete tab
|
|
177
|
-
if (path.match(/^\/api\/tabs\/[^/]+$/) && req.method === 'DELETE') {
|
|
178
|
-
const tabName = decodeURIComponent(path.split('/')[3]);
|
|
179
|
-
withWriteDb(db => {
|
|
180
|
-
const tab = db.prepare('SELECT id FROM tabs WHERE name = ?').get(tabName);
|
|
181
|
-
if (tab) {
|
|
182
|
-
db.prepare('DELETE FROM messages WHERE tab_id = ?').run(tab.id);
|
|
183
|
-
db.prepare('DELETE FROM tabs WHERE id = ?').run(tab.id);
|
|
184
|
-
}
|
|
185
|
-
});
|
|
186
|
-
json(res, { success: true });
|
|
187
|
-
return;
|
|
188
|
-
}
|
|
189
|
-
// POST: Create task (supports both /api/tasks and /api/crons)
|
|
190
|
-
if ((path === '/api/tasks' || path === '/api/crons') && req.method === 'POST') {
|
|
191
|
-
let body = '';
|
|
192
|
-
for await (const chunk of req) {
|
|
193
|
-
body += chunk;
|
|
194
|
-
if (body.length > 1_000_000) {
|
|
195
|
-
json(res, { error: 'Payload too large' }, 413);
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
let parsedCron;
|
|
200
|
-
try {
|
|
201
|
-
parsedCron = JSON.parse(body);
|
|
202
|
-
}
|
|
203
|
-
catch {
|
|
204
|
-
json(res, { error: 'Invalid JSON' }, 400);
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
const { name, scheduleType, schedule, tabName, message } = parsedCron;
|
|
208
|
-
if (!name || !schedule || !message) {
|
|
209
|
-
json(res, { error: 'Missing required fields' }, 400);
|
|
210
|
-
return;
|
|
211
|
-
}
|
|
212
|
-
const effectiveTab = tabName || 'default';
|
|
213
|
-
const cronTabErr = validateTabName(effectiveTab);
|
|
214
|
-
if (cronTabErr) {
|
|
215
|
-
json(res, { error: cronTabErr }, 400);
|
|
216
|
-
return;
|
|
217
|
-
}
|
|
218
|
-
const id = crypto.randomUUID();
|
|
219
|
-
withWriteDb(db => db.prepare('INSERT INTO tasks (id, name, schedule_type, schedule, tab_name, message, enabled) VALUES (?, ?, ?, ?, ?, ?, 1)').run(id, name, scheduleType || 'every', schedule, effectiveTab, message));
|
|
220
|
-
json(res, { success: true, id });
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
// DELETE: Delete task (supports both /api/tasks and /api/crons)
|
|
224
|
-
if ((path.match(/^\/api\/tasks\/[^/]+$/) || path.match(/^\/api\/crons\/[^/]+$/)) && req.method === 'DELETE') {
|
|
225
|
-
const taskId = decodeURIComponent(path.split('/')[3]);
|
|
226
|
-
withWriteDb(db => db.prepare('DELETE FROM tasks WHERE id = ?').run(taskId));
|
|
227
|
-
json(res, { success: true });
|
|
228
|
-
return;
|
|
229
|
-
}
|
|
230
|
-
// GET: List watchers
|
|
231
|
-
if (path === '/api/watchers' && req.method === 'GET') {
|
|
232
|
-
const watchers = getDashDb().prepare('SELECT * FROM watchers ORDER BY created_at').all();
|
|
233
|
-
json(res, watchers);
|
|
234
|
-
return;
|
|
235
|
-
}
|
|
236
|
-
// DELETE: Delete watcher
|
|
237
|
-
if (path.match(/^\/api\/watchers\/[^/]+$/) && req.method === 'DELETE') {
|
|
238
|
-
const watcherId = decodeURIComponent(path.split('/')[3]);
|
|
239
|
-
withWriteDb(db => db.prepare('DELETE FROM watchers WHERE id = ?').run(watcherId));
|
|
240
|
-
json(res, { success: true });
|
|
241
|
-
return;
|
|
242
|
-
}
|
|
243
|
-
// POST: Create memory
|
|
244
|
-
if (path === '/api/memories' && req.method === 'POST') {
|
|
245
|
-
let body = '';
|
|
246
|
-
for await (const chunk of req) {
|
|
247
|
-
body += chunk;
|
|
248
|
-
if (body.length > 1_000_000) {
|
|
249
|
-
json(res, { error: 'Payload too large' }, 413);
|
|
250
|
-
return;
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
let parsedMemory;
|
|
254
|
-
try {
|
|
255
|
-
parsedMemory = JSON.parse(body);
|
|
256
|
-
}
|
|
257
|
-
catch {
|
|
258
|
-
json(res, { error: 'Invalid JSON' }, 400);
|
|
259
|
-
return;
|
|
260
|
-
}
|
|
261
|
-
const { content, tabName } = parsedMemory;
|
|
262
|
-
if (!content) {
|
|
263
|
-
json(res, { error: 'Missing content' }, 400);
|
|
264
|
-
return;
|
|
265
|
-
}
|
|
266
|
-
withWriteDb(db => db.prepare('INSERT INTO memories (content, tab_name, source) VALUES (?, ?, ?)').run(content, tabName || null, 'tool'));
|
|
267
|
-
json(res, { success: true });
|
|
268
|
-
return;
|
|
269
|
-
}
|
|
270
|
-
// DELETE: Delete memory
|
|
271
|
-
if (path.match(/^\/api\/memories\/\d+$/) && req.method === 'DELETE') {
|
|
272
|
-
const memoryId = path.split('/')[3];
|
|
273
|
-
withWriteDb(db => db.prepare('DELETE FROM memories WHERE id = ?').run(memoryId));
|
|
274
|
-
json(res, { success: true });
|
|
275
|
-
return;
|
|
276
|
-
}
|
|
277
|
-
if (path === '/api/media/config') {
|
|
278
|
-
const { getConfig } = await import('../config.js');
|
|
279
|
-
const config = getConfig();
|
|
280
|
-
const generators = config.mediaGenerators || [];
|
|
281
|
-
const info = generators.map((g) => ({
|
|
282
|
-
provider: g.provider,
|
|
283
|
-
model: g.model,
|
|
284
|
-
configured: !!g.apiKey,
|
|
285
|
-
}));
|
|
286
|
-
json(res, { generators: info });
|
|
287
|
-
return;
|
|
288
|
-
}
|
|
289
|
-
if (path === '/api/channels/config') {
|
|
290
|
-
const { getConfig } = await import('../config.js');
|
|
291
|
-
const config = getConfig();
|
|
292
|
-
const channels = {
|
|
293
|
-
telegram: { configured: !!config.telegram?.token, botUsername: null },
|
|
294
|
-
discord: { configured: !!config.discord?.token },
|
|
295
|
-
whatsapp: { configured: !!config.whatsapp?.enabled },
|
|
296
|
-
webhook: { configured: !!config.webhook?.enabled, port: config.webhook?.port },
|
|
297
|
-
};
|
|
298
|
-
json(res, channels);
|
|
299
|
-
return;
|
|
300
|
-
}
|
|
301
|
-
if (path === '/api/computer-use' && req.method === 'POST') {
|
|
302
|
-
let body = '';
|
|
303
|
-
for await (const chunk of req) {
|
|
304
|
-
body += chunk;
|
|
305
|
-
if (body.length > 1_000_000) {
|
|
306
|
-
json(res, { error: 'Payload too large' }, 413);
|
|
307
|
-
return;
|
|
308
|
-
}
|
|
309
|
-
}
|
|
310
|
-
let parsedCU;
|
|
311
|
-
try {
|
|
312
|
-
parsedCU = JSON.parse(body);
|
|
313
|
-
}
|
|
314
|
-
catch {
|
|
315
|
-
json(res, { error: 'Invalid JSON' }, 400);
|
|
316
|
-
return;
|
|
317
|
-
}
|
|
318
|
-
const { enabled } = parsedCU;
|
|
319
|
-
const { getConfig, saveConfig } = await import('../config.js');
|
|
320
|
-
const config = getConfig();
|
|
321
|
-
config.claudeCode.computerUse = !!enabled;
|
|
322
|
-
saveConfig(config);
|
|
323
|
-
json(res, { enabled: !!enabled, message: 'Restart daemon to apply.' });
|
|
324
|
-
return;
|
|
325
|
-
}
|
|
326
|
-
if (path === '/api/computer-use') {
|
|
327
|
-
const { getConfig } = await import('../config.js');
|
|
328
|
-
const config = getConfig();
|
|
329
|
-
json(res, { enabled: !!config.claudeCode.computerUse });
|
|
330
|
-
return;
|
|
331
|
-
}
|
|
332
|
-
if (path === '/api/timeline') {
|
|
333
|
-
const { getTimeline } = await import('../timeline/index.js');
|
|
334
|
-
const date = url.searchParams.get('date') || new Date().toISOString().slice(0, 10);
|
|
335
|
-
const limit = parseInt(url.searchParams.get('limit') || '50');
|
|
336
|
-
const events = getTimeline({ date, limit });
|
|
337
|
-
json(res, { events });
|
|
338
|
-
return;
|
|
339
|
-
}
|
|
340
|
-
const db = getDashDb();
|
|
341
|
-
if (path === '/api/status') {
|
|
342
|
-
const pid = getDaemonPid();
|
|
343
|
-
const tabCount = db.prepare('SELECT COUNT(*) as c FROM tabs').get().c;
|
|
344
|
-
const activeCount = db.prepare("SELECT COUNT(*) as c FROM tabs WHERE status = 'running'").get().c;
|
|
345
|
-
const cronCount = db.prepare("SELECT COUNT(*) as c FROM tasks WHERE enabled = 1").get().c;
|
|
346
|
-
const memoryCount = db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
|
|
347
|
-
json(res, { version: VERSION, daemonPid: pid, tabs: tabCount, activeTabs: activeCount, cronJobs: cronCount, memories: memoryCount });
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
350
|
-
if (path === '/api/tabs' && req.method === 'GET') {
|
|
351
|
-
const tabs = db.prepare(`
|
|
352
|
-
SELECT t.*,
|
|
353
|
-
(SELECT COUNT(*) FROM messages WHERE tab_id = t.id) as message_count,
|
|
354
|
-
(SELECT COALESCE(SUM(cost_usd), 0) FROM messages WHERE tab_id = t.id) as total_cost
|
|
355
|
-
FROM tabs t ORDER BY t.last_activity_at DESC
|
|
356
|
-
`).all();
|
|
357
|
-
json(res, tabs);
|
|
358
|
-
return;
|
|
359
|
-
}
|
|
360
|
-
const tabMsgMatch = path.match(/^\/api\/tabs\/([^/]+)\/messages$/);
|
|
361
|
-
if (tabMsgMatch) {
|
|
362
|
-
const tabName = decodeURIComponent(tabMsgMatch[1]);
|
|
363
|
-
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 200);
|
|
364
|
-
const offset = parseInt(url.searchParams.get('offset') || '0');
|
|
365
|
-
const tab = db.prepare('SELECT id FROM tabs WHERE name = ?').get(tabName);
|
|
366
|
-
if (!tab) {
|
|
367
|
-
json(res, { error: 'Tab not found' }, 404);
|
|
368
|
-
return;
|
|
369
|
-
}
|
|
370
|
-
const messages = db.prepare('SELECT role, content, cost_usd, tokens_in, tokens_out, created_at FROM messages WHERE tab_id = ? ORDER BY created_at DESC LIMIT ? OFFSET ?').all(tab.id, limit, offset);
|
|
371
|
-
const total = db.prepare('SELECT COUNT(*) as c FROM messages WHERE tab_id = ?').get(tab.id).c;
|
|
372
|
-
json(res, { messages: messages.reverse(), total, limit, offset });
|
|
373
|
-
return;
|
|
374
|
-
}
|
|
375
|
-
if (path === '/api/memories' && req.method === 'GET') {
|
|
376
|
-
const limit = Math.min(parseInt(url.searchParams.get('limit') || '50'), 200);
|
|
377
|
-
const offset = parseInt(url.searchParams.get('offset') || '0');
|
|
378
|
-
const q = url.searchParams.get('q') || '';
|
|
379
|
-
let memories, total;
|
|
380
|
-
if (q) {
|
|
381
|
-
memories = db.prepare('SELECT id, content, tab_name, source, created_at FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT ? OFFSET ?').all(`%${q}%`, limit, offset);
|
|
382
|
-
total = db.prepare('SELECT COUNT(*) as c FROM memories WHERE content LIKE ?').get(`%${q}%`).c;
|
|
383
|
-
}
|
|
384
|
-
else {
|
|
385
|
-
memories = db.prepare('SELECT id, content, tab_name, source, created_at FROM memories ORDER BY created_at DESC LIMIT ? OFFSET ?').all(limit, offset);
|
|
386
|
-
total = db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
|
|
387
|
-
}
|
|
388
|
-
json(res, { memories, total, limit, offset });
|
|
389
|
-
return;
|
|
390
|
-
}
|
|
391
|
-
if ((path === '/api/tasks' || path === '/api/crons') && req.method === 'GET') {
|
|
392
|
-
const crons = db.prepare('SELECT * FROM tasks ORDER BY created_at').all();
|
|
393
|
-
json(res, crons);
|
|
394
|
-
return;
|
|
395
|
-
}
|
|
396
|
-
if (path === '/api/costs') {
|
|
397
|
-
const costs = db.prepare(`
|
|
398
|
-
SELECT date(created_at) as day,
|
|
399
|
-
SUM(cost_usd) as total_cost,
|
|
400
|
-
COUNT(*) as message_count
|
|
401
|
-
FROM messages
|
|
402
|
-
WHERE role = 'assistant' AND cost_usd > 0
|
|
403
|
-
AND created_at > datetime('now', '-30 days')
|
|
404
|
-
GROUP BY date(created_at)
|
|
405
|
-
ORDER BY day
|
|
406
|
-
`).all();
|
|
407
|
-
json(res, costs);
|
|
408
|
-
return;
|
|
409
|
-
}
|
|
410
|
-
// GET /api/update/status — check versions for beecork, claude code, and other packages
|
|
411
|
-
if (path === '/api/update/status') {
|
|
412
|
-
const execAsync = promisify(exec);
|
|
413
|
-
async function checkPackage(name, installCmd) {
|
|
414
|
-
const pkg = { name };
|
|
415
|
-
try {
|
|
416
|
-
const { stdout } = await execAsync(`${installCmd || name} --version`, { timeout: 10000 });
|
|
417
|
-
pkg.installed = stdout.trim().replace(/^v/, '');
|
|
418
|
-
}
|
|
419
|
-
catch {
|
|
420
|
-
pkg.installed = null;
|
|
421
|
-
}
|
|
422
|
-
try {
|
|
423
|
-
const { stdout } = await execAsync(`npm view ${name} version`, { timeout: 10000 });
|
|
424
|
-
pkg.latest = stdout.trim();
|
|
425
|
-
}
|
|
426
|
-
catch {
|
|
427
|
-
pkg.latest = null;
|
|
428
|
-
}
|
|
429
|
-
pkg.updateAvailable = !!(pkg.installed && pkg.latest && pkg.installed !== pkg.latest);
|
|
430
|
-
return pkg;
|
|
431
|
-
}
|
|
432
|
-
const packages = await Promise.all([
|
|
433
|
-
(async () => {
|
|
434
|
-
const p = await checkPackage('beecork');
|
|
435
|
-
p.installed = VERSION; // use our known version, more reliable
|
|
436
|
-
p.updateAvailable = !!(p.latest && p.installed !== p.latest);
|
|
437
|
-
return p;
|
|
438
|
-
})(),
|
|
439
|
-
(async () => {
|
|
440
|
-
const p = { name: '@anthropic-ai/claude-code' };
|
|
441
|
-
try {
|
|
442
|
-
const { stdout } = await execAsync('claude --version', { timeout: 10000 });
|
|
443
|
-
p.installed = stdout.trim().replace(/^.*?(\d+\.\d+\.\d+).*$/, '$1');
|
|
444
|
-
}
|
|
445
|
-
catch {
|
|
446
|
-
p.installed = null;
|
|
447
|
-
}
|
|
448
|
-
try {
|
|
449
|
-
const { stdout } = await execAsync('npm view @anthropic-ai/claude-code version', { timeout: 10000 });
|
|
450
|
-
p.latest = stdout.trim();
|
|
451
|
-
}
|
|
452
|
-
catch {
|
|
453
|
-
p.latest = null;
|
|
454
|
-
}
|
|
455
|
-
p.updateAvailable = !!(p.installed && p.latest && p.installed !== p.latest);
|
|
456
|
-
return p;
|
|
457
|
-
})(),
|
|
458
|
-
]);
|
|
459
|
-
json(res, { packages });
|
|
460
|
-
return;
|
|
461
|
-
}
|
|
462
|
-
// POST /api/update/:package — update a specific package
|
|
463
|
-
if (path.match(/^\/api\/update\/[^/]+$/) && req.method === 'POST') {
|
|
464
|
-
const pkgName = decodeURIComponent(path.split('/')[3]);
|
|
465
|
-
const execAsync = promisify(exec);
|
|
466
|
-
const allowedPackages = {
|
|
467
|
-
'beecork': 'npm install -g beecork@latest',
|
|
468
|
-
'@anthropic-ai/claude-code': 'npm install -g @anthropic-ai/claude-code@latest',
|
|
469
|
-
};
|
|
470
|
-
const cmd = allowedPackages[pkgName];
|
|
471
|
-
if (!cmd) {
|
|
472
|
-
json(res, { error: `Package "${pkgName}" is not in the allowed update list.` }, 400);
|
|
473
|
-
return;
|
|
474
|
-
}
|
|
475
|
-
try {
|
|
476
|
-
const { stdout } = await execAsync(cmd, { timeout: 120000 });
|
|
477
|
-
json(res, { success: true, package: pkgName, output: stdout.trim() });
|
|
478
|
-
}
|
|
479
|
-
catch (err) {
|
|
480
|
-
json(res, { error: err instanceof Error ? err.message : String(err) }, 500);
|
|
481
|
-
}
|
|
482
|
-
return;
|
|
483
|
-
}
|
|
484
|
-
// GET /api/capabilities — list all packs + enabled status
|
|
485
|
-
if (path === '/api/capabilities') {
|
|
486
|
-
const { getAvailablePacks, isEnabled } = await import('../capabilities/index.js');
|
|
487
|
-
const packs = getAvailablePacks().map(p => ({
|
|
488
|
-
...p,
|
|
489
|
-
enabled: isEnabled(p.id),
|
|
490
|
-
// Don't expose API keys in the response
|
|
491
|
-
mcpServer: { package: p.mcpServer.package },
|
|
492
|
-
}));
|
|
493
|
-
json(res, { packs });
|
|
494
|
-
return;
|
|
495
|
-
}
|
|
496
|
-
// POST /api/capabilities/:id/enable — enable a pack
|
|
497
|
-
if (path.match(/^\/api\/capabilities\/[^/]+\/enable$/) && req.method === 'POST') {
|
|
498
|
-
const packId = path.split('/')[3];
|
|
499
|
-
let body = '';
|
|
500
|
-
for await (const chunk of req) {
|
|
501
|
-
body += chunk;
|
|
502
|
-
if (body.length > 1_000_000) {
|
|
503
|
-
json(res, { error: 'Payload too large' }, 413);
|
|
504
|
-
return;
|
|
505
|
-
}
|
|
506
|
-
}
|
|
507
|
-
let parsedCap;
|
|
508
|
-
try {
|
|
509
|
-
parsedCap = JSON.parse(body);
|
|
510
|
-
}
|
|
511
|
-
catch {
|
|
512
|
-
json(res, { error: 'Invalid JSON' }, 400);
|
|
513
|
-
return;
|
|
514
|
-
}
|
|
515
|
-
const { apiKey } = parsedCap;
|
|
516
|
-
const { enablePack } = await import('../capabilities/index.js');
|
|
517
|
-
try {
|
|
518
|
-
enablePack(packId, apiKey);
|
|
519
|
-
json(res, { success: true, message: 'Restart daemon to activate.' });
|
|
520
|
-
}
|
|
521
|
-
catch (err) {
|
|
522
|
-
json(res, { error: err instanceof Error ? err.message : String(err) }, 400);
|
|
523
|
-
}
|
|
524
|
-
return;
|
|
525
|
-
}
|
|
526
|
-
// POST /api/capabilities/:id/disable — disable a pack
|
|
527
|
-
if (path.match(/^\/api\/capabilities\/[^/]+\/disable$/) && req.method === 'POST') {
|
|
528
|
-
const packId = path.split('/')[3];
|
|
529
|
-
const { disablePack } = await import('../capabilities/index.js');
|
|
530
|
-
disablePack(packId);
|
|
531
|
-
json(res, { success: true });
|
|
75
|
+
const route = dispatch(req.method || 'GET', path);
|
|
76
|
+
if (!route) {
|
|
77
|
+
json(res, { error: 'Not found' }, 404);
|
|
532
78
|
return;
|
|
533
79
|
}
|
|
534
|
-
|
|
535
|
-
json(res, { error: 'Not found' }, 404);
|
|
80
|
+
await route.handler({ req, res, url, path });
|
|
536
81
|
}
|
|
537
82
|
catch (err) {
|
|
538
|
-
|
|
83
|
+
logger.error('Dashboard error:', err);
|
|
539
84
|
json(res, { error: 'Internal server error' }, 500);
|
|
540
85
|
}
|
|
541
86
|
});
|
package/dist/db/index.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
+
import fs from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
2
4
|
import Database from 'better-sqlite3';
|
|
3
|
-
import { getDbPath, ensureBeecorkDirs } from '../util/paths.js';
|
|
5
|
+
import { getDbPath, ensureBeecorkDirs, expandHome } from '../util/paths.js';
|
|
4
6
|
import { runMigrations } from './migrations.js';
|
|
5
7
|
import { logger } from '../util/logger.js';
|
|
6
8
|
const SCHEMA = `
|
|
@@ -66,6 +68,9 @@ export function getDb() {
|
|
|
66
68
|
// Prune old permission history (keep last 1000 entries)
|
|
67
69
|
db?.exec('DELETE FROM permission_history WHERE created_at < (SELECT created_at FROM permission_history ORDER BY created_at DESC LIMIT 1 OFFSET 999)');
|
|
68
70
|
db?.exec("DELETE FROM activity_log WHERE created_at < datetime('now', '-90 days')");
|
|
71
|
+
// Prune unused routing patterns so the per-message learned-routing scan
|
|
72
|
+
// doesn't grow unbounded. Keep patterns that have been hit recently or often.
|
|
73
|
+
db?.exec("DELETE FROM routing_preferences WHERE hit_count < 3 AND created_at < datetime('now', '-30 days')");
|
|
69
74
|
}
|
|
70
75
|
catch (err) {
|
|
71
76
|
logger.warn('WAL checkpoint/cleanup error:', err);
|
|
@@ -78,8 +83,17 @@ export function createTabRecord(db, opts) {
|
|
|
78
83
|
const existing = db.prepare('SELECT name FROM tabs WHERE name = ?').get(opts.name);
|
|
79
84
|
if (existing)
|
|
80
85
|
return { id: '', name: opts.name, created: false };
|
|
86
|
+
// Resolve and validate workingDir. Done here so every caller (CLI, dashboard, MCP)
|
|
87
|
+
// gets the same checks instead of each rolling its own.
|
|
88
|
+
let dir = opts.workingDir || process.env.HOME || '/';
|
|
89
|
+
dir = path.resolve(expandHome(dir));
|
|
90
|
+
if (!fs.existsSync(dir)) {
|
|
91
|
+
throw new Error(`workingDir does not exist: ${dir}`);
|
|
92
|
+
}
|
|
93
|
+
if (!fs.statSync(dir).isDirectory()) {
|
|
94
|
+
throw new Error(`workingDir is not a directory: ${dir}`);
|
|
95
|
+
}
|
|
81
96
|
const id = crypto.randomUUID();
|
|
82
|
-
const dir = opts.workingDir || process.env.HOME || '/';
|
|
83
97
|
db.prepare('INSERT INTO tabs (id, name, session_id, status, working_dir, system_prompt) VALUES (?, ?, ?, ?, ?, ?)').run(id, opts.name, crypto.randomUUID(), 'idle', dir, opts.systemPrompt || null);
|
|
84
98
|
return { id, name: opts.name, created: true };
|
|
85
99
|
}
|