beecork 1.7.0 → 2.0.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 (223) hide show
  1. package/README.md +52 -111
  2. package/dist/index.js +1394 -581
  3. package/package.json +26 -54
  4. package/dist/capabilities/index.d.ts +0 -3
  5. package/dist/capabilities/index.js +0 -2
  6. package/dist/capabilities/manager.d.ts +0 -13
  7. package/dist/capabilities/manager.js +0 -115
  8. package/dist/capabilities/packs.d.ts +0 -2
  9. package/dist/capabilities/packs.js +0 -54
  10. package/dist/capabilities/types.d.ts +0 -21
  11. package/dist/capabilities/types.js +0 -1
  12. package/dist/channels/admin.d.ts +0 -10
  13. package/dist/channels/admin.js +0 -20
  14. package/dist/channels/command-handler.d.ts +0 -19
  15. package/dist/channels/command-handler.js +0 -235
  16. package/dist/channels/discord.d.ts +0 -17
  17. package/dist/channels/discord.js +0 -266
  18. package/dist/channels/index.d.ts +0 -7
  19. package/dist/channels/index.js +0 -6
  20. package/dist/channels/loader.d.ts +0 -7
  21. package/dist/channels/loader.js +0 -87
  22. package/dist/channels/pipeline.d.ts +0 -50
  23. package/dist/channels/pipeline.js +0 -92
  24. package/dist/channels/registry.d.ts +0 -36
  25. package/dist/channels/registry.js +0 -80
  26. package/dist/channels/send-helpers.d.ts +0 -19
  27. package/dist/channels/send-helpers.js +0 -21
  28. package/dist/channels/telegram.d.ts +0 -47
  29. package/dist/channels/telegram.js +0 -649
  30. package/dist/channels/types.d.ts +0 -41
  31. package/dist/channels/types.js +0 -1
  32. package/dist/channels/voice-state.d.ts +0 -29
  33. package/dist/channels/voice-state.js +0 -45
  34. package/dist/channels/webhook.d.ts +0 -16
  35. package/dist/channels/webhook.js +0 -222
  36. package/dist/channels/whatsapp.d.ts +0 -28
  37. package/dist/channels/whatsapp.js +0 -323
  38. package/dist/cli/capabilities.d.ts +0 -3
  39. package/dist/cli/capabilities.js +0 -72
  40. package/dist/cli/channel.d.ts +0 -3
  41. package/dist/cli/channel.js +0 -202
  42. package/dist/cli/commands.d.ts +0 -15
  43. package/dist/cli/commands.js +0 -279
  44. package/dist/cli/doctor.d.ts +0 -1
  45. package/dist/cli/doctor.js +0 -208
  46. package/dist/cli/handoff.d.ts +0 -8
  47. package/dist/cli/handoff.js +0 -33
  48. package/dist/cli/helpers.d.ts +0 -2
  49. package/dist/cli/helpers.js +0 -21
  50. package/dist/cli/mcp.d.ts +0 -3
  51. package/dist/cli/mcp.js +0 -47
  52. package/dist/cli/media.d.ts +0 -2
  53. package/dist/cli/media.js +0 -125
  54. package/dist/cli/setup.d.ts +0 -1
  55. package/dist/cli/setup.js +0 -239
  56. package/dist/cli/store.d.ts +0 -3
  57. package/dist/cli/store.js +0 -108
  58. package/dist/config.d.ts +0 -11
  59. package/dist/config.js +0 -115
  60. package/dist/daemon.d.ts +0 -1
  61. package/dist/daemon.js +0 -345
  62. package/dist/dashboard/html.d.ts +0 -1
  63. package/dist/dashboard/html.js +0 -849
  64. package/dist/dashboard/routes.d.ts +0 -17
  65. package/dist/dashboard/routes.js +0 -623
  66. package/dist/dashboard/server.d.ts +0 -1
  67. package/dist/dashboard/server.js +0 -104
  68. package/dist/db/connection.d.ts +0 -29
  69. package/dist/db/connection.js +0 -37
  70. package/dist/db/index.d.ts +0 -14
  71. package/dist/db/index.js +0 -127
  72. package/dist/db/migrations.d.ts +0 -2
  73. package/dist/db/migrations.js +0 -384
  74. package/dist/delegation/manager.d.ts +0 -18
  75. package/dist/delegation/manager.js +0 -89
  76. package/dist/index.d.ts +0 -2
  77. package/dist/knowledge/index.d.ts +0 -2
  78. package/dist/knowledge/index.js +0 -1
  79. package/dist/knowledge/manager.d.ts +0 -17
  80. package/dist/knowledge/manager.js +0 -160
  81. package/dist/knowledge/types.d.ts +0 -7
  82. package/dist/knowledge/types.js +0 -1
  83. package/dist/mcp/handlers.d.ts +0 -37
  84. package/dist/mcp/handlers.js +0 -520
  85. package/dist/mcp/server.d.ts +0 -2
  86. package/dist/mcp/server.js +0 -86
  87. package/dist/mcp/tool-definitions.d.ts +0 -1225
  88. package/dist/mcp/tool-definitions.js +0 -412
  89. package/dist/mcp/validate.d.ts +0 -23
  90. package/dist/mcp/validate.js +0 -65
  91. package/dist/media/factory.d.ts +0 -6
  92. package/dist/media/factory.js +0 -36
  93. package/dist/media/generators/dall-e.d.ts +0 -10
  94. package/dist/media/generators/dall-e.js +0 -41
  95. package/dist/media/generators/elevenlabs-music.d.ts +0 -9
  96. package/dist/media/generators/elevenlabs-music.js +0 -34
  97. package/dist/media/generators/elevenlabs-sfx.d.ts +0 -9
  98. package/dist/media/generators/elevenlabs-sfx.js +0 -33
  99. package/dist/media/generators/kling.d.ts +0 -9
  100. package/dist/media/generators/kling.js +0 -47
  101. package/dist/media/generators/lyria.d.ts +0 -10
  102. package/dist/media/generators/lyria.js +0 -41
  103. package/dist/media/generators/nano-banana.d.ts +0 -10
  104. package/dist/media/generators/nano-banana.js +0 -45
  105. package/dist/media/generators/poll-util.d.ts +0 -12
  106. package/dist/media/generators/poll-util.js +0 -22
  107. package/dist/media/generators/recraft.d.ts +0 -9
  108. package/dist/media/generators/recraft.js +0 -52
  109. package/dist/media/generators/runway.d.ts +0 -9
  110. package/dist/media/generators/runway.js +0 -49
  111. package/dist/media/generators/stable-diffusion.d.ts +0 -9
  112. package/dist/media/generators/stable-diffusion.js +0 -35
  113. package/dist/media/generators/veo.d.ts +0 -9
  114. package/dist/media/generators/veo.js +0 -32
  115. package/dist/media/index.d.ts +0 -6
  116. package/dist/media/index.js +0 -8
  117. package/dist/media/store.d.ts +0 -17
  118. package/dist/media/store.js +0 -69
  119. package/dist/media/types.d.ts +0 -43
  120. package/dist/media/types.js +0 -1
  121. package/dist/notifications/index.d.ts +0 -4
  122. package/dist/notifications/index.js +0 -21
  123. package/dist/notifications/ntfy.d.ts +0 -9
  124. package/dist/notifications/ntfy.js +0 -25
  125. package/dist/notifications/pushover.d.ts +0 -9
  126. package/dist/notifications/pushover.js +0 -26
  127. package/dist/notifications/types.d.ts +0 -5
  128. package/dist/notifications/types.js +0 -1
  129. package/dist/notifications/webhook-provider.d.ts +0 -9
  130. package/dist/notifications/webhook-provider.js +0 -24
  131. package/dist/observability/analytics.d.ts +0 -30
  132. package/dist/observability/analytics.js +0 -118
  133. package/dist/projects/index.d.ts +0 -4
  134. package/dist/projects/index.js +0 -2
  135. package/dist/projects/manager.d.ts +0 -13
  136. package/dist/projects/manager.js +0 -166
  137. package/dist/projects/router.d.ts +0 -17
  138. package/dist/projects/router.js +0 -246
  139. package/dist/projects/types.d.ts +0 -19
  140. package/dist/projects/types.js +0 -1
  141. package/dist/service/install.d.ts +0 -4
  142. package/dist/service/install.js +0 -146
  143. package/dist/service/templates.d.ts +0 -4
  144. package/dist/service/templates.js +0 -52
  145. package/dist/service/windows.d.ts +0 -8
  146. package/dist/service/windows.js +0 -59
  147. package/dist/session/budget-guard.d.ts +0 -20
  148. package/dist/session/budget-guard.js +0 -31
  149. package/dist/session/circuit-breaker.d.ts +0 -19
  150. package/dist/session/circuit-breaker.js +0 -91
  151. package/dist/session/context-compactor.d.ts +0 -32
  152. package/dist/session/context-compactor.js +0 -45
  153. package/dist/session/context-monitor.d.ts +0 -15
  154. package/dist/session/context-monitor.js +0 -44
  155. package/dist/session/handoff.d.ts +0 -21
  156. package/dist/session/handoff.js +0 -50
  157. package/dist/session/manager.d.ts +0 -60
  158. package/dist/session/manager.js +0 -379
  159. package/dist/session/memory-store.d.ts +0 -29
  160. package/dist/session/memory-store.js +0 -45
  161. package/dist/session/message-queue.d.ts +0 -28
  162. package/dist/session/message-queue.js +0 -52
  163. package/dist/session/pending-dispatcher.d.ts +0 -31
  164. package/dist/session/pending-dispatcher.js +0 -120
  165. package/dist/session/pending-store.d.ts +0 -60
  166. package/dist/session/pending-store.js +0 -118
  167. package/dist/session/stale-session.d.ts +0 -31
  168. package/dist/session/stale-session.js +0 -45
  169. package/dist/session/subprocess.d.ts +0 -25
  170. package/dist/session/subprocess.js +0 -189
  171. package/dist/session/tab-store.d.ts +0 -28
  172. package/dist/session/tab-store.js +0 -78
  173. package/dist/tasks/scheduler.d.ts +0 -45
  174. package/dist/tasks/scheduler.js +0 -329
  175. package/dist/tasks/store.d.ts +0 -11
  176. package/dist/tasks/store.js +0 -103
  177. package/dist/timeline/index.d.ts +0 -3
  178. package/dist/timeline/index.js +0 -2
  179. package/dist/timeline/logger.d.ts +0 -8
  180. package/dist/timeline/logger.js +0 -11
  181. package/dist/timeline/query.d.ts +0 -11
  182. package/dist/timeline/query.js +0 -67
  183. package/dist/timeline/types.d.ts +0 -12
  184. package/dist/timeline/types.js +0 -1
  185. package/dist/types.d.ts +0 -216
  186. package/dist/types.js +0 -3
  187. package/dist/util/auto-heal.d.ts +0 -43
  188. package/dist/util/auto-heal.js +0 -107
  189. package/dist/util/install-info.d.ts +0 -37
  190. package/dist/util/install-info.js +0 -80
  191. package/dist/util/logger.d.ts +0 -18
  192. package/dist/util/logger.js +0 -118
  193. package/dist/util/paths.d.ts +0 -13
  194. package/dist/util/paths.js +0 -55
  195. package/dist/util/platform.d.ts +0 -2
  196. package/dist/util/platform.js +0 -11
  197. package/dist/util/progress.d.ts +0 -34
  198. package/dist/util/progress.js +0 -133
  199. package/dist/util/rate-limiter.d.ts +0 -14
  200. package/dist/util/rate-limiter.js +0 -50
  201. package/dist/util/retry.d.ts +0 -2
  202. package/dist/util/retry.js +0 -18
  203. package/dist/util/text.d.ts +0 -32
  204. package/dist/util/text.js +0 -101
  205. package/dist/version.d.ts +0 -1
  206. package/dist/version.js +0 -4
  207. package/dist/voice/index.d.ts +0 -17
  208. package/dist/voice/index.js +0 -42
  209. package/dist/voice/stt.d.ts +0 -16
  210. package/dist/voice/stt.js +0 -57
  211. package/dist/voice/tts.d.ts +0 -23
  212. package/dist/voice/tts.js +0 -84
  213. package/dist/watchers/evaluator.d.ts +0 -5
  214. package/dist/watchers/evaluator.js +0 -45
  215. package/dist/watchers/index.d.ts +0 -4
  216. package/dist/watchers/index.js +0 -3
  217. package/dist/watchers/scheduler.d.ts +0 -20
  218. package/dist/watchers/scheduler.js +0 -183
  219. package/dist/watchers/store.d.ts +0 -10
  220. package/dist/watchers/store.js +0 -56
  221. package/dist/watchers/types.d.ts +0 -15
  222. package/dist/watchers/types.js +0 -1
  223. package/scripts/postinstall.mjs +0 -50
@@ -1,120 +0,0 @@
1
- /**
2
- * Polls `pending_messages` and dispatches each row by type.
3
- *
4
- * Replaces the inline `processPendingMessages` block that previously lived in
5
- * TabManager. Two functional changes from that code:
6
- *
7
- * 1. Claim-before-dispatch (status='processing' set atomically before send).
8
- * The original poll-every-5s code re-picked the same row on every tick
9
- * while its dispatch was in flight — long Claude runs could re-fire the
10
- * same prompt many times.
11
- *
12
- * 2. Branch on type. The original consumer only recognized 'notification' and
13
- * fell through to "send as plain user prompt" for everything else, which
14
- * meant `type='media'` rows were handed to Claude as raw JSON and never
15
- * actually delivered to channels.
16
- */
17
- import { getDb } from '../db/index.js';
18
- import { logger } from '../util/logger.js';
19
- import { PendingMessageStore } from './pending-store.js';
20
- const POLL_LIMIT = 50;
21
- const CLEANUP_EVERY_N_TICKS = 100;
22
- export class PendingMessageDispatcher {
23
- tabManager;
24
- channels;
25
- onNotify;
26
- pollCount = 0;
27
- constructor(tabManager, channels, onNotify) {
28
- this.tabManager = tabManager;
29
- this.channels = channels;
30
- this.onNotify = onNotify;
31
- }
32
- /** Reset any rows stuck in 'processing' from a previous daemon run. */
33
- recoverOnStart() {
34
- PendingMessageStore.recoverProcessing();
35
- }
36
- /** Called on every daemon poll tick. */
37
- tick() {
38
- const db = getDb();
39
- this.pollCount++;
40
- if (this.pollCount % CLEANUP_EVERY_N_TICKS === 0) {
41
- db.prepare("DELETE FROM pending_messages WHERE (status = 'done' OR status = 'failed') AND created_at < datetime('now', '-1 day')").run();
42
- }
43
- const claimed = PendingMessageStore.claimBatch(POLL_LIMIT, db);
44
- for (const row of claimed) {
45
- // Fire-and-forget per row — the row is already marked 'processing' so
46
- // the next tick won't double-dispatch. The .finally() handles the
47
- // terminal transition.
48
- void this.dispatch(row);
49
- }
50
- }
51
- async dispatch(row) {
52
- try {
53
- switch (row.type) {
54
- case 'notification':
55
- await this.onNotify?.(row.message);
56
- break;
57
- case 'media': {
58
- const payload = PendingMessageStore.parseMediaPayload(row.message);
59
- if (!payload) {
60
- logger.warn(`Pending media row ${row.id} has malformed payload, skipping`);
61
- break;
62
- }
63
- const attachment = inferAttachmentType(payload.filePath, payload.caption);
64
- await this.channels.broadcastMedia(attachment);
65
- break;
66
- }
67
- case 'user':
68
- case 'delegation':
69
- case 'delegation_result':
70
- case 'replay': {
71
- if (!row.tabName) {
72
- logger.warn(`Pending row ${row.id} (type=${row.type}) has no tab_name, skipping`);
73
- break;
74
- }
75
- await this.tabManager.sendMessage(row.tabName, row.message);
76
- break;
77
- }
78
- default: {
79
- // Unknown discriminant — log and bail rather than fall through to
80
- // "send as a plain prompt" (which is how H5 ended up shipping).
81
- logger.warn(`Pending row ${row.id} has unknown type "${row.type}", marking failed`);
82
- PendingMessageStore.markFailed(row.id);
83
- return;
84
- }
85
- }
86
- PendingMessageStore.markDone(row.id);
87
- }
88
- catch (err) {
89
- logger.error(`Failed to process pending message ${row.id} (type=${row.type}):`, err);
90
- await this.onNotify?.(`Pending message ${row.type === 'notification' ? '' : `for tab "${row.tabName}"`} failed: ${err instanceof Error ? err.message : String(err)}`).catch(() => { });
91
- PendingMessageStore.markFailed(row.id);
92
- }
93
- }
94
- }
95
- /**
96
- * Heuristic: derive a MediaAttachment from a file path so the channel's
97
- * broadcastMedia knows whether to send as voice / image / video / document.
98
- */
99
- function inferAttachmentType(filePath, caption) {
100
- const ext = filePath.split('.').pop()?.toLowerCase() ?? '';
101
- let type = 'document';
102
- let mimeType = 'application/octet-stream';
103
- if (['png', 'jpg', 'jpeg', 'gif', 'webp'].includes(ext)) {
104
- type = 'image';
105
- mimeType = `image/${ext === 'jpg' ? 'jpeg' : ext}`;
106
- }
107
- else if (['mp4', 'mov', 'webm', 'mkv'].includes(ext)) {
108
- type = 'video';
109
- mimeType = `video/${ext === 'mov' ? 'quicktime' : ext}`;
110
- }
111
- else if (['ogg', 'opus', 'oga'].includes(ext)) {
112
- type = 'voice';
113
- mimeType = 'audio/ogg';
114
- }
115
- else if (['mp3', 'm4a', 'wav', 'flac', 'aac'].includes(ext)) {
116
- type = 'audio';
117
- mimeType = `audio/${ext}`;
118
- }
119
- return { type, mimeType, filePath, caption };
120
- }
@@ -1,60 +0,0 @@
1
- /**
2
- * Inter-process IPC store for the `pending_messages` table.
3
- *
4
- * Beecork uses this table as a shared queue between writers (MCP handlers,
5
- * dashboard routes, watchers, delegations) and the daemon's poll loop. Before
6
- * this module the writes were inline `INSERT INTO pending_messages` calls
7
- * scattered across nine sites; the consumer in TabManager only branched on
8
- * type='notification' which meant every other type fell through to "send as
9
- * a plain user prompt" — including `type='media'` rows whose JSON envelope
10
- * was being handed to Claude as text instead of being rendered.
11
- *
12
- * This store owns:
13
- * - the typed envelope (`PendingMessageType` union, no more magic strings)
14
- * - 3-state status (`pending` → `processing` → `done|failed`) so the 5s
15
- * poll loop doesn't re-dispatch long-running rows
16
- * - the `_notify` sentinel removal (notifications now use type alone,
17
- * tab_name is nullable)
18
- * - claim/recover semantics so a daemon crash mid-dispatch resets in-flight
19
- * rows back to pending after a 30-minute timeout
20
- */
21
- import type Database from 'better-sqlite3';
22
- export type PendingMessageType = 'user' | 'notification' | 'media' | 'delegation' | 'delegation_result' | 'replay';
23
- export interface PendingRow {
24
- id: number;
25
- tabName: string | null;
26
- message: string;
27
- type: PendingMessageType;
28
- status: 'pending' | 'processing' | 'done' | 'failed';
29
- createdAt: string;
30
- }
31
- interface MediaPayload {
32
- type: 'media';
33
- filePath: string;
34
- caption?: string;
35
- }
36
- export declare const PendingMessageStore: {
37
- enqueueUser(tabName: string, message: string, db?: Database.Database): void;
38
- enqueueNotification(message: string, db?: Database.Database): void;
39
- enqueueMedia(tabName: string, payload: MediaPayload, db?: Database.Database): void;
40
- enqueueDelegation(tabName: string, message: string, db?: Database.Database): void;
41
- enqueueDelegationResult(tabName: string, message: string, db?: Database.Database): void;
42
- enqueueReplay(tabName: string, message: string, db?: Database.Database): void;
43
- /**
44
- * Claim up to `limit` pending rows atomically. The rows are flipped to
45
- * status='processing' inside a transaction so a second poll tick can't pick
46
- * them up. Returns the claimed rows.
47
- */
48
- claimBatch(limit: number, db?: Database.Database): PendingRow[];
49
- markDone(id: number, db?: Database.Database): void;
50
- markFailed(id: number, db?: Database.Database): void;
51
- /**
52
- * Reset stuck `processing` rows back to `pending`. Run at daemon startup so
53
- * a crash mid-dispatch doesn't leave rows permanently in-flight. The 30min
54
- * window is well beyond the 30min subprocess runtime cap.
55
- */
56
- recoverProcessing(db?: Database.Database): number;
57
- /** Parse a media row's message JSON. Returns null on malformed JSON. */
58
- parseMediaPayload(message: string): MediaPayload | null;
59
- };
60
- export {};
@@ -1,118 +0,0 @@
1
- /**
2
- * Inter-process IPC store for the `pending_messages` table.
3
- *
4
- * Beecork uses this table as a shared queue between writers (MCP handlers,
5
- * dashboard routes, watchers, delegations) and the daemon's poll loop. Before
6
- * this module the writes were inline `INSERT INTO pending_messages` calls
7
- * scattered across nine sites; the consumer in TabManager only branched on
8
- * type='notification' which meant every other type fell through to "send as
9
- * a plain user prompt" — including `type='media'` rows whose JSON envelope
10
- * was being handed to Claude as text instead of being rendered.
11
- *
12
- * This store owns:
13
- * - the typed envelope (`PendingMessageType` union, no more magic strings)
14
- * - 3-state status (`pending` → `processing` → `done|failed`) so the 5s
15
- * poll loop doesn't re-dispatch long-running rows
16
- * - the `_notify` sentinel removal (notifications now use type alone,
17
- * tab_name is nullable)
18
- * - claim/recover semantics so a daemon crash mid-dispatch resets in-flight
19
- * rows back to pending after a 30-minute timeout
20
- */
21
- import { getDb } from '../db/index.js';
22
- import { logger } from '../util/logger.js';
23
- function rowToPending(r) {
24
- return {
25
- id: r.id,
26
- tabName: r.tab_name,
27
- message: r.message,
28
- type: r.type || 'user',
29
- status: r.status || 'pending',
30
- createdAt: r.created_at,
31
- };
32
- }
33
- const STALE_PROCESSING_MS = 30 * 60 * 1000; // 30 minutes
34
- // Private INSERT helper — all five enqueue* methods were the same INSERT with
35
- // different literals. Notification rows use '' for tab_name since the consumer
36
- // branches on type rather than reading the tab.
37
- function insertPending(db, tabName, message, type) {
38
- db.prepare('INSERT INTO pending_messages (tab_name, message, type, status) VALUES (?, ?, ?, ?)').run(tabName, message, type, 'pending');
39
- }
40
- export const PendingMessageStore = {
41
- enqueueUser(tabName, message, db = getDb()) {
42
- insertPending(db, tabName, message, 'user');
43
- },
44
- enqueueNotification(message, db = getDb()) {
45
- insertPending(db, '', message, 'notification');
46
- },
47
- enqueueMedia(tabName, payload, db = getDb()) {
48
- insertPending(db, tabName, JSON.stringify(payload), 'media');
49
- },
50
- enqueueDelegation(tabName, message, db = getDb()) {
51
- insertPending(db, tabName, message, 'delegation');
52
- },
53
- enqueueDelegationResult(tabName, message, db = getDb()) {
54
- insertPending(db, tabName, message, 'delegation_result');
55
- },
56
- enqueueReplay(tabName, message, db = getDb()) {
57
- insertPending(db, tabName, message, 'replay');
58
- },
59
- /**
60
- * Claim up to `limit` pending rows atomically. The rows are flipped to
61
- * status='processing' inside a transaction so a second poll tick can't pick
62
- * them up. Returns the claimed rows.
63
- */
64
- claimBatch(limit, db = getDb()) {
65
- const claim = db.transaction((max) => {
66
- const rows = db
67
- .prepare("SELECT id, tab_name, message, type, status, created_at FROM pending_messages WHERE status = 'pending' ORDER BY created_at ASC LIMIT ?")
68
- .all(max);
69
- if (rows.length === 0)
70
- return [];
71
- const stmt = db.prepare("UPDATE pending_messages SET status = 'processing' WHERE id = ? AND status = 'pending'");
72
- const claimed = [];
73
- for (const r of rows) {
74
- const result = stmt.run(r.id);
75
- if (result.changes > 0)
76
- claimed.push(rowToPending(r));
77
- }
78
- return claimed;
79
- });
80
- return claim(limit);
81
- },
82
- markDone(id, db = getDb()) {
83
- db.prepare("UPDATE pending_messages SET status = 'done', processed = 1 WHERE id = ?").run(id);
84
- },
85
- markFailed(id, db = getDb()) {
86
- // 'failed' rows are kept for diagnostics until the periodic cleanup sweeps
87
- // them. They are NOT re-dispatched — preserves the original "no infinite
88
- // retries" policy while keeping the row visible for postmortem queries.
89
- db.prepare("UPDATE pending_messages SET status = 'failed', processed = 1 WHERE id = ?").run(id);
90
- },
91
- /**
92
- * Reset stuck `processing` rows back to `pending`. Run at daemon startup so
93
- * a crash mid-dispatch doesn't leave rows permanently in-flight. The 30min
94
- * window is well beyond the 30min subprocess runtime cap.
95
- */
96
- recoverProcessing(db = getDb()) {
97
- const result = db
98
- .prepare(`UPDATE pending_messages SET status = 'pending'
99
- WHERE status = 'processing' AND created_at < datetime('now', '-' || ? || ' seconds')`)
100
- .run(Math.floor(STALE_PROCESSING_MS / 1000));
101
- if (result.changes > 0) {
102
- logger.warn(`Recovered ${result.changes} stuck pending_messages rows from 'processing' state`);
103
- }
104
- return result.changes;
105
- },
106
- /** Parse a media row's message JSON. Returns null on malformed JSON. */
107
- parseMediaPayload(message) {
108
- try {
109
- const parsed = JSON.parse(message);
110
- if (parsed?.type === 'media' && typeof parsed.filePath === 'string')
111
- return parsed;
112
- return null;
113
- }
114
- catch {
115
- return null;
116
- }
117
- },
118
- };
@@ -1,31 +0,0 @@
1
- /**
2
- * Detection + recovery for "Claude Code session not found / expired" errors.
3
- *
4
- * Extracted from TabManager.executeMessage so the two responsibilities
5
- * (recognize the symptom, build the recovery prompt) live in one focused
6
- * module instead of tangled inline with the subprocess callbacks.
7
- */
8
- import type Database from 'better-sqlite3';
9
- import type { StreamResult } from '../types.js';
10
- import type { SendResult } from './manager.js';
11
- export interface StaleSessionRecovery {
12
- newSessionId: string;
13
- /** A prompt that wraps the original enrichedPrompt with the last few messages
14
- * for context, since Claude Code lost the session and won't have any. */
15
- contextPrompt: string;
16
- }
17
- export declare const StaleSessionDetector: {
18
- /**
19
- * Was this result an "I can't resume that session" failure?
20
- *
21
- * Covers both the legacy text-based error and the modern
22
- * `error_during_execution` event shape.
23
- */
24
- isStale(result: SendResult, resultEvent: StreamResult | null, shouldResume: boolean, retryDepth: number): boolean;
25
- /**
26
- * Build a recovery prompt seeded with the tab's last 5 messages as context,
27
- * and allocate a fresh session ID. Caller is responsible for updating the
28
- * tab's session_id in the DB before retrying.
29
- */
30
- buildRecovery(tabId: string, enrichedPrompt: string, db: Database.Database): StaleSessionRecovery;
31
- };
@@ -1,45 +0,0 @@
1
- /**
2
- * Detection + recovery for "Claude Code session not found / expired" errors.
3
- *
4
- * Extracted from TabManager.executeMessage so the two responsibilities
5
- * (recognize the symptom, build the recovery prompt) live in one focused
6
- * module instead of tangled inline with the subprocess callbacks.
7
- */
8
- import { v4 as uuidv4 } from 'uuid';
9
- export const StaleSessionDetector = {
10
- /**
11
- * Was this result an "I can't resume that session" failure?
12
- *
13
- * Covers both the legacy text-based error and the modern
14
- * `error_during_execution` event shape.
15
- */
16
- isStale(result, resultEvent, shouldResume, retryDepth) {
17
- if (!result.error || !shouldResume || retryDepth !== 0)
18
- return false;
19
- if (/session (not found|expired|invalid)/i.test(result.text))
20
- return true;
21
- if (resultEvent?.subtype === 'error_during_execution' &&
22
- resultEvent.errors?.some((e) => /no conversation found|session.*not found|session.*expired|session.*invalid/i.test(e))) {
23
- return true;
24
- }
25
- return false;
26
- },
27
- /**
28
- * Build a recovery prompt seeded with the tab's last 5 messages as context,
29
- * and allocate a fresh session ID. Caller is responsible for updating the
30
- * tab's session_id in the DB before retrying.
31
- */
32
- buildRecovery(tabId, enrichedPrompt, db) {
33
- const recentMsgs = db
34
- .prepare("SELECT role, content FROM messages WHERE tab_id = ? AND content != '' ORDER BY created_at DESC LIMIT 5")
35
- .all(tabId);
36
- const context = recentMsgs
37
- .reverse()
38
- .map((m) => `${m.role}: ${m.content.slice(0, 200)}`)
39
- .join('\n');
40
- const contextPrompt = context
41
- ? `[Previous conversation context:\n${context}\n]\n\n${enrichedPrompt}`
42
- : enrichedPrompt;
43
- return { newSessionId: uuidv4(), contextPrompt };
44
- },
45
- };
@@ -1,25 +0,0 @@
1
- import type { BeecorkConfig, StreamEvent } from '../types.js';
2
- /** Test-only — reset the existsSync cache between test runs that toggle the mock. */
3
- export declare function _resetMcpConfigExistsCacheForTests(): void;
4
- export interface SubprocessCallbacks {
5
- onEvent: (event: StreamEvent) => void;
6
- onExit: (code: number | null) => void;
7
- onError: (err: Error) => void;
8
- }
9
- export declare class ClaudeSubprocess {
10
- private tabName;
11
- private workingDir;
12
- private config;
13
- private tabSystemPrompt?;
14
- private proc;
15
- private buffer;
16
- private killTimer;
17
- private runtimeTimer;
18
- readonly sessionId: string;
19
- constructor(tabName: string, workingDir: string, config: BeecorkConfig, sessionId?: string, tabSystemPrompt?: string | null | undefined);
20
- send(prompt: string, callbacks: SubprocessCallbacks, resume?: boolean): Promise<void>;
21
- kill(): void;
22
- get isRunning(): boolean;
23
- get pid(): number | null;
24
- private buildArgs;
25
- }
@@ -1,189 +0,0 @@
1
- import { spawn } from 'node:child_process';
2
- import fs from 'node:fs';
3
- import { v4 as uuidv4 } from 'uuid';
4
- import { logger } from '../util/logger.js';
5
- import { getMcpConfigPath } from '../util/paths.js';
6
- // Cache the MCP-config existence check. The file is created at setup time and
7
- // stable for the daemon's lifetime; we don't need to stat() it per spawn.
8
- let mcpConfigExistsCache = null;
9
- function mcpConfigExists() {
10
- if (mcpConfigExistsCache !== null)
11
- return mcpConfigExistsCache;
12
- mcpConfigExistsCache = fs.existsSync(getMcpConfigPath());
13
- return mcpConfigExistsCache;
14
- }
15
- /** Test-only — reset the existsSync cache between test runs that toggle the mock. */
16
- export function _resetMcpConfigExistsCacheForTests() {
17
- mcpConfigExistsCache = null;
18
- }
19
- const BEECORK_SYSTEM_PROMPT = `You are running inside Beecork, an always-on infrastructure for Claude Code.
20
-
21
- You have these special MCP tools available:
22
- - beecork_remember: Store important facts for future sessions (preferences, server addresses, decisions, outcomes)
23
- - beecork_recall: Search stored memories — ALWAYS call this at the start of complex tasks
24
- - beecork_task_create: Schedule recurring tasks (types: "at" for one-time, "every" for interval like "30m", "cron" for expressions like "0 9 * * 1")
25
- - beecork_task_list: List scheduled tasks
26
- - beecork_task_delete: Remove a scheduled task
27
- - beecork_tab_create: Create a new virtual tab for parallel work
28
- - beecork_tab_list: List all tabs
29
- - beecork_send_message: Send a message to another tab
30
- - beecork_notify: Send the user a notification mid-task without stopping
31
- - beecork_status: Check system status
32
-
33
- Guidelines:
34
- - You are running unattended. Be thorough and complete tasks fully.
35
- - Always call beecork_recall at the start of any task to check relevant memories.
36
- - Always call beecork_remember when you learn something important.
37
- - When asked for recurring tasks, use beecork_task_create.
38
- - Use beecork_notify for progress updates during long tasks.`;
39
- export class ClaudeSubprocess {
40
- tabName;
41
- workingDir;
42
- config;
43
- tabSystemPrompt;
44
- proc = null;
45
- buffer = '';
46
- killTimer = null;
47
- runtimeTimer = null;
48
- sessionId;
49
- constructor(tabName, workingDir, config, sessionId, tabSystemPrompt) {
50
- this.tabName = tabName;
51
- this.workingDir = workingDir;
52
- this.config = config;
53
- this.tabSystemPrompt = tabSystemPrompt;
54
- this.sessionId = sessionId ?? uuidv4();
55
- }
56
- async send(prompt, callbacks, resume = false) {
57
- if (this.proc) {
58
- throw new Error(`Subprocess for tab "${this.tabName}" is already running`);
59
- }
60
- const args = this.buildArgs(prompt, resume);
61
- logger.debug(`[${this.tabName}] Spawning: ${this.config.claudeCode.bin} ${args.join(' ')}`);
62
- this.proc = spawn(this.config.claudeCode.bin, args, {
63
- cwd: this.workingDir,
64
- stdio: ['ignore', 'pipe', 'pipe'],
65
- env: { ...process.env },
66
- });
67
- const pid = this.proc.pid;
68
- logger.info(`[${this.tabName}] Claude subprocess started (PID: ${pid})`);
69
- this.proc.stdout.on('data', (chunk) => {
70
- this.buffer += chunk.toString();
71
- const lines = this.buffer.split('\n');
72
- this.buffer = lines.pop() || '';
73
- for (const line of lines) {
74
- if (!line.trim())
75
- continue;
76
- try {
77
- const event = JSON.parse(line);
78
- callbacks.onEvent(event);
79
- }
80
- catch {
81
- // Non-JSON line (verbose debug output), skip
82
- logger.debug(`[${this.tabName}] non-json: ${line.slice(0, 200)}`);
83
- }
84
- }
85
- });
86
- this.proc.stderr.on('data', (chunk) => {
87
- const text = chunk.toString().trim();
88
- if (text) {
89
- // claude prints auth failures, rate limits, license issues, and other
90
- // operational errors to stderr. Surface at warn so they reach daemon.log
91
- // at the default level — debugging "why did claude exit?" otherwise
92
- // requires recompiling with a different log level.
93
- logger.warn(`[${this.tabName}] claude stderr: ${text.slice(0, 500)}`);
94
- }
95
- });
96
- this.proc.on('error', (err) => {
97
- this.proc = null;
98
- if (this.runtimeTimer) {
99
- clearTimeout(this.runtimeTimer);
100
- this.runtimeTimer = null;
101
- }
102
- callbacks.onError(err);
103
- });
104
- this.proc.on('exit', (code) => {
105
- this.proc = null;
106
- if (this.killTimer) {
107
- clearTimeout(this.killTimer);
108
- this.killTimer = null;
109
- }
110
- if (this.runtimeTimer) {
111
- clearTimeout(this.runtimeTimer);
112
- this.runtimeTimer = null;
113
- }
114
- logger.info(`[${this.tabName}] Claude subprocess exited (code: ${code})`);
115
- callbacks.onExit(code);
116
- });
117
- // Hard runtime cap so a wedged claude can't pin a tab forever.
118
- // Default 30 minutes. Disable by setting maxRuntimeMs to 0.
119
- const maxRuntimeMs = this.config.claudeCode.maxRuntimeMs ?? 30 * 60 * 1000;
120
- if (maxRuntimeMs > 0) {
121
- this.runtimeTimer = setTimeout(() => {
122
- if (!this.proc)
123
- return;
124
- logger.warn(`[${this.tabName}] Subprocess exceeded maxRuntimeMs (${maxRuntimeMs}ms) — killing`);
125
- callbacks.onError(new Error(`Subprocess timed out after ${Math.round(maxRuntimeMs / 1000)}s`));
126
- this.kill();
127
- }, maxRuntimeMs);
128
- }
129
- }
130
- kill() {
131
- if (!this.proc)
132
- return;
133
- logger.info(`[${this.tabName}] Killing subprocess (PID: ${this.proc.pid})`);
134
- this.proc.kill('SIGTERM');
135
- const proc = this.proc;
136
- this.killTimer = setTimeout(() => {
137
- try {
138
- proc.kill('SIGKILL');
139
- }
140
- catch {
141
- /* already dead */
142
- }
143
- }, 5000);
144
- }
145
- get isRunning() {
146
- return this.proc !== null;
147
- }
148
- get pid() {
149
- return this.proc?.pid ?? null;
150
- }
151
- buildArgs(prompt, resume) {
152
- const args = [
153
- '-p',
154
- '--output-format',
155
- 'stream-json',
156
- '--verbose',
157
- ...this.config.claudeCode.defaultFlags,
158
- ];
159
- // Only add MCP config if the file exists. existsSync result is cached at
160
- // module scope — the path is stable for the daemon's lifetime, so doing
161
- // this once-per-process saves a syscall per claude spawn (every message,
162
- // every task firing, every watcher trigger).
163
- if (mcpConfigExists()) {
164
- args.push('--mcp-config', getMcpConfigPath());
165
- }
166
- // Inject Beecork system context so Claude knows about available tools
167
- if (!resume) {
168
- let systemPrompt = BEECORK_SYSTEM_PROMPT;
169
- if (this.tabSystemPrompt) {
170
- systemPrompt += `\n\n[Tab-specific instructions for "${this.tabName}"]\n${this.tabSystemPrompt}`;
171
- }
172
- args.push('--system-prompt', systemPrompt);
173
- }
174
- if (resume) {
175
- args.push('--resume', this.sessionId);
176
- }
177
- else {
178
- args.push('--session-id', this.sessionId);
179
- }
180
- if (this.config.claudeCode.computerUse) {
181
- args.push('--computer-use');
182
- }
183
- if (this.config.claudeCode.maxBudgetUsd) {
184
- args.push('--max-budget-usd', String(this.config.claudeCode.maxBudgetUsd));
185
- }
186
- args.push(prompt);
187
- return args;
188
- }
189
- }
@@ -1,28 +0,0 @@
1
- import type Database from 'better-sqlite3';
2
- import type { Tab, TabStatus } from '../types.js';
3
- /**
4
- * SQL helpers for the `tabs` table. Single place for any code that needs to
5
- * read/write tab rows. TabManager owns subprocess lifecycle; TabStore owns
6
- * the schema knowledge.
7
- *
8
- * All methods accept an optional `db` param so they're testable against an
9
- * in-memory database. The no-arg form uses the singleton.
10
- */
11
- export declare const TabStore: {
12
- listAll(db?: Database.Database): Tab[];
13
- findByName(name: string, db?: Database.Database): Tab | undefined;
14
- getIdByName(name: string, db?: Database.Database): string | undefined;
15
- countAll(db?: Database.Database): number;
16
- countRunning(db?: Database.Database): number;
17
- /** All tabs marked 'running' — used by daemon crash-recovery on startup. */
18
- findRunning(db?: Database.Database): Tab[];
19
- /** Most-recently-active tab — used by MCP to surface "current" context. */
20
- mostRecent(db?: Database.Database): Tab | undefined;
21
- setStatus(name: string, status: TabStatus, db?: Database.Database): void;
22
- setIdleById(id: string, db?: Database.Database): void;
23
- /** Used by MCP close-tab to nudge daemon recovery loop. */
24
- markRunningAsStopped(name: string, db?: Database.Database): void;
25
- setSystemPrompt(name: string, systemPrompt: string, db?: Database.Database): boolean;
26
- /** Delete a tab and all of its messages atomically. Returns true if it existed. */
27
- deleteWithMessages(name: string, db?: Database.Database): boolean;
28
- };