beecork 1.5.0 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/capabilities/index.d.ts +1 -1
- package/dist/capabilities/index.js +1 -1
- package/dist/capabilities/manager.js +13 -9
- package/dist/capabilities/packs.js +3 -1
- package/dist/channels/command-handler.js +46 -14
- package/dist/channels/discord.d.ts +3 -6
- package/dist/channels/discord.js +40 -23
- package/dist/channels/index.d.ts +1 -1
- package/dist/channels/loader.js +13 -3
- package/dist/channels/pipeline.js +14 -5
- package/dist/channels/registry.d.ts +17 -1
- package/dist/channels/registry.js +33 -4
- package/dist/channels/telegram.d.ts +20 -5
- package/dist/channels/telegram.js +177 -42
- package/dist/channels/types.d.ts +11 -28
- package/dist/channels/voice-state.js +3 -1
- package/dist/channels/webhook.d.ts +1 -4
- package/dist/channels/webhook.js +26 -11
- package/dist/channels/whatsapp.d.ts +8 -4
- package/dist/channels/whatsapp.js +65 -29
- package/dist/cli/capabilities.js +4 -4
- package/dist/cli/channel.js +16 -6
- package/dist/cli/commands.js +12 -9
- package/dist/cli/doctor.js +80 -25
- package/dist/cli/handoff.d.ts +7 -14
- package/dist/cli/handoff.js +9 -44
- package/dist/cli/mcp.js +5 -5
- package/dist/cli/media.js +21 -8
- package/dist/cli/setup.js +9 -8
- package/dist/cli/store.js +29 -12
- package/dist/config.js +5 -10
- package/dist/daemon.js +88 -38
- package/dist/dashboard/html.js +80 -12
- package/dist/dashboard/routes.js +143 -79
- package/dist/dashboard/server.js +5 -1
- package/dist/db/connection.d.ts +29 -0
- package/dist/db/connection.js +37 -0
- package/dist/db/index.js +30 -12
- package/dist/db/migrations.js +84 -28
- package/dist/delegation/manager.js +10 -4
- package/dist/index.js +39 -59
- package/dist/knowledge/manager.js +26 -12
- package/dist/mcp/handlers.js +126 -57
- package/dist/mcp/server.js +20 -10
- package/dist/mcp/tool-definitions.js +68 -20
- package/dist/mcp/validate.d.ts +23 -0
- package/dist/mcp/validate.js +65 -0
- package/dist/media/factory.js +18 -14
- package/dist/media/generators/dall-e.js +2 -2
- package/dist/media/generators/kling.js +4 -4
- package/dist/media/generators/lyria.js +1 -1
- package/dist/media/generators/nano-banana.d.ts +1 -1
- package/dist/media/generators/nano-banana.js +2 -2
- package/dist/media/generators/poll-util.js +4 -4
- package/dist/media/generators/recraft.js +3 -3
- package/dist/media/generators/runway.js +4 -4
- package/dist/media/generators/stable-diffusion.js +2 -2
- package/dist/media/generators/veo.js +1 -1
- package/dist/media/index.js +1 -1
- package/dist/media/store.d.ts +7 -0
- package/dist/media/store.js +18 -4
- package/dist/media/types.d.ts +22 -0
- package/dist/notifications/index.d.ts +2 -4
- package/dist/notifications/index.js +6 -19
- package/dist/notifications/ntfy.js +3 -3
- package/dist/observability/analytics.js +35 -13
- package/dist/projects/index.d.ts +1 -1
- package/dist/projects/index.js +1 -1
- package/dist/projects/manager.d.ts +0 -4
- package/dist/projects/manager.js +51 -28
- package/dist/projects/router.d.ts +2 -0
- package/dist/projects/router.js +70 -45
- package/dist/service/install.js +15 -5
- package/dist/service/windows.js +1 -1
- package/dist/session/budget-guard.d.ts +20 -0
- package/dist/session/budget-guard.js +31 -0
- package/dist/session/circuit-breaker.d.ts +5 -3
- package/dist/session/circuit-breaker.js +45 -20
- package/dist/session/context-compactor.d.ts +32 -0
- package/dist/session/context-compactor.js +45 -0
- package/dist/session/context-monitor.js +2 -2
- package/dist/session/handoff.d.ts +21 -0
- package/dist/session/handoff.js +50 -0
- package/dist/session/manager.d.ts +17 -5
- package/dist/session/manager.js +153 -146
- package/dist/session/memory-store.d.ts +29 -0
- package/dist/session/memory-store.js +45 -0
- package/dist/session/message-queue.d.ts +28 -0
- package/dist/session/message-queue.js +52 -0
- package/dist/session/pending-dispatcher.d.ts +31 -0
- package/dist/session/pending-dispatcher.js +120 -0
- package/dist/session/pending-store.d.ts +60 -0
- package/dist/session/pending-store.js +118 -0
- package/dist/session/stale-session.d.ts +31 -0
- package/dist/session/stale-session.js +45 -0
- package/dist/session/subprocess.d.ts +2 -0
- package/dist/session/subprocess.js +33 -11
- package/dist/session/tab-store.js +4 -3
- package/dist/tasks/scheduler.d.ts +7 -0
- package/dist/tasks/scheduler.js +46 -6
- package/dist/tasks/store.js +20 -6
- package/dist/timeline/logger.js +3 -1
- package/dist/timeline/query.js +9 -3
- package/dist/types.d.ts +34 -9
- package/dist/util/auto-heal.js +15 -5
- package/dist/util/install-info.js +3 -1
- package/dist/util/logger.d.ts +1 -1
- package/dist/util/logger.js +63 -24
- package/dist/util/paths.d.ts +1 -0
- package/dist/util/paths.js +12 -2
- package/dist/util/retry.js +1 -1
- package/dist/util/text.js +13 -7
- package/dist/voice/index.js +5 -1
- package/dist/voice/stt.js +14 -6
- package/dist/voice/tts.js +1 -1
- package/dist/watchers/scheduler.js +9 -2
- package/package.json +18 -13
- package/dist/session/tool-classifier.d.ts +0 -4
- package/dist/session/tool-classifier.js +0 -56
package/dist/dashboard/html.js
CHANGED
|
@@ -125,8 +125,9 @@ export function getDashboardHtml(token) {
|
|
|
125
125
|
<!-- Message input -->
|
|
126
126
|
<div id="msg-input-area" class="hidden shrink-0 border-t border-bee-700 bg-bee-800 p-3">
|
|
127
127
|
<form id="send-form" onsubmit="sendMessage(event)" class="flex gap-2">
|
|
128
|
+
<label for="msg-input" class="sr-only">Message</label>
|
|
128
129
|
<input id="msg-input" type="text" placeholder="Send a message to this tab..."
|
|
129
|
-
class="input-field flex-1 px-3 py-2 text-sm" autocomplete="off">
|
|
130
|
+
class="input-field flex-1 px-3 py-2 text-sm" autocomplete="off" aria-label="Message">
|
|
130
131
|
<button type="submit" class="btn-primary px-4 py-2 text-sm">Send</button>
|
|
131
132
|
</form>
|
|
132
133
|
</div>
|
|
@@ -140,8 +141,9 @@ export function getDashboardHtml(token) {
|
|
|
140
141
|
<div class="px-4 py-3 border-b border-bee-700 flex flex-col sm:flex-row items-start sm:items-center justify-between gap-2">
|
|
141
142
|
<h2 class="text-sm font-semibold text-gray-300">Memories</h2>
|
|
142
143
|
<div class="flex items-center gap-3 w-full sm:w-auto">
|
|
144
|
+
<label for="memory-search" class="sr-only">Search memories</label>
|
|
143
145
|
<input id="memory-search" type="text" placeholder="Search..."
|
|
144
|
-
class="input-field px-3 py-1.5 text-sm flex-1 sm:w-48" oninput="debounceMemorySearch()">
|
|
146
|
+
class="input-field px-3 py-1.5 text-sm flex-1 sm:w-48" oninput="debounceMemorySearch()" aria-label="Search memories">
|
|
145
147
|
<button onclick="showCreateMemoryModal()" class="btn-ghost px-2 py-1 text-xs whitespace-nowrap">+ Add</button>
|
|
146
148
|
<span id="memory-count" class="text-xs text-gray-500 whitespace-nowrap"></span>
|
|
147
149
|
</div>
|
|
@@ -181,9 +183,8 @@ export function getDashboardHtml(token) {
|
|
|
181
183
|
<div id="cost-chart" class="p-6"></div>
|
|
182
184
|
</div>
|
|
183
185
|
</div>
|
|
184
|
-
</div>
|
|
185
186
|
|
|
186
|
-
<!-- Update Panel -->
|
|
187
|
+
<!-- Update Panel — must live inside #app so it inherits the height/scroll setup -->
|
|
187
188
|
<div id="panel-update" class="panel hidden h-full overflow-y-auto p-4">
|
|
188
189
|
<div class="max-w-lg space-y-3">
|
|
189
190
|
<div id="update-packages" class="space-y-3 text-sm text-gray-400">Checking for updates...</div>
|
|
@@ -198,10 +199,21 @@ export function getDashboardHtml(token) {
|
|
|
198
199
|
<script>
|
|
199
200
|
const API_TOKEN = ${JSON.stringify(token)};
|
|
200
201
|
|
|
202
|
+
// Scrub the auth token out of the URL bar once the cookie is set. The token
|
|
203
|
+
// is required for first-load but lingers in browser history / referrer-able
|
|
204
|
+
// copies of the URL otherwise. Referrer-Policy: no-referrer prevents the
|
|
205
|
+
// worst case; this is defense-in-depth.
|
|
206
|
+
if (location.search.includes('token=')) {
|
|
207
|
+
try { history.replaceState(null, '', location.pathname); } catch {}
|
|
208
|
+
}
|
|
209
|
+
|
|
201
210
|
// State
|
|
202
211
|
let selectedTab = null;
|
|
203
212
|
let memorySearchTimer = null;
|
|
204
213
|
let tabsData = [];
|
|
214
|
+
// Track last-seen message count per tab so the background refresh can skip
|
|
215
|
+
// the DOM rebuild entirely when nothing changed (M23 — fixes scroll jumping).
|
|
216
|
+
const lastMessageTotals = new Map();
|
|
205
217
|
|
|
206
218
|
// Toast notifications
|
|
207
219
|
function showToast(msg, isError) {
|
|
@@ -323,7 +335,12 @@ export function getDashboardHtml(token) {
|
|
|
323
335
|
|
|
324
336
|
// --- Tabs ---
|
|
325
337
|
async function loadTabs() {
|
|
326
|
-
try { tabsData = await api('/api/tabs'); }
|
|
338
|
+
try { tabsData = await api('/api/tabs'); }
|
|
339
|
+
catch (err) {
|
|
340
|
+
const list = document.getElementById('tab-list');
|
|
341
|
+
if (list) list.innerHTML = '<p class="text-red-400 text-xs text-center py-8">Failed to load tabs (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
327
344
|
const list = document.getElementById('tab-list');
|
|
328
345
|
document.getElementById('tab-count').textContent = tabsData.length.toString();
|
|
329
346
|
|
|
@@ -335,7 +352,10 @@ export function getDashboardHtml(token) {
|
|
|
335
352
|
list.innerHTML = tabsData.map(t => {
|
|
336
353
|
const isActive = selectedTab === t.name ? ' active' : '';
|
|
337
354
|
const cost = t.total_cost > 0 ? '$' + t.total_cost.toFixed(4) : '';
|
|
338
|
-
|
|
355
|
+
// esc() already replaces ' with ', and tab names are regex-validated
|
|
356
|
+
// to [a-zA-Z0-9-] anyway, so the previous chained .replace(/'/g, ...) was
|
|
357
|
+
// dead code. Drop it for clarity.
|
|
358
|
+
return '<div class="tab-item px-3 py-2.5 cursor-pointer' + isActive + '" data-tab-name="' + esc(t.name) + '" role="button" tabindex="0" onclick="selectTab(\\'' + esc(t.name) + '\\')" onkeydown="if(event.key===\\'Enter\\')selectTab(\\'' + esc(t.name) + '\\')">' +
|
|
339
359
|
'<div class="flex items-center justify-between">' +
|
|
340
360
|
'<div class="flex items-center gap-2 min-w-0">' +
|
|
341
361
|
'<span class="status-dot tab-status-dot status-' + esc(t.status) + '"></span>' +
|
|
@@ -368,15 +388,32 @@ export function getDashboardHtml(token) {
|
|
|
368
388
|
document.getElementById('msg-tab-status').textContent = tab.status;
|
|
369
389
|
}
|
|
370
390
|
|
|
371
|
-
let data; try { data = await api('/api/tabs/' + encodeURIComponent(name) + '/messages?limit=100'); } catch {
|
|
391
|
+
let data; try { data = await api('/api/tabs/' + encodeURIComponent(name) + '/messages?limit=100'); } catch (err) {
|
|
392
|
+
const list = document.getElementById('msg-list');
|
|
393
|
+
if (list) list.innerHTML = '<p class="text-red-400 text-sm text-center py-8">Failed to load messages (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
372
396
|
const list = document.getElementById('msg-list');
|
|
373
397
|
document.getElementById('msg-count').textContent = data.total + ' messages';
|
|
374
398
|
|
|
399
|
+
// Skip the full DOM rebuild when nothing changed — preserves user's scroll
|
|
400
|
+
// position, expanded <details> blocks, text selection. Without this guard
|
|
401
|
+
// the 8s background refresh kept teleporting the user to the top of the
|
|
402
|
+
// message list every tick.
|
|
403
|
+
const prevTotal = lastMessageTotals.get(name);
|
|
404
|
+
if (!fromUser && prevTotal === data.total) return;
|
|
405
|
+
lastMessageTotals.set(name, data.total);
|
|
406
|
+
|
|
375
407
|
if (data.messages.length === 0) {
|
|
376
408
|
list.innerHTML = '<p class="text-gray-600 text-sm text-center py-16">No messages in this tab</p>';
|
|
377
409
|
return;
|
|
378
410
|
}
|
|
379
411
|
|
|
412
|
+
// Capture scroll position relative to the bottom so we can restore after
|
|
413
|
+
// the rewrite. distanceFromBottom = scrollHeight - scrollTop - clientHeight
|
|
414
|
+
const wasNearBottom = !fromUser && (list.scrollHeight - list.scrollTop - list.clientHeight < 60);
|
|
415
|
+
const distanceFromBottom = list.scrollHeight - list.scrollTop;
|
|
416
|
+
|
|
380
417
|
list.innerHTML = data.messages.map(m => {
|
|
381
418
|
const cls = m.role === 'user' ? 'msg-user' : 'msg-assistant';
|
|
382
419
|
const label = m.role === 'user' ? 'You' : 'Claude';
|
|
@@ -406,6 +443,13 @@ export function getDashboardHtml(token) {
|
|
|
406
443
|
if (fromUser) {
|
|
407
444
|
list.scrollTop = list.scrollHeight;
|
|
408
445
|
document.getElementById('msg-input').focus();
|
|
446
|
+
} else if (wasNearBottom) {
|
|
447
|
+
// User was reading the latest messages — keep them pinned to the bottom.
|
|
448
|
+
list.scrollTop = list.scrollHeight;
|
|
449
|
+
} else {
|
|
450
|
+
// User was scrolled up reading older messages — restore their position
|
|
451
|
+
// relative to the bottom so new messages don't yank them around.
|
|
452
|
+
list.scrollTop = list.scrollHeight - distanceFromBottom;
|
|
409
453
|
}
|
|
410
454
|
}
|
|
411
455
|
|
|
@@ -492,7 +536,13 @@ export function getDashboardHtml(token) {
|
|
|
492
536
|
// --- Memories ---
|
|
493
537
|
async function loadMemories(query) {
|
|
494
538
|
const q = query || document.getElementById('memory-search').value || '';
|
|
495
|
-
let data;
|
|
539
|
+
let data;
|
|
540
|
+
try { data = await api('/api/memories?limit=100&q=' + encodeURIComponent(q)); }
|
|
541
|
+
catch (err) {
|
|
542
|
+
const list = document.getElementById('memory-list');
|
|
543
|
+
if (list) list.innerHTML = '<p class="text-red-400 text-sm text-center py-8">Failed to load memories (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
496
546
|
const list = document.getElementById('memory-list');
|
|
497
547
|
document.getElementById('memory-count').textContent = data.total + ' total';
|
|
498
548
|
|
|
@@ -560,7 +610,13 @@ export function getDashboardHtml(token) {
|
|
|
560
610
|
|
|
561
611
|
// --- Tasks (formerly Crons) ---
|
|
562
612
|
async function loadCrons() {
|
|
563
|
-
let crons;
|
|
613
|
+
let crons;
|
|
614
|
+
try { crons = await api('/api/tasks'); }
|
|
615
|
+
catch (err) {
|
|
616
|
+
const list = document.getElementById('cron-list');
|
|
617
|
+
if (list) list.innerHTML = '<p class="text-red-400 text-sm text-center py-8">Failed to load tasks (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
|
|
618
|
+
return;
|
|
619
|
+
}
|
|
564
620
|
const list = document.getElementById('cron-list');
|
|
565
621
|
|
|
566
622
|
if (crons.length === 0) {
|
|
@@ -577,7 +633,7 @@ export function getDashboardHtml(token) {
|
|
|
577
633
|
'<span class="text-sm font-medium text-white">' + esc(c.name) + '</span>' +
|
|
578
634
|
'</div>' +
|
|
579
635
|
'<div class="flex items-center gap-2">' +
|
|
580
|
-
'<span class="text-xs font-mono text-gray-400">' + c.schedule_type + ': ' + esc(c.schedule) + '</span>' +
|
|
636
|
+
'<span class="text-xs font-mono text-gray-400">' + esc(c.schedule_type) + ': ' + esc(c.schedule) + '</span>' +
|
|
581
637
|
'<button class="btn-danger px-1.5 py-0.5 text-xs opacity-0 group-hover:opacity-100" aria-label="Delete task" onclick="deleteCron(\\'' + esc(c.id) + '\\')">x</button>' +
|
|
582
638
|
'</div>' +
|
|
583
639
|
'</div>' +
|
|
@@ -637,7 +693,13 @@ export function getDashboardHtml(token) {
|
|
|
637
693
|
|
|
638
694
|
// --- Watchers ---
|
|
639
695
|
async function loadWatchers() {
|
|
640
|
-
let watchers;
|
|
696
|
+
let watchers;
|
|
697
|
+
try { watchers = await api('/api/watchers'); }
|
|
698
|
+
catch (err) {
|
|
699
|
+
const list = document.getElementById('watcher-list');
|
|
700
|
+
if (list) list.innerHTML = '<p class="text-red-400 text-sm text-center py-8">Failed to load watchers (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
641
703
|
const list = document.getElementById('watcher-list');
|
|
642
704
|
|
|
643
705
|
if (watchers.length === 0) {
|
|
@@ -675,7 +737,13 @@ export function getDashboardHtml(token) {
|
|
|
675
737
|
|
|
676
738
|
// --- Costs ---
|
|
677
739
|
async function loadCosts() {
|
|
678
|
-
let costs;
|
|
740
|
+
let costs;
|
|
741
|
+
try { costs = await api('/api/costs'); }
|
|
742
|
+
catch (err) {
|
|
743
|
+
const chart = document.getElementById('cost-chart');
|
|
744
|
+
if (chart) chart.innerHTML = '<p class="text-red-400 text-sm text-center py-8">Failed to load costs (' + esc(err && err.message ? err.message : String(err)) + ')</p>';
|
|
745
|
+
return;
|
|
746
|
+
}
|
|
679
747
|
const chart = document.getElementById('cost-chart');
|
|
680
748
|
|
|
681
749
|
if (costs.length === 0) {
|
package/dist/dashboard/routes.js
CHANGED
|
@@ -2,17 +2,37 @@
|
|
|
2
2
|
// pathPattern is a regex string (or exact path). The dispatcher in server.ts
|
|
3
3
|
// chooses the first matching entry and invokes its handler.
|
|
4
4
|
import crypto from 'node:crypto';
|
|
5
|
-
import
|
|
5
|
+
import os from 'node:os';
|
|
6
|
+
import path from 'node:path';
|
|
7
|
+
import { execFile } from 'node:child_process';
|
|
6
8
|
import { promisify } from 'node:util';
|
|
7
9
|
import { getDb } from '../db/index.js';
|
|
8
10
|
import { logger } from '../util/logger.js';
|
|
9
|
-
import { validateTabName, validateTabNameOrDefault } from '../config.js';
|
|
11
|
+
import { validateTabName, validateTabNameOrDefault, getConfig } from '../config.js';
|
|
10
12
|
import { createTabRecord } from '../db/index.js';
|
|
11
13
|
import { VERSION } from '../version.js';
|
|
12
14
|
import { getDaemonPid } from '../cli/helpers.js';
|
|
13
15
|
import { MESSAGE_LIMITS } from '../util/text.js';
|
|
14
16
|
import { TabStore } from '../session/tab-store.js';
|
|
15
|
-
|
|
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_/.-]+$/;
|
|
16
36
|
function parseIntParam(value, def, max) {
|
|
17
37
|
if (value === null)
|
|
18
38
|
return def;
|
|
@@ -38,10 +58,10 @@ async function readBody(req, res) {
|
|
|
38
58
|
return body;
|
|
39
59
|
}
|
|
40
60
|
function exactPath(p) {
|
|
41
|
-
return path => path === p;
|
|
61
|
+
return (path) => path === p;
|
|
42
62
|
}
|
|
43
63
|
function regexPath(re) {
|
|
44
|
-
return path => re.test(path);
|
|
64
|
+
return (path) => re.test(path);
|
|
45
65
|
}
|
|
46
66
|
export const ROUTES = [
|
|
47
67
|
// SSE — never log a "broken pipe" write as a hard error
|
|
@@ -52,18 +72,27 @@ export const ROUTES = [
|
|
|
52
72
|
res.writeHead(200, {
|
|
53
73
|
'Content-Type': 'text/event-stream',
|
|
54
74
|
'Cache-Control': 'no-cache',
|
|
55
|
-
|
|
75
|
+
Connection: 'keep-alive',
|
|
56
76
|
});
|
|
57
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 = '';
|
|
58
81
|
const interval = setInterval(() => {
|
|
59
82
|
if (res.writableEnded)
|
|
60
83
|
return;
|
|
61
84
|
try {
|
|
62
|
-
const tabs = TabStore.listAll().map(t => ({
|
|
63
|
-
name: t.name,
|
|
85
|
+
const tabs = TabStore.listAll().map((t) => ({
|
|
86
|
+
name: t.name,
|
|
87
|
+
status: t.status,
|
|
88
|
+
last_activity_at: t.lastActivityAt,
|
|
64
89
|
}));
|
|
65
|
-
const activeCount = tabs.filter(t => t.status === 'running').length;
|
|
66
|
-
|
|
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`);
|
|
67
96
|
}
|
|
68
97
|
catch (err) {
|
|
69
98
|
logger.warn('Dashboard SSE tick failed:', err);
|
|
@@ -98,7 +127,7 @@ export const ROUTES = [
|
|
|
98
127
|
json(res, { error: err }, 400);
|
|
99
128
|
return;
|
|
100
129
|
}
|
|
101
|
-
|
|
130
|
+
PendingMessageStore.enqueueUser(tabName, parsed.message, getDb());
|
|
102
131
|
json(res, { success: true, tab: tabName });
|
|
103
132
|
},
|
|
104
133
|
},
|
|
@@ -127,8 +156,18 @@ export const ROUTES = [
|
|
|
127
156
|
json(res, { error: err }, 400);
|
|
128
157
|
return;
|
|
129
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
|
+
}
|
|
130
165
|
try {
|
|
131
|
-
createTabRecord(getDb(), {
|
|
166
|
+
createTabRecord(getDb(), {
|
|
167
|
+
name: parsed.name,
|
|
168
|
+
workingDir: parsed.workingDir,
|
|
169
|
+
systemPrompt: parsed.systemPrompt,
|
|
170
|
+
});
|
|
132
171
|
json(res, { success: true, name: parsed.name });
|
|
133
172
|
}
|
|
134
173
|
catch (e) {
|
|
@@ -149,7 +188,7 @@ export const ROUTES = [
|
|
|
149
188
|
// POST /api/tasks or /api/crons
|
|
150
189
|
{
|
|
151
190
|
method: 'POST',
|
|
152
|
-
test: path => path === '/api/tasks' || path === '/api/crons',
|
|
191
|
+
test: (path) => path === '/api/tasks' || path === '/api/crons',
|
|
153
192
|
handler: async ({ req, res }) => {
|
|
154
193
|
const body = await readBody(req, res);
|
|
155
194
|
if (body === null)
|
|
@@ -180,18 +219,31 @@ export const ROUTES = [
|
|
|
180
219
|
return;
|
|
181
220
|
}
|
|
182
221
|
const id = crypto.randomUUID();
|
|
183
|
-
|
|
184
|
-
|
|
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
|
+
});
|
|
185
236
|
json(res, { success: true, id });
|
|
186
237
|
},
|
|
187
238
|
},
|
|
188
239
|
// DELETE /api/tasks/:id or /api/crons/:id
|
|
189
240
|
{
|
|
190
241
|
method: 'DELETE',
|
|
191
|
-
test: path => /^\/api\/(tasks|crons)\/[^/]+$/.test(path),
|
|
192
|
-
handler: ({ res, path }) => {
|
|
242
|
+
test: (path) => /^\/api\/(tasks|crons)\/[^/]+$/.test(path),
|
|
243
|
+
handler: async ({ res, path }) => {
|
|
193
244
|
const taskId = decodeURIComponent(path.split('/')[3]);
|
|
194
|
-
|
|
245
|
+
const { TaskStore } = await import('../tasks/store.js');
|
|
246
|
+
new TaskStore().delete(taskId);
|
|
195
247
|
json(res, { success: true });
|
|
196
248
|
},
|
|
197
249
|
},
|
|
@@ -199,19 +251,19 @@ export const ROUTES = [
|
|
|
199
251
|
{
|
|
200
252
|
method: 'GET',
|
|
201
253
|
test: exactPath('/api/watchers'),
|
|
202
|
-
handler: ({ res }) => {
|
|
203
|
-
const
|
|
204
|
-
|
|
205
|
-
json(res, watchers);
|
|
254
|
+
handler: async ({ res }) => {
|
|
255
|
+
const { WatcherStore } = await import('../watchers/store.js');
|
|
256
|
+
json(res, new WatcherStore().list());
|
|
206
257
|
},
|
|
207
258
|
},
|
|
208
259
|
// DELETE /api/watchers/:id
|
|
209
260
|
{
|
|
210
261
|
method: 'DELETE',
|
|
211
262
|
test: regexPath(/^\/api\/watchers\/[^/]+$/),
|
|
212
|
-
handler: ({ res, path }) => {
|
|
263
|
+
handler: async ({ res, path }) => {
|
|
213
264
|
const id = decodeURIComponent(path.split('/')[3]);
|
|
214
|
-
|
|
265
|
+
const { WatcherStore } = await import('../watchers/store.js');
|
|
266
|
+
new WatcherStore().delete(id);
|
|
215
267
|
json(res, { success: true });
|
|
216
268
|
},
|
|
217
269
|
},
|
|
@@ -235,7 +287,8 @@ export const ROUTES = [
|
|
|
235
287
|
json(res, { error: 'Missing content' }, 400);
|
|
236
288
|
return;
|
|
237
289
|
}
|
|
238
|
-
|
|
290
|
+
const { MemoryStore } = await import('../session/memory-store.js');
|
|
291
|
+
MemoryStore.add(parsed.content, { tabName: parsed.tabName });
|
|
239
292
|
json(res, { success: true });
|
|
240
293
|
},
|
|
241
294
|
},
|
|
@@ -243,9 +296,10 @@ export const ROUTES = [
|
|
|
243
296
|
{
|
|
244
297
|
method: 'DELETE',
|
|
245
298
|
test: regexPath(/^\/api\/memories\/\d+$/),
|
|
246
|
-
handler: ({ res, path }) => {
|
|
299
|
+
handler: async ({ res, path }) => {
|
|
247
300
|
const id = path.split('/')[3];
|
|
248
|
-
|
|
301
|
+
const { MemoryStore } = await import('../session/memory-store.js');
|
|
302
|
+
MemoryStore.delete(id);
|
|
249
303
|
json(res, { success: true });
|
|
250
304
|
},
|
|
251
305
|
},
|
|
@@ -256,7 +310,13 @@ export const ROUTES = [
|
|
|
256
310
|
handler: async ({ res }) => {
|
|
257
311
|
const { getConfig } = await import('../config.js');
|
|
258
312
|
const generators = getConfig().mediaGenerators || [];
|
|
259
|
-
json(res, {
|
|
313
|
+
json(res, {
|
|
314
|
+
generators: generators.map((g) => ({
|
|
315
|
+
provider: g.provider,
|
|
316
|
+
model: g.model,
|
|
317
|
+
configured: !!g.apiKey,
|
|
318
|
+
})),
|
|
319
|
+
});
|
|
260
320
|
},
|
|
261
321
|
},
|
|
262
322
|
// GET /api/channels/config
|
|
@@ -323,7 +383,7 @@ export const ROUTES = [
|
|
|
323
383
|
test: exactPath('/api/status'),
|
|
324
384
|
handler: ({ res }) => {
|
|
325
385
|
const db = getDb();
|
|
326
|
-
const activeTasks = db.prepare(
|
|
386
|
+
const activeTasks = db.prepare('SELECT COUNT(*) as c FROM tasks WHERE enabled = 1').get().c;
|
|
327
387
|
json(res, {
|
|
328
388
|
version: VERSION,
|
|
329
389
|
daemonPid: getDaemonPid(),
|
|
@@ -341,12 +401,18 @@ export const ROUTES = [
|
|
|
341
401
|
method: 'GET',
|
|
342
402
|
test: exactPath('/api/tabs'),
|
|
343
403
|
handler: ({ res }) => {
|
|
344
|
-
|
|
345
|
-
|
|
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,
|
|
346
411
|
(SELECT COUNT(*) FROM messages WHERE tab_id = t.id) as message_count,
|
|
347
412
|
(SELECT COALESCE(SUM(cost_usd), 0) FROM messages WHERE tab_id = t.id) as total_cost
|
|
348
413
|
FROM tabs t ORDER BY t.last_activity_at DESC
|
|
349
|
-
`)
|
|
414
|
+
`)
|
|
415
|
+
.all();
|
|
350
416
|
json(res, tabs);
|
|
351
417
|
},
|
|
352
418
|
},
|
|
@@ -364,7 +430,9 @@ export const ROUTES = [
|
|
|
364
430
|
return;
|
|
365
431
|
}
|
|
366
432
|
const db = getDb();
|
|
367
|
-
const messages = db
|
|
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);
|
|
368
436
|
const total = db.prepare('SELECT COUNT(*) as c FROM messages WHERE tab_id = ?').get(tabId).c;
|
|
369
437
|
json(res, { messages: messages.reverse(), total, limit, offset });
|
|
370
438
|
},
|
|
@@ -373,27 +441,28 @@ export const ROUTES = [
|
|
|
373
441
|
{
|
|
374
442
|
method: 'GET',
|
|
375
443
|
test: exactPath('/api/memories'),
|
|
376
|
-
handler: ({ res, url }) => {
|
|
444
|
+
handler: async ({ res, url }) => {
|
|
377
445
|
const limit = parseIntParam(url.searchParams.get('limit'), 50, 200);
|
|
378
446
|
const offset = parseIntParam(url.searchParams.get('offset'), 0, 100000);
|
|
379
447
|
const q = url.searchParams.get('q') || '';
|
|
380
|
-
const
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
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
|
+
}));
|
|
390
459
|
json(res, { memories, total, limit, offset });
|
|
391
460
|
},
|
|
392
461
|
},
|
|
393
462
|
// GET /api/tasks or /api/crons
|
|
394
463
|
{
|
|
395
464
|
method: 'GET',
|
|
396
|
-
test: path => path === '/api/tasks' || path === '/api/crons',
|
|
465
|
+
test: (path) => path === '/api/tasks' || path === '/api/crons',
|
|
397
466
|
handler: ({ res, url }) => {
|
|
398
467
|
const limit = parseIntParam(url.searchParams.get('limit'), 100, 500);
|
|
399
468
|
const tasks = getDb().prepare('SELECT * FROM tasks ORDER BY created_at LIMIT ?').all(limit);
|
|
@@ -405,7 +474,8 @@ export const ROUTES = [
|
|
|
405
474
|
method: 'GET',
|
|
406
475
|
test: exactPath('/api/costs'),
|
|
407
476
|
handler: ({ res }) => {
|
|
408
|
-
const costs = getDb()
|
|
477
|
+
const costs = getDb()
|
|
478
|
+
.prepare(`
|
|
409
479
|
SELECT date(created_at) as day,
|
|
410
480
|
SUM(cost_usd) as total_cost,
|
|
411
481
|
COUNT(*) as message_count
|
|
@@ -414,7 +484,8 @@ export const ROUTES = [
|
|
|
414
484
|
AND created_at > datetime('now', '-30 days')
|
|
415
485
|
GROUP BY date(created_at)
|
|
416
486
|
ORDER BY day
|
|
417
|
-
`)
|
|
487
|
+
`)
|
|
488
|
+
.all();
|
|
418
489
|
json(res, costs);
|
|
419
490
|
},
|
|
420
491
|
},
|
|
@@ -423,48 +494,37 @@ export const ROUTES = [
|
|
|
423
494
|
method: 'GET',
|
|
424
495
|
test: exactPath('/api/update/status'),
|
|
425
496
|
handler: async ({ res }) => {
|
|
426
|
-
async function
|
|
427
|
-
const pkg = { name };
|
|
497
|
+
async function npmViewLatest(name) {
|
|
428
498
|
try {
|
|
429
|
-
const { stdout } = await
|
|
430
|
-
|
|
499
|
+
const { stdout } = await execFileAsync('npm', ['view', name, 'version'], {
|
|
500
|
+
timeout: 10000,
|
|
501
|
+
});
|
|
502
|
+
return stdout.trim();
|
|
431
503
|
}
|
|
432
504
|
catch {
|
|
433
|
-
|
|
434
|
-
}
|
|
435
|
-
try {
|
|
436
|
-
const { stdout } = await execAsync(`npm view ${name} version`, { timeout: 10000 });
|
|
437
|
-
pkg.latest = stdout.trim();
|
|
505
|
+
return null;
|
|
438
506
|
}
|
|
439
|
-
catch {
|
|
440
|
-
pkg.latest = null;
|
|
441
|
-
}
|
|
442
|
-
pkg.updateAvailable = !!(pkg.installed && pkg.latest && pkg.installed !== pkg.latest);
|
|
443
|
-
return pkg;
|
|
444
507
|
}
|
|
445
508
|
const packages = await Promise.all([
|
|
446
509
|
(async () => {
|
|
447
|
-
const p =
|
|
448
|
-
|
|
510
|
+
const p = {
|
|
511
|
+
name: 'beecork',
|
|
512
|
+
installed: VERSION,
|
|
513
|
+
latest: await npmViewLatest('beecork'),
|
|
514
|
+
};
|
|
449
515
|
p.updateAvailable = !!(p.latest && p.installed !== p.latest);
|
|
450
516
|
return p;
|
|
451
517
|
})(),
|
|
452
518
|
(async () => {
|
|
453
519
|
const p = { name: '@anthropic-ai/claude-code' };
|
|
454
520
|
try {
|
|
455
|
-
const { stdout } = await
|
|
521
|
+
const { stdout } = await execFileAsync('claude', ['--version'], { timeout: 10000 });
|
|
456
522
|
p.installed = stdout.trim().replace(/^.*?(\d+\.\d+\.\d+).*$/, '$1');
|
|
457
523
|
}
|
|
458
524
|
catch {
|
|
459
525
|
p.installed = null;
|
|
460
526
|
}
|
|
461
|
-
|
|
462
|
-
const { stdout } = await execAsync('npm view @anthropic-ai/claude-code version', { timeout: 10000 });
|
|
463
|
-
p.latest = stdout.trim();
|
|
464
|
-
}
|
|
465
|
-
catch {
|
|
466
|
-
p.latest = null;
|
|
467
|
-
}
|
|
527
|
+
p.latest = await npmViewLatest('@anthropic-ai/claude-code');
|
|
468
528
|
p.updateAvailable = !!(p.installed && p.latest && p.installed !== p.latest);
|
|
469
529
|
return p;
|
|
470
530
|
})(),
|
|
@@ -478,17 +538,21 @@ export const ROUTES = [
|
|
|
478
538
|
test: regexPath(/^\/api\/update\/[^/]+$/),
|
|
479
539
|
handler: async ({ res, path }) => {
|
|
480
540
|
const pkgName = decodeURIComponent(path.split('/')[3]);
|
|
481
|
-
const allowedPackages =
|
|
482
|
-
|
|
483
|
-
'@anthropic-ai/claude-code': 'npm install -g @anthropic-ai/claude-code@latest',
|
|
484
|
-
};
|
|
485
|
-
const cmd = allowedPackages[pkgName];
|
|
486
|
-
if (!cmd) {
|
|
541
|
+
const allowedPackages = new Set(['beecork', '@anthropic-ai/claude-code']);
|
|
542
|
+
if (!allowedPackages.has(pkgName)) {
|
|
487
543
|
json(res, { error: `Package "${pkgName}" is not in the allowed update list.` }, 400);
|
|
488
544
|
return;
|
|
489
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
|
+
}
|
|
490
552
|
try {
|
|
491
|
-
const { stdout } = await
|
|
553
|
+
const { stdout } = await execFileAsync('npm', ['install', '-g', `${pkgName}@latest`], {
|
|
554
|
+
timeout: 120000,
|
|
555
|
+
});
|
|
492
556
|
json(res, { success: true, package: pkgName, output: stdout.trim() });
|
|
493
557
|
}
|
|
494
558
|
catch (err) {
|
|
@@ -502,7 +566,7 @@ export const ROUTES = [
|
|
|
502
566
|
test: exactPath('/api/capabilities'),
|
|
503
567
|
handler: async ({ res }) => {
|
|
504
568
|
const { getAvailablePacks, isEnabled } = await import('../capabilities/index.js');
|
|
505
|
-
const packs = getAvailablePacks().map(p => ({
|
|
569
|
+
const packs = getAvailablePacks().map((p) => ({
|
|
506
570
|
...p,
|
|
507
571
|
enabled: isEnabled(p.id),
|
|
508
572
|
mcpServer: { package: p.mcpServer.package },
|
package/dist/dashboard/server.js
CHANGED
|
@@ -64,7 +64,11 @@ export function startDashboardServer(port = 0) {
|
|
|
64
64
|
if (path.startsWith('/api/')) {
|
|
65
65
|
const authHeader = req.headers.authorization;
|
|
66
66
|
const queryToken = url.searchParams.get('token');
|
|
67
|
-
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];
|
|
68
72
|
const providedToken = authHeader?.replace('Bearer ', '') || queryToken || cookieToken;
|
|
69
73
|
if (!safeEqualToken(providedToken, authToken)) {
|
|
70
74
|
json(res, { error: 'Unauthorized' }, 401);
|