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
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,48 @@ 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
|
-
const cookieToken = req.headers.cookie
|
|
67
|
+
const cookieToken = req.headers.cookie
|
|
68
|
+
?.split(';')
|
|
69
|
+
.map((c) => c.trim())
|
|
70
|
+
.find((c) => c.startsWith('beecork_dash='))
|
|
71
|
+
?.split('=')[1];
|
|
81
72
|
const providedToken = authHeader?.replace('Bearer ', '') || queryToken || cookieToken;
|
|
82
|
-
if (providedToken
|
|
73
|
+
if (!safeEqualToken(providedToken, authToken)) {
|
|
83
74
|
json(res, { error: 'Unauthorized' }, 401);
|
|
84
75
|
return;
|
|
85
76
|
}
|
|
86
77
|
}
|
|
87
|
-
// API routes
|
|
88
78
|
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 });
|
|
79
|
+
const route = dispatch(req.method || 'GET', path);
|
|
80
|
+
if (!route) {
|
|
81
|
+
json(res, { error: 'Not found' }, 404);
|
|
532
82
|
return;
|
|
533
83
|
}
|
|
534
|
-
|
|
535
|
-
json(res, { error: 'Not found' }, 404);
|
|
84
|
+
await route.handler({ req, res, url, path });
|
|
536
85
|
}
|
|
537
86
|
catch (err) {
|
|
538
|
-
|
|
87
|
+
logger.error('Dashboard error:', err);
|
|
539
88
|
json(res, { error: 'Internal server error' }, 500);
|
|
540
89
|
}
|
|
541
90
|
});
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for opening the Beecork SQLite database.
|
|
3
|
+
*
|
|
4
|
+
* Previously three sites each opened the DB with their own pragma setup:
|
|
5
|
+
* - daemon side (db/index.ts) — applied migrations + WAL checkpoint interval
|
|
6
|
+
* - MCP side (mcp/server.ts) — read-only-ish singleton, no migrations
|
|
7
|
+
* - doctor (cli/doctor.ts) — ad-hoc read-only handle
|
|
8
|
+
*
|
|
9
|
+
* The pragmas matched today but the duplication was silent-drift bait. This
|
|
10
|
+
* helper centralizes pragma setup so future tweaks land in one place.
|
|
11
|
+
*/
|
|
12
|
+
import Database from 'better-sqlite3';
|
|
13
|
+
export interface OpenDbOptions {
|
|
14
|
+
/** Open the file read-only (used by `beecork doctor` for status snapshots). */
|
|
15
|
+
readonly?: boolean;
|
|
16
|
+
/**
|
|
17
|
+
* If true, restrict file mode to 0o600 after open. Daemon side wants this;
|
|
18
|
+
* read-only sidecar handles don't need to retouch perms.
|
|
19
|
+
*/
|
|
20
|
+
enforcePerms?: boolean;
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Open a Beecork SQLite database with consistent pragmas.
|
|
24
|
+
*
|
|
25
|
+
* Migrations are NOT applied here — the daemon's `getDb()` runs migrations
|
|
26
|
+
* separately, so this helper can be reused by the MCP child + doctor without
|
|
27
|
+
* accidentally double-applying migrations.
|
|
28
|
+
*/
|
|
29
|
+
export declare function openDb(dbPath: string, opts?: OpenDbOptions): Database.Database;
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Single source of truth for opening the Beecork SQLite database.
|
|
3
|
+
*
|
|
4
|
+
* Previously three sites each opened the DB with their own pragma setup:
|
|
5
|
+
* - daemon side (db/index.ts) — applied migrations + WAL checkpoint interval
|
|
6
|
+
* - MCP side (mcp/server.ts) — read-only-ish singleton, no migrations
|
|
7
|
+
* - doctor (cli/doctor.ts) — ad-hoc read-only handle
|
|
8
|
+
*
|
|
9
|
+
* The pragmas matched today but the duplication was silent-drift bait. This
|
|
10
|
+
* helper centralizes pragma setup so future tweaks land in one place.
|
|
11
|
+
*/
|
|
12
|
+
import Database from 'better-sqlite3';
|
|
13
|
+
import fs from 'node:fs';
|
|
14
|
+
/**
|
|
15
|
+
* Open a Beecork SQLite database with consistent pragmas.
|
|
16
|
+
*
|
|
17
|
+
* Migrations are NOT applied here — the daemon's `getDb()` runs migrations
|
|
18
|
+
* separately, so this helper can be reused by the MCP child + doctor without
|
|
19
|
+
* accidentally double-applying migrations.
|
|
20
|
+
*/
|
|
21
|
+
export function openDb(dbPath, opts = {}) {
|
|
22
|
+
const db = new Database(dbPath, opts.readonly ? { readonly: true } : undefined);
|
|
23
|
+
db.pragma('journal_mode = WAL');
|
|
24
|
+
db.pragma('foreign_keys = ON');
|
|
25
|
+
db.pragma('busy_timeout = 5000');
|
|
26
|
+
if (opts.enforcePerms && !opts.readonly) {
|
|
27
|
+
for (const p of [dbPath, dbPath + '-wal', dbPath + '-shm']) {
|
|
28
|
+
try {
|
|
29
|
+
fs.chmodSync(p, 0o600);
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
/* sidecar may not exist yet */
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return db;
|
|
37
|
+
}
|