@synergenius/flow-weaver-pack-weaver 0.9.0 → 0.9.4

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 (235) hide show
  1. package/dist/bot/ai-client.d.ts +22 -2
  2. package/dist/bot/ai-client.d.ts.map +1 -1
  3. package/dist/bot/ai-client.js +168 -20
  4. package/dist/bot/ai-client.js.map +1 -1
  5. package/dist/bot/ansi.d.ts +13 -0
  6. package/dist/bot/ansi.d.ts.map +1 -0
  7. package/dist/bot/ansi.js +13 -0
  8. package/dist/bot/ansi.js.map +1 -0
  9. package/dist/bot/assistant-core.d.ts +25 -0
  10. package/dist/bot/assistant-core.d.ts.map +1 -0
  11. package/dist/bot/assistant-core.js +272 -0
  12. package/dist/bot/assistant-core.js.map +1 -0
  13. package/dist/bot/assistant-tools.d.ts +10 -0
  14. package/dist/bot/assistant-tools.d.ts.map +1 -0
  15. package/dist/bot/assistant-tools.js +324 -0
  16. package/dist/bot/assistant-tools.js.map +1 -0
  17. package/dist/bot/audit-logger.d.ts.map +1 -1
  18. package/dist/bot/audit-logger.js +9 -5
  19. package/dist/bot/audit-logger.js.map +1 -1
  20. package/dist/bot/bot-manager.d.ts +49 -0
  21. package/dist/bot/bot-manager.d.ts.map +1 -0
  22. package/dist/bot/bot-manager.js +279 -0
  23. package/dist/bot/bot-manager.js.map +1 -0
  24. package/dist/bot/child-process-tracker.d.ts +6 -0
  25. package/dist/bot/child-process-tracker.d.ts.map +1 -0
  26. package/dist/bot/child-process-tracker.js +35 -0
  27. package/dist/bot/child-process-tracker.js.map +1 -0
  28. package/dist/bot/cli-provider.d.ts.map +1 -1
  29. package/dist/bot/cli-provider.js +13 -8
  30. package/dist/bot/cli-provider.js.map +1 -1
  31. package/dist/bot/conversation-store.d.ts +40 -0
  32. package/dist/bot/conversation-store.d.ts.map +1 -0
  33. package/dist/bot/conversation-store.js +182 -0
  34. package/dist/bot/conversation-store.js.map +1 -0
  35. package/dist/bot/error-classifier.d.ts +27 -0
  36. package/dist/bot/error-classifier.d.ts.map +1 -0
  37. package/dist/bot/error-classifier.js +71 -0
  38. package/dist/bot/error-classifier.js.map +1 -0
  39. package/dist/bot/error-guide.d.ts +5 -0
  40. package/dist/bot/error-guide.d.ts.map +1 -0
  41. package/dist/bot/error-guide.js +5 -0
  42. package/dist/bot/error-guide.js.map +1 -0
  43. package/dist/bot/knowledge-store.d.ts +17 -0
  44. package/dist/bot/knowledge-store.d.ts.map +1 -0
  45. package/dist/bot/knowledge-store.js +53 -0
  46. package/dist/bot/knowledge-store.js.map +1 -0
  47. package/dist/bot/paths.d.ts +11 -0
  48. package/dist/bot/paths.d.ts.map +1 -0
  49. package/dist/bot/paths.js +26 -0
  50. package/dist/bot/paths.js.map +1 -0
  51. package/dist/bot/retry-utils.d.ts +5 -0
  52. package/dist/bot/retry-utils.d.ts.map +1 -0
  53. package/dist/bot/retry-utils.js +5 -0
  54. package/dist/bot/retry-utils.js.map +1 -0
  55. package/dist/bot/runner.d.ts.map +1 -1
  56. package/dist/bot/runner.js +12 -1
  57. package/dist/bot/runner.js.map +1 -1
  58. package/dist/bot/safety.d.ts +10 -0
  59. package/dist/bot/safety.d.ts.map +1 -0
  60. package/dist/bot/safety.js +14 -0
  61. package/dist/bot/safety.js.map +1 -0
  62. package/dist/bot/session-state.d.ts.map +1 -1
  63. package/dist/bot/session-state.js +3 -1
  64. package/dist/bot/session-state.js.map +1 -1
  65. package/dist/bot/steering.js +2 -2
  66. package/dist/bot/steering.js.map +1 -1
  67. package/dist/bot/step-executor.d.ts +10 -5
  68. package/dist/bot/step-executor.d.ts.map +1 -1
  69. package/dist/bot/step-executor.js +252 -3
  70. package/dist/bot/step-executor.js.map +1 -1
  71. package/dist/bot/system-prompt.d.ts +1 -1
  72. package/dist/bot/system-prompt.d.ts.map +1 -1
  73. package/dist/bot/system-prompt.js +69 -43
  74. package/dist/bot/system-prompt.js.map +1 -1
  75. package/dist/bot/task-decomposer.d.ts +24 -0
  76. package/dist/bot/task-decomposer.d.ts.map +1 -0
  77. package/dist/bot/task-decomposer.js +75 -0
  78. package/dist/bot/task-decomposer.js.map +1 -0
  79. package/dist/bot/task-queue.d.ts +17 -4
  80. package/dist/bot/task-queue.d.ts.map +1 -1
  81. package/dist/bot/task-queue.js +83 -5
  82. package/dist/bot/task-queue.js.map +1 -1
  83. package/dist/bot/terminal-renderer.d.ts +60 -0
  84. package/dist/bot/terminal-renderer.d.ts.map +1 -0
  85. package/dist/bot/terminal-renderer.js +204 -0
  86. package/dist/bot/terminal-renderer.js.map +1 -0
  87. package/dist/bot/tool-registry.d.ts +24 -0
  88. package/dist/bot/tool-registry.d.ts.map +1 -0
  89. package/dist/bot/tool-registry.js +458 -0
  90. package/dist/bot/tool-registry.js.map +1 -0
  91. package/dist/bot/types.d.ts +7 -0
  92. package/dist/bot/types.d.ts.map +1 -1
  93. package/dist/bot/weaver-tools.d.ts +18 -0
  94. package/dist/bot/weaver-tools.d.ts.map +1 -0
  95. package/dist/bot/weaver-tools.js +124 -0
  96. package/dist/bot/weaver-tools.js.map +1 -0
  97. package/dist/cli-bridge.d.ts.map +1 -1
  98. package/dist/cli-bridge.js +5 -1
  99. package/dist/cli-bridge.js.map +1 -1
  100. package/dist/cli-handlers.d.ts +13 -1
  101. package/dist/cli-handlers.d.ts.map +1 -1
  102. package/dist/cli-handlers.js +615 -48
  103. package/dist/cli-handlers.js.map +1 -1
  104. package/dist/mcp-tools.js +2 -2
  105. package/dist/mcp-tools.js.map +1 -1
  106. package/dist/node-types/abort-task.d.ts.map +1 -1
  107. package/dist/node-types/abort-task.js +4 -3
  108. package/dist/node-types/abort-task.js.map +1 -1
  109. package/dist/node-types/agent-execute.d.ts +38 -0
  110. package/dist/node-types/agent-execute.d.ts.map +1 -0
  111. package/dist/node-types/agent-execute.js +252 -0
  112. package/dist/node-types/agent-execute.js.map +1 -0
  113. package/dist/node-types/bot-report.d.ts +5 -3
  114. package/dist/node-types/bot-report.d.ts.map +1 -1
  115. package/dist/node-types/bot-report.js +39 -7
  116. package/dist/node-types/bot-report.js.map +1 -1
  117. package/dist/node-types/build-context.d.ts +3 -3
  118. package/dist/node-types/build-context.d.ts.map +1 -1
  119. package/dist/node-types/build-context.js +108 -24
  120. package/dist/node-types/build-context.js.map +1 -1
  121. package/dist/node-types/detect-provider.d.ts +2 -2
  122. package/dist/node-types/detect-provider.d.ts.map +1 -1
  123. package/dist/node-types/detect-provider.js +3 -1
  124. package/dist/node-types/detect-provider.js.map +1 -1
  125. package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
  126. package/dist/node-types/exec-validate-retry.js +43 -6
  127. package/dist/node-types/exec-validate-retry.js.map +1 -1
  128. package/dist/node-types/execute-plan.d.ts.map +1 -1
  129. package/dist/node-types/execute-plan.js +31 -8
  130. package/dist/node-types/execute-plan.js.map +1 -1
  131. package/dist/node-types/execute-target.d.ts.map +1 -1
  132. package/dist/node-types/execute-target.js +3 -1
  133. package/dist/node-types/execute-target.js.map +1 -1
  134. package/dist/node-types/fix-errors.d.ts.map +1 -1
  135. package/dist/node-types/fix-errors.js +21 -5
  136. package/dist/node-types/fix-errors.js.map +1 -1
  137. package/dist/node-types/genesis-observe.d.ts.map +1 -1
  138. package/dist/node-types/genesis-observe.js +3 -1
  139. package/dist/node-types/genesis-observe.js.map +1 -1
  140. package/dist/node-types/genesis-report.js +4 -1
  141. package/dist/node-types/genesis-report.js.map +1 -1
  142. package/dist/node-types/git-ops.d.ts.map +1 -1
  143. package/dist/node-types/git-ops.js +98 -4
  144. package/dist/node-types/git-ops.js.map +1 -1
  145. package/dist/node-types/index.d.ts +2 -0
  146. package/dist/node-types/index.d.ts.map +1 -1
  147. package/dist/node-types/index.js +2 -0
  148. package/dist/node-types/index.js.map +1 -1
  149. package/dist/node-types/load-config.d.ts +2 -2
  150. package/dist/node-types/load-config.d.ts.map +1 -1
  151. package/dist/node-types/load-config.js.map +1 -1
  152. package/dist/node-types/plan-task.d.ts.map +1 -1
  153. package/dist/node-types/plan-task.js +14 -2
  154. package/dist/node-types/plan-task.js.map +1 -1
  155. package/dist/node-types/read-workflow.js +8 -2
  156. package/dist/node-types/read-workflow.js.map +1 -1
  157. package/dist/node-types/receive-task.d.ts.map +1 -1
  158. package/dist/node-types/receive-task.js +35 -26
  159. package/dist/node-types/receive-task.js.map +1 -1
  160. package/dist/node-types/send-notify.js +2 -1
  161. package/dist/node-types/send-notify.js.map +1 -1
  162. package/dist/node-types/validate-gate.d.ts +18 -0
  163. package/dist/node-types/validate-gate.d.ts.map +1 -0
  164. package/dist/node-types/validate-gate.js +96 -0
  165. package/dist/node-types/validate-gate.js.map +1 -0
  166. package/dist/workflows/genesis-task.d.ts +20 -12
  167. package/dist/workflows/genesis-task.d.ts.map +1 -1
  168. package/dist/workflows/genesis-task.js +20 -12
  169. package/dist/workflows/genesis-task.js.map +1 -1
  170. package/dist/workflows/weaver-agent.d.ts +35 -0
  171. package/dist/workflows/weaver-agent.d.ts.map +1 -0
  172. package/dist/workflows/weaver-agent.js +777 -0
  173. package/dist/workflows/weaver-agent.js.map +1 -0
  174. package/dist/workflows/weaver-bot-batch.d.ts +19 -26
  175. package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
  176. package/dist/workflows/weaver-bot-batch.js +1043 -27
  177. package/dist/workflows/weaver-bot-batch.js.map +1 -1
  178. package/dist/workflows/weaver-bot.d.ts +21 -35
  179. package/dist/workflows/weaver-bot.d.ts.map +1 -1
  180. package/dist/workflows/weaver-bot.js +1119 -36
  181. package/dist/workflows/weaver-bot.js.map +1 -1
  182. package/flowweaver.manifest.json +21 -1
  183. package/package.json +5 -2
  184. package/src/bot/ai-client.ts +180 -19
  185. package/src/bot/ansi.ts +12 -0
  186. package/src/bot/assistant-core.ts +312 -0
  187. package/src/bot/assistant-tools.ts +318 -0
  188. package/src/bot/audit-logger.ts +6 -5
  189. package/src/bot/bot-manager.ts +293 -0
  190. package/src/bot/child-process-tracker.ts +40 -0
  191. package/src/bot/cli-provider.ts +13 -8
  192. package/src/bot/conversation-store.ts +222 -0
  193. package/src/bot/error-classifier.ts +90 -0
  194. package/src/bot/error-guide.ts +4 -0
  195. package/src/bot/knowledge-store.ts +59 -0
  196. package/src/bot/paths.ts +27 -0
  197. package/src/bot/retry-utils.ts +4 -0
  198. package/src/bot/runner.ts +12 -1
  199. package/src/bot/safety.ts +16 -0
  200. package/src/bot/session-state.ts +2 -1
  201. package/src/bot/steering.ts +2 -2
  202. package/src/bot/step-executor.ts +313 -5
  203. package/src/bot/system-prompt.ts +70 -47
  204. package/src/bot/task-decomposer.ts +100 -0
  205. package/src/bot/task-queue.ts +100 -8
  206. package/src/bot/terminal-renderer.ts +238 -0
  207. package/src/bot/tool-registry.ts +477 -0
  208. package/src/bot/types.ts +8 -0
  209. package/src/bot/weaver-tools.ts +134 -0
  210. package/src/cli-bridge.ts +7 -1
  211. package/src/cli-handlers.ts +624 -48
  212. package/src/mcp-tools.ts +2 -2
  213. package/src/node-types/abort-task.ts +5 -4
  214. package/src/node-types/agent-execute.ts +303 -0
  215. package/src/node-types/bot-report.ts +40 -9
  216. package/src/node-types/build-context.ts +112 -25
  217. package/src/node-types/detect-provider.ts +4 -3
  218. package/src/node-types/exec-validate-retry.ts +47 -8
  219. package/src/node-types/execute-plan.ts +32 -8
  220. package/src/node-types/execute-target.ts +2 -1
  221. package/src/node-types/fix-errors.ts +20 -5
  222. package/src/node-types/genesis-observe.ts +2 -1
  223. package/src/node-types/genesis-report.ts +1 -1
  224. package/src/node-types/git-ops.ts +93 -4
  225. package/src/node-types/index.ts +2 -0
  226. package/src/node-types/load-config.ts +3 -3
  227. package/src/node-types/plan-task.ts +15 -3
  228. package/src/node-types/read-workflow.ts +2 -2
  229. package/src/node-types/receive-task.ts +31 -26
  230. package/src/node-types/send-notify.ts +1 -1
  231. package/src/node-types/validate-gate.ts +112 -0
  232. package/src/workflows/genesis-task.ts +20 -12
  233. package/src/workflows/weaver-agent.ts +799 -0
  234. package/src/workflows/weaver-bot-batch.ts +1049 -27
  235. package/src/workflows/weaver-bot.ts +1123 -36
@@ -0,0 +1,293 @@
1
+ /**
2
+ * Bot Manager — spawns and manages multiple weaver bot sessions
3
+ * as separate processes. Each bot has its own queue, steering file,
4
+ * and output log under ~/.weaver/bots/{name}/.
5
+ */
6
+
7
+ import { spawn, execFileSync, type ChildProcess } from 'node:child_process';
8
+ import * as fs from 'node:fs';
9
+ import * as path from 'node:path';
10
+ import * as os from 'node:os';
11
+ import { TaskQueue } from './task-queue.js';
12
+ import { SteeringController } from './steering.js';
13
+
14
+ export interface ManagedBot {
15
+ name: string;
16
+ pid: number;
17
+ projectDir: string;
18
+ botDir: string;
19
+ startedAt: number;
20
+ status: 'running' | 'paused' | 'stopped';
21
+ }
22
+
23
+ export interface SpawnOpts {
24
+ projectDir: string;
25
+ parallel?: number;
26
+ deadline?: string;
27
+ autoApprove?: boolean;
28
+ /** Git branch for commits (creates if needed). Keeps main clean for overnight runs. */
29
+ branch?: string;
30
+ }
31
+
32
+ const BOTS_DIR = path.join(os.homedir(), '.weaver', 'bots');
33
+
34
+ export class BotManager {
35
+ private bots = new Map<string, { meta: ManagedBot; process: ChildProcess }>();
36
+
37
+ constructor() {
38
+ // Ensure base dir exists
39
+ fs.mkdirSync(BOTS_DIR, { recursive: true });
40
+
41
+ // Clean up on process exit
42
+ const cleanup = () => this.cleanup();
43
+ process.on('exit', cleanup);
44
+ process.on('SIGTERM', cleanup);
45
+ }
46
+
47
+ spawn(name: string, opts: SpawnOpts): ManagedBot {
48
+ if (this.bots.has(name)) {
49
+ throw new Error(`Bot "${name}" already exists. Stop it first or use a different name.`);
50
+ }
51
+
52
+ const botDir = path.join(BOTS_DIR, name);
53
+ fs.mkdirSync(botDir, { recursive: true });
54
+
55
+ // Create git branch if specified (keeps main clean for overnight runs)
56
+ if (opts.branch) {
57
+ try {
58
+ execFileSync('git', ['checkout', '-B', opts.branch], { cwd: opts.projectDir, encoding: 'utf-8', stdio: 'pipe' });
59
+ } catch { /* branch may already exist */ }
60
+ }
61
+
62
+ const logPath = path.join(botDir, 'output.log');
63
+ // Touch the file synchronously so it exists immediately after spawn() returns
64
+ fs.writeFileSync(logPath, '', { flag: 'a' });
65
+ const logStream = fs.createWriteStream(logPath, { flags: 'a' });
66
+
67
+ const sessionArgs = [
68
+ 'flow-weaver', 'weaver', 'session',
69
+ '--continuous',
70
+ '--project-dir', opts.projectDir,
71
+ ];
72
+ if (opts.autoApprove !== false) sessionArgs.push('--auto-approve');
73
+ if (opts.parallel && opts.parallel > 1) sessionArgs.push('--parallel', String(opts.parallel));
74
+ if (opts.deadline) sessionArgs.push('--until', opts.deadline);
75
+
76
+ // Prevent system sleep during long runs (cross-platform)
77
+ const { cmd, args } = wrapWithSleepInhibitor('npx', sessionArgs);
78
+
79
+ // Set queue/steering to bot-specific paths
80
+ const env = {
81
+ ...process.env,
82
+ WEAVER_QUEUE_DIR: botDir,
83
+ WEAVER_STEERING_DIR: botDir,
84
+ };
85
+
86
+ const child = spawn(cmd, args, {
87
+ cwd: opts.projectDir,
88
+ env,
89
+ stdio: ['ignore', 'pipe', 'pipe'],
90
+ detached: false,
91
+ });
92
+
93
+ // Capture output to log file
94
+ child.stdout?.pipe(logStream);
95
+ child.stderr?.pipe(logStream);
96
+
97
+ const meta: ManagedBot = {
98
+ name,
99
+ pid: child.pid ?? 0,
100
+ projectDir: opts.projectDir,
101
+ botDir,
102
+ startedAt: Date.now(),
103
+ status: 'running',
104
+ };
105
+
106
+ // Write metadata for persistence
107
+ fs.writeFileSync(path.join(botDir, 'meta.json'), JSON.stringify(meta, null, 2));
108
+
109
+ child.on('exit', (code) => {
110
+ const bot = this.bots.get(name);
111
+ if (bot) {
112
+ bot.meta.status = 'stopped';
113
+ fs.writeFileSync(path.join(botDir, 'meta.json'), JSON.stringify(bot.meta, null, 2));
114
+ }
115
+ logStream.write(`\n[bot-manager] Process exited with code ${code}\n`);
116
+ logStream.end();
117
+ });
118
+
119
+ this.bots.set(name, { meta, process: child });
120
+ return meta;
121
+ }
122
+
123
+ list(): ManagedBot[] {
124
+ this.discoverExistingBots();
125
+ // Health check: update status of bots that died
126
+ for (const [, bot] of this.bots) {
127
+ if (bot.meta.status === 'running' && !this.isAlive(bot.meta)) {
128
+ bot.meta.status = 'stopped';
129
+ try {
130
+ fs.writeFileSync(path.join(bot.meta.botDir, 'meta.json'), JSON.stringify(bot.meta, null, 2));
131
+ } catch { /* non-fatal */ }
132
+ }
133
+ }
134
+ return [...this.bots.values()].map(b => b.meta);
135
+ }
136
+
137
+ get(name: string): ManagedBot | null {
138
+ const bot = this.bots.get(name);
139
+ if (!bot) return null;
140
+ // Health check on access
141
+ if (bot.meta.status === 'running' && !this.isAlive(bot.meta)) {
142
+ bot.meta.status = 'stopped';
143
+ try {
144
+ fs.writeFileSync(path.join(bot.meta.botDir, 'meta.json'), JSON.stringify(bot.meta, null, 2));
145
+ } catch { /* non-fatal */ }
146
+ }
147
+ return bot.meta;
148
+ }
149
+
150
+ /** Check if a bot process is still alive. */
151
+ private isAlive(bot: ManagedBot): boolean {
152
+ if (!bot.pid || bot.pid === 0) return false;
153
+ try { process.kill(bot.pid, 0); return true; } catch { return false; }
154
+ }
155
+
156
+ getQueue(name: string): TaskQueue {
157
+ const bot = this.bots.get(name);
158
+ if (!bot) throw new Error(`Bot "${name}" not found.`);
159
+ return new TaskQueue(bot.meta.botDir);
160
+ }
161
+
162
+ getSteering(name: string): SteeringController {
163
+ const bot = this.bots.get(name);
164
+ if (!bot) throw new Error(`Bot "${name}" not found.`);
165
+ return new SteeringController(bot.meta.botDir);
166
+ }
167
+
168
+ async steer(name: string, command: 'pause' | 'resume' | 'cancel'): Promise<void> {
169
+ const steering = this.getSteering(name);
170
+ await steering.write({ command, timestamp: Date.now() });
171
+ if (command === 'pause') {
172
+ const bot = this.bots.get(name);
173
+ if (bot) bot.meta.status = 'paused';
174
+ } else if (command === 'resume') {
175
+ const bot = this.bots.get(name);
176
+ if (bot) bot.meta.status = 'running';
177
+ }
178
+ }
179
+
180
+ stop(name: string): void {
181
+ const bot = this.bots.get(name);
182
+ if (!bot) throw new Error(`Bot "${name}" not found.`);
183
+ // Send SIGTERM for graceful shutdown
184
+ if (bot.process.pid && !bot.process.killed) {
185
+ bot.process.kill('SIGTERM');
186
+ }
187
+ bot.meta.status = 'stopped';
188
+ }
189
+
190
+ kill(name: string): void {
191
+ const bot = this.bots.get(name);
192
+ if (!bot) throw new Error(`Bot "${name}" not found.`);
193
+ if (bot.process.pid && !bot.process.killed) {
194
+ bot.process.kill('SIGKILL');
195
+ }
196
+ bot.meta.status = 'stopped';
197
+ }
198
+
199
+ logs(name: string, lines = 50): string {
200
+ const bot = this.bots.get(name);
201
+ if (!bot) throw new Error(`Bot "${name}" not found.`);
202
+ const logPath = path.join(bot.meta.botDir, 'output.log');
203
+ if (!fs.existsSync(logPath)) return '(no logs yet)';
204
+ const content = fs.readFileSync(logPath, 'utf-8');
205
+ const allLines = content.split('\n');
206
+ return allLines.slice(-lines).join('\n');
207
+ }
208
+
209
+ cleanup(): void {
210
+ for (const [, bot] of this.bots) {
211
+ if (bot.process.pid && !bot.process.killed) {
212
+ try { bot.process.kill('SIGTERM'); } catch (err) {
213
+ if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] SIGTERM failed for bot: ${err}\n`);
214
+ }
215
+ }
216
+ }
217
+ }
218
+
219
+ /** Discover bots from disk that were spawned by a previous assistant session. */
220
+ private discoverExistingBots(): void {
221
+ if (!fs.existsSync(BOTS_DIR)) return;
222
+ for (const name of fs.readdirSync(BOTS_DIR)) {
223
+ if (this.bots.has(name)) continue;
224
+ const metaPath = path.join(BOTS_DIR, name, 'meta.json');
225
+ if (!fs.existsSync(metaPath)) continue;
226
+ try {
227
+ const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8')) as ManagedBot;
228
+ // Check if process is still running
229
+ if (meta.pid > 0) {
230
+ try {
231
+ process.kill(meta.pid, 0); // test if process exists
232
+ meta.status = 'running';
233
+ } catch {
234
+ meta.status = 'stopped';
235
+ }
236
+ }
237
+ // Store without a process handle (can only steer via file, not kill directly)
238
+ this.bots.set(name, { meta, process: null as unknown as ChildProcess });
239
+ } catch { /* corrupt meta */ }
240
+ }
241
+ }
242
+ }
243
+
244
+ /**
245
+ * Cross-platform sleep inhibitor. Wraps a command to prevent the OS from sleeping.
246
+ * - macOS: caffeinate -i -s
247
+ * - Linux: systemd-inhibit (if available)
248
+ * - Windows/other: no wrapper (runs command directly)
249
+ */
250
+ function wrapWithSleepInhibitor(command: string, args: string[]): { cmd: string; args: string[] } {
251
+ switch (process.platform) {
252
+ case 'darwin':
253
+ return { cmd: 'caffeinate', args: ['-i', '-s', command, ...args] };
254
+ case 'linux': {
255
+ // Check if systemd-inhibit is available
256
+ try {
257
+ execFileSync('which', ['systemd-inhibit'], { stdio: 'pipe' });
258
+ return {
259
+ cmd: 'systemd-inhibit',
260
+ args: ['--what=idle:sleep', '--who=weaver', '--why=Bot session running', command, ...args],
261
+ };
262
+ } catch {
263
+ return { cmd: command, args };
264
+ }
265
+ }
266
+ default:
267
+ return { cmd: command, args };
268
+ }
269
+ }
270
+
271
+ /**
272
+ * Cross-platform desktop notification.
273
+ * - macOS: osascript
274
+ * - Linux: notify-send (if available)
275
+ * - Windows: PowerShell toast (if available)
276
+ */
277
+ export function sendDesktopNotification(title: string, message: string): void {
278
+ try {
279
+ switch (process.platform) {
280
+ case 'darwin':
281
+ execFileSync('osascript', ['-e', `display notification "${message}" with title "${title}"`], { stdio: 'ignore' });
282
+ break;
283
+ case 'linux':
284
+ execFileSync('notify-send', [title, message], { stdio: 'ignore' });
285
+ break;
286
+ case 'win32':
287
+ execFileSync('powershell', ['-Command', `[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('${message}','${title}')`], { stdio: 'ignore' });
288
+ break;
289
+ }
290
+ } catch {
291
+ // Non-fatal — notification is best-effort
292
+ }
293
+ }
@@ -0,0 +1,40 @@
1
+ import type { ChildProcess } from 'node:child_process';
2
+
3
+ /**
4
+ * Tracks spawned child processes and kills them on SIGINT/SIGTERM.
5
+ * Prevents zombie `claude` processes when user hits Ctrl+C.
6
+ */
7
+
8
+ const activeChildren = new Set<ChildProcess>();
9
+ let signalHandlersInstalled = false;
10
+
11
+ function installSignalHandlers(): void {
12
+ if (signalHandlersInstalled) return;
13
+ signalHandlersInstalled = true;
14
+
15
+ const cleanup = () => {
16
+ for (const child of activeChildren) {
17
+ try {
18
+ child.kill('SIGTERM');
19
+ } catch { /* already dead */ }
20
+ }
21
+ activeChildren.clear();
22
+ };
23
+
24
+ process.on('SIGINT', cleanup);
25
+ process.on('SIGTERM', cleanup);
26
+ process.on('exit', cleanup);
27
+ }
28
+
29
+ /** Register a child process for cleanup on signal. */
30
+ export function trackChild(child: ChildProcess): void {
31
+ installSignalHandlers();
32
+ activeChildren.add(child);
33
+ child.on('exit', () => activeChildren.delete(child));
34
+ child.on('error', () => activeChildren.delete(child));
35
+ }
36
+
37
+ /** Get the count of active tracked children. */
38
+ export function activeChildCount(): number {
39
+ return activeChildren.size;
40
+ }
@@ -2,6 +2,7 @@ import { execSync, spawn } from 'node:child_process';
2
2
  import type { BotAgentProvider, OnUsageCallback, StreamChunk, ToolDefinition, ToolUseResult } from './types.js';
3
3
  import { buildSystemPrompt } from './system-prompt.js';
4
4
  import { parseStreamLine, extractTextFromChunks } from './cli-stream-parser.js';
5
+ import { trackChild } from './child-process-tracker.js';
5
6
 
6
7
  // Strip CLAUDECODE from child env so nested claude CLI invocations work.
7
8
  const childEnv = { ...process.env };
@@ -29,20 +30,20 @@ export class CliAgentProvider implements BotAgentProvider {
29
30
  ? request.context
30
31
  : JSON.stringify(request.context, null, 2);
31
32
 
32
- const fullPrompt = `${systemPrompt}\n\nContext:\n${contextStr}\n\nInstructions:\n${request.prompt}`;
33
+ const userPrompt = `Context:\n${contextStr}\n\nInstructions:\n${request.prompt}`;
33
34
 
34
35
  if (this.cli === 'claude-cli') {
35
36
  const chunks: StreamChunk[] = [];
36
- for await (const chunk of this.streamRaw(fullPrompt)) {
37
+ for await (const chunk of this.streamRaw(userPrompt, systemPrompt)) {
37
38
  chunks.push(chunk);
38
39
  }
39
40
  const raw = extractTextFromChunks(chunks);
40
41
  return this.parseJson(raw);
41
42
  }
42
43
 
43
- // copilot-cli: no stream-json support, keep execSync
44
+ // copilot-cli: no --system-prompt support, keep concatenated
44
45
  const raw = execSync('copilot -p --silent --allow-all-tools', {
45
- input: fullPrompt,
46
+ input: systemPrompt + '\n\n' + userPrompt,
46
47
  encoding: 'utf-8',
47
48
  stdio: ['pipe', 'pipe', 'pipe'],
48
49
  timeout: 120_000,
@@ -64,10 +65,10 @@ export class CliAgentProvider implements BotAgentProvider {
64
65
  ? request.context
65
66
  : JSON.stringify(request.context, null, 2);
66
67
 
67
- const fullPrompt = `${systemPrompt}\n\nContext:\n${contextStr}\n\nInstructions:\n${request.prompt}`;
68
+ const userPrompt = `Context:\n${contextStr}\n\nInstructions:\n${request.prompt}`;
68
69
 
69
70
  if (this.cli === 'claude-cli') {
70
- yield* this.streamRaw(fullPrompt);
71
+ yield* this.streamRaw(userPrompt, systemPrompt);
71
72
  } else {
72
73
  const result = await this.decide(request);
73
74
  yield { type: 'text', text: JSON.stringify(result) };
@@ -85,18 +86,22 @@ export class CliAgentProvider implements BotAgentProvider {
85
86
  return { result };
86
87
  }
87
88
 
88
- private async *streamRaw(fullPrompt: string): AsyncGenerator<StreamChunk> {
89
+ private async *streamRaw(userPrompt: string, systemPrompt?: string): AsyncGenerator<StreamChunk> {
89
90
  const args = ['-p', '--output-format', 'stream-json', '--include-partial-messages'];
90
91
  if (this.model) {
91
92
  args.push('--model', this.model);
92
93
  }
94
+ if (systemPrompt) {
95
+ args.push('--system-prompt', systemPrompt);
96
+ }
93
97
 
94
98
  const child = spawn('claude', args, {
95
99
  stdio: ['pipe', 'pipe', 'pipe'],
96
100
  env: childEnv,
97
101
  });
102
+ trackChild(child);
98
103
 
99
- child.stdin.write(fullPrompt);
104
+ child.stdin.write(userPrompt);
100
105
  child.stdin.end();
101
106
 
102
107
  const timeout = setTimeout(() => {
@@ -0,0 +1,222 @@
1
+ /**
2
+ * Conversation Store — persistent conversation history for the assistant.
3
+ * Mirrors the platform's aiConversations/aiChatMessages tables
4
+ * using file-based storage for CLI use.
5
+ *
6
+ * Layout:
7
+ * ~/.weaver/conversations/
8
+ * index.json # ConversationRecord[]
9
+ * {id}/messages.ndjson # StoredMessage[] (append-only)
10
+ */
11
+
12
+ import * as fs from 'node:fs';
13
+ import * as path from 'node:path';
14
+ import * as os from 'node:os';
15
+ import * as crypto from 'node:crypto';
16
+ import type { AgentMessage } from '@synergenius/flow-weaver/agent';
17
+ import { withFileLock } from './file-lock.js';
18
+ import { CHARS_PER_TOKEN } from './safety.js';
19
+
20
+ export interface ConversationRecord {
21
+ id: string;
22
+ title: string;
23
+ projectDir: string;
24
+ messageCount: number;
25
+ totalTokens: number;
26
+ createdAt: number;
27
+ lastMessageAt: number;
28
+ botIds: string[];
29
+ }
30
+
31
+ interface StoredMessage {
32
+ role: 'user' | 'assistant' | 'tool';
33
+ content: string;
34
+ toolCalls?: Array<{ id: string; name: string; arguments: Record<string, unknown> }>;
35
+ toolCallId?: string;
36
+ timestamp: number;
37
+ tokens?: number;
38
+ }
39
+
40
+ const MAX_INDEX_SIZE = 20;
41
+
42
+ export class ConversationStore {
43
+ private baseDir: string;
44
+ private indexPath: string;
45
+
46
+ constructor(baseDir?: string) {
47
+ this.baseDir = baseDir ?? path.join(os.homedir(), '.weaver', 'conversations');
48
+ this.indexPath = path.join(this.baseDir, 'index.json');
49
+ fs.mkdirSync(this.baseDir, { recursive: true });
50
+ }
51
+
52
+ create(projectDir: string): ConversationRecord {
53
+ const id = crypto.randomUUID().slice(0, 8);
54
+ const now = Date.now();
55
+ const record: ConversationRecord = {
56
+ id,
57
+ title: '',
58
+ projectDir,
59
+ messageCount: 0,
60
+ totalTokens: 0,
61
+ createdAt: now,
62
+ lastMessageAt: now,
63
+ botIds: [],
64
+ };
65
+
66
+ // Create conversation directory
67
+ const convDir = path.join(this.baseDir, id);
68
+ fs.mkdirSync(convDir, { recursive: true });
69
+
70
+ // Add to index (sync write — no lock needed for create, index is new or we're the only writer)
71
+ const index = this.readIndex();
72
+ index.unshift(record);
73
+ this.writeIndexUnsafe(index);
74
+
75
+ return record;
76
+ }
77
+
78
+ list(): ConversationRecord[] {
79
+ return this.readIndex();
80
+ }
81
+
82
+ get(id: string): ConversationRecord | null {
83
+ const index = this.readIndex();
84
+ return index.find(c => c.id === id) ?? null;
85
+ }
86
+
87
+ getMostRecent(): ConversationRecord | null {
88
+ const index = this.readIndex();
89
+ return index.length > 0 ? index[0] : null;
90
+ }
91
+
92
+ delete(id: string): void {
93
+ // Remove from index (sync — delete doesn't need lock)
94
+ const index = this.readIndex();
95
+ const filtered = index.filter(c => c.id !== id);
96
+ this.writeIndexUnsafe(filtered);
97
+
98
+ // Remove files
99
+ const convDir = path.join(this.baseDir, id);
100
+ if (fs.existsSync(convDir)) {
101
+ fs.rmSync(convDir, { recursive: true, force: true });
102
+ }
103
+ }
104
+
105
+ loadMessages(id: string): AgentMessage[] {
106
+ const msgPath = path.join(this.baseDir, id, 'messages.ndjson');
107
+ if (!fs.existsSync(msgPath)) return [];
108
+
109
+ const content = fs.readFileSync(msgPath, 'utf-8');
110
+ const messages: AgentMessage[] = [];
111
+
112
+ for (const line of content.split('\n')) {
113
+ if (!line.trim()) continue;
114
+ try {
115
+ const stored = JSON.parse(line) as StoredMessage;
116
+ const msg: AgentMessage = {
117
+ role: stored.role,
118
+ content: stored.content,
119
+ };
120
+ if (stored.toolCalls) msg.toolCalls = stored.toolCalls;
121
+ if (stored.toolCallId) msg.toolCallId = stored.toolCallId;
122
+ messages.push(msg);
123
+ } catch {
124
+ // Skip corrupt lines
125
+ }
126
+ }
127
+
128
+ return messages;
129
+ }
130
+
131
+ appendMessages(id: string, messages: AgentMessage[]): void {
132
+ if (messages.length === 0) return;
133
+
134
+ const convDir = path.join(this.baseDir, id);
135
+ fs.mkdirSync(convDir, { recursive: true });
136
+ const msgPath = path.join(convDir, 'messages.ndjson');
137
+
138
+ const lines = messages.map(m => {
139
+ const stored: StoredMessage = {
140
+ role: m.role,
141
+ content: typeof m.content === 'string' ? m.content : JSON.stringify(m.content),
142
+ timestamp: Date.now(),
143
+ tokens: Math.ceil((typeof m.content === 'string' ? m.content.length : JSON.stringify(m.content).length) / CHARS_PER_TOKEN),
144
+ };
145
+ if (m.toolCalls) stored.toolCalls = m.toolCalls;
146
+ if (m.toolCallId) stored.toolCallId = m.toolCallId;
147
+ return JSON.stringify(stored);
148
+ });
149
+
150
+ fs.appendFileSync(msgPath, lines.join('\n') + '\n');
151
+ }
152
+
153
+ async updateAfterTurn(id: string, newMessages: AgentMessage[], tokensUsed: number): Promise<void> {
154
+ await withFileLock(this.indexPath, () => {
155
+ const index = this.readIndex();
156
+ const record = index.find(c => c.id === id);
157
+ if (!record) return;
158
+
159
+ record.messageCount += newMessages.length;
160
+ record.totalTokens += tokensUsed;
161
+ record.lastMessageAt = Date.now();
162
+
163
+ // Move to front (most recent)
164
+ const idx = index.indexOf(record);
165
+ if (idx > 0) {
166
+ index.splice(idx, 1);
167
+ index.unshift(record);
168
+ }
169
+
170
+ // Cap index size
171
+ if (index.length > MAX_INDEX_SIZE) {
172
+ index.splice(MAX_INDEX_SIZE);
173
+ }
174
+
175
+ this.writeIndexUnsafe(index);
176
+ });
177
+ }
178
+
179
+ async setTitle(id: string, title: string): Promise<void> {
180
+ await withFileLock(this.indexPath, () => {
181
+ const index = this.readIndex();
182
+ const record = index.find(c => c.id === id);
183
+ if (record) {
184
+ record.title = title.slice(0, 80).replace(/\n/g, ' ').trim();
185
+ this.writeIndexUnsafe(index);
186
+ }
187
+ });
188
+ }
189
+
190
+ async addBotId(id: string, botId: string): Promise<void> {
191
+ await withFileLock(this.indexPath, () => {
192
+ const index = this.readIndex();
193
+ const record = index.find(c => c.id === id);
194
+ if (record && !record.botIds.includes(botId)) {
195
+ record.botIds.push(botId);
196
+ this.writeIndexUnsafe(index);
197
+ }
198
+ });
199
+ }
200
+
201
+ // --- Private ---
202
+
203
+ private readIndex(): ConversationRecord[] {
204
+ if (!fs.existsSync(this.indexPath)) return [];
205
+ try {
206
+ return JSON.parse(fs.readFileSync(this.indexPath, 'utf-8'));
207
+ } catch (err) {
208
+ if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] conversation index parse failed: ${err}\n`);
209
+ return [];
210
+ }
211
+ }
212
+
213
+ private async writeIndex(index: ConversationRecord[]): Promise<void> {
214
+ await withFileLock(this.indexPath, () => {
215
+ this.writeIndexUnsafe(index);
216
+ });
217
+ }
218
+
219
+ private writeIndexUnsafe(index: ConversationRecord[]): void {
220
+ fs.writeFileSync(this.indexPath, JSON.stringify(index, null, 2));
221
+ }
222
+ }