@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.
- package/dist/bot/ai-client.d.ts +22 -2
- package/dist/bot/ai-client.d.ts.map +1 -1
- package/dist/bot/ai-client.js +168 -20
- package/dist/bot/ai-client.js.map +1 -1
- package/dist/bot/ansi.d.ts +13 -0
- package/dist/bot/ansi.d.ts.map +1 -0
- package/dist/bot/ansi.js +13 -0
- package/dist/bot/ansi.js.map +1 -0
- package/dist/bot/assistant-core.d.ts +25 -0
- package/dist/bot/assistant-core.d.ts.map +1 -0
- package/dist/bot/assistant-core.js +272 -0
- package/dist/bot/assistant-core.js.map +1 -0
- package/dist/bot/assistant-tools.d.ts +10 -0
- package/dist/bot/assistant-tools.d.ts.map +1 -0
- package/dist/bot/assistant-tools.js +324 -0
- package/dist/bot/assistant-tools.js.map +1 -0
- package/dist/bot/audit-logger.d.ts.map +1 -1
- package/dist/bot/audit-logger.js +9 -5
- package/dist/bot/audit-logger.js.map +1 -1
- package/dist/bot/bot-manager.d.ts +49 -0
- package/dist/bot/bot-manager.d.ts.map +1 -0
- package/dist/bot/bot-manager.js +279 -0
- package/dist/bot/bot-manager.js.map +1 -0
- package/dist/bot/child-process-tracker.d.ts +6 -0
- package/dist/bot/child-process-tracker.d.ts.map +1 -0
- package/dist/bot/child-process-tracker.js +35 -0
- package/dist/bot/child-process-tracker.js.map +1 -0
- package/dist/bot/cli-provider.d.ts.map +1 -1
- package/dist/bot/cli-provider.js +13 -8
- package/dist/bot/cli-provider.js.map +1 -1
- package/dist/bot/conversation-store.d.ts +40 -0
- package/dist/bot/conversation-store.d.ts.map +1 -0
- package/dist/bot/conversation-store.js +182 -0
- package/dist/bot/conversation-store.js.map +1 -0
- package/dist/bot/error-classifier.d.ts +27 -0
- package/dist/bot/error-classifier.d.ts.map +1 -0
- package/dist/bot/error-classifier.js +71 -0
- package/dist/bot/error-classifier.js.map +1 -0
- package/dist/bot/error-guide.d.ts +5 -0
- package/dist/bot/error-guide.d.ts.map +1 -0
- package/dist/bot/error-guide.js +5 -0
- package/dist/bot/error-guide.js.map +1 -0
- package/dist/bot/knowledge-store.d.ts +17 -0
- package/dist/bot/knowledge-store.d.ts.map +1 -0
- package/dist/bot/knowledge-store.js +53 -0
- package/dist/bot/knowledge-store.js.map +1 -0
- package/dist/bot/paths.d.ts +11 -0
- package/dist/bot/paths.d.ts.map +1 -0
- package/dist/bot/paths.js +26 -0
- package/dist/bot/paths.js.map +1 -0
- package/dist/bot/retry-utils.d.ts +5 -0
- package/dist/bot/retry-utils.d.ts.map +1 -0
- package/dist/bot/retry-utils.js +5 -0
- package/dist/bot/retry-utils.js.map +1 -0
- package/dist/bot/runner.d.ts.map +1 -1
- package/dist/bot/runner.js +12 -1
- package/dist/bot/runner.js.map +1 -1
- package/dist/bot/safety.d.ts +10 -0
- package/dist/bot/safety.d.ts.map +1 -0
- package/dist/bot/safety.js +14 -0
- package/dist/bot/safety.js.map +1 -0
- package/dist/bot/session-state.d.ts.map +1 -1
- package/dist/bot/session-state.js +3 -1
- package/dist/bot/session-state.js.map +1 -1
- package/dist/bot/steering.js +2 -2
- package/dist/bot/steering.js.map +1 -1
- package/dist/bot/step-executor.d.ts +10 -5
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +252 -3
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/system-prompt.d.ts +1 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +69 -43
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/task-decomposer.d.ts +24 -0
- package/dist/bot/task-decomposer.d.ts.map +1 -0
- package/dist/bot/task-decomposer.js +75 -0
- package/dist/bot/task-decomposer.js.map +1 -0
- package/dist/bot/task-queue.d.ts +17 -4
- package/dist/bot/task-queue.d.ts.map +1 -1
- package/dist/bot/task-queue.js +83 -5
- package/dist/bot/task-queue.js.map +1 -1
- package/dist/bot/terminal-renderer.d.ts +60 -0
- package/dist/bot/terminal-renderer.d.ts.map +1 -0
- package/dist/bot/terminal-renderer.js +204 -0
- package/dist/bot/terminal-renderer.js.map +1 -0
- package/dist/bot/tool-registry.d.ts +24 -0
- package/dist/bot/tool-registry.d.ts.map +1 -0
- package/dist/bot/tool-registry.js +458 -0
- package/dist/bot/tool-registry.js.map +1 -0
- package/dist/bot/types.d.ts +7 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/weaver-tools.d.ts +18 -0
- package/dist/bot/weaver-tools.d.ts.map +1 -0
- package/dist/bot/weaver-tools.js +124 -0
- package/dist/bot/weaver-tools.js.map +1 -0
- package/dist/cli-bridge.d.ts.map +1 -1
- package/dist/cli-bridge.js +5 -1
- package/dist/cli-bridge.js.map +1 -1
- package/dist/cli-handlers.d.ts +13 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +615 -48
- package/dist/cli-handlers.js.map +1 -1
- package/dist/mcp-tools.js +2 -2
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/abort-task.d.ts.map +1 -1
- package/dist/node-types/abort-task.js +4 -3
- package/dist/node-types/abort-task.js.map +1 -1
- package/dist/node-types/agent-execute.d.ts +38 -0
- package/dist/node-types/agent-execute.d.ts.map +1 -0
- package/dist/node-types/agent-execute.js +252 -0
- package/dist/node-types/agent-execute.js.map +1 -0
- package/dist/node-types/bot-report.d.ts +5 -3
- package/dist/node-types/bot-report.d.ts.map +1 -1
- package/dist/node-types/bot-report.js +39 -7
- package/dist/node-types/bot-report.js.map +1 -1
- package/dist/node-types/build-context.d.ts +3 -3
- package/dist/node-types/build-context.d.ts.map +1 -1
- package/dist/node-types/build-context.js +108 -24
- package/dist/node-types/build-context.js.map +1 -1
- package/dist/node-types/detect-provider.d.ts +2 -2
- package/dist/node-types/detect-provider.d.ts.map +1 -1
- package/dist/node-types/detect-provider.js +3 -1
- package/dist/node-types/detect-provider.js.map +1 -1
- package/dist/node-types/exec-validate-retry.d.ts.map +1 -1
- package/dist/node-types/exec-validate-retry.js +43 -6
- package/dist/node-types/exec-validate-retry.js.map +1 -1
- package/dist/node-types/execute-plan.d.ts.map +1 -1
- package/dist/node-types/execute-plan.js +31 -8
- package/dist/node-types/execute-plan.js.map +1 -1
- package/dist/node-types/execute-target.d.ts.map +1 -1
- package/dist/node-types/execute-target.js +3 -1
- package/dist/node-types/execute-target.js.map +1 -1
- package/dist/node-types/fix-errors.d.ts.map +1 -1
- package/dist/node-types/fix-errors.js +21 -5
- package/dist/node-types/fix-errors.js.map +1 -1
- package/dist/node-types/genesis-observe.d.ts.map +1 -1
- package/dist/node-types/genesis-observe.js +3 -1
- package/dist/node-types/genesis-observe.js.map +1 -1
- package/dist/node-types/genesis-report.js +4 -1
- package/dist/node-types/genesis-report.js.map +1 -1
- package/dist/node-types/git-ops.d.ts.map +1 -1
- package/dist/node-types/git-ops.js +98 -4
- package/dist/node-types/git-ops.js.map +1 -1
- package/dist/node-types/index.d.ts +2 -0
- package/dist/node-types/index.d.ts.map +1 -1
- package/dist/node-types/index.js +2 -0
- package/dist/node-types/index.js.map +1 -1
- package/dist/node-types/load-config.d.ts +2 -2
- package/dist/node-types/load-config.d.ts.map +1 -1
- package/dist/node-types/load-config.js.map +1 -1
- package/dist/node-types/plan-task.d.ts.map +1 -1
- package/dist/node-types/plan-task.js +14 -2
- package/dist/node-types/plan-task.js.map +1 -1
- package/dist/node-types/read-workflow.js +8 -2
- package/dist/node-types/read-workflow.js.map +1 -1
- package/dist/node-types/receive-task.d.ts.map +1 -1
- package/dist/node-types/receive-task.js +35 -26
- package/dist/node-types/receive-task.js.map +1 -1
- package/dist/node-types/send-notify.js +2 -1
- package/dist/node-types/send-notify.js.map +1 -1
- package/dist/node-types/validate-gate.d.ts +18 -0
- package/dist/node-types/validate-gate.d.ts.map +1 -0
- package/dist/node-types/validate-gate.js +96 -0
- package/dist/node-types/validate-gate.js.map +1 -0
- package/dist/workflows/genesis-task.d.ts +20 -12
- package/dist/workflows/genesis-task.d.ts.map +1 -1
- package/dist/workflows/genesis-task.js +20 -12
- package/dist/workflows/genesis-task.js.map +1 -1
- package/dist/workflows/weaver-agent.d.ts +35 -0
- package/dist/workflows/weaver-agent.d.ts.map +1 -0
- package/dist/workflows/weaver-agent.js +777 -0
- package/dist/workflows/weaver-agent.js.map +1 -0
- package/dist/workflows/weaver-bot-batch.d.ts +19 -26
- package/dist/workflows/weaver-bot-batch.d.ts.map +1 -1
- package/dist/workflows/weaver-bot-batch.js +1043 -27
- package/dist/workflows/weaver-bot-batch.js.map +1 -1
- package/dist/workflows/weaver-bot.d.ts +21 -35
- package/dist/workflows/weaver-bot.d.ts.map +1 -1
- package/dist/workflows/weaver-bot.js +1119 -36
- package/dist/workflows/weaver-bot.js.map +1 -1
- package/flowweaver.manifest.json +21 -1
- package/package.json +5 -2
- package/src/bot/ai-client.ts +180 -19
- package/src/bot/ansi.ts +12 -0
- package/src/bot/assistant-core.ts +312 -0
- package/src/bot/assistant-tools.ts +318 -0
- package/src/bot/audit-logger.ts +6 -5
- package/src/bot/bot-manager.ts +293 -0
- package/src/bot/child-process-tracker.ts +40 -0
- package/src/bot/cli-provider.ts +13 -8
- package/src/bot/conversation-store.ts +222 -0
- package/src/bot/error-classifier.ts +90 -0
- package/src/bot/error-guide.ts +4 -0
- package/src/bot/knowledge-store.ts +59 -0
- package/src/bot/paths.ts +27 -0
- package/src/bot/retry-utils.ts +4 -0
- package/src/bot/runner.ts +12 -1
- package/src/bot/safety.ts +16 -0
- package/src/bot/session-state.ts +2 -1
- package/src/bot/steering.ts +2 -2
- package/src/bot/step-executor.ts +313 -5
- package/src/bot/system-prompt.ts +70 -47
- package/src/bot/task-decomposer.ts +100 -0
- package/src/bot/task-queue.ts +100 -8
- package/src/bot/terminal-renderer.ts +238 -0
- package/src/bot/tool-registry.ts +477 -0
- package/src/bot/types.ts +8 -0
- package/src/bot/weaver-tools.ts +134 -0
- package/src/cli-bridge.ts +7 -1
- package/src/cli-handlers.ts +624 -48
- package/src/mcp-tools.ts +2 -2
- package/src/node-types/abort-task.ts +5 -4
- package/src/node-types/agent-execute.ts +303 -0
- package/src/node-types/bot-report.ts +40 -9
- package/src/node-types/build-context.ts +112 -25
- package/src/node-types/detect-provider.ts +4 -3
- package/src/node-types/exec-validate-retry.ts +47 -8
- package/src/node-types/execute-plan.ts +32 -8
- package/src/node-types/execute-target.ts +2 -1
- package/src/node-types/fix-errors.ts +20 -5
- package/src/node-types/genesis-observe.ts +2 -1
- package/src/node-types/genesis-report.ts +1 -1
- package/src/node-types/git-ops.ts +93 -4
- package/src/node-types/index.ts +2 -0
- package/src/node-types/load-config.ts +3 -3
- package/src/node-types/plan-task.ts +15 -3
- package/src/node-types/read-workflow.ts +2 -2
- package/src/node-types/receive-task.ts +31 -26
- package/src/node-types/send-notify.ts +1 -1
- package/src/node-types/validate-gate.ts +112 -0
- package/src/workflows/genesis-task.ts +20 -12
- package/src/workflows/weaver-agent.ts +799 -0
- package/src/workflows/weaver-bot-batch.ts +1049 -27
- 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
|
+
}
|
package/src/bot/cli-provider.ts
CHANGED
|
@@ -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
|
|
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(
|
|
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
|
|
44
|
+
// copilot-cli: no --system-prompt support, keep concatenated
|
|
44
45
|
const raw = execSync('copilot -p --silent --allow-all-tools', {
|
|
45
|
-
input:
|
|
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
|
|
68
|
+
const userPrompt = `Context:\n${contextStr}\n\nInstructions:\n${request.prompt}`;
|
|
68
69
|
|
|
69
70
|
if (this.cli === 'claude-cli') {
|
|
70
|
-
yield* this.streamRaw(
|
|
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(
|
|
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(
|
|
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
|
+
}
|