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.
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 +18 -13
  118. package/dist/session/tool-classifier.d.ts +0 -4
  119. package/dist/session/tool-classifier.js +0 -56
@@ -4,17 +4,29 @@ import { getCrontabPath } from '../util/paths.js';
4
4
  import { logger } from '../util/logger.js';
5
5
  function rowToTask(row) {
6
6
  return {
7
- id: row.id, name: row.name,
7
+ id: row.id,
8
+ name: row.name,
8
9
  scheduleType: row.schedule_type,
9
- schedule: row.schedule, tabName: row.tab_name, message: row.message,
10
+ schedule: row.schedule,
11
+ tabName: row.tab_name,
12
+ message: row.message,
10
13
  payloadType: row.payload_type || 'agentTurn',
11
- enabled: row.enabled === 1, createdAt: row.created_at,
12
- lastRunAt: row.last_run_at, nextRunAt: row.next_run_at,
14
+ enabled: row.enabled === 1,
15
+ createdAt: row.created_at,
16
+ lastRunAt: row.last_run_at,
17
+ nextRunAt: row.next_run_at,
13
18
  };
14
19
  }
20
+ // One-shot JSON migration only needs to run once per process lifetime, but the
21
+ // dashboard creates a fresh TaskStore per request and MCP creates one per task
22
+ // call. Without this flag, every instantiation re-fired existsSync + COUNT(*).
23
+ let migrationChecked = false;
15
24
  export class TaskStore {
16
25
  constructor() {
17
- this.migrateFromJson();
26
+ if (!migrationChecked) {
27
+ migrationChecked = true;
28
+ this.migrateFromJson();
29
+ }
18
30
  }
19
31
  list() {
20
32
  const db = getDb();
@@ -63,7 +75,9 @@ export class TaskStore {
63
75
  try {
64
76
  fs.renameSync(jsonPath, jsonPath + '.bak');
65
77
  }
66
- catch { /* ok */ }
78
+ catch {
79
+ /* ok */
80
+ }
67
81
  return;
68
82
  }
69
83
  try {
@@ -5,5 +5,7 @@ export function logActivity(eventType, summary, options) {
5
5
  const db = getDb();
6
6
  db.prepare('INSERT INTO activity_log (id, event_type, project_name, tab_name, summary, details, duration_ms, cost_usd) VALUES (?, ?, ?, ?, ?, ?, ?, ?)').run(uuidv4(), eventType, options?.projectName || null, options?.tabName || null, summary, options?.details || null, options?.durationMs || null, options?.costUsd || null);
7
7
  }
8
- catch { /* non-critical — don't crash if logging fails */ }
8
+ catch {
9
+ /* non-critical — don't crash if logging fails */
10
+ }
9
11
  }
@@ -1,9 +1,15 @@
1
1
  import { getDb } from '../db/index.js';
2
2
  function rowToEvent(r) {
3
3
  return {
4
- id: r.id, eventType: r.event_type, projectName: r.project_name,
5
- tabName: r.tab_name, summary: r.summary, details: r.details,
6
- durationMs: r.duration_ms, costUsd: r.cost_usd, createdAt: r.created_at,
4
+ id: r.id,
5
+ eventType: r.event_type,
6
+ projectName: r.project_name,
7
+ tabName: r.tab_name,
8
+ summary: r.summary,
9
+ details: r.details,
10
+ durationMs: r.duration_ms,
11
+ costUsd: r.cost_usd,
12
+ createdAt: r.created_at,
7
13
  };
8
14
  }
9
15
  export function getTimeline(options) {
package/dist/types.d.ts CHANGED
@@ -13,7 +13,6 @@ export interface ClaudeCodeConfig {
13
13
  }
14
14
  export interface MemoryConfig {
15
15
  dbPath: string;
16
- maxLongTermEntries: number;
17
16
  }
18
17
  export interface TabConfig {
19
18
  workingDir: string;
@@ -44,11 +43,26 @@ export interface DiscordConfig {
44
43
  /** Optional admin user ID. Defaults to allowedUserIds[0]. */
45
44
  adminUserId?: string;
46
45
  }
46
+ /**
47
+ * Webhook channel auth semantics:
48
+ *
49
+ * - When both `authToken` and `hmacSecret` are configured, EITHER mode
50
+ * satisfies authentication (OR semantics). A valid Bearer token is enough
51
+ * even if the HMAC signature is missing. There is no AND mode today.
52
+ * - Replay protection is NOT implemented. Callers must keep their payloads
53
+ * idempotent — see L10 in the audit. Acceptable while the server binds to
54
+ * 127.0.0.1 only; revisit if remote exposure becomes a thing.
55
+ * - `allowUnauthLocalhost` is the explicit opt-in to run with no auth at all.
56
+ * Useful for `curl localhost` from a trusted shell, but on a multi-user
57
+ * host any local process can inject prompts into your tabs.
58
+ */
47
59
  export interface WebhookConfig {
48
60
  enabled: boolean;
49
61
  port: number;
50
62
  authToken?: string;
51
63
  hmacSecret?: string;
64
+ /** Set to true to skip the fail-secure auth check at start. NOT recommended on shared hosts. */
65
+ allowUnauthLocalhost?: boolean;
52
66
  }
53
67
  export interface GroupConfig {
54
68
  activationMode: 'mention' | 'reply' | 'keyword' | 'always';
@@ -56,15 +70,25 @@ export interface GroupConfig {
56
70
  tabPerGroup: boolean;
57
71
  keywords?: string[];
58
72
  }
59
- export interface NotificationConfig {
60
- type: 'pushover' | 'ntfy' | 'webhook';
61
- userKey?: string;
62
- appToken?: string;
63
- topic?: string;
73
+ /**
74
+ * Discriminated union of notification provider configs. The `type` field
75
+ * narrows which fields are required so callers get full type safety instead
76
+ * of treating every field as optional. createNotificationProvider switches
77
+ * exhaustively on `type`.
78
+ */
79
+ export type NotificationConfig = {
80
+ type: 'pushover';
81
+ userKey: string;
82
+ appToken: string;
83
+ } | {
84
+ type: 'ntfy';
85
+ topic: string;
64
86
  server?: string;
65
- url?: string;
87
+ } | {
88
+ type: 'webhook';
89
+ url: string;
66
90
  headers?: Record<string, string>;
67
- }
91
+ };
68
92
  export interface MediaGeneratorConfig {
69
93
  provider: string;
70
94
  apiKey?: string;
@@ -77,7 +101,6 @@ export interface MediaAttachment {
77
101
  mimeType: string;
78
102
  filePath: string;
79
103
  fileName?: string;
80
- duration?: number;
81
104
  caption?: string;
82
105
  }
83
106
  export interface BeecorkConfig {
@@ -96,6 +119,8 @@ export interface BeecorkConfig {
96
119
  notifications?: NotificationConfig[];
97
120
  mediaGenerators?: MediaGeneratorConfig[];
98
121
  communityChannels?: string[];
122
+ /** Capability packs enabled via `beecork capabilities enable`. Owned by src/capabilities. */
123
+ capabilities?: import('./capabilities/types.js').EnabledCapability[];
99
124
  deployment: 'local' | 'vps';
100
125
  }
101
126
  export type TabStatus = 'idle' | 'running' | 'error' | 'stopped';
@@ -41,10 +41,20 @@ export function autoHealInstall(fromFileUrl) {
41
41
  }
42
42
  }
43
43
  if (rewroteUnit && signaledDaemon) {
44
- return { action: 'rewrote-and-signaled', unitPath, oldDaemonScript: oldDaemonScript, newDaemonScript: currentDaemonScript };
44
+ return {
45
+ action: 'rewrote-and-signaled',
46
+ unitPath,
47
+ oldDaemonScript: oldDaemonScript,
48
+ newDaemonScript: currentDaemonScript,
49
+ };
45
50
  }
46
51
  if (rewroteUnit)
47
- return { action: 'rewrote-unit', unitPath, oldDaemonScript: oldDaemonScript, newDaemonScript: currentDaemonScript };
52
+ return {
53
+ action: 'rewrote-unit',
54
+ unitPath,
55
+ oldDaemonScript: oldDaemonScript,
56
+ newDaemonScript: currentDaemonScript,
57
+ };
48
58
  if (signaledDaemon)
49
59
  return { action: 'signaled-daemon', unitPath };
50
60
  return { action: 'noop' };
@@ -64,8 +74,8 @@ export function extractDaemonScript(content) {
64
74
  // launchd plist: pull the second <string> inside ProgramArguments
65
75
  const launchdMatch = content.match(/<key>\s*ProgramArguments\s*<\/key>\s*<array>([\s\S]*?)<\/array>/);
66
76
  if (launchdMatch) {
67
- const args = Array.from(launchdMatch[1].matchAll(/<string>([^<]+)<\/string>/g)).map(m => m[1]);
68
- const jsArg = args.find(a => a.endsWith('daemon.js'));
77
+ const args = Array.from(launchdMatch[1].matchAll(/<string>([^<]+)<\/string>/g)).map((m) => m[1]);
78
+ const jsArg = args.find((a) => a.endsWith('daemon.js'));
69
79
  if (jsArg)
70
80
  return jsArg;
71
81
  }
@@ -73,7 +83,7 @@ export function extractDaemonScript(content) {
73
83
  const systemdMatch = content.match(/^ExecStart\s*=\s*(.+)$/m);
74
84
  if (systemdMatch) {
75
85
  const parts = systemdMatch[1].split(/\s+/);
76
- const jsArg = parts.find(p => p.endsWith('daemon.js'));
86
+ const jsArg = parts.find((p) => p.endsWith('daemon.js'));
77
87
  if (jsArg)
78
88
  return jsArg;
79
89
  }
@@ -52,7 +52,9 @@ export function removeRuntimeInfo() {
52
52
  try {
53
53
  fs.unlinkSync(filePath);
54
54
  }
55
- catch { /* not present, fine */ }
55
+ catch {
56
+ /* not present, fine */
57
+ }
56
58
  }
57
59
  export function isPidAlive(pid) {
58
60
  try {
@@ -3,7 +3,7 @@ declare class Logger {
3
3
  private minLevel;
4
4
  private logFile;
5
5
  private stream;
6
- private writeCount;
6
+ private bytesWritten;
7
7
  setLevel(level: LogLevel): void;
8
8
  setLogFile(name: string): void;
9
9
  private write;
@@ -7,11 +7,24 @@ const LEVEL_PRIORITY = {
7
7
  warn: 2,
8
8
  error: 3,
9
9
  };
10
+ // Patterns that should never reach disk or stdout. Each channel uses a per-call
11
+ // sanitizer too (defense-in-depth), but the Logger is the last line of defense
12
+ // for any third-party error object that happens to embed a secret in its message.
13
+ const REDACTION_PATTERNS = [
14
+ { re: /bot\d+:[A-Za-z0-9_-]+/g, replacement: 'bot<REDACTED>' }, // Telegram bot token
15
+ ];
16
+ function redact(s) {
17
+ let out = s;
18
+ for (const { re, replacement } of REDACTION_PATTERNS)
19
+ out = out.replace(re, replacement);
20
+ return out;
21
+ }
22
+ const ROTATE_BYTES = 10 * 1024 * 1024;
10
23
  class Logger {
11
24
  minLevel = 'info';
12
25
  logFile = null;
13
26
  stream = null;
14
- writeCount = 0;
27
+ bytesWritten = 0;
15
28
  setLevel(level) {
16
29
  this.minLevel = level;
17
30
  }
@@ -20,18 +33,30 @@ class Logger {
20
33
  fs.mkdirSync(dir, { recursive: true });
21
34
  this.logFile = path.join(dir, name);
22
35
  this.stream = fs.createWriteStream(this.logFile, { flags: 'a' });
36
+ // Seed bytesWritten from the existing file size so we don't reset the
37
+ // rotation counter on every daemon restart.
38
+ try {
39
+ this.bytesWritten = fs.statSync(this.logFile).size;
40
+ }
41
+ catch {
42
+ this.bytesWritten = 0;
43
+ }
23
44
  }
24
45
  write(level, msg, ...args) {
25
46
  if (LEVEL_PRIORITY[level] < LEVEL_PRIORITY[this.minLevel])
26
47
  return;
27
48
  const timestamp = new Date().toISOString();
28
49
  const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
29
- const line = args.length > 0
30
- ? `${prefix} ${msg} ${args.map(a => typeof a === 'string' ? a : JSON.stringify(a)).join(' ')}`
50
+ const raw = args.length > 0
51
+ ? `${prefix} ${msg} ${args.map((a) => (typeof a === 'string' ? a : JSON.stringify(a))).join(' ')}`
31
52
  : `${prefix} ${msg}`;
53
+ const line = redact(raw);
32
54
  if (this.stream) {
33
- this.stream.write(line + '\n');
34
- this.checkRotation();
55
+ const out = line + '\n';
56
+ this.stream.write(out);
57
+ this.bytesWritten += out.length;
58
+ if (this.bytesWritten > ROTATE_BYTES)
59
+ this.checkRotation();
35
60
  }
36
61
  if (level === 'error') {
37
62
  console.error(line);
@@ -43,31 +68,38 @@ class Logger {
43
68
  console.log(line);
44
69
  }
45
70
  }
46
- debug(msg, ...args) { this.write('debug', msg, ...args); }
47
- info(msg, ...args) { this.write('info', msg, ...args); }
48
- warn(msg, ...args) { this.write('warn', msg, ...args); }
49
- error(msg, ...args) { this.write('error', msg, ...args); }
71
+ debug(msg, ...args) {
72
+ this.write('debug', msg, ...args);
73
+ }
74
+ info(msg, ...args) {
75
+ this.write('info', msg, ...args);
76
+ }
77
+ warn(msg, ...args) {
78
+ this.write('warn', msg, ...args);
79
+ }
80
+ error(msg, ...args) {
81
+ this.write('error', msg, ...args);
82
+ }
50
83
  checkRotation() {
51
84
  if (!this.logFile || !this.stream)
52
85
  return;
53
- this.writeCount++;
54
- if (this.writeCount < 100)
55
- return;
56
- this.writeCount = 0;
86
+ // bytesWritten-based gate already triggered us; rotate without statSync.
57
87
  try {
58
- const stats = fs.statSync(this.logFile);
59
- if (stats.size > 10 * 1024 * 1024) {
60
- this.stream.end();
61
- const rotated = this.logFile + '.1';
62
- try {
63
- fs.unlinkSync(rotated);
64
- }
65
- catch { }
66
- fs.renameSync(this.logFile, rotated);
67
- this.stream = fs.createWriteStream(this.logFile, { flags: 'a' });
88
+ this.stream.end();
89
+ const rotated = this.logFile + '.1';
90
+ try {
91
+ fs.unlinkSync(rotated);
68
92
  }
93
+ catch {
94
+ /* not fatal */
95
+ }
96
+ fs.renameSync(this.logFile, rotated);
97
+ this.stream = fs.createWriteStream(this.logFile, { flags: 'a' });
98
+ this.bytesWritten = 0;
99
+ }
100
+ catch {
101
+ /* rotation failure shouldn't kill the daemon */
69
102
  }
70
- catch { }
71
103
  }
72
104
  close() {
73
105
  if (this.stream) {
@@ -77,3 +109,10 @@ class Logger {
77
109
  }
78
110
  }
79
111
  export const logger = new Logger();
112
+ // Allow operators to bump verbosity at runtime without recompiling.
113
+ // e.g. `BEECORK_LOG_LEVEL=debug beecork start` surfaces every claude subprocess
114
+ // stdout/stderr line via the logger.debug calls.
115
+ const envLevel = process.env.BEECORK_LOG_LEVEL?.toLowerCase();
116
+ if (envLevel === 'debug' || envLevel === 'info' || envLevel === 'warn' || envLevel === 'error') {
117
+ logger.setLevel(envLevel);
118
+ }
@@ -8,5 +8,6 @@ export declare function getPidPath(): string;
8
8
  export declare function getRuntimeInfoPath(): string;
9
9
  export declare function getCronReloadSignalPath(): string;
10
10
  export declare function getWatcherReloadSignalPath(): string;
11
+ export declare function getWhatsappSessionPath(): string;
11
12
  export declare function ensureBeecorkDirs(): void;
12
13
  export declare function expandHome(p: string): string;
@@ -32,10 +32,20 @@ export function getCronReloadSignalPath() {
32
32
  export function getWatcherReloadSignalPath() {
33
33
  return path.join(getBeecorkHome(), '.watcher-reload');
34
34
  }
35
+ export function getWhatsappSessionPath() {
36
+ return path.join(getBeecorkHome(), 'whatsapp-session');
37
+ }
35
38
  export function ensureBeecorkDirs() {
36
39
  const home = getBeecorkHome();
37
- fs.mkdirSync(home, { recursive: true });
38
- fs.mkdirSync(getLogsDir(), { recursive: true });
40
+ fs.mkdirSync(home, { recursive: true, mode: 0o700 });
41
+ // Upgrade an existing dir that was created with a looser umask before this fix landed.
42
+ try {
43
+ fs.chmodSync(home, 0o700);
44
+ }
45
+ catch {
46
+ /* not fatal if mode change fails */
47
+ }
48
+ fs.mkdirSync(getLogsDir(), { recursive: true, mode: 0o700 });
39
49
  }
40
50
  export function expandHome(p) {
41
51
  if (p.startsWith('~/') || p === '~') {
@@ -10,7 +10,7 @@ export async function retryWithBackoff(fn, delays = [1000, 5000, 15000], label =
10
10
  lastError = err instanceof Error ? err : new Error(String(err));
11
11
  if (attempt < delays.length) {
12
12
  logger.warn(`${label} failed (attempt ${attempt + 1}/${delays.length + 1}), retrying in ${delays[attempt]}ms: ${lastError.message}`);
13
- await new Promise(resolve => setTimeout(resolve, delays[attempt]));
13
+ await new Promise((resolve) => setTimeout(resolve, delays[attempt]));
14
14
  }
15
15
  }
16
16
  }
package/dist/util/text.js CHANGED
@@ -78,16 +78,22 @@ export function formatTabbedResponse(text, tabName) {
78
78
  export function buildMediaPrompt(media, textPrompt) {
79
79
  if (media.length === 0)
80
80
  return textPrompt;
81
- const descriptions = media.map(m => {
81
+ const descriptions = media.map((m) => {
82
82
  if (m.type === 'voice' && m.caption?.startsWith('[Transcribed'))
83
83
  return m.caption;
84
84
  switch (m.type) {
85
- case 'image': return `User sent an image: ${m.filePath}`;
86
- case 'voice': return `User sent a voice message: ${m.filePath}`;
87
- case 'audio': return `User sent an audio file: ${m.filePath}${m.fileName ? ` (${m.fileName})` : ''}`;
88
- case 'video': return `User sent a video: ${m.filePath}`;
89
- case 'document': return `User sent a file: ${m.filePath}${m.fileName ? ` (${m.fileName})` : ''}`;
90
- default: return `User sent a file: ${m.filePath}`;
85
+ case 'image':
86
+ return `User sent an image: ${m.filePath}`;
87
+ case 'voice':
88
+ return `User sent a voice message: ${m.filePath}`;
89
+ case 'audio':
90
+ return `User sent an audio file: ${m.filePath}${m.fileName ? ` (${m.fileName})` : ''}`;
91
+ case 'video':
92
+ return `User sent a video: ${m.filePath}`;
93
+ case 'document':
94
+ return `User sent a file: ${m.filePath}${m.fileName ? ` (${m.fileName})` : ''}`;
95
+ default:
96
+ return `User sent a file: ${m.filePath}`;
91
97
  }
92
98
  });
93
99
  const mediaText = descriptions.join('\n');
@@ -10,7 +10,11 @@ export function initVoiceProviders(voice) {
10
10
  stt = createSTTProvider({ provider: voice.sttProvider, apiKey: voice.sttApiKey });
11
11
  }
12
12
  if (voice?.ttsProvider && voice.ttsProvider !== 'none') {
13
- tts = createTTSProvider({ provider: voice.ttsProvider, apiKey: voice.ttsApiKey, voice: voice.ttsVoice });
13
+ tts = createTTSProvider({
14
+ provider: voice.ttsProvider,
15
+ apiKey: voice.ttsApiKey,
16
+ voice: voice.ttsVoice,
17
+ });
14
18
  }
15
19
  return { stt, tts };
16
20
  }
package/dist/voice/stt.js CHANGED
@@ -1,5 +1,6 @@
1
- import fs from 'node:fs';
1
+ import { readFile } from 'node:fs/promises';
2
2
  import { logger } from '../util/logger.js';
3
+ import { assertInsideMediaDir } from '../media/store.js';
3
4
  /** OpenAI Whisper API provider */
4
5
  export class WhisperAPIProvider {
5
6
  apiKey;
@@ -10,26 +11,33 @@ export class WhisperAPIProvider {
10
11
  try {
11
12
  // Make a lightweight request to warm up the HTTPS connection
12
13
  await fetch('https://api.openai.com/v1/models', {
13
- headers: { 'Authorization': `Bearer ${this.apiKey}` },
14
+ headers: { Authorization: `Bearer ${this.apiKey}` },
14
15
  signal: AbortSignal.timeout(5000),
15
16
  });
16
17
  }
17
- catch { /* non-critical */ }
18
+ catch {
19
+ /* non-critical */
20
+ }
18
21
  }
19
22
  async transcribe(filePath) {
23
+ // Defense-in-depth: callers should already constrain filePath to the media dir,
24
+ // but verify here so a leaked/forged MediaAttachment.filePath cannot exfiltrate
25
+ // arbitrary files (e.g. ~/.ssh/id_rsa) to the Whisper API.
26
+ const safePath = assertInsideMediaDir(filePath);
27
+ const buffer = await readFile(safePath);
20
28
  const formData = new FormData();
21
- formData.append('file', new Blob([fs.readFileSync(filePath)]), 'audio.ogg');
29
+ formData.append('file', new Blob([buffer]), 'audio.ogg');
22
30
  formData.append('model', 'whisper-1');
23
31
  const response = await fetch('https://api.openai.com/v1/audio/transcriptions', {
24
32
  method: 'POST',
25
- headers: { 'Authorization': `Bearer ${this.apiKey}` },
33
+ headers: { Authorization: `Bearer ${this.apiKey}` },
26
34
  body: formData,
27
35
  signal: AbortSignal.timeout(60000),
28
36
  });
29
37
  if (!response.ok) {
30
38
  throw new Error(`Whisper API error: ${response.status} ${response.statusText}`);
31
39
  }
32
- const result = await response.json();
40
+ const result = (await response.json());
33
41
  return result.text;
34
42
  }
35
43
  }
package/dist/voice/tts.js CHANGED
@@ -14,7 +14,7 @@ export class OpenAITTSProvider {
14
14
  const response = await fetch('https://api.openai.com/v1/audio/speech', {
15
15
  method: 'POST',
16
16
  headers: {
17
- 'Authorization': `Bearer ${this.apiKey}`,
17
+ Authorization: `Bearer ${this.apiKey}`,
18
18
  'Content-Type': 'application/json',
19
19
  },
20
20
  body: JSON.stringify({
@@ -5,6 +5,8 @@ import { evaluateWatcher } from './evaluator.js';
5
5
  import { execAsync, intervalToMs } from '../tasks/scheduler.js';
6
6
  import { getLogsDir, getWatcherReloadSignalPath } from '../util/paths.js';
7
7
  import { logger } from '../util/logger.js';
8
+ import { PendingMessageStore } from '../session/pending-store.js';
9
+ import { logActivity } from '../timeline/index.js';
8
10
  export class WatcherScheduler {
9
11
  store = new WatcherStore();
10
12
  // watcherId -> due time in ms epoch for next check
@@ -52,7 +54,9 @@ export class WatcherScheduler {
52
54
  try {
53
55
  fs.unlinkSync(signalPath);
54
56
  }
55
- catch { /* race condition, ok */ }
57
+ catch {
58
+ /* race condition, ok */
59
+ }
56
60
  logger.info('Watchers: reload signal detected, reloading');
57
61
  this.loadAndSchedule();
58
62
  }
@@ -101,6 +105,9 @@ export class WatcherScheduler {
101
105
  if (result.triggered) {
102
106
  this.store.markTriggered(watcher.id);
103
107
  await fs.promises.appendFile(logFile, `[${new Date().toISOString()}] TRIGGERED: ${result.output.slice(0, 500)}\n`);
108
+ logActivity('watcher_triggered', `Watcher "${watcher.name}" triggered`, {
109
+ details: result.output.slice(0, 500),
110
+ });
104
111
  await this.executeAction(watcher, result.output);
105
112
  }
106
113
  else {
@@ -155,7 +162,7 @@ export class WatcherScheduler {
155
162
  message = watcher.actionDetails.slice(colonIdx + 1).trim();
156
163
  }
157
164
  const fullMessage = `[Watcher "${watcher.name}" triggered] ${message}\n\nWatcher output:\n${output.slice(0, 500)}`;
158
- db.prepare('INSERT INTO pending_messages (tab_name, message, type) VALUES (?, ?, ?)').run(tabName, fullMessage, 'delegation');
165
+ PendingMessageStore.enqueueDelegation(tabName, fullMessage, db);
159
166
  if (this.onNotify) {
160
167
  await this.onNotify(`Watcher "${watcher.name}" triggered -- delegated to tab:${tabName}`);
161
168
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "beecork",
3
- "version": "1.5.0",
3
+ "version": "1.7.0",
4
4
  "description": "Claude Code always-on infrastructure — a phone number, a memory, and an alarm clock",
5
5
  "type": "module",
6
6
  "bin": {
@@ -15,32 +15,37 @@
15
15
  "dev:daemon": "tsx src/daemon.ts",
16
16
  "test": "vitest",
17
17
  "lint": "eslint src/",
18
+ "format": "prettier --write \"src/**/*.ts\"",
19
+ "format:check": "prettier --check \"src/**/*.ts\"",
18
20
  "build:css": "tailwindcss --content src/dashboard/html.ts --minify",
19
21
  "prepublishOnly": "npm test && npm run build",
20
22
  "postinstall": "node scripts/postinstall.mjs"
21
23
  },
22
24
  "dependencies": {
23
25
  "@modelcontextprotocol/sdk": "^1.29.0",
24
- "@whiskeysockets/baileys": "^6.7.0",
25
- "better-sqlite3": "^12.8.0",
26
+ "@whiskeysockets/baileys": "^7.0.0-rc11",
27
+ "better-sqlite3": "^12.10.0",
26
28
  "commander": "^14.0.3",
27
29
  "cron-parser": "^5.5.0",
28
- "discord.js": "^14.26.2",
30
+ "discord.js": "^14.26.4",
29
31
  "node-telegram-bot-api": "^0.67.0",
32
+ "pino": "^10.3.1",
30
33
  "qrcode-terminal": "^0.12.0",
31
- "uuid": "^13.0.0"
34
+ "uuid": "^14.0.0"
32
35
  },
33
36
  "devDependencies": {
34
- "@tailwindcss/cli": "^4.2.2",
37
+ "@tailwindcss/cli": "^4.3.0",
35
38
  "@types/better-sqlite3": "^7.6.13",
36
- "@types/node": "^24.0.0",
39
+ "@types/node": "^24.12.4",
37
40
  "@types/node-telegram-bot-api": "^0.64.14",
38
- "eslint": "^10.2.0",
39
- "tailwindcss": "^4.2.2",
40
- "tsx": "^4.21.0",
41
- "typescript": "^6.0.2",
42
- "typescript-eslint": "^8.58.1",
43
- "vitest": "^4.1.4"
41
+ "eslint": "^10.4.0",
42
+ "eslint-config-prettier": "^10.1.8",
43
+ "prettier": "^3.8.3",
44
+ "tailwindcss": "^4.3.0",
45
+ "tsx": "^4.22.0",
46
+ "typescript": "^6.0.3",
47
+ "typescript-eslint": "^8.59.3",
48
+ "vitest": "^4.1.6"
44
49
  },
45
50
  "files": [
46
51
  "dist/",
@@ -1,4 +0,0 @@
1
- /** Reserved for future approval mode implementation. Not currently wired into the runtime. */
2
- export type ToolRisk = 'safe' | 'dangerous';
3
- /** Classify a tool call as safe or dangerous */
4
- export declare function classifyTool(toolName: string, input: Record<string, unknown>): ToolRisk;
@@ -1,56 +0,0 @@
1
- const SAFE_TOOLS = new Set([
2
- 'Read', 'Glob', 'Grep', 'LSP', 'WebFetch', 'WebSearch',
3
- 'ToolSearch', 'TaskGet', 'TaskList',
4
- ]);
5
- const DANGEROUS_TOOLS = new Set([
6
- 'Write', 'Edit', 'NotebookEdit',
7
- 'TaskCreate', 'TaskUpdate', 'TaskStop',
8
- ]);
9
- const SAFE_BASH_PATTERNS = [
10
- /^(ls|cat|head|tail|wc|file|stat|which|type|echo|printf)\b/,
11
- /^git\s+(status|log|diff|show|branch|tag|remote)\b/,
12
- /^(pwd|whoami|hostname|date|uname|env|printenv)\b/,
13
- /^(find|grep|rg|fd|ag)\b/,
14
- /^(node|python|ruby|go)\s+--?(version|help)/,
15
- /^npm\s+(list|ls|view|info|outdated|audit)\b/,
16
- /^curl\s.*-X\s*GET\b/,
17
- /^curl\s+(?!.*-X\s*(POST|PUT|DELETE|PATCH))(?!.*--data)(?!.*-d\s)/,
18
- ];
19
- const DANGEROUS_BASH_PATTERNS = [
20
- /^rm\b/,
21
- /^(mv|cp)\b.*--?(force|f)\b/,
22
- /^chmod\b/,
23
- /^chown\b/,
24
- /^git\s+(push|reset|rebase|merge|checkout\s+--)\b/,
25
- /^(docker|kubectl|terraform|ansible)\b/,
26
- /^(sudo|su)\b/,
27
- /^npm\s+(publish|install|uninstall|link)\b/,
28
- /^(kill|killall|pkill)\b/,
29
- /^curl\s.*-X\s*(POST|PUT|DELETE|PATCH)\b/,
30
- ];
31
- /** Classify a tool call as safe or dangerous */
32
- export function classifyTool(toolName, input) {
33
- if (SAFE_TOOLS.has(toolName))
34
- return 'safe';
35
- if (DANGEROUS_TOOLS.has(toolName))
36
- return 'dangerous';
37
- // Bash tool: inspect the command
38
- if (toolName === 'Bash') {
39
- const command = String(input.command || '').trim();
40
- for (const pattern of SAFE_BASH_PATTERNS) {
41
- if (pattern.test(command))
42
- return 'safe';
43
- }
44
- for (const pattern of DANGEROUS_BASH_PATTERNS) {
45
- if (pattern.test(command))
46
- return 'dangerous';
47
- }
48
- // Default: unknown bash commands are dangerous
49
- return 'dangerous';
50
- }
51
- // MCP tools from external servers: default to dangerous
52
- if (toolName.startsWith('mcp__'))
53
- return 'dangerous';
54
- // Unknown tools: default to dangerous
55
- return 'dangerous';
56
- }