beecork 1.5.0 → 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.
Files changed (119) hide show
  1. package/dist/capabilities/index.d.ts +1 -1
  2. package/dist/capabilities/index.js +1 -1
  3. package/dist/capabilities/manager.js +13 -9
  4. package/dist/capabilities/packs.js +3 -1
  5. package/dist/channels/command-handler.js +46 -14
  6. package/dist/channels/discord.d.ts +3 -6
  7. package/dist/channels/discord.js +40 -23
  8. package/dist/channels/index.d.ts +1 -1
  9. package/dist/channels/loader.js +13 -3
  10. package/dist/channels/pipeline.js +14 -5
  11. package/dist/channels/registry.d.ts +17 -1
  12. package/dist/channels/registry.js +33 -4
  13. package/dist/channels/telegram.d.ts +20 -5
  14. package/dist/channels/telegram.js +177 -42
  15. package/dist/channels/types.d.ts +11 -28
  16. package/dist/channels/voice-state.js +3 -1
  17. package/dist/channels/webhook.d.ts +1 -4
  18. package/dist/channels/webhook.js +26 -11
  19. package/dist/channels/whatsapp.d.ts +8 -4
  20. package/dist/channels/whatsapp.js +65 -29
  21. package/dist/cli/capabilities.js +4 -4
  22. package/dist/cli/channel.js +16 -6
  23. package/dist/cli/commands.js +12 -9
  24. package/dist/cli/doctor.js +80 -25
  25. package/dist/cli/handoff.d.ts +7 -14
  26. package/dist/cli/handoff.js +9 -44
  27. package/dist/cli/mcp.js +5 -5
  28. package/dist/cli/media.js +21 -8
  29. package/dist/cli/setup.js +9 -8
  30. package/dist/cli/store.js +29 -12
  31. package/dist/config.js +5 -10
  32. package/dist/daemon.js +88 -38
  33. package/dist/dashboard/html.js +80 -12
  34. package/dist/dashboard/routes.js +143 -79
  35. package/dist/dashboard/server.js +5 -1
  36. package/dist/db/connection.d.ts +29 -0
  37. package/dist/db/connection.js +37 -0
  38. package/dist/db/index.js +30 -12
  39. package/dist/db/migrations.js +84 -28
  40. package/dist/delegation/manager.js +10 -4
  41. package/dist/index.js +39 -59
  42. package/dist/knowledge/manager.js +26 -12
  43. package/dist/mcp/handlers.js +126 -57
  44. package/dist/mcp/server.js +20 -10
  45. package/dist/mcp/tool-definitions.js +68 -20
  46. package/dist/mcp/validate.d.ts +23 -0
  47. package/dist/mcp/validate.js +65 -0
  48. package/dist/media/factory.js +18 -14
  49. package/dist/media/generators/dall-e.js +2 -2
  50. package/dist/media/generators/kling.js +4 -4
  51. package/dist/media/generators/lyria.js +1 -1
  52. package/dist/media/generators/nano-banana.d.ts +1 -1
  53. package/dist/media/generators/nano-banana.js +2 -2
  54. package/dist/media/generators/poll-util.js +4 -4
  55. package/dist/media/generators/recraft.js +3 -3
  56. package/dist/media/generators/runway.js +4 -4
  57. package/dist/media/generators/stable-diffusion.js +2 -2
  58. package/dist/media/generators/veo.js +1 -1
  59. package/dist/media/index.js +1 -1
  60. package/dist/media/store.d.ts +7 -0
  61. package/dist/media/store.js +18 -4
  62. package/dist/media/types.d.ts +22 -0
  63. package/dist/notifications/index.d.ts +2 -4
  64. package/dist/notifications/index.js +6 -19
  65. package/dist/notifications/ntfy.js +3 -3
  66. package/dist/observability/analytics.js +35 -13
  67. package/dist/projects/index.d.ts +1 -1
  68. package/dist/projects/index.js +1 -1
  69. package/dist/projects/manager.d.ts +0 -4
  70. package/dist/projects/manager.js +51 -28
  71. package/dist/projects/router.d.ts +2 -0
  72. package/dist/projects/router.js +70 -45
  73. package/dist/service/install.js +15 -5
  74. package/dist/service/windows.js +1 -1
  75. package/dist/session/budget-guard.d.ts +20 -0
  76. package/dist/session/budget-guard.js +31 -0
  77. package/dist/session/circuit-breaker.d.ts +5 -3
  78. package/dist/session/circuit-breaker.js +45 -20
  79. package/dist/session/context-compactor.d.ts +32 -0
  80. package/dist/session/context-compactor.js +45 -0
  81. package/dist/session/context-monitor.js +2 -2
  82. package/dist/session/handoff.d.ts +21 -0
  83. package/dist/session/handoff.js +50 -0
  84. package/dist/session/manager.d.ts +17 -5
  85. package/dist/session/manager.js +153 -146
  86. package/dist/session/memory-store.d.ts +29 -0
  87. package/dist/session/memory-store.js +45 -0
  88. package/dist/session/message-queue.d.ts +28 -0
  89. package/dist/session/message-queue.js +52 -0
  90. package/dist/session/pending-dispatcher.d.ts +31 -0
  91. package/dist/session/pending-dispatcher.js +120 -0
  92. package/dist/session/pending-store.d.ts +60 -0
  93. package/dist/session/pending-store.js +118 -0
  94. package/dist/session/stale-session.d.ts +31 -0
  95. package/dist/session/stale-session.js +45 -0
  96. package/dist/session/subprocess.d.ts +2 -0
  97. package/dist/session/subprocess.js +33 -11
  98. package/dist/session/tab-store.js +4 -3
  99. package/dist/tasks/scheduler.d.ts +7 -0
  100. package/dist/tasks/scheduler.js +46 -6
  101. package/dist/tasks/store.js +20 -6
  102. package/dist/timeline/logger.js +3 -1
  103. package/dist/timeline/query.js +9 -3
  104. package/dist/types.d.ts +34 -9
  105. package/dist/util/auto-heal.js +15 -5
  106. package/dist/util/install-info.js +3 -1
  107. package/dist/util/logger.d.ts +1 -1
  108. package/dist/util/logger.js +63 -24
  109. package/dist/util/paths.d.ts +1 -0
  110. package/dist/util/paths.js +12 -2
  111. package/dist/util/retry.js +1 -1
  112. package/dist/util/text.js +13 -7
  113. package/dist/voice/index.js +5 -1
  114. package/dist/voice/stt.js +14 -6
  115. package/dist/voice/tts.js +1 -1
  116. package/dist/watchers/scheduler.js +9 -2
  117. package/package.json +6 -1
  118. package/dist/session/tool-classifier.d.ts +0 -4
  119. 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 = new Database(dbPath);
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
- // Prune old permission history (keep last 1000 entries)
69
- db?.exec('DELETE FROM permission_history WHERE created_at < (SELECT created_at FROM permission_history ORDER BY created_at DESC LIMIT 1 OFFSET 999)');
70
- db?.exec("DELETE FROM activity_log WHERE created_at < datetime('now', '-90 days')");
71
- // Prune unused routing patterns so the per-message learned-routing scan
72
- // doesn't grow unbounded. Keep patterns that have been hit recently or often.
73
- db?.exec("DELETE FROM routing_preferences WHERE hit_count < 3 AND created_at < datetime('now', '-30 days')");
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);
@@ -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: "ALTER TABLE tabs ADD COLUMN system_prompt TEXT DEFAULT NULL",
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.split(';').map(s => s.trim()).filter(s => s.length > 0);
295
- for (const stmt of statements) {
296
- try {
297
- // For ALTER TABLE ADD COLUMN, check if column already exists first
298
- const addMatch = stmt.match(/ALTER\s+TABLE\s+(\S+)\s+ADD\s+COLUMN\s+(\S+)/i);
299
- if (addMatch) {
300
- const columns = db.pragma(`table_info(${addMatch[1]})`);
301
- if (columns.some(c => c.name === addMatch[2])) {
302
- logger.debug(`Migration v${migration.version}: column ${addMatch[1]}.${addMatch[2]} already exists, skipping`);
303
- continue;
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
- // For ALTER TABLE DROP COLUMN, skip if column already gone
307
- const dropMatch = stmt.match(/ALTER\s+TABLE\s+(\S+)\s+DROP\s+COLUMN\s+(\S+)/i);
308
- if (dropMatch) {
309
- const columns = db.pragma(`table_info(${dropMatch[1]})`);
310
- if (!columns.some(c => c.name === dropMatch[2])) {
311
- logger.debug(`Migration v${migration.version}: column ${dropMatch[1]}.${dropMatch[2]} already absent, skipping`);
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
- catch (err) {
318
- const msg = err instanceof Error ? err.message : String(err);
319
- if (msg.includes('already exists') || msg.includes('no such column') || msg.includes('no such table') || msg.includes('no such index')) {
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.prepare("SELECT COUNT(*) as c FROM delegations WHERE source_tab = ? AND status IN ('pending', 'running')").get(sourceTab).c;
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.prepare("SELECT * FROM delegations WHERE target_tab = ? AND status IN ('pending', 'running') ORDER BY created_at DESC LIMIT 1").get(targetTab);
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.prepare("SELECT MAX(depth) as d FROM delegations WHERE source_tab = ? AND status IN ('pending', 'running')").get(tabName);
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('start')
32
- .description('Start the Beecork daemon')
33
- .action(startDaemon);
34
- program
35
- .command('stop')
36
- .description('Stop the Beecork daemon')
37
- .action(stopDaemon);
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
- .command('tasks')
53
- .description('Manage scheduled tasks');
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
- .command('list')
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
- .command('memory')
111
- .description('Manage long-term memories');
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 { getBeecorkHome } = await import('./util/paths.js');
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 = `${getBeecorkHome()}/whatsapp-session`;
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 { getBeecorkHome } = await import('./util/paths.js');
207
+ const { getWhatsappSessionPath } = await import('./util/paths.js');
227
208
  const config = getConfig();
228
- const sessionPath = `${getBeecorkHome()}/whatsapp-session`;
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
- const reason = update.lastDisconnect?.error?.output?.statusCode;
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.prepare('SELECT content FROM memories WHERE tab_name = ? ORDER BY created_at DESC LIMIT 50').all(tabName);
75
- return rows.map(r => ({ content: r.content, scope: 'tab', source: tabName }));
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
  }