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
|
@@ -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
|
+
}
|
package/dist/db/index.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import Database from 'better-sqlite3';
|
|
5
4
|
import { getDbPath, ensureBeecorkDirs, expandHome } from '../util/paths.js';
|
|
6
5
|
import { runMigrations } from './migrations.js';
|
|
6
|
+
import { openDb } from './connection.js';
|
|
7
7
|
import { logger } from '../util/logger.js';
|
|
8
8
|
const SCHEMA = `
|
|
9
9
|
CREATE TABLE IF NOT EXISTS tabs (
|
|
@@ -55,22 +55,40 @@ export function getDb() {
|
|
|
55
55
|
return db;
|
|
56
56
|
ensureBeecorkDirs();
|
|
57
57
|
const dbPath = getDbPath();
|
|
58
|
-
db =
|
|
59
|
-
db.pragma('journal_mode = WAL');
|
|
60
|
-
db.pragma('foreign_keys = ON');
|
|
61
|
-
db.pragma('busy_timeout = 5000');
|
|
58
|
+
db = openDb(dbPath, { enforcePerms: true });
|
|
62
59
|
db.exec(SCHEMA);
|
|
63
60
|
runMigrations(db);
|
|
64
|
-
// Periodic WAL checkpointing + table cleanup
|
|
61
|
+
// Periodic WAL checkpointing + table cleanup. Cheap precheck `LIMIT 1`
|
|
62
|
+
// SELECTs skip the DELETE entirely when nothing matches — saves the WAL
|
|
63
|
+
// write on a quiet system.
|
|
65
64
|
walInterval = setInterval(() => {
|
|
66
65
|
try {
|
|
67
66
|
db?.pragma('wal_checkpoint(PASSIVE)');
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
//
|
|
72
|
-
//
|
|
73
|
-
|
|
67
|
+
if (!db)
|
|
68
|
+
return;
|
|
69
|
+
// Prune old permission history (keep last 1000 entries). Two-pass: count
|
|
70
|
+
// first, then bulk-delete the oldest N. The previous correlated-subquery
|
|
71
|
+
// version re-evaluated the OFFSET 999 row per candidate, which scaled
|
|
72
|
+
// poorly past ~5k rows.
|
|
73
|
+
const permCount = db.prepare('SELECT COUNT(*) as c FROM permission_history').get().c;
|
|
74
|
+
if (permCount > 1000) {
|
|
75
|
+
db.prepare(`DELETE FROM permission_history WHERE id IN (
|
|
76
|
+
SELECT id FROM permission_history ORDER BY created_at ASC LIMIT ?
|
|
77
|
+
)`).run(permCount - 1000);
|
|
78
|
+
}
|
|
79
|
+
// Activity log: only DELETE if a candidate row exists, otherwise no-op.
|
|
80
|
+
const oldActivity = db
|
|
81
|
+
.prepare("SELECT 1 FROM activity_log WHERE created_at < datetime('now', '-90 days') LIMIT 1")
|
|
82
|
+
.get();
|
|
83
|
+
if (oldActivity)
|
|
84
|
+
db.exec("DELETE FROM activity_log WHERE created_at < datetime('now', '-90 days')");
|
|
85
|
+
// Routing preferences: same precheck pattern.
|
|
86
|
+
const stalePref = db
|
|
87
|
+
.prepare("SELECT 1 FROM routing_preferences WHERE hit_count < 3 AND created_at < datetime('now', '-30 days') LIMIT 1")
|
|
88
|
+
.get();
|
|
89
|
+
if (stalePref) {
|
|
90
|
+
db.exec("DELETE FROM routing_preferences WHERE hit_count < 3 AND created_at < datetime('now', '-30 days')");
|
|
91
|
+
}
|
|
74
92
|
}
|
|
75
93
|
catch (err) {
|
|
76
94
|
logger.warn('WAL checkpoint/cleanup error:', err);
|
package/dist/db/migrations.js
CHANGED
|
@@ -1,4 +1,24 @@
|
|
|
1
1
|
import { logger } from '../util/logger.js';
|
|
2
|
+
/*
|
|
3
|
+
* Migration authoring conventions:
|
|
4
|
+
*
|
|
5
|
+
* - Every migration runs inside a single transaction so partial failures are
|
|
6
|
+
* atomically rolled back. Idempotency checks for ALTER TABLE ADD/DROP COLUMN
|
|
7
|
+
* are performed inline below.
|
|
8
|
+
*
|
|
9
|
+
* - For destructive reshapes (column type change, rename, etc.) follow the
|
|
10
|
+
* SQLite-recommended pattern, all in one migration so the transaction wraps
|
|
11
|
+
* it cleanly:
|
|
12
|
+
*
|
|
13
|
+
* CREATE TABLE new_X (...);
|
|
14
|
+
* INSERT INTO new_X (a, b, c) SELECT a, b, c FROM X;
|
|
15
|
+
* DROP TABLE X;
|
|
16
|
+
* ALTER TABLE new_X RENAME TO X;
|
|
17
|
+
*
|
|
18
|
+
* AVOID `DROP TABLE X; CREATE TABLE X (...)` without a copy step — migration
|
|
19
|
+
* v13 did this and silently nuked user-created rows. The audit history flags
|
|
20
|
+
* that as a one-off; do not repeat it.
|
|
21
|
+
*/
|
|
2
22
|
const MIGRATIONS = [
|
|
3
23
|
{
|
|
4
24
|
version: 2,
|
|
@@ -99,7 +119,7 @@ const MIGRATIONS = [
|
|
|
99
119
|
{
|
|
100
120
|
version: 8,
|
|
101
121
|
description: 'Add system_prompt column to tabs',
|
|
102
|
-
up:
|
|
122
|
+
up: 'ALTER TABLE tabs ADD COLUMN system_prompt TEXT DEFAULT NULL',
|
|
103
123
|
},
|
|
104
124
|
{
|
|
105
125
|
version: 9,
|
|
@@ -265,6 +285,29 @@ const MIGRATIONS = [
|
|
|
265
285
|
description: 'Drop idx_memories_content — leading-wildcard LIKE cannot use a B-tree index',
|
|
266
286
|
up: 'DROP INDEX IF EXISTS idx_memories_content',
|
|
267
287
|
},
|
|
288
|
+
{
|
|
289
|
+
version: 26,
|
|
290
|
+
description: 'Add status column to pending_messages for 3-state lifecycle (pending → processing → done|failed); allow nullable tab_name for notifications',
|
|
291
|
+
up: `
|
|
292
|
+
ALTER TABLE pending_messages ADD COLUMN status TEXT NOT NULL DEFAULT 'pending';
|
|
293
|
+
UPDATE pending_messages SET status = 'done' WHERE processed = 1;
|
|
294
|
+
UPDATE pending_messages SET status = 'pending' WHERE processed = 0;
|
|
295
|
+
CREATE INDEX IF NOT EXISTS idx_pending_status_created ON pending_messages(status, created_at);
|
|
296
|
+
`,
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
version: 27,
|
|
300
|
+
description: 'Add indexes for delegations (source_tab, target_tab) to prevent scans on completion lookup',
|
|
301
|
+
up: `
|
|
302
|
+
CREATE INDEX IF NOT EXISTS idx_delegations_source ON delegations(source_tab, status);
|
|
303
|
+
CREATE INDEX IF NOT EXISTS idx_delegations_target ON delegations(target_tab, status, created_at);
|
|
304
|
+
`,
|
|
305
|
+
},
|
|
306
|
+
{
|
|
307
|
+
version: 28,
|
|
308
|
+
description: 'Add idx_tabs_working_dir for the per-message routing query that finds tabs by project path',
|
|
309
|
+
up: 'CREATE INDEX IF NOT EXISTS idx_tabs_working_dir ON tabs(working_dir, last_activity_at)',
|
|
310
|
+
},
|
|
268
311
|
];
|
|
269
312
|
export function runMigrations(db) {
|
|
270
313
|
// Ensure schema_version table exists
|
|
@@ -291,38 +334,51 @@ export function runMigrations(db) {
|
|
|
291
334
|
logger.info(`DB migration v${migration.version}: ${migration.description}`);
|
|
292
335
|
// Split multi-statement migrations and apply each safely
|
|
293
336
|
// (handles partial failures from previous runs where some statements succeeded)
|
|
294
|
-
const statements = migration.up
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
337
|
+
const statements = migration.up
|
|
338
|
+
.split(';')
|
|
339
|
+
.map((s) => s.trim())
|
|
340
|
+
.filter((s) => s.length > 0);
|
|
341
|
+
// Wrap the whole migration in a transaction so a mid-statement failure
|
|
342
|
+
// rolls back to the pre-migration state. The schema_version bump is the
|
|
343
|
+
// final statement inside the transaction, so an aborted migration won't
|
|
344
|
+
// leave the DB in a half-applied state with the version already bumped.
|
|
345
|
+
const applyMigration = db.transaction(() => {
|
|
346
|
+
for (const stmt of statements) {
|
|
347
|
+
try {
|
|
348
|
+
// For ALTER TABLE ADD COLUMN, check if column already exists first
|
|
349
|
+
const addMatch = stmt.match(/ALTER\s+TABLE\s+(\S+)\s+ADD\s+COLUMN\s+(\S+)/i);
|
|
350
|
+
if (addMatch) {
|
|
351
|
+
const columns = db.pragma(`table_info(${addMatch[1]})`);
|
|
352
|
+
if (columns.some((c) => c.name === addMatch[2])) {
|
|
353
|
+
logger.debug(`Migration v${migration.version}: column ${addMatch[1]}.${addMatch[2]} already exists, skipping`);
|
|
354
|
+
continue;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
// For ALTER TABLE DROP COLUMN, skip if column already gone
|
|
358
|
+
const dropMatch = stmt.match(/ALTER\s+TABLE\s+(\S+)\s+DROP\s+COLUMN\s+(\S+)/i);
|
|
359
|
+
if (dropMatch) {
|
|
360
|
+
const columns = db.pragma(`table_info(${dropMatch[1]})`);
|
|
361
|
+
if (!columns.some((c) => c.name === dropMatch[2])) {
|
|
362
|
+
logger.debug(`Migration v${migration.version}: column ${dropMatch[1]}.${dropMatch[2]} already absent, skipping`);
|
|
363
|
+
continue;
|
|
364
|
+
}
|
|
304
365
|
}
|
|
366
|
+
db.exec(stmt);
|
|
305
367
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
368
|
+
catch (err) {
|
|
369
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
370
|
+
if (msg.includes('already exists') ||
|
|
371
|
+
msg.includes('no such column') ||
|
|
372
|
+
msg.includes('no such table') ||
|
|
373
|
+
msg.includes('no such index')) {
|
|
374
|
+
logger.debug(`Migration v${migration.version}: object already in target state, skipping statement`);
|
|
312
375
|
continue;
|
|
313
376
|
}
|
|
377
|
+
throw err;
|
|
314
378
|
}
|
|
315
|
-
db.exec(stmt);
|
|
316
379
|
}
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
logger.debug(`Migration v${migration.version}: object already in target state, skipping statement`);
|
|
321
|
-
continue;
|
|
322
|
-
}
|
|
323
|
-
throw err;
|
|
324
|
-
}
|
|
325
|
-
}
|
|
326
|
-
db.prepare('UPDATE schema_version SET version = ?').run(migration.version);
|
|
380
|
+
db.prepare('UPDATE schema_version SET version = ?').run(migration.version);
|
|
381
|
+
});
|
|
382
|
+
applyMigration();
|
|
327
383
|
}
|
|
328
384
|
}
|
|
@@ -12,7 +12,9 @@ export function createDelegation(sourceTab, targetTab, message, returnToTab) {
|
|
|
12
12
|
throw new Error(`Delegation depth limit reached (max ${MAX_DELEGATION_DEPTH}). Tab "${sourceTab}" cannot delegate further.`);
|
|
13
13
|
}
|
|
14
14
|
// Check pending limit
|
|
15
|
-
const pending = db
|
|
15
|
+
const pending = db
|
|
16
|
+
.prepare("SELECT COUNT(*) as c FROM delegations WHERE source_tab = ? AND status IN ('pending', 'running')")
|
|
17
|
+
.get(sourceTab).c;
|
|
16
18
|
if (pending >= MAX_PENDING_PER_TAB) {
|
|
17
19
|
throw new Error(`Too many pending delegations for tab "${sourceTab}" (max ${MAX_PENDING_PER_TAB}).`);
|
|
18
20
|
}
|
|
@@ -37,7 +39,9 @@ export function createDelegation(sourceTab, targetTab, message, returnToTab) {
|
|
|
37
39
|
export function completeDelegation(targetTab, result) {
|
|
38
40
|
const db = getDb();
|
|
39
41
|
// Find the most recent pending/running delegation to this tab
|
|
40
|
-
const row = db
|
|
42
|
+
const row = db
|
|
43
|
+
.prepare("SELECT * FROM delegations WHERE target_tab = ? AND status IN ('pending', 'running') ORDER BY created_at DESC LIMIT 1")
|
|
44
|
+
.get(targetTab);
|
|
41
45
|
if (!row)
|
|
42
46
|
return null;
|
|
43
47
|
db.prepare("UPDATE delegations SET status = 'completed', result = ?, completed_at = datetime('now') WHERE id = ?").run(result.slice(0, 50000), row.id); // Cap result at 50KB
|
|
@@ -62,7 +66,7 @@ export function getPendingDelegations(tabName) {
|
|
|
62
66
|
? "SELECT * FROM delegations WHERE source_tab = ? AND status IN ('pending', 'running') ORDER BY created_at"
|
|
63
67
|
: "SELECT * FROM delegations WHERE status IN ('pending', 'running') ORDER BY created_at";
|
|
64
68
|
const rows = tabName ? db.prepare(query).all(tabName) : db.prepare(query).all();
|
|
65
|
-
return rows.map(r => ({
|
|
69
|
+
return rows.map((r) => ({
|
|
66
70
|
id: r.id,
|
|
67
71
|
sourceTab: r.source_tab,
|
|
68
72
|
targetTab: r.target_tab,
|
|
@@ -78,6 +82,8 @@ export function getPendingDelegations(tabName) {
|
|
|
78
82
|
/** Get current delegation depth for a tab */
|
|
79
83
|
function getCurrentDepth(tabName) {
|
|
80
84
|
const db = getDb();
|
|
81
|
-
const row = db
|
|
85
|
+
const row = db
|
|
86
|
+
.prepare("SELECT MAX(depth) as d FROM delegations WHERE source_tab = ? AND status IN ('pending', 'running')")
|
|
87
|
+
.get(tabName);
|
|
82
88
|
return row.d ?? 0;
|
|
83
89
|
}
|
package/dist/index.js
CHANGED
|
@@ -27,38 +27,35 @@ program
|
|
|
27
27
|
.action(async () => {
|
|
28
28
|
await setupWizard();
|
|
29
29
|
});
|
|
30
|
+
program.command('start').description('Start the Beecork daemon').action(startDaemon);
|
|
31
|
+
program.command('stop').description('Stop the Beecork daemon').action(stopDaemon);
|
|
30
32
|
program
|
|
31
|
-
.command('
|
|
32
|
-
.description('
|
|
33
|
-
.action(
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
33
|
+
.command('uninstall')
|
|
34
|
+
.description('Uninstall the Beecork system service (launchd / systemd / Task Scheduler)')
|
|
35
|
+
.action(async () => {
|
|
36
|
+
const { uninstallService } = await import('./service/install.js');
|
|
37
|
+
try {
|
|
38
|
+
const result = uninstallService();
|
|
39
|
+
console.log(result);
|
|
40
|
+
}
|
|
41
|
+
catch (err) {
|
|
42
|
+
console.error('Service uninstall failed:', err instanceof Error ? err.message : String(err));
|
|
43
|
+
process.exit(1);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
38
46
|
program
|
|
39
47
|
.command('status')
|
|
40
48
|
.description('Show daemon status, running tabs, and tasks')
|
|
41
49
|
.action(showStatus);
|
|
42
|
-
program
|
|
43
|
-
.command('tabs')
|
|
44
|
-
.description('List all virtual tabs')
|
|
45
|
-
.action(listTabs);
|
|
50
|
+
program.command('tabs').description('List all virtual tabs').action(listTabs);
|
|
46
51
|
program
|
|
47
52
|
.command('logs [tab]')
|
|
48
53
|
.description('Tail logs for a tab (default: daemon logs)')
|
|
49
54
|
.action(tailLogs);
|
|
50
55
|
// Tasks (new name)
|
|
51
|
-
const taskCmd = program
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
taskCmd
|
|
55
|
-
.command('list')
|
|
56
|
-
.description('List all tasks')
|
|
57
|
-
.action(listCrons);
|
|
58
|
-
taskCmd
|
|
59
|
-
.command('delete <id>')
|
|
60
|
-
.description('Delete a task by ID')
|
|
61
|
-
.action(deleteCron);
|
|
56
|
+
const taskCmd = program.command('tasks').description('Manage scheduled tasks');
|
|
57
|
+
taskCmd.command('list').description('List all tasks').action(listCrons);
|
|
58
|
+
taskCmd.command('delete <id>').description('Delete a task by ID').action(deleteCron);
|
|
62
59
|
program
|
|
63
60
|
.command('task')
|
|
64
61
|
.description('Alias for tasks')
|
|
@@ -76,14 +73,8 @@ program
|
|
|
76
73
|
const cronCmd = program
|
|
77
74
|
.command('cron', { hidden: true })
|
|
78
75
|
.description('Manage cron jobs (alias for tasks)');
|
|
79
|
-
cronCmd
|
|
80
|
-
|
|
81
|
-
.description('List all cron jobs')
|
|
82
|
-
.action(listCrons);
|
|
83
|
-
cronCmd
|
|
84
|
-
.command('delete <id>')
|
|
85
|
-
.description('Delete a cron job by ID')
|
|
86
|
-
.action(deleteCron);
|
|
76
|
+
cronCmd.command('list').description('List all cron jobs').action(listCrons);
|
|
77
|
+
cronCmd.command('delete <id>').description('Delete a cron job by ID').action(deleteCron);
|
|
87
78
|
// Watcher commands
|
|
88
79
|
program
|
|
89
80
|
.command('watches')
|
|
@@ -106,24 +97,14 @@ program
|
|
|
106
97
|
await listWatchers();
|
|
107
98
|
}
|
|
108
99
|
});
|
|
109
|
-
const memoryCmd = program
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
memoryCmd
|
|
113
|
-
.command('list')
|
|
114
|
-
.description('List stored memories')
|
|
115
|
-
.action(listMemories);
|
|
116
|
-
memoryCmd
|
|
117
|
-
.command('delete <id>')
|
|
118
|
-
.description('Delete a memory by ID')
|
|
119
|
-
.action(deleteMemory);
|
|
100
|
+
const memoryCmd = program.command('memory').description('Manage long-term memories');
|
|
101
|
+
memoryCmd.command('list').description('List stored memories').action(listMemories);
|
|
102
|
+
memoryCmd.command('delete <id>').description('Delete a memory by ID').action(deleteMemory);
|
|
120
103
|
program
|
|
121
104
|
.command('send <message>')
|
|
122
105
|
.description('Send a message to the default tab (for testing)')
|
|
123
106
|
.action(sendMessage);
|
|
124
|
-
const channelCmd = program
|
|
125
|
-
.command('channel')
|
|
126
|
-
.description('Manage community channel plugins');
|
|
107
|
+
const channelCmd = program.command('channel').description('Manage community channel plugins');
|
|
127
108
|
channelCmd
|
|
128
109
|
.command('install <package>')
|
|
129
110
|
.description('Install a community channel (npm package)')
|
|
@@ -151,7 +132,7 @@ program
|
|
|
151
132
|
.action(async () => {
|
|
152
133
|
const readline = await import('node:readline');
|
|
153
134
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
154
|
-
const ask = (q, def) => new Promise(r => rl.question(def ? `${q} [${def}]: ` : `${q}: `, a => r(a.trim() || def || '')));
|
|
135
|
+
const ask = (q, def) => new Promise((r) => rl.question(def ? `${q} [${def}]: ` : `${q}: `, (a) => r(a.trim() || def || '')));
|
|
155
136
|
console.log('\nDiscord Setup\n');
|
|
156
137
|
console.log(' 1. Go to https://discord.com/developers/applications');
|
|
157
138
|
console.log(' 2. Click "New Application", give it a name');
|
|
@@ -180,7 +161,7 @@ program
|
|
|
180
161
|
.action(async (opts) => {
|
|
181
162
|
if (opts.disable) {
|
|
182
163
|
const { getConfig, saveConfig } = await import('./config.js');
|
|
183
|
-
const {
|
|
164
|
+
const { getWhatsappSessionPath } = await import('./util/paths.js');
|
|
184
165
|
const fs = await import('node:fs');
|
|
185
166
|
const config = getConfig();
|
|
186
167
|
if (config.whatsapp) {
|
|
@@ -188,7 +169,7 @@ program
|
|
|
188
169
|
saveConfig(config);
|
|
189
170
|
}
|
|
190
171
|
// Remove session files
|
|
191
|
-
const sessionPath =
|
|
172
|
+
const sessionPath = getWhatsappSessionPath();
|
|
192
173
|
if (fs.existsSync(sessionPath)) {
|
|
193
174
|
fs.rmSync(sessionPath, { recursive: true, force: true });
|
|
194
175
|
}
|
|
@@ -211,7 +192,7 @@ program
|
|
|
211
192
|
const readline = await import('node:readline');
|
|
212
193
|
const fs = await import('node:fs');
|
|
213
194
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
214
|
-
const ask = (q, def) => new Promise(r => rl.question(def ? `${q} [${def}]: ` : `${q}: `, a => r(a.trim() || def || '')));
|
|
195
|
+
const ask = (q, def) => new Promise((r) => rl.question(def ? `${q} [${def}]: ` : `${q}: `, (a) => r(a.trim() || def || '')));
|
|
215
196
|
console.log('\nWhatsApp Setup\n');
|
|
216
197
|
console.log(' You need two WhatsApp accounts:');
|
|
217
198
|
console.log(' 1. A bot account (separate SIM) — will scan the QR code to pair');
|
|
@@ -223,9 +204,9 @@ program
|
|
|
223
204
|
return;
|
|
224
205
|
}
|
|
225
206
|
const { getConfig, saveConfig } = await import('./config.js');
|
|
226
|
-
const {
|
|
207
|
+
const { getWhatsappSessionPath } = await import('./util/paths.js');
|
|
227
208
|
const config = getConfig();
|
|
228
|
-
const sessionPath =
|
|
209
|
+
const sessionPath = getWhatsappSessionPath();
|
|
229
210
|
config.whatsapp = {
|
|
230
211
|
enabled: true,
|
|
231
212
|
mode: 'baileys',
|
|
@@ -237,7 +218,7 @@ program
|
|
|
237
218
|
rl.close();
|
|
238
219
|
// Pair immediately — show QR code in this terminal
|
|
239
220
|
try {
|
|
240
|
-
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion } = await import('@whiskeysockets/baileys');
|
|
221
|
+
const { default: makeWASocket, useMultiFileAuthState, DisconnectReason, fetchLatestBaileysVersion, } = await import('@whiskeysockets/baileys');
|
|
241
222
|
const pino = (await import('pino')).default;
|
|
242
223
|
const silentLogger = pino({ level: 'silent' });
|
|
243
224
|
fs.mkdirSync(sessionPath, { recursive: true, mode: 0o700 });
|
|
@@ -288,7 +269,10 @@ program
|
|
|
288
269
|
if (update.connection === 'close') {
|
|
289
270
|
if (paired)
|
|
290
271
|
return; // Expected disconnect after pairing
|
|
291
|
-
|
|
272
|
+
// baileys' DisconnectError type isn't exported cleanly; this is the
|
|
273
|
+
// standard shape its error objects have.
|
|
274
|
+
const reason = update.lastDisconnect?.error
|
|
275
|
+
?.output?.statusCode;
|
|
292
276
|
if (reason === DisconnectReason.loggedOut) {
|
|
293
277
|
console.log('\n✗ WhatsApp logged out. Please try again.\n');
|
|
294
278
|
process.exit(1);
|
|
@@ -317,7 +301,7 @@ program
|
|
|
317
301
|
const readline = await import('node:readline');
|
|
318
302
|
const crypto = await import('node:crypto');
|
|
319
303
|
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
320
|
-
const ask = (q, def) => new Promise(r => rl.question(def ? `${q} [${def}]: ` : `${q}: `, a => r(a.trim() || def || '')));
|
|
304
|
+
const ask = (q, def) => new Promise((r) => rl.question(def ? `${q} [${def}]: ` : `${q}: `, (a) => r(a.trim() || def || '')));
|
|
321
305
|
console.log('\nWebhook Setup\n');
|
|
322
306
|
console.log(' Webhooks let any service trigger Beecork via HTTP.');
|
|
323
307
|
console.log(' Send POST requests to: http://localhost:PORT/webhook/tabName\n');
|
|
@@ -438,9 +422,7 @@ program
|
|
|
438
422
|
const { runDoctor } = await import('./cli/doctor.js');
|
|
439
423
|
await runDoctor();
|
|
440
424
|
});
|
|
441
|
-
const mcpCmd = program
|
|
442
|
-
.command('mcp')
|
|
443
|
-
.description('Manage MCP server configurations');
|
|
425
|
+
const mcpCmd = program.command('mcp').description('Manage MCP server configurations');
|
|
444
426
|
mcpCmd
|
|
445
427
|
.command('add <name> <command> [args...]')
|
|
446
428
|
.description('Register an MCP server')
|
|
@@ -590,9 +572,7 @@ program
|
|
|
590
572
|
}
|
|
591
573
|
console.log(formatKnowledgeForContext(entries));
|
|
592
574
|
});
|
|
593
|
-
const storeCmd = program
|
|
594
|
-
.command('store')
|
|
595
|
-
.description('Browse and install community extensions');
|
|
575
|
+
const storeCmd = program.command('store').description('Browse and install community extensions');
|
|
596
576
|
storeCmd
|
|
597
577
|
.command('search <query>')
|
|
598
578
|
.description('Search for beecork packages on npm')
|
|
@@ -7,14 +7,15 @@ const GLOBAL_KNOWLEDGE_DIR = path.join(getBeecorkHome(), 'knowledge');
|
|
|
7
7
|
const GLOBAL_CATEGORIES = ['people', 'preferences', 'routines', 'general'];
|
|
8
8
|
/** Ensure global knowledge directory exists */
|
|
9
9
|
function ensureGlobalDir() {
|
|
10
|
-
fs.mkdirSync(GLOBAL_KNOWLEDGE_DIR, { recursive: true });
|
|
10
|
+
fs.mkdirSync(GLOBAL_KNOWLEDGE_DIR, { recursive: true, mode: 0o700 });
|
|
11
11
|
}
|
|
12
12
|
const knowledgeCache = new Map();
|
|
13
13
|
/** Read a knowledge file with mtime-based caching, return content or empty string */
|
|
14
14
|
function readKnowledgeCached(filePath) {
|
|
15
|
+
// Skip the existsSync precheck — fs.statSync throws ENOENT on missing files,
|
|
16
|
+
// catching that gets us the same fall-through. This halves the syscall count
|
|
17
|
+
// on every Claude message that triggers knowledge injection.
|
|
15
18
|
try {
|
|
16
|
-
if (!fs.existsSync(filePath))
|
|
17
|
-
return '';
|
|
18
19
|
const stat = fs.statSync(filePath);
|
|
19
20
|
const cached = knowledgeCache.get(filePath);
|
|
20
21
|
if (cached && cached.mtime === stat.mtimeMs)
|
|
@@ -24,16 +25,24 @@ function readKnowledgeCached(filePath) {
|
|
|
24
25
|
return content;
|
|
25
26
|
}
|
|
26
27
|
catch (err) {
|
|
28
|
+
if (err?.code === 'ENOENT')
|
|
29
|
+
return '';
|
|
27
30
|
logger.warn(`Failed to read knowledge file ${filePath}:`, err);
|
|
28
31
|
return '';
|
|
29
32
|
}
|
|
30
33
|
}
|
|
31
34
|
/** Append to a knowledge file */
|
|
32
35
|
function appendToFile(filePath, content) {
|
|
33
|
-
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
36
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
34
37
|
const existing = readKnowledgeCached(filePath);
|
|
35
38
|
const separator = existing.endsWith('\n') || existing === '' ? '' : '\n';
|
|
36
|
-
fs.writeFileSync(filePath, existing + separator + content + '\n');
|
|
39
|
+
fs.writeFileSync(filePath, existing + separator + content + '\n', { mode: 0o600 });
|
|
40
|
+
try {
|
|
41
|
+
fs.chmodSync(filePath, 0o600);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
/* not fatal */
|
|
45
|
+
}
|
|
37
46
|
}
|
|
38
47
|
// ─── Layer 1: Global Knowledge ───
|
|
39
48
|
export function getGlobalKnowledge() {
|
|
@@ -71,8 +80,10 @@ export function addProjectKnowledge(projectPath, content) {
|
|
|
71
80
|
// ─── Layer 3: Tab Knowledge (database) ───
|
|
72
81
|
export function getTabKnowledge(tabName) {
|
|
73
82
|
const db = getDb();
|
|
74
|
-
const rows = db
|
|
75
|
-
|
|
83
|
+
const rows = db
|
|
84
|
+
.prepare('SELECT content FROM memories WHERE tab_name = ? ORDER BY created_at DESC LIMIT 50')
|
|
85
|
+
.all(tabName);
|
|
86
|
+
return rows.map((r) => ({ content: r.content, scope: 'tab', source: tabName }));
|
|
76
87
|
}
|
|
77
88
|
// ─── Combined Knowledge ───
|
|
78
89
|
export function getAllKnowledge(projectPath, tabName) {
|
|
@@ -94,7 +105,7 @@ export function formatKnowledgeForContext(entries) {
|
|
|
94
105
|
if (entries.length === 0)
|
|
95
106
|
return '';
|
|
96
107
|
const sections = [];
|
|
97
|
-
const global = entries.filter(e => e.scope === 'global');
|
|
108
|
+
const global = entries.filter((e) => e.scope === 'global');
|
|
98
109
|
if (global.length > 0) {
|
|
99
110
|
sections.push('[Your knowledge (global)]');
|
|
100
111
|
for (const entry of global) {
|
|
@@ -103,14 +114,14 @@ export function formatKnowledgeForContext(entries) {
|
|
|
103
114
|
sections.push(entry.content);
|
|
104
115
|
}
|
|
105
116
|
}
|
|
106
|
-
const project = entries.filter(e => e.scope === 'project');
|
|
117
|
+
const project = entries.filter((e) => e.scope === 'project');
|
|
107
118
|
if (project.length > 0) {
|
|
108
119
|
sections.push('\n[Project knowledge]');
|
|
109
120
|
for (const entry of project) {
|
|
110
121
|
sections.push(entry.content);
|
|
111
122
|
}
|
|
112
123
|
}
|
|
113
|
-
const tab = entries.filter(e => e.scope === 'tab');
|
|
124
|
+
const tab = entries.filter((e) => e.scope === 'tab');
|
|
114
125
|
if (tab.length > 0) {
|
|
115
126
|
sections.push('\n[Context from memory]');
|
|
116
127
|
for (const entry of tab) {
|
|
@@ -131,7 +142,10 @@ export function addKnowledge(content, scope, options) {
|
|
|
131
142
|
addProjectKnowledge(options.projectPath, content);
|
|
132
143
|
break;
|
|
133
144
|
case 'tab': {
|
|
134
|
-
// Use existing memory system
|
|
145
|
+
// Use existing memory system. Inline INSERT here (rather than the
|
|
146
|
+
// MemoryStore wrapper) because knowledge → session would invert the
|
|
147
|
+
// module dep direction; the schema duplication is one column list and
|
|
148
|
+
// the shape is stable.
|
|
135
149
|
const db = getDb();
|
|
136
150
|
db.prepare('INSERT INTO memories (content, tab_name, source) VALUES (?, ?, ?)').run(content, options?.tabName || null, 'tool');
|
|
137
151
|
break;
|
|
@@ -142,5 +156,5 @@ export function addKnowledge(content, scope, options) {
|
|
|
142
156
|
export function searchKnowledge(query, projectPath, tabName) {
|
|
143
157
|
const all = getAllKnowledge(projectPath, tabName);
|
|
144
158
|
const lower = query.toLowerCase();
|
|
145
|
-
return all.filter(e => e.content.toLowerCase().includes(lower));
|
|
159
|
+
return all.filter((e) => e.content.toLowerCase().includes(lower));
|
|
146
160
|
}
|