beecork 1.7.1 → 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 (253) 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/cron/scheduler.d.ts +0 -22
  61. package/dist/cron/scheduler.js +0 -220
  62. package/dist/cron/store.d.ts +0 -12
  63. package/dist/cron/store.js +0 -83
  64. package/dist/daemon.d.ts +0 -1
  65. package/dist/daemon.js +0 -345
  66. package/dist/dashboard/html.d.ts +0 -1
  67. package/dist/dashboard/html.js +0 -849
  68. package/dist/dashboard/routes.d.ts +0 -17
  69. package/dist/dashboard/routes.js +0 -623
  70. package/dist/dashboard/server.d.ts +0 -1
  71. package/dist/dashboard/server.js +0 -104
  72. package/dist/db/connection.d.ts +0 -29
  73. package/dist/db/connection.js +0 -37
  74. package/dist/db/index.d.ts +0 -14
  75. package/dist/db/index.js +0 -127
  76. package/dist/db/migrations.d.ts +0 -2
  77. package/dist/db/migrations.js +0 -384
  78. package/dist/delegation/manager.d.ts +0 -18
  79. package/dist/delegation/manager.js +0 -89
  80. package/dist/index.d.ts +0 -2
  81. package/dist/knowledge/index.d.ts +0 -2
  82. package/dist/knowledge/index.js +0 -1
  83. package/dist/knowledge/manager.d.ts +0 -17
  84. package/dist/knowledge/manager.js +0 -160
  85. package/dist/knowledge/types.d.ts +0 -7
  86. package/dist/knowledge/types.js +0 -1
  87. package/dist/machines/forwarder.d.ts +0 -7
  88. package/dist/machines/forwarder.js +0 -35
  89. package/dist/machines/index.d.ts +0 -1
  90. package/dist/machines/index.js +0 -1
  91. package/dist/machines/registry.d.ts +0 -15
  92. package/dist/machines/registry.js +0 -46
  93. package/dist/mcp/handlers.d.ts +0 -37
  94. package/dist/mcp/handlers.js +0 -520
  95. package/dist/mcp/server.d.ts +0 -2
  96. package/dist/mcp/server.js +0 -86
  97. package/dist/mcp/tool-definitions.d.ts +0 -1225
  98. package/dist/mcp/tool-definitions.js +0 -412
  99. package/dist/mcp/validate.d.ts +0 -23
  100. package/dist/mcp/validate.js +0 -65
  101. package/dist/media/factory.d.ts +0 -6
  102. package/dist/media/factory.js +0 -36
  103. package/dist/media/generators/dall-e.d.ts +0 -10
  104. package/dist/media/generators/dall-e.js +0 -41
  105. package/dist/media/generators/elevenlabs-music.d.ts +0 -9
  106. package/dist/media/generators/elevenlabs-music.js +0 -34
  107. package/dist/media/generators/elevenlabs-sfx.d.ts +0 -9
  108. package/dist/media/generators/elevenlabs-sfx.js +0 -33
  109. package/dist/media/generators/kling.d.ts +0 -9
  110. package/dist/media/generators/kling.js +0 -47
  111. package/dist/media/generators/lyria.d.ts +0 -10
  112. package/dist/media/generators/lyria.js +0 -41
  113. package/dist/media/generators/nano-banana.d.ts +0 -10
  114. package/dist/media/generators/nano-banana.js +0 -45
  115. package/dist/media/generators/poll-util.d.ts +0 -12
  116. package/dist/media/generators/poll-util.js +0 -22
  117. package/dist/media/generators/recraft.d.ts +0 -9
  118. package/dist/media/generators/recraft.js +0 -52
  119. package/dist/media/generators/runway.d.ts +0 -9
  120. package/dist/media/generators/runway.js +0 -49
  121. package/dist/media/generators/stable-diffusion.d.ts +0 -9
  122. package/dist/media/generators/stable-diffusion.js +0 -35
  123. package/dist/media/generators/veo.d.ts +0 -9
  124. package/dist/media/generators/veo.js +0 -32
  125. package/dist/media/index.d.ts +0 -6
  126. package/dist/media/index.js +0 -8
  127. package/dist/media/loader.d.ts +0 -8
  128. package/dist/media/loader.js +0 -46
  129. package/dist/media/store.d.ts +0 -17
  130. package/dist/media/store.js +0 -69
  131. package/dist/media/types.d.ts +0 -43
  132. package/dist/media/types.js +0 -1
  133. package/dist/memory/extractor.d.ts +0 -5
  134. package/dist/memory/extractor.js +0 -157
  135. package/dist/notifications/index.d.ts +0 -4
  136. package/dist/notifications/index.js +0 -21
  137. package/dist/notifications/ntfy.d.ts +0 -9
  138. package/dist/notifications/ntfy.js +0 -25
  139. package/dist/notifications/pushover.d.ts +0 -9
  140. package/dist/notifications/pushover.js +0 -26
  141. package/dist/notifications/types.d.ts +0 -5
  142. package/dist/notifications/types.js +0 -1
  143. package/dist/notifications/webhook-provider.d.ts +0 -9
  144. package/dist/notifications/webhook-provider.js +0 -24
  145. package/dist/observability/analytics.d.ts +0 -30
  146. package/dist/observability/analytics.js +0 -118
  147. package/dist/pipe/anthropic-client.d.ts +0 -9
  148. package/dist/pipe/anthropic-client.js +0 -50
  149. package/dist/pipe/brain.d.ts +0 -16
  150. package/dist/pipe/brain.js +0 -89
  151. package/dist/pipe/memory-store.d.ts +0 -8
  152. package/dist/pipe/memory-store.js +0 -39
  153. package/dist/pipe/project-scanner.d.ts +0 -6
  154. package/dist/pipe/project-scanner.js +0 -26
  155. package/dist/pipe/types.d.ts +0 -34
  156. package/dist/pipe/types.js +0 -1
  157. package/dist/projects/index.d.ts +0 -4
  158. package/dist/projects/index.js +0 -2
  159. package/dist/projects/manager.d.ts +0 -13
  160. package/dist/projects/manager.js +0 -166
  161. package/dist/projects/router.d.ts +0 -17
  162. package/dist/projects/router.js +0 -246
  163. package/dist/projects/types.d.ts +0 -19
  164. package/dist/projects/types.js +0 -1
  165. package/dist/service/install.d.ts +0 -4
  166. package/dist/service/install.js +0 -146
  167. package/dist/service/templates.d.ts +0 -4
  168. package/dist/service/templates.js +0 -52
  169. package/dist/service/windows.d.ts +0 -8
  170. package/dist/service/windows.js +0 -59
  171. package/dist/session/budget-guard.d.ts +0 -20
  172. package/dist/session/budget-guard.js +0 -31
  173. package/dist/session/circuit-breaker.d.ts +0 -19
  174. package/dist/session/circuit-breaker.js +0 -91
  175. package/dist/session/context-compactor.d.ts +0 -32
  176. package/dist/session/context-compactor.js +0 -45
  177. package/dist/session/context-monitor.d.ts +0 -15
  178. package/dist/session/context-monitor.js +0 -44
  179. package/dist/session/handoff.d.ts +0 -21
  180. package/dist/session/handoff.js +0 -50
  181. package/dist/session/manager.d.ts +0 -60
  182. package/dist/session/manager.js +0 -416
  183. package/dist/session/memory-store.d.ts +0 -29
  184. package/dist/session/memory-store.js +0 -45
  185. package/dist/session/message-queue.d.ts +0 -28
  186. package/dist/session/message-queue.js +0 -52
  187. package/dist/session/pending-dispatcher.d.ts +0 -31
  188. package/dist/session/pending-dispatcher.js +0 -120
  189. package/dist/session/pending-store.d.ts +0 -60
  190. package/dist/session/pending-store.js +0 -118
  191. package/dist/session/stale-session.d.ts +0 -31
  192. package/dist/session/stale-session.js +0 -45
  193. package/dist/session/subprocess.d.ts +0 -32
  194. package/dist/session/subprocess.js +0 -230
  195. package/dist/session/tab-store.d.ts +0 -28
  196. package/dist/session/tab-store.js +0 -78
  197. package/dist/session/tool-classifier.d.ts +0 -4
  198. package/dist/session/tool-classifier.js +0 -56
  199. package/dist/tasks/scheduler.d.ts +0 -45
  200. package/dist/tasks/scheduler.js +0 -329
  201. package/dist/tasks/store.d.ts +0 -11
  202. package/dist/tasks/store.js +0 -103
  203. package/dist/timeline/index.d.ts +0 -3
  204. package/dist/timeline/index.js +0 -2
  205. package/dist/timeline/logger.d.ts +0 -8
  206. package/dist/timeline/logger.js +0 -11
  207. package/dist/timeline/query.d.ts +0 -11
  208. package/dist/timeline/query.js +0 -67
  209. package/dist/timeline/types.d.ts +0 -12
  210. package/dist/timeline/types.js +0 -1
  211. package/dist/types.d.ts +0 -225
  212. package/dist/types.js +0 -3
  213. package/dist/users/index.d.ts +0 -2
  214. package/dist/users/index.js +0 -1
  215. package/dist/users/service.d.ts +0 -17
  216. package/dist/users/service.js +0 -46
  217. package/dist/util/auto-heal.d.ts +0 -43
  218. package/dist/util/auto-heal.js +0 -107
  219. package/dist/util/install-info.d.ts +0 -37
  220. package/dist/util/install-info.js +0 -80
  221. package/dist/util/logger.d.ts +0 -18
  222. package/dist/util/logger.js +0 -118
  223. package/dist/util/paths.d.ts +0 -13
  224. package/dist/util/paths.js +0 -55
  225. package/dist/util/platform.d.ts +0 -2
  226. package/dist/util/platform.js +0 -11
  227. package/dist/util/progress.d.ts +0 -34
  228. package/dist/util/progress.js +0 -133
  229. package/dist/util/rate-limiter.d.ts +0 -14
  230. package/dist/util/rate-limiter.js +0 -50
  231. package/dist/util/retry.d.ts +0 -2
  232. package/dist/util/retry.js +0 -18
  233. package/dist/util/text.d.ts +0 -32
  234. package/dist/util/text.js +0 -101
  235. package/dist/version.d.ts +0 -1
  236. package/dist/version.js +0 -4
  237. package/dist/voice/index.d.ts +0 -17
  238. package/dist/voice/index.js +0 -42
  239. package/dist/voice/stt.d.ts +0 -16
  240. package/dist/voice/stt.js +0 -57
  241. package/dist/voice/tts.d.ts +0 -23
  242. package/dist/voice/tts.js +0 -84
  243. package/dist/watchers/evaluator.d.ts +0 -5
  244. package/dist/watchers/evaluator.js +0 -45
  245. package/dist/watchers/index.d.ts +0 -4
  246. package/dist/watchers/index.js +0 -3
  247. package/dist/watchers/scheduler.d.ts +0 -20
  248. package/dist/watchers/scheduler.js +0 -183
  249. package/dist/watchers/store.d.ts +0 -10
  250. package/dist/watchers/store.js +0 -56
  251. package/dist/watchers/types.d.ts +0 -15
  252. package/dist/watchers/types.js +0 -1
  253. package/scripts/postinstall.mjs +0 -50
@@ -1,50 +0,0 @@
1
- /**
2
- * Session handoff helpers — used by both the CLI (`beecork attach`) and the
3
- * shared command handler (`/handoff` from any channel).
4
- *
5
- * Moved here from `cli/handoff.ts` so daemon-shared code (channels, MCP) can
6
- * import without reaching into the CLI layer. The CLI-specific `attachTab`
7
- * (which spawns claude and calls process.exit) stays in `cli/handoff.ts`.
8
- */
9
- import { getDb } from '../db/index.js';
10
- import { TabStore } from './tab-store.js';
11
- export function exportTab(tabName) {
12
- const tab = TabStore.findByName(tabName);
13
- if (!tab)
14
- return null;
15
- const messages = getDb()
16
- .prepare('SELECT role, content FROM messages WHERE tab_id = ? ORDER BY created_at DESC LIMIT 5')
17
- .all(tab.id);
18
- return {
19
- name: tab.name,
20
- sessionId: tab.sessionId,
21
- workingDir: tab.workingDir,
22
- status: tab.status,
23
- lastActivity: tab.lastActivityAt,
24
- recentMessages: messages.reverse(),
25
- };
26
- }
27
- export function formatHandoffInfo(info) {
28
- const lines = [
29
- `Session Handoff — tab "${info.name}"`,
30
- '',
31
- `Session ID: ${info.sessionId}`,
32
- `Working dir: ${info.workingDir}`,
33
- `Status: ${info.status}`,
34
- `Last activity: ${info.lastActivity}`,
35
- '',
36
- 'To resume in terminal:',
37
- ` beecork attach ${info.name}`,
38
- '',
39
- 'Or manually:',
40
- ` cd ${info.workingDir}`,
41
- ` claude --session-id ${info.sessionId} --resume`,
42
- ];
43
- if (info.recentMessages.length > 0) {
44
- lines.push('', 'Recent context:');
45
- for (const msg of info.recentMessages) {
46
- lines.push(` [${msg.role}] ${msg.content.slice(0, 150)}${msg.content.length > 150 ? '...' : ''}`);
47
- }
48
- }
49
- return lines.join('\n');
50
- }
@@ -1,60 +0,0 @@
1
- import type { BeecorkConfig, Tab } from '../types.js';
2
- export type { StreamResult } from '../types.js';
3
- export interface SendResult {
4
- text: string;
5
- costUsd: number;
6
- durationMs: number;
7
- sessionId: string;
8
- error: boolean;
9
- }
10
- export type NotifyCallback = (text: string) => Promise<void>;
11
- /**
12
- * TabManager orchestrates per-tab Claude subprocess lifecycles. Heavy lifting
13
- * (queueing, budget enforcement, stale-session recovery, context compaction,
14
- * pending-message dispatch) is delegated to collaborators in this directory
15
- * so this class stays focused on:
16
- *
17
- * - tab CRUD (ensureTab, listTabs, stopTab, closeTab, stopAll)
18
- * - subprocess orchestration (executeMessage)
19
- * - wiring the collaborators together
20
- *
21
- * The split happened in the 2026-05-15 audit fix; see the audit file for the
22
- * rationale. Before the split this file was 467 lines and 10 responsibilities.
23
- */
24
- export declare class TabManager {
25
- private config;
26
- private subprocesses;
27
- private queue;
28
- private budget;
29
- private tabCostCache;
30
- private onNotify;
31
- constructor(config: BeecorkConfig);
32
- /** Set a callback for sending notifications (e.g., via Telegram) */
33
- setNotifyCallback(cb: NotifyCallback): void;
34
- /** Ensure a tab exists in the database. Creates it if missing. */
35
- ensureTab(tabName: string, workingDirOverride?: string): Tab;
36
- /** Send a message to a tab. Creates the tab if it doesn't exist. Queues if busy. */
37
- sendMessage(tabName: string, prompt: string, options?: {
38
- resume?: boolean;
39
- onTextChunk?: (text: string) => void;
40
- onToolUse?: (toolName: string, toolInput: Record<string, unknown>) => void;
41
- projectPath?: string;
42
- _compactionDepth?: number;
43
- }): Promise<SendResult>;
44
- /** Get all tabs from the database */
45
- listTabs(): Tab[];
46
- /** Get a specific tab */
47
- getTab(tabName: string): Tab | undefined;
48
- /** Stop a tab's running subprocess */
49
- stopTab(tabName: string): void;
50
- /** Update a tab's system_prompt. Returns true if the tab existed. */
51
- setSystemPrompt(tabName: string, systemPrompt: string): boolean;
52
- /** Close a tab — stop subprocess and delete its rows. Returns true if the tab existed. */
53
- closeTab(tabName: string): boolean;
54
- /** Stop all running subprocesses (clean shutdown) */
55
- stopAll(): void;
56
- private executeMessage;
57
- private processNextInQueue;
58
- private updateTabStatus;
59
- private clearQueue;
60
- }
@@ -1,416 +0,0 @@
1
- import { v4 as uuidv4 } from 'uuid';
2
- import { ClaudeSubprocess } from './subprocess.js';
3
- import { CircuitBreaker } from './circuit-breaker.js';
4
- import { ContextMonitor } from './context-monitor.js';
5
- import { TabStore } from './tab-store.js';
6
- import { MessageQueue } from './message-queue.js';
7
- import { BudgetGuard } from './budget-guard.js';
8
- import { StaleSessionDetector } from './stale-session.js';
9
- import { ContextCompactor } from './context-compactor.js';
10
- import { PendingMessageStore } from './pending-store.js';
11
- import { completeDelegation } from '../delegation/manager.js';
12
- import { getDb } from '../db/index.js';
13
- import { resolveWorkingDir, validateTabName } from '../config.js';
14
- import { logger } from '../util/logger.js';
15
- import { logActivity } from '../timeline/index.js';
16
- import { getAllKnowledge, formatKnowledgeForContext } from '../knowledge/index.js';
17
- /**
18
- * TabManager orchestrates per-tab Claude subprocess lifecycles. Heavy lifting
19
- * (queueing, budget enforcement, stale-session recovery, context compaction,
20
- * pending-message dispatch) is delegated to collaborators in this directory
21
- * so this class stays focused on:
22
- *
23
- * - tab CRUD (ensureTab, listTabs, stopTab, closeTab, stopAll)
24
- * - subprocess orchestration (executeMessage)
25
- * - wiring the collaborators together
26
- *
27
- * The split happened in the 2026-05-15 audit fix; see the audit file for the
28
- * rationale. Before the split this file was 467 lines and 10 responsibilities.
29
- */
30
- export class TabManager {
31
- config;
32
- subprocesses = new Map();
33
- queue = new MessageQueue();
34
- budget;
35
- // In-memory cache of per-tab cumulative cost. Lazily seeded with a single
36
- // SUM query when a tab is first checked; thereafter updated incrementally
37
- // from the assistant-message insert. Saves an O(N messages) SUM scan on
38
- // every inbound message — the previous code re-aggregated the entire tab
39
- // history just to gate budget.
40
- tabCostCache = new Map();
41
- onNotify = null;
42
- constructor(config) {
43
- this.config = config;
44
- this.budget = new BudgetGuard(config.claudeCode.maxBudgetUsd);
45
- }
46
- /** Set a callback for sending notifications (e.g., via Telegram) */
47
- setNotifyCallback(cb) {
48
- this.onNotify = cb;
49
- }
50
- /** Ensure a tab exists in the database. Creates it if missing. */
51
- ensureTab(tabName, workingDirOverride) {
52
- const db = getDb();
53
- const existing = TabStore.findByName(tabName);
54
- if (existing)
55
- return existing;
56
- // Validate tab name before creating (centralized for all channels + MCP)
57
- if (tabName !== 'default') {
58
- const validationError = validateTabName(tabName);
59
- if (validationError)
60
- throw new Error(validationError);
61
- }
62
- // Check for a matching tab template
63
- const template = this.config.tabTemplates?.[tabName];
64
- const tab = {
65
- id: uuidv4(),
66
- name: tabName,
67
- sessionId: uuidv4(),
68
- status: 'idle',
69
- workingDir: workingDirOverride || template?.workingDir || resolveWorkingDir(tabName),
70
- createdAt: new Date().toISOString(),
71
- lastActivityAt: new Date().toISOString(),
72
- pid: null,
73
- systemPrompt: template?.systemPrompt || null,
74
- };
75
- db.prepare(`
76
- INSERT INTO tabs (id, name, session_id, status, working_dir, created_at, last_activity_at, system_prompt)
77
- VALUES (?, ?, ?, ?, ?, ?, ?, ?)
78
- `).run(tab.id, tab.name, tab.sessionId, tab.status, tab.workingDir, tab.createdAt, tab.lastActivityAt, tab.systemPrompt);
79
- logger.info(`Created tab: ${tabName}`);
80
- return tab;
81
- }
82
- /** Send a message to a tab. Creates the tab if it doesn't exist. Queues if busy. */
83
- async sendMessage(tabName, prompt, options) {
84
- const tab = this.ensureTab(tabName, options?.projectPath);
85
- // If a subprocess is already running on this tab, queue the message
86
- if (this.subprocesses.get(tabName)?.isRunning) {
87
- return new Promise((resolve, reject) => {
88
- const accepted = this.queue.enqueue(tabName, { prompt, resolve, reject });
89
- if (!accepted) {
90
- reject(new Error(`Queue full for tab "${tabName}". Try again later.`));
91
- return;
92
- }
93
- logger.info(`[${tabName}] Message queued (queue size: ${this.queue.size(tabName)})`);
94
- });
95
- }
96
- return this.executeMessage(tab, prompt, options?.resume ?? false, options?.onTextChunk, options?.onToolUse, options?._compactionDepth);
97
- }
98
- /** Get all tabs from the database */
99
- listTabs() {
100
- return TabStore.listAll();
101
- }
102
- /** Get a specific tab */
103
- getTab(tabName) {
104
- return TabStore.findByName(tabName);
105
- }
106
- /** Stop a tab's running subprocess */
107
- stopTab(tabName) {
108
- const sub = this.subprocesses.get(tabName);
109
- if (sub?.isRunning)
110
- sub.kill();
111
- this.updateTabStatus(tabName, 'stopped');
112
- this.clearQueue(tabName);
113
- }
114
- /** Update a tab's system_prompt. Returns true if the tab existed. */
115
- setSystemPrompt(tabName, systemPrompt) {
116
- return TabStore.setSystemPrompt(tabName, systemPrompt);
117
- }
118
- /** Close a tab — stop subprocess and delete its rows. Returns true if the tab existed. */
119
- closeTab(tabName) {
120
- const tab = TabStore.findByName(tabName);
121
- this.stopTab(tabName);
122
- if (tab)
123
- this.tabCostCache.delete(tab.id);
124
- return TabStore.deleteWithMessages(tabName);
125
- }
126
- /** Stop all running subprocesses (clean shutdown) */
127
- stopAll() {
128
- for (const [tabName, sub] of this.subprocesses) {
129
- if (sub.isRunning)
130
- sub.kill();
131
- this.updateTabStatus(tabName, 'stopped');
132
- }
133
- this.subprocesses.clear();
134
- for (const dropped of this.queue.clearAll()) {
135
- for (const item of dropped) {
136
- item.reject(new Error('Daemon shutting down'));
137
- }
138
- }
139
- }
140
- async executeMessage(tab, prompt, resume, onTextChunk, onToolUse, compactionDepth, forceFresh = false, retryDepth = 0) {
141
- const db = getDb();
142
- logActivity('task_started', 'Processing message', {
143
- tabName: tab.name,
144
- details: prompt.slice(0, 500),
145
- });
146
- // Budget check before spawning
147
- if (this.config.claudeCode.maxBudgetUsd) {
148
- let tabSpend = this.tabCostCache.get(tab.id);
149
- if (tabSpend === undefined) {
150
- tabSpend = db
151
- .prepare('SELECT COALESCE(SUM(cost_usd), 0) as total FROM messages WHERE tab_id = ?')
152
- .get(tab.id).total;
153
- this.tabCostCache.set(tab.id, tabSpend);
154
- }
155
- const decision = this.budget.check(tabSpend, tab.name);
156
- if (!decision.allowed) {
157
- this.onNotify?.(decision.reason).catch(() => { });
158
- return {
159
- text: decision.reason,
160
- costUsd: 0,
161
- durationMs: 0,
162
- sessionId: tab.sessionId,
163
- error: true,
164
- };
165
- }
166
- if (decision.warning) {
167
- this.onNotify?.(decision.warning).catch(() => { });
168
- }
169
- }
170
- // Inject knowledge (global markdown + project markdown + tab facts)
171
- const knowledge = getAllKnowledge(tab.workingDir, tab.name);
172
- const knowledgeContext = formatKnowledgeForContext(knowledge);
173
- const enrichedPrompt = knowledgeContext ? `${knowledgeContext}\n\n${prompt}` : prompt;
174
- // Store user message
175
- db.prepare('INSERT INTO messages (tab_id, role, content) VALUES (?, ?, ?)').run(tab.id, 'user', prompt);
176
- this.updateTabStatus(tab.name, 'running');
177
- // Get fresh tab to pick up system_prompt
178
- const freshTab = TabStore.findByName(tab.name) || tab;
179
- const subprocess = new ClaudeSubprocess(tab.name, tab.workingDir, this.config, tab.sessionId, freshTab.systemPrompt);
180
- this.subprocesses.set(tab.name, subprocess);
181
- // Circuit breaker is per-call state — its lifetime is exactly the
182
- // subprocess's lifetime. Keep it as a local const; the previous
183
- // circuitBreakers Map suggested cross-call state but nothing read it
184
- // outside this scope.
185
- const breaker = new CircuitBreaker(tab.name);
186
- const contextMonitor = new ContextMonitor(tab.name);
187
- // Resume if: explicitly requested or DB has prior successful responses for this tab.
188
- // forceFresh overrides both — used after a stale-session retry to guarantee --session-id, not --resume.
189
- // LIMIT 1 short-circuits on the first matching row — a long-running tab with
190
- // 10k messages used to walk the whole index for a boolean answer.
191
- const hasDbHistory = db
192
- .prepare("SELECT 1 FROM messages WHERE tab_id = ? AND role = 'assistant' AND content != '' LIMIT 1")
193
- .get(tab.id) !== undefined;
194
- const shouldResume = !forceFresh && (resume || hasDbHistory);
195
- return new Promise((resolve, reject) => {
196
- let resultText = '';
197
- let resultEvent = null;
198
- let loopWarningPending = false;
199
- let checkpointTriggered = false;
200
- // Prevent the SIGTERM-then-exit double dispatch: when onError already
201
- // routed the failure through reject() + processNextInQueue, suppress
202
- // the inevitable subsequent onExit so the queue doesn't shift twice.
203
- let errored = false;
204
- const callbacks = {
205
- onEvent: (event) => {
206
- // Capture session_id from StreamInit and update tab record
207
- if (event.type === 'system' && 'subtype' in event && event.subtype === 'init') {
208
- const initEvent = event;
209
- if (initEvent.session_id) {
210
- db.prepare('UPDATE tabs SET session_id = ? WHERE id = ?').run(initEvent.session_id, tab.id);
211
- }
212
- }
213
- if (event.type === 'assistant') {
214
- const assistant = event;
215
- // Track context usage
216
- if (assistant.message.usage) {
217
- const contextAction = contextMonitor.recordUsage(assistant.message.usage);
218
- if (contextAction === 'warn') {
219
- logger.info(`[${tab.name}] Context window warning — will summarize on next turn`);
220
- }
221
- else if (contextAction === 'checkpoint') {
222
- checkpointTriggered = true;
223
- logger.warn(`[${tab.name}] Context window checkpoint triggered`);
224
- }
225
- }
226
- for (const block of assistant.message.content) {
227
- if (block.type === 'text') {
228
- resultText += block.text;
229
- onTextChunk?.(block.text);
230
- }
231
- else if (block.type === 'tool_use') {
232
- const toolUse = block;
233
- onToolUse?.(toolUse.name, toolUse.input);
234
- const action = breaker.recordToolCall(toolUse);
235
- if (action === 'break') {
236
- logger.warn(`[${tab.name}] Circuit breaker tripped, killing subprocess`);
237
- subprocess.kill();
238
- }
239
- else if (action === 'notify') {
240
- this.onNotify?.(`Loop detected in tab "${tab.name}": ${toolUse.name} repeated 10+ times. Send /stop ${tab.name} to kill it.`).catch((err) => logger.warn('Notify failed:', err));
241
- }
242
- else if (action === 'warn') {
243
- loopWarningPending = true;
244
- }
245
- }
246
- }
247
- }
248
- else if (event.type === 'result') {
249
- resultEvent = event;
250
- }
251
- },
252
- onExit: (code) => {
253
- if (errored) {
254
- // onError already rejected and drained the queue; don't double-dispatch.
255
- return;
256
- }
257
- this.subprocesses.delete(tab.name);
258
- const result = {
259
- text: resultEvent?.result ?? resultText,
260
- costUsd: resultEvent?.total_cost_usd ?? 0,
261
- durationMs: resultEvent?.duration_ms ?? 0,
262
- sessionId: subprocess.sessionId,
263
- error: resultEvent?.is_error ?? code !== 0,
264
- };
265
- // Handle resume failure (Claude Code session cache rotated / never existed / expired).
266
- if (StaleSessionDetector.isStale(result, resultEvent, shouldResume, retryDepth)) {
267
- const detail = resultEvent?.errors?.[0] ?? result.text.split('\n')[0];
268
- logger.warn(`[${tab.name}] Resume session ${tab.sessionId} unavailable in Claude Code cache (${detail}). Retrying with fresh session.`);
269
- const recovery = StaleSessionDetector.buildRecovery(tab.id, enrichedPrompt, db);
270
- db.prepare('UPDATE tabs SET session_id = ?, status = ? WHERE id = ?').run(recovery.newSessionId, 'idle', tab.id);
271
- this.executeMessage({ ...tab, sessionId: recovery.newSessionId }, recovery.contextPrompt, false, onTextChunk, onToolUse, compactionDepth, true, retryDepth + 1)
272
- .then(resolve)
273
- .catch(reject);
274
- return;
275
- }
276
- // Silent network-stall recovery. The startup watchdog killed a
277
- // subprocess that never emitted a single event — almost always a
278
- // transient overnight DNS/connection blip that recovers within
279
- // seconds. Mirror the stale-session retry above (capped by retryDepth)
280
- // so a blip recovers inside a single fire instead of burning the full
281
- // maxRuntime and counting toward the 5-failure auto-disable.
282
- if (subprocess.killReason === 'silent') {
283
- if (retryDepth === 0) {
284
- logger.warn(`[${tab.name}] Subprocess produced no output (suspected transient network stall) — retrying once.`);
285
- // Don't touch the session — the subprocess may never have
286
- // initialized one. Retry with identical args.
287
- this.executeMessage(tab, prompt, resume, onTextChunk, onToolUse, compactionDepth, forceFresh, retryDepth + 1)
288
- .then(resolve)
289
- .catch(reject);
290
- return;
291
- }
292
- // Retry already attempted and it stalled again — surface a
293
- // network-specific failure instead of an empty "(no output)" so the
294
- // user checks connectivity, not their auth/subscription. Resolve
295
- // (not reject) with error:true; the scheduler counts this toward
296
- // auto-disable exactly like the maxRuntime path, and reports it via
297
- // the existing task-failure notification.
298
- logger.warn(`[${tab.name}] Still no output after retry — surfacing suspected network stall.`);
299
- db.prepare('UPDATE tabs SET status = ?, last_activity_at = ?, pid = NULL WHERE name = ?').run('idle', new Date().toISOString(), tab.name);
300
- logActivity('task_failed', 'Subprocess silent stall (retry exhausted)', {
301
- tabName: tab.name,
302
- });
303
- resolve({
304
- text: 'Subprocess produced no output, even after a retry — suspected network/DNS stall. Check connectivity.',
305
- costUsd: 0,
306
- durationMs: 0,
307
- sessionId: subprocess.sessionId,
308
- error: true,
309
- });
310
- this.processNextInQueue(tab.name);
311
- return;
312
- }
313
- // Store assistant response. Skip empty content (typically failed/error runs) so
314
- // it doesn't trigger the hasDbHistory shouldResume override on future calls.
315
- if (result.text.trim() !== '') {
316
- db.prepare('INSERT INTO messages (tab_id, role, content, cost_usd, tokens_in, tokens_out) VALUES (?, ?, ?, ?, ?, ?)').run(tab.id, 'assistant', result.text, result.costUsd, resultEvent?.usage?.input_tokens ?? null, resultEvent?.usage?.output_tokens ?? null);
317
- // Keep the budget cache in sync with the row we just inserted —
318
- // avoids the per-message SUM scan in the next budget check.
319
- if (this.tabCostCache.has(tab.id)) {
320
- this.tabCostCache.set(tab.id, (this.tabCostCache.get(tab.id) ?? 0) + (result.costUsd || 0));
321
- }
322
- }
323
- // Update tab
324
- db.prepare('UPDATE tabs SET status = ?, last_activity_at = ?, pid = NULL WHERE name = ?').run('idle', new Date().toISOString(), tab.name);
325
- logActivity('task_completed', 'Message processed', {
326
- tabName: tab.name,
327
- durationMs: result.durationMs,
328
- costUsd: result.costUsd,
329
- details: result.text.slice(0, 500),
330
- });
331
- // Context window compaction: if checkpoint was triggered, restart with summary.
332
- const currentDepth = compactionDepth ?? 0;
333
- if (checkpointTriggered &&
334
- !result.error &&
335
- result.text &&
336
- currentDepth < ContextCompactor.MAX_DEPTH) {
337
- // Compactor handles its own try/catch and always returns a SendResult.
338
- // The queue drain MUST happen regardless of compaction outcome — the
339
- // previous pattern omitted it in this branch, so a queued message
340
- // could wait indefinitely while compaction recursion was in flight.
341
- ContextCompactor.compact(tab, enrichedPrompt, result, onTextChunk, currentDepth, (tabName, p, opts) => this.sendMessage(tabName, p, opts), this.onNotify, db)
342
- .then(resolve)
343
- .finally(() => this.processNextInQueue(tab.name));
344
- return;
345
- }
346
- // Check for delegation completion
347
- try {
348
- const delegation = completeDelegation(tab.name, result.text);
349
- if (delegation && delegation.returnToTab) {
350
- PendingMessageStore.enqueueDelegationResult(delegation.returnToTab, `[Result from tab:${tab.name}]: ${result.text.slice(0, 10000)}`);
351
- logActivity('delegation_completed', `Delegation ${tab.name} → ${delegation.returnToTab}`, {
352
- tabName: tab.name,
353
- details: result.text.slice(0, 500),
354
- });
355
- this.onNotify?.(`Delegation complete: ${tab.name} → result sent back to ${delegation.returnToTab}`).catch(() => { });
356
- logger.info(`Delegation result sent: ${tab.name} → ${delegation.returnToTab}`);
357
- }
358
- }
359
- catch (err) {
360
- logger.warn('Delegation completion check failed:', err);
361
- }
362
- resolve(result);
363
- // Process next queued message (prepend loop warning if needed)
364
- if (loopWarningPending) {
365
- const next = this.queue.peek(tab.name);
366
- if (next) {
367
- next.prompt = `[WARNING: You appear to be repeating the same action. Reassess your approach.]\n\n${next.prompt}`;
368
- loopWarningPending = false;
369
- }
370
- }
371
- this.processNextInQueue(tab.name);
372
- },
373
- onError: (err) => {
374
- errored = true;
375
- this.subprocesses.delete(tab.name);
376
- this.updateTabStatus(tab.name, 'error');
377
- logActivity('task_failed', 'Message failed', {
378
- tabName: tab.name,
379
- details: err instanceof Error ? err.message : String(err),
380
- });
381
- reject(err);
382
- this.processNextInQueue(tab.name);
383
- },
384
- };
385
- subprocess.send(enrichedPrompt, callbacks, shouldResume).catch((err) => {
386
- if (!errored) {
387
- errored = true;
388
- this.subprocesses.delete(tab.name);
389
- this.updateTabStatus(tab.name, 'error');
390
- reject(err);
391
- this.processNextInQueue(tab.name);
392
- }
393
- });
394
- });
395
- }
396
- processNextInQueue(tabName) {
397
- const next = this.queue.dequeue(tabName);
398
- if (!next)
399
- return;
400
- const tab = this.getTab(tabName);
401
- if (!tab) {
402
- next.reject(new Error(`Tab "${tabName}" not found`));
403
- return;
404
- }
405
- this.executeMessage(tab, next.prompt, false).then(next.resolve).catch(next.reject);
406
- }
407
- updateTabStatus(tabName, status) {
408
- TabStore.setStatus(tabName, status);
409
- }
410
- clearQueue(tabName) {
411
- const dropped = this.queue.clear(tabName);
412
- for (const item of dropped) {
413
- item.reject(new Error(`Tab "${tabName}" was stopped`));
414
- }
415
- }
416
- }
@@ -1,29 +0,0 @@
1
- /**
2
- * Small store wrapping the `memories` table. Used by the MCP and dashboard
3
- * layers instead of inline `INSERT INTO memories` so schema details stay in
4
- * one place.
5
- */
6
- import type Database from 'better-sqlite3';
7
- export interface MemoryRow {
8
- id: number;
9
- content: string;
10
- tabName: string | null;
11
- source: string;
12
- createdAt: string;
13
- }
14
- export declare const MemoryStore: {
15
- add(content: string, opts?: {
16
- tabName?: string | null;
17
- source?: string;
18
- }, db?: Database.Database): void;
19
- delete(id: number | string, db?: Database.Database): boolean;
20
- count(db?: Database.Database): number;
21
- list(opts?: {
22
- limit?: number;
23
- offset?: number;
24
- query?: string;
25
- }, db?: Database.Database): {
26
- memories: MemoryRow[];
27
- total: number;
28
- };
29
- };
@@ -1,45 +0,0 @@
1
- /**
2
- * Small store wrapping the `memories` table. Used by the MCP and dashboard
3
- * layers instead of inline `INSERT INTO memories` so schema details stay in
4
- * one place.
5
- */
6
- import { getDb } from '../db/index.js';
7
- function rowToMemory(r) {
8
- return {
9
- id: r.id,
10
- content: r.content,
11
- tabName: r.tab_name,
12
- source: r.source,
13
- createdAt: r.created_at,
14
- };
15
- }
16
- export const MemoryStore = {
17
- add(content, opts, db = getDb()) {
18
- db.prepare('INSERT INTO memories (content, tab_name, source) VALUES (?, ?, ?)').run(content, opts?.tabName ?? null, opts?.source ?? 'tool');
19
- },
20
- delete(id, db = getDb()) {
21
- const result = db.prepare('DELETE FROM memories WHERE id = ?').run(id);
22
- return result.changes > 0;
23
- },
24
- count(db = getDb()) {
25
- return db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
26
- },
27
- list(opts = {}, db = getDb()) {
28
- const limit = opts.limit ?? 50;
29
- const offset = opts.offset ?? 0;
30
- if (opts.query) {
31
- const rows = db
32
- .prepare('SELECT id, content, tab_name, source, created_at FROM memories WHERE content LIKE ? ORDER BY created_at DESC LIMIT ? OFFSET ?')
33
- .all(`%${opts.query}%`, limit, offset);
34
- const total = db
35
- .prepare('SELECT COUNT(*) as c FROM memories WHERE content LIKE ?')
36
- .get(`%${opts.query}%`).c;
37
- return { memories: rows.map(rowToMemory), total };
38
- }
39
- const rows = db
40
- .prepare('SELECT id, content, tab_name, source, created_at FROM memories ORDER BY created_at DESC LIMIT ? OFFSET ?')
41
- .all(limit, offset);
42
- const total = db.prepare('SELECT COUNT(*) as c FROM memories').get().c;
43
- return { memories: rows.map(rowToMemory), total };
44
- },
45
- };
@@ -1,28 +0,0 @@
1
- /**
2
- * Per-tab FIFO queue for messages waiting on a busy subprocess.
3
- *
4
- * Extracted from TabManager so the queue's "max size, enqueue, dequeue,
5
- * peek-and-mutate-next, clear-on-stop" semantics are self-contained and
6
- * unit-testable without spinning up a real subprocess.
7
- */
8
- import type { SendResult } from './manager.js';
9
- export declare const MAX_QUEUE_SIZE = 10;
10
- export interface QueuedMessage {
11
- prompt: string;
12
- resolve: (r: SendResult) => void;
13
- reject: (e: Error) => void;
14
- }
15
- export declare class MessageQueue {
16
- private queues;
17
- /** Attempt to enqueue. Returns false if the per-tab queue is full. */
18
- enqueue(tabName: string, msg: QueuedMessage): boolean;
19
- /** Remove and return the next message. Drops the per-tab entry when empty. */
20
- dequeue(tabName: string): QueuedMessage | undefined;
21
- /** Peek at the next queued message without removing it (used to inject loop warning prefix). */
22
- peek(tabName: string): QueuedMessage | undefined;
23
- size(tabName: string): number;
24
- /** Drain the queue for a tab, returning the dropped messages so callers can reject them. */
25
- clear(tabName: string): QueuedMessage[];
26
- /** Drain every queue. Returns the dropped messages grouped by tab. */
27
- clearAll(): QueuedMessage[][];
28
- }