beecork 1.4.11 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (138) 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/admin.d.ts +10 -0
  6. package/dist/channels/admin.js +20 -0
  7. package/dist/channels/command-handler.d.ts +2 -10
  8. package/dist/channels/command-handler.js +90 -84
  9. package/dist/channels/discord.d.ts +4 -9
  10. package/dist/channels/discord.js +59 -42
  11. package/dist/channels/index.d.ts +1 -1
  12. package/dist/channels/loader.js +13 -4
  13. package/dist/channels/pipeline.js +14 -5
  14. package/dist/channels/registry.d.ts +17 -1
  15. package/dist/channels/registry.js +33 -4
  16. package/dist/channels/send-helpers.d.ts +19 -0
  17. package/dist/channels/send-helpers.js +21 -0
  18. package/dist/channels/telegram.d.ts +21 -14
  19. package/dist/channels/telegram.js +214 -104
  20. package/dist/channels/types.d.ts +13 -38
  21. package/dist/channels/voice-state.d.ts +29 -0
  22. package/dist/channels/voice-state.js +45 -0
  23. package/dist/channels/webhook.d.ts +2 -5
  24. package/dist/channels/webhook.js +88 -29
  25. package/dist/channels/whatsapp.d.ts +9 -7
  26. package/dist/channels/whatsapp.js +141 -100
  27. package/dist/cli/capabilities.js +4 -4
  28. package/dist/cli/channel.js +16 -6
  29. package/dist/cli/commands.js +12 -9
  30. package/dist/cli/doctor.js +85 -27
  31. package/dist/cli/handoff.d.ts +7 -14
  32. package/dist/cli/handoff.js +9 -44
  33. package/dist/cli/mcp.js +5 -5
  34. package/dist/cli/media.js +21 -8
  35. package/dist/cli/setup.js +9 -8
  36. package/dist/cli/store.js +29 -12
  37. package/dist/config.d.ts +5 -1
  38. package/dist/config.js +20 -22
  39. package/dist/daemon.js +113 -51
  40. package/dist/dashboard/html.js +100 -20
  41. package/dist/dashboard/routes.d.ts +17 -0
  42. package/dist/dashboard/routes.js +623 -0
  43. package/dist/dashboard/server.js +38 -489
  44. package/dist/db/connection.d.ts +29 -0
  45. package/dist/db/connection.js +37 -0
  46. package/dist/db/index.js +43 -11
  47. package/dist/db/migrations.js +114 -22
  48. package/dist/delegation/manager.js +10 -4
  49. package/dist/index.js +39 -59
  50. package/dist/knowledge/manager.js +26 -12
  51. package/dist/mcp/handlers.d.ts +37 -0
  52. package/dist/mcp/handlers.js +520 -0
  53. package/dist/mcp/server.js +44 -858
  54. package/dist/mcp/tool-definitions.d.ts +1225 -0
  55. package/dist/mcp/tool-definitions.js +412 -0
  56. package/dist/mcp/validate.d.ts +23 -0
  57. package/dist/mcp/validate.js +65 -0
  58. package/dist/media/factory.js +18 -14
  59. package/dist/media/generators/dall-e.js +2 -2
  60. package/dist/media/generators/kling.js +4 -4
  61. package/dist/media/generators/lyria.js +1 -1
  62. package/dist/media/generators/nano-banana.d.ts +1 -1
  63. package/dist/media/generators/nano-banana.js +2 -2
  64. package/dist/media/generators/poll-util.js +4 -4
  65. package/dist/media/generators/recraft.js +3 -3
  66. package/dist/media/generators/runway.js +4 -4
  67. package/dist/media/generators/stable-diffusion.js +2 -2
  68. package/dist/media/generators/veo.js +1 -1
  69. package/dist/media/index.d.ts +2 -7
  70. package/dist/media/index.js +2 -2
  71. package/dist/media/store.d.ts +7 -0
  72. package/dist/media/store.js +18 -4
  73. package/dist/media/types.d.ts +22 -0
  74. package/dist/notifications/index.d.ts +2 -4
  75. package/dist/notifications/index.js +6 -19
  76. package/dist/notifications/ntfy.js +3 -3
  77. package/dist/observability/analytics.d.ts +1 -1
  78. package/dist/observability/analytics.js +41 -16
  79. package/dist/projects/index.d.ts +3 -2
  80. package/dist/projects/index.js +2 -2
  81. package/dist/projects/manager.d.ts +1 -7
  82. package/dist/projects/manager.js +66 -42
  83. package/dist/projects/router.d.ts +12 -0
  84. package/dist/projects/router.js +98 -45
  85. package/dist/service/install.js +15 -5
  86. package/dist/service/windows.js +1 -1
  87. package/dist/session/budget-guard.d.ts +20 -0
  88. package/dist/session/budget-guard.js +31 -0
  89. package/dist/session/circuit-breaker.d.ts +5 -3
  90. package/dist/session/circuit-breaker.js +45 -20
  91. package/dist/session/context-compactor.d.ts +32 -0
  92. package/dist/session/context-compactor.js +45 -0
  93. package/dist/session/context-monitor.js +2 -2
  94. package/dist/session/handoff.d.ts +21 -0
  95. package/dist/session/handoff.js +50 -0
  96. package/dist/session/manager.d.ts +21 -5
  97. package/dist/session/manager.js +166 -153
  98. package/dist/session/memory-store.d.ts +29 -0
  99. package/dist/session/memory-store.js +45 -0
  100. package/dist/session/message-queue.d.ts +28 -0
  101. package/dist/session/message-queue.js +52 -0
  102. package/dist/session/pending-dispatcher.d.ts +31 -0
  103. package/dist/session/pending-dispatcher.js +120 -0
  104. package/dist/session/pending-store.d.ts +60 -0
  105. package/dist/session/pending-store.js +118 -0
  106. package/dist/session/stale-session.d.ts +31 -0
  107. package/dist/session/stale-session.js +45 -0
  108. package/dist/session/subprocess.d.ts +3 -0
  109. package/dist/session/subprocess.js +54 -11
  110. package/dist/session/tab-store.d.ts +28 -0
  111. package/dist/session/tab-store.js +78 -0
  112. package/dist/tasks/scheduler.d.ts +13 -0
  113. package/dist/tasks/scheduler.js +97 -18
  114. package/dist/tasks/store.js +26 -12
  115. package/dist/timeline/logger.js +3 -1
  116. package/dist/timeline/query.js +15 -5
  117. package/dist/types.d.ts +49 -9
  118. package/dist/util/auto-heal.js +15 -5
  119. package/dist/util/install-info.js +3 -1
  120. package/dist/util/logger.d.ts +1 -1
  121. package/dist/util/logger.js +63 -24
  122. package/dist/util/paths.d.ts +2 -0
  123. package/dist/util/paths.js +16 -3
  124. package/dist/util/rate-limiter.js +8 -0
  125. package/dist/util/retry.js +1 -1
  126. package/dist/util/text.d.ts +21 -1
  127. package/dist/util/text.js +38 -8
  128. package/dist/voice/index.js +5 -1
  129. package/dist/voice/stt.js +14 -6
  130. package/dist/voice/tts.js +1 -1
  131. package/dist/watchers/scheduler.js +11 -5
  132. package/package.json +6 -1
  133. package/dist/session/tool-classifier.d.ts +0 -4
  134. package/dist/session/tool-classifier.js +0 -56
  135. package/dist/users/index.d.ts +0 -2
  136. package/dist/users/index.js +0 -1
  137. package/dist/users/service.d.ts +0 -17
  138. package/dist/users/service.js +0 -46
@@ -0,0 +1,78 @@
1
+ import { getDb } from '../db/index.js';
2
+ function rowToTab(row) {
3
+ return {
4
+ id: row.id,
5
+ name: row.name,
6
+ sessionId: row.session_id,
7
+ status: row.status,
8
+ workingDir: row.working_dir,
9
+ createdAt: row.created_at,
10
+ lastActivityAt: row.last_activity_at,
11
+ pid: row.pid,
12
+ systemPrompt: row.system_prompt,
13
+ };
14
+ }
15
+ /**
16
+ * SQL helpers for the `tabs` table. Single place for any code that needs to
17
+ * read/write tab rows. TabManager owns subprocess lifecycle; TabStore owns
18
+ * the schema knowledge.
19
+ *
20
+ * All methods accept an optional `db` param so they're testable against an
21
+ * in-memory database. The no-arg form uses the singleton.
22
+ */
23
+ export const TabStore = {
24
+ listAll(db = getDb()) {
25
+ return db.prepare('SELECT * FROM tabs ORDER BY last_activity_at DESC').all().map(rowToTab);
26
+ },
27
+ findByName(name, db = getDb()) {
28
+ const row = db.prepare('SELECT * FROM tabs WHERE name = ?').get(name);
29
+ return row ? rowToTab(row) : undefined;
30
+ },
31
+ getIdByName(name, db = getDb()) {
32
+ const row = db.prepare('SELECT id FROM tabs WHERE name = ?').get(name);
33
+ return row?.id;
34
+ },
35
+ countAll(db = getDb()) {
36
+ return db.prepare('SELECT COUNT(*) as c FROM tabs').get().c;
37
+ },
38
+ countRunning(db = getDb()) {
39
+ return db.prepare("SELECT COUNT(*) as c FROM tabs WHERE status = 'running'").get().c;
40
+ },
41
+ /** All tabs marked 'running' — used by daemon crash-recovery on startup. */
42
+ findRunning(db = getDb()) {
43
+ return db.prepare("SELECT * FROM tabs WHERE status = 'running'").all().map(rowToTab);
44
+ },
45
+ /** Most-recently-active tab — used by MCP to surface "current" context. */
46
+ mostRecent(db = getDb()) {
47
+ const row = db.prepare('SELECT * FROM tabs ORDER BY last_activity_at DESC LIMIT 1').get();
48
+ return row ? rowToTab(row) : undefined;
49
+ },
50
+ setStatus(name, status, db = getDb()) {
51
+ db.prepare('UPDATE tabs SET status = ?, last_activity_at = ?, pid = NULL WHERE name = ?').run(status, new Date().toISOString(), name);
52
+ },
53
+ setIdleById(id, db = getDb()) {
54
+ db.prepare("UPDATE tabs SET status = 'idle', pid = NULL WHERE id = ?").run(id);
55
+ },
56
+ /** Used by MCP close-tab to nudge daemon recovery loop. */
57
+ markRunningAsStopped(name, db = getDb()) {
58
+ db.prepare("UPDATE tabs SET status = 'stopped', pid = NULL WHERE name = ? AND status = 'running'").run(name);
59
+ },
60
+ setSystemPrompt(name, systemPrompt, db = getDb()) {
61
+ const result = db
62
+ .prepare('UPDATE tabs SET system_prompt = ? WHERE name = ?')
63
+ .run(systemPrompt, name);
64
+ return result.changes > 0;
65
+ },
66
+ /** Delete a tab and all of its messages atomically. Returns true if it existed. */
67
+ deleteWithMessages(name, db = getDb()) {
68
+ const id = this.getIdByName(name, db);
69
+ if (!id)
70
+ return false;
71
+ const tx = db.transaction(() => {
72
+ db.prepare('DELETE FROM messages WHERE tab_id = ?').run(id);
73
+ db.prepare('DELETE FROM tabs WHERE id = ?').run(id);
74
+ });
75
+ tx();
76
+ return true;
77
+ },
78
+ };
@@ -6,6 +6,7 @@ export declare class TaskScheduler {
6
6
  private onNotify;
7
7
  private nextRunAt;
8
8
  private running;
9
+ private failureCounts;
9
10
  private stopping;
10
11
  private store;
11
12
  constructor(tabManager: TabManager, onNotify: NotifyCallback | null);
@@ -24,9 +25,21 @@ export declare class TaskScheduler {
24
25
  /** Compute next run time in ms epoch, given a "from" anchor. Returns null if invalid. */
25
26
  private computeNextRun;
26
27
  private fireJob;
28
+ /**
29
+ * Track consecutive failures per task. After MAX_CONSECUTIVE_FAILURES the
30
+ * task is auto-disabled in the DB and a single "auto-disabled" notification
31
+ * is sent. Counter resets on the next successful fire.
32
+ */
33
+ private recordFailure;
27
34
  private handleSystemEvent;
28
35
  }
29
36
  /** Convert human interval (30m, 2h, 1d, 1h30m, 2w) to milliseconds */
30
37
  export declare function intervalToMs(interval: string): number | null;
31
38
  /** Convert human interval (30m, 2h, 1d, 1h30m, 2w) to cron expression */
32
39
  export declare function intervalToCron(interval: string): string | null;
40
+ /**
41
+ * Validate a schedule string for a given scheduleType. Returns an error string
42
+ * on invalid input or null when valid. Used by MCP + dashboard before insert
43
+ * so misconfigured tasks fail loud instead of silently never firing.
44
+ */
45
+ export declare function validateSchedule(scheduleType: string, schedule: string): string | null;
@@ -7,6 +7,12 @@ import { TaskStore } from './store.js';
7
7
  import { getCronReloadSignalPath, getLogsDir } from '../util/paths.js';
8
8
  import { logger } from '../util/logger.js';
9
9
  export const execAsync = promisify(exec);
10
+ /**
11
+ * Disable a task after this many consecutive failures so a misconfigured
12
+ * cron doesn't fire (and notify) every interval indefinitely. Reset on
13
+ * success.
14
+ */
15
+ const MAX_CONSECUTIVE_FAILURES = 5;
10
16
  export class TaskScheduler {
11
17
  tabManager;
12
18
  onNotify;
@@ -14,6 +20,8 @@ export class TaskScheduler {
14
20
  nextRunAt = new Map();
15
21
  // taskIds with an in-flight fireJob
16
22
  running = new Set();
23
+ // taskId -> consecutive failure count, used for auto-disable
24
+ failureCounts = new Map();
17
25
  stopping = false;
18
26
  store = new TaskStore();
19
27
  constructor(tabManager, onNotify) {
@@ -69,7 +77,9 @@ export class TaskScheduler {
69
77
  try {
70
78
  fs.unlinkSync(signalPath);
71
79
  }
72
- catch { /* race condition, ok */ }
80
+ catch {
81
+ /* race condition, ok */
82
+ }
73
83
  logger.info('Tasks: reload signal detected, reloading schedules');
74
84
  this.loadAndSchedule();
75
85
  }
@@ -124,11 +134,15 @@ export class TaskScheduler {
124
134
  try {
125
135
  switch (job.scheduleType) {
126
136
  case 'cron':
127
- return CronExpressionParser.parse(job.schedule, { currentDate: new Date(fromMs) }).next().getTime();
137
+ return CronExpressionParser.parse(job.schedule, { currentDate: new Date(fromMs) })
138
+ .next()
139
+ .getTime();
128
140
  case 'every': {
129
141
  const expr = intervalToCron(job.schedule);
130
142
  if (expr) {
131
- return CronExpressionParser.parse(expr, { currentDate: new Date(fromMs) }).next().getTime();
143
+ return CronExpressionParser.parse(expr, { currentDate: new Date(fromMs) })
144
+ .next()
145
+ .getTime();
132
146
  }
133
147
  const ms = intervalToMs(job.schedule);
134
148
  return ms ? fromMs + ms : null;
@@ -169,6 +183,12 @@ export class TaskScheduler {
169
183
  const status = result.error ? 'ERROR' : 'SUCCESS';
170
184
  // Log result (status reflects subprocess exit / is_error, not just completion)
171
185
  await fs.promises.appendFile(logFile, `[${new Date().toISOString()}] ${status}: ${firstLine}\n`);
186
+ if (result.error) {
187
+ this.recordFailure(job);
188
+ }
189
+ else {
190
+ this.failureCounts.delete(job.id);
191
+ }
172
192
  // Notify (separate try/catch -- notification failure shouldn't be reported as job failure)
173
193
  try {
174
194
  if (this.onNotify) {
@@ -188,10 +208,30 @@ export class TaskScheduler {
188
208
  const errMsg = err instanceof Error ? err.message : String(err);
189
209
  logger.error(`Task "${job.name}" failed:`, err);
190
210
  await fs.promises.appendFile(logFile, `[${new Date().toISOString()}] ERROR: ${errMsg}\n`);
211
+ this.recordFailure(job);
191
212
  try {
192
213
  await this.onNotify?.(`[${job.name}] Failed -- ${errMsg}`);
193
214
  }
194
- catch { /* notification best-effort */ }
215
+ catch {
216
+ /* notification best-effort */
217
+ }
218
+ }
219
+ }
220
+ /**
221
+ * Track consecutive failures per task. After MAX_CONSECUTIVE_FAILURES the
222
+ * task is auto-disabled in the DB and a single "auto-disabled" notification
223
+ * is sent. Counter resets on the next successful fire.
224
+ */
225
+ recordFailure(job) {
226
+ const count = (this.failureCounts.get(job.id) ?? 0) + 1;
227
+ this.failureCounts.set(job.id, count);
228
+ if (count >= MAX_CONSECUTIVE_FAILURES) {
229
+ logger.warn(`Task "${job.name}" hit ${count} consecutive failures — auto-disabling`);
230
+ this.store.update(job.id, { enabled: false });
231
+ this.nextRunAt.delete(job.id);
232
+ this.failureCounts.delete(job.id);
233
+ // Notify once. Subsequent enable + re-failure will trigger again.
234
+ this.onNotify?.(`Task "${job.name}" auto-disabled after ${count} consecutive failures. Re-enable from the dashboard or via beecork_task_update.`).catch(() => { });
195
235
  }
196
236
  }
197
237
  async handleSystemEvent(job) {
@@ -207,28 +247,33 @@ export class TaskScheduler {
207
247
  }
208
248
  }
209
249
  }
250
+ /** Parse a "1w2d3h45m"-style interval into its parts. Returns null on invalid input. */
251
+ function parseInterval(interval) {
252
+ const match = interval.match(/^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/);
253
+ if (!match || match.slice(1).every((g) => g === undefined))
254
+ return null;
255
+ return {
256
+ weeks: parseInt(match[1] || '0', 10),
257
+ days: parseInt(match[2] || '0', 10),
258
+ hours: parseInt(match[3] || '0', 10),
259
+ mins: parseInt(match[4] || '0', 10),
260
+ };
261
+ }
210
262
  /** Convert human interval (30m, 2h, 1d, 1h30m, 2w) to milliseconds */
211
263
  export function intervalToMs(interval) {
212
- const match = interval.match(/^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/);
213
- if (!match || match.slice(1).every(g => g === undefined))
264
+ const parts = parseInterval(interval);
265
+ if (!parts)
214
266
  return null;
215
- const weeks = parseInt(match[1] || '0', 10);
216
- const days = parseInt(match[2] || '0', 10);
217
- const hours = parseInt(match[3] || '0', 10);
218
- const mins = parseInt(match[4] || '0', 10);
219
- const totalMs = ((weeks * 7 * 24 * 60) + (days * 24 * 60) + (hours * 60) + mins) * 60 * 1000;
267
+ const { weeks, days, hours, mins } = parts;
268
+ const totalMs = (weeks * 7 * 24 * 60 + days * 24 * 60 + hours * 60 + mins) * 60 * 1000;
220
269
  return totalMs > 0 ? totalMs : null;
221
270
  }
222
271
  /** Convert human interval (30m, 2h, 1d, 1h30m, 2w) to cron expression */
223
272
  export function intervalToCron(interval) {
224
- // Try combined format: 1h30m, 2h, 30m, 1d, 2w
225
- const match = interval.match(/^(?:(\d+)w)?(?:(\d+)d)?(?:(\d+)h)?(?:(\d+)m)?$/);
226
- if (!match || match.slice(1).every(g => g === undefined))
273
+ const parts = parseInterval(interval);
274
+ if (!parts)
227
275
  return null;
228
- const weeks = parseInt(match[1] || '0', 10);
229
- const days = parseInt(match[2] || '0', 10);
230
- const hours = parseInt(match[3] || '0', 10);
231
- const mins = parseInt(match[4] || '0', 10);
276
+ const { weeks, days, hours, mins } = parts;
232
277
  // Convert to total minutes for simple intervals
233
278
  const totalMins = weeks * 7 * 24 * 60 + days * 24 * 60 + hours * 60 + mins;
234
279
  if (totalMins <= 0)
@@ -248,3 +293,37 @@ export function intervalToCron(interval) {
248
293
  // Combined or large intervals -- return null, handled by intervalToMs fallback
249
294
  return null;
250
295
  }
296
+ /**
297
+ * Validate a schedule string for a given scheduleType. Returns an error string
298
+ * on invalid input or null when valid. Used by MCP + dashboard before insert
299
+ * so misconfigured tasks fail loud instead of silently never firing.
300
+ */
301
+ export function validateSchedule(scheduleType, schedule) {
302
+ if (!schedule)
303
+ return 'schedule is required';
304
+ switch (scheduleType) {
305
+ case 'at': {
306
+ const ts = Date.parse(schedule);
307
+ if (Number.isNaN(ts))
308
+ return `"${schedule}" is not a valid ISO datetime`;
309
+ return null;
310
+ }
311
+ case 'every': {
312
+ if (intervalToMs(schedule) === null) {
313
+ return `"${schedule}" is not a valid interval. Use formats like "30m", "2h", "1d", "1h30m".`;
314
+ }
315
+ return null;
316
+ }
317
+ case 'cron': {
318
+ try {
319
+ CronExpressionParser.parse(schedule);
320
+ return null;
321
+ }
322
+ catch (err) {
323
+ return `"${schedule}" is not a valid cron expression: ${err instanceof Error ? err.message : String(err)}`;
324
+ }
325
+ }
326
+ default:
327
+ return `unknown scheduleType "${scheduleType}"`;
328
+ }
329
+ }
@@ -4,21 +4,33 @@ 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();
21
- return db.prepare('SELECT * FROM tasks WHERE user_id = ? ORDER BY created_at').all('local').map(rowToTask);
33
+ return db.prepare('SELECT * FROM tasks ORDER BY created_at').all().map(rowToTask);
22
34
  }
23
35
  get(id) {
24
36
  const db = getDb();
@@ -27,8 +39,8 @@ export class TaskStore {
27
39
  }
28
40
  add(job) {
29
41
  const db = getDb();
30
- db.prepare(`INSERT INTO tasks (id, name, schedule_type, schedule, tab_name, message, payload_type, enabled, user_id, created_at, last_run_at, next_run_at)
31
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(job.id, job.name, job.scheduleType, job.schedule, job.tabName, job.message, job.payloadType || 'agentTurn', job.enabled ? 1 : 0, 'local', job.createdAt, job.lastRunAt, job.nextRunAt);
42
+ db.prepare(`INSERT INTO tasks (id, name, schedule_type, schedule, tab_name, message, payload_type, enabled, created_at, last_run_at, next_run_at)
43
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(job.id, job.name, job.scheduleType, job.schedule, job.tabName, job.message, job.payloadType || 'agentTurn', job.enabled ? 1 : 0, job.createdAt, job.lastRunAt, job.nextRunAt);
32
44
  }
33
45
  update(id, updates) {
34
46
  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 {
@@ -71,11 +85,11 @@ export class TaskStore {
71
85
  const jobs = data.jobs || [];
72
86
  if (jobs.length === 0)
73
87
  return;
74
- const insert = db.prepare(`INSERT OR IGNORE INTO tasks (id, name, schedule_type, schedule, tab_name, message, enabled, user_id, created_at, last_run_at, next_run_at)
75
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
88
+ const insert = db.prepare(`INSERT OR IGNORE INTO tasks (id, name, schedule_type, schedule, tab_name, message, enabled, created_at, last_run_at, next_run_at)
89
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`);
76
90
  const tx = db.transaction(() => {
77
91
  for (const j of jobs) {
78
- insert.run(j.id, j.name, j.scheduleType, j.schedule, j.tabName || 'default', j.message, j.enabled ? 1 : 0, 'local', j.createdAt, j.lastRunAt, j.nextRunAt);
92
+ insert.run(j.id, j.name, j.scheduleType, j.schedule, j.tabName || 'default', j.message, j.enabled ? 1 : 0, j.createdAt, j.lastRunAt, j.nextRunAt);
79
93
  }
80
94
  });
81
95
  tx();
@@ -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) {
@@ -12,8 +18,12 @@ export function getTimeline(options) {
12
18
  const conditions = [];
13
19
  const params = [];
14
20
  if (options?.date) {
15
- conditions.push('date(created_at) = ?');
16
- params.push(options.date);
21
+ // Sargable range so idx_activity_log_created can serve the query.
22
+ // `date(created_at) = ?` is non-sargable and forces a full scan.
23
+ const dayStart = `${options.date} 00:00:00`;
24
+ const dayEnd = `${options.date} 23:59:59.999`;
25
+ conditions.push('created_at >= ? AND created_at <= ?');
26
+ params.push(dayStart, dayEnd);
17
27
  }
18
28
  if (options?.tabName) {
19
29
  conditions.push('tab_name = ?');
package/dist/types.d.ts CHANGED
@@ -8,10 +8,11 @@ export interface ClaudeCodeConfig {
8
8
  defaultFlags: string[];
9
9
  maxBudgetUsd?: number;
10
10
  computerUse?: boolean;
11
+ /** Hard timeout per subprocess turn. Default 30 min. Set to 0 to disable. */
12
+ maxRuntimeMs?: number;
11
13
  }
12
14
  export interface MemoryConfig {
13
15
  dbPath: string;
14
- maxLongTermEntries: number;
15
16
  }
16
17
  export interface TabConfig {
17
18
  workingDir: string;
@@ -25,6 +26,8 @@ export interface WhatsAppConfig {
25
26
  mode: 'baileys';
26
27
  sessionPath: string;
27
28
  allowedNumbers: string[];
29
+ /** Optional admin phone number. Defaults to allowedNumbers[0]. */
30
+ adminNumber?: string;
28
31
  }
29
32
  export interface VoiceConfig {
30
33
  sttProvider: 'whisper-api' | 'none';
@@ -37,12 +40,29 @@ export interface VoiceConfig {
37
40
  export interface DiscordConfig {
38
41
  token: string;
39
42
  allowedUserIds?: string[];
40
- }
43
+ /** Optional admin user ID. Defaults to allowedUserIds[0]. */
44
+ adminUserId?: string;
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
+ */
41
59
  export interface WebhookConfig {
42
60
  enabled: boolean;
43
61
  port: number;
44
62
  authToken?: string;
45
63
  hmacSecret?: string;
64
+ /** Set to true to skip the fail-secure auth check at start. NOT recommended on shared hosts. */
65
+ allowUnauthLocalhost?: boolean;
46
66
  }
47
67
  export interface GroupConfig {
48
68
  activationMode: 'mention' | 'reply' | 'keyword' | 'always';
@@ -50,21 +70,39 @@ export interface GroupConfig {
50
70
  tabPerGroup: boolean;
51
71
  keywords?: string[];
52
72
  }
53
- export interface NotificationConfig {
54
- type: 'pushover' | 'ntfy' | 'webhook';
55
- userKey?: string;
56
- appToken?: string;
57
- 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;
58
86
  server?: string;
59
- url?: string;
87
+ } | {
88
+ type: 'webhook';
89
+ url: string;
60
90
  headers?: Record<string, string>;
61
- }
91
+ };
62
92
  export interface MediaGeneratorConfig {
63
93
  provider: string;
64
94
  apiKey?: string;
65
95
  model?: string;
66
96
  style?: string;
67
97
  }
98
+ /** A media file attached to a message. Shared by channels + util/text + media. */
99
+ export interface MediaAttachment {
100
+ type: 'image' | 'audio' | 'video' | 'document' | 'voice';
101
+ mimeType: string;
102
+ filePath: string;
103
+ fileName?: string;
104
+ caption?: string;
105
+ }
68
106
  export interface BeecorkConfig {
69
107
  telegram: TelegramConfig;
70
108
  whatsapp?: WhatsAppConfig;
@@ -81,6 +119,8 @@ export interface BeecorkConfig {
81
119
  notifications?: NotificationConfig[];
82
120
  mediaGenerators?: MediaGeneratorConfig[];
83
121
  communityChannels?: string[];
122
+ /** Capability packs enabled via `beecork capabilities enable`. Owned by src/capabilities. */
123
+ capabilities?: import('./capabilities/types.js').EnabledCapability[];
84
124
  deployment: 'local' | 'vps';
85
125
  }
86
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;