@synergenius/flow-weaver-pack-weaver 0.9.7 → 0.9.9
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/agent-loop.d.ts +20 -0
- package/dist/bot/agent-loop.d.ts.map +1 -0
- package/dist/bot/agent-loop.js +331 -0
- package/dist/bot/agent-loop.js.map +1 -0
- package/dist/bot/agent-provider.d.ts.map +1 -1
- package/dist/bot/agent-provider.js +3 -2
- package/dist/bot/agent-provider.js.map +1 -1
- package/dist/bot/approvals.js +17 -8
- package/dist/bot/approvals.js.map +1 -1
- package/dist/bot/assistant-core.d.ts +17 -0
- package/dist/bot/assistant-core.d.ts.map +1 -1
- package/dist/bot/assistant-core.js +418 -60
- package/dist/bot/assistant-core.js.map +1 -1
- package/dist/bot/assistant-tools.d.ts +1 -1
- package/dist/bot/assistant-tools.d.ts.map +1 -1
- package/dist/bot/assistant-tools.js +283 -9
- package/dist/bot/assistant-tools.js.map +1 -1
- package/dist/bot/bot-agent-channel.d.ts.map +1 -1
- package/dist/bot/bot-agent-channel.js +2 -0
- package/dist/bot/bot-agent-channel.js.map +1 -1
- package/dist/bot/bot-manager.d.ts +4 -0
- package/dist/bot/bot-manager.d.ts.map +1 -1
- package/dist/bot/bot-manager.js +72 -27
- package/dist/bot/bot-manager.js.map +1 -1
- package/dist/bot/conversation-store.d.ts +6 -5
- package/dist/bot/conversation-store.d.ts.map +1 -1
- package/dist/bot/conversation-store.js +98 -42
- package/dist/bot/conversation-store.js.map +1 -1
- package/dist/bot/cost-store.d.ts +3 -0
- package/dist/bot/cost-store.d.ts.map +1 -1
- package/dist/bot/cost-store.js +21 -10
- package/dist/bot/cost-store.js.map +1 -1
- package/dist/bot/cost-tracker.d.ts.map +1 -1
- package/dist/bot/cost-tracker.js +14 -1
- package/dist/bot/cost-tracker.js.map +1 -1
- package/dist/bot/cron-parser.d.ts.map +1 -1
- package/dist/bot/cron-parser.js +2 -0
- package/dist/bot/cron-parser.js.map +1 -1
- package/dist/bot/cron-scheduler.d.ts.map +1 -1
- package/dist/bot/cron-scheduler.js +1 -0
- package/dist/bot/cron-scheduler.js.map +1 -1
- package/dist/bot/device-connection.d.ts +13 -0
- package/dist/bot/device-connection.d.ts.map +1 -0
- package/dist/bot/device-connection.js +102 -0
- package/dist/bot/device-connection.js.map +1 -0
- package/dist/bot/error-classifier.d.ts.map +1 -1
- package/dist/bot/error-classifier.js +5 -0
- package/dist/bot/error-classifier.js.map +1 -1
- package/dist/bot/file-lock.d.ts.map +1 -1
- package/dist/bot/file-lock.js +13 -3
- package/dist/bot/file-lock.js.map +1 -1
- package/dist/bot/file-watcher.d.ts.map +1 -1
- package/dist/bot/file-watcher.js +1 -0
- package/dist/bot/file-watcher.js.map +1 -1
- package/dist/bot/genesis-prompt-context.d.ts +5 -0
- package/dist/bot/genesis-prompt-context.d.ts.map +1 -1
- package/dist/bot/genesis-prompt-context.js +55 -0
- package/dist/bot/genesis-prompt-context.js.map +1 -1
- package/dist/bot/genesis-store.d.ts +4 -0
- package/dist/bot/genesis-store.d.ts.map +1 -1
- package/dist/bot/genesis-store.js +79 -12
- package/dist/bot/genesis-store.js.map +1 -1
- package/dist/bot/improve-loop.d.ts +46 -0
- package/dist/bot/improve-loop.d.ts.map +1 -0
- package/dist/bot/improve-loop.js +592 -0
- package/dist/bot/improve-loop.js.map +1 -0
- package/dist/bot/insight-engine.d.ts +12 -0
- package/dist/bot/insight-engine.d.ts.map +1 -0
- package/dist/bot/insight-engine.js +256 -0
- package/dist/bot/insight-engine.js.map +1 -0
- package/dist/bot/knowledge-store.d.ts.map +1 -1
- package/dist/bot/knowledge-store.js +4 -1
- package/dist/bot/knowledge-store.js.map +1 -1
- package/dist/bot/pipeline-runner.d.ts.map +1 -1
- package/dist/bot/pipeline-runner.js +12 -4
- package/dist/bot/pipeline-runner.js.map +1 -1
- package/dist/bot/project-model.d.ts +25 -0
- package/dist/bot/project-model.d.ts.map +1 -0
- package/dist/bot/project-model.js +372 -0
- package/dist/bot/project-model.js.map +1 -0
- package/dist/bot/response-formatter.js +2 -3
- package/dist/bot/response-formatter.js.map +1 -1
- package/dist/bot/run-store.d.ts.map +1 -1
- package/dist/bot/run-store.js +10 -2
- package/dist/bot/run-store.js.map +1 -1
- package/dist/bot/safe-path.d.ts +1 -1
- package/dist/bot/safe-path.d.ts.map +1 -1
- package/dist/bot/safe-path.js +20 -1
- package/dist/bot/safe-path.js.map +1 -1
- package/dist/bot/safety.d.ts +10 -2
- package/dist/bot/safety.d.ts.map +1 -1
- package/dist/bot/safety.js +45 -2
- package/dist/bot/safety.js.map +1 -1
- package/dist/bot/session-state.d.ts +4 -0
- package/dist/bot/session-state.d.ts.map +1 -1
- package/dist/bot/session-state.js +52 -9
- package/dist/bot/session-state.js.map +1 -1
- package/dist/bot/slash-commands.d.ts.map +1 -1
- package/dist/bot/slash-commands.js +109 -3
- package/dist/bot/slash-commands.js.map +1 -1
- package/dist/bot/steering-engine.d.ts +67 -0
- package/dist/bot/steering-engine.d.ts.map +1 -0
- package/dist/bot/steering-engine.js +198 -0
- package/dist/bot/steering-engine.js.map +1 -0
- package/dist/bot/step-executor.d.ts.map +1 -1
- package/dist/bot/step-executor.js +62 -25
- package/dist/bot/step-executor.js.map +1 -1
- package/dist/bot/system-prompt.d.ts.map +1 -1
- package/dist/bot/system-prompt.js +5 -2
- package/dist/bot/system-prompt.js.map +1 -1
- package/dist/bot/task-queue.d.ts +6 -1
- package/dist/bot/task-queue.d.ts.map +1 -1
- package/dist/bot/task-queue.js +43 -4
- package/dist/bot/task-queue.js.map +1 -1
- package/dist/bot/tool-registry.d.ts +1 -1
- package/dist/bot/tool-registry.d.ts.map +1 -1
- package/dist/bot/tool-registry.js +65 -4
- package/dist/bot/tool-registry.js.map +1 -1
- package/dist/bot/trust-calculator.d.ts +34 -0
- package/dist/bot/trust-calculator.d.ts.map +1 -0
- package/dist/bot/trust-calculator.js +67 -0
- package/dist/bot/trust-calculator.js.map +1 -0
- package/dist/bot/types.d.ts +97 -0
- package/dist/bot/types.d.ts.map +1 -1
- package/dist/bot/update-checker.d.ts +21 -0
- package/dist/bot/update-checker.d.ts.map +1 -0
- package/dist/bot/update-checker.js +129 -0
- package/dist/bot/update-checker.js.map +1 -0
- package/dist/bot/weaver-tools.d.ts.map +1 -1
- package/dist/bot/weaver-tools.js +11 -4
- package/dist/bot/weaver-tools.js.map +1 -1
- package/dist/cli-bridge.d.ts +2 -0
- package/dist/cli-bridge.d.ts.map +1 -1
- package/dist/cli-bridge.js +3 -1
- package/dist/cli-bridge.js.map +1 -1
- package/dist/cli-handlers.d.ts +10 -1
- package/dist/cli-handlers.d.ts.map +1 -1
- package/dist/cli-handlers.js +141 -24
- package/dist/cli-handlers.js.map +1 -1
- package/dist/cli.d.ts +3 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +749 -0
- package/dist/cli.js.map +1 -0
- package/dist/docs/weaver-config.md +15 -9
- package/dist/handlers/on-execution-completed.d.ts +11 -0
- package/dist/handlers/on-execution-completed.d.ts.map +1 -0
- package/dist/handlers/on-execution-completed.js +25 -0
- package/dist/handlers/on-execution-completed.js.map +1 -0
- package/dist/mcp-tools.d.ts.map +1 -1
- package/dist/mcp-tools.js +33 -0
- package/dist/mcp-tools.js.map +1 -1
- package/dist/node-types/genesis-approve.d.ts.map +1 -1
- package/dist/node-types/genesis-approve.js +28 -3
- package/dist/node-types/genesis-approve.js.map +1 -1
- package/dist/node-types/genesis-observe.d.ts.map +1 -1
- package/dist/node-types/genesis-observe.js +23 -13
- package/dist/node-types/genesis-observe.js.map +1 -1
- package/dist/node-types/genesis-propose.d.ts.map +1 -1
- package/dist/node-types/genesis-propose.js +8 -0
- package/dist/node-types/genesis-propose.js.map +1 -1
- package/dist/node-types/genesis-update-history.d.ts.map +1 -1
- package/dist/node-types/genesis-update-history.js +13 -0
- package/dist/node-types/genesis-update-history.js.map +1 -1
- package/dist/templates/weaver-template.d.ts +11 -0
- package/dist/templates/weaver-template.d.ts.map +1 -0
- package/dist/templates/weaver-template.js +53 -0
- package/dist/templates/weaver-template.js.map +1 -0
- package/dist/workflows/weaver-bot-session.d.ts +65 -0
- package/dist/workflows/weaver-bot-session.d.ts.map +1 -0
- package/dist/workflows/weaver-bot-session.js +68 -0
- package/dist/workflows/weaver-bot-session.js.map +1 -0
- package/dist/workflows/weaver.d.ts +24 -0
- package/dist/workflows/weaver.d.ts.map +1 -0
- package/dist/workflows/weaver.js +28 -0
- package/dist/workflows/weaver.js.map +1 -0
- package/flowweaver.manifest.json +28 -1
- package/package.json +6 -3
- package/src/bot/agent-provider.ts +3 -2
- package/src/bot/approvals.ts +16 -8
- package/src/bot/assistant-core.ts +420 -63
- package/src/bot/assistant-tools.ts +291 -9
- package/src/bot/bot-agent-channel.ts +2 -0
- package/src/bot/bot-manager.ts +70 -29
- package/src/bot/conversation-store.ts +87 -42
- package/src/bot/cost-store.ts +20 -9
- package/src/bot/cost-tracker.ts +13 -1
- package/src/bot/cron-parser.ts +1 -0
- package/src/bot/cron-scheduler.ts +1 -0
- package/src/bot/device-connection.ts +102 -0
- package/src/bot/error-classifier.ts +5 -0
- package/src/bot/file-lock.ts +12 -2
- package/src/bot/file-watcher.ts +1 -0
- package/src/bot/genesis-prompt-context.ts +61 -0
- package/src/bot/genesis-store.ts +68 -16
- package/src/bot/improve-loop.ts +651 -0
- package/src/bot/insight-engine.ts +273 -0
- package/src/bot/knowledge-store.ts +4 -1
- package/src/bot/pipeline-runner.ts +11 -6
- package/src/bot/project-model.ts +404 -0
- package/src/bot/response-formatter.ts +2 -3
- package/src/bot/run-store.ts +5 -2
- package/src/bot/safe-path.ts +20 -1
- package/src/bot/safety.ts +57 -3
- package/src/bot/session-state.ts +47 -7
- package/src/bot/slash-commands.ts +111 -3
- package/src/bot/steering-engine.ts +233 -0
- package/src/bot/step-executor.ts +66 -26
- package/src/bot/system-prompt.ts +5 -2
- package/src/bot/task-queue.ts +40 -4
- package/src/bot/tool-registry.ts +67 -5
- package/src/bot/trust-calculator.ts +87 -0
- package/src/bot/types.ts +104 -0
- package/src/bot/update-checker.ts +138 -0
- package/src/bot/weaver-tools.ts +10 -4
- package/src/cli-bridge.ts +4 -1
- package/src/cli-handlers.ts +150 -29
- package/src/handlers/on-execution-completed.ts +30 -0
- package/src/mcp-tools.ts +38 -0
- package/src/node-types/genesis-approve.ts +28 -3
- package/src/node-types/genesis-observe.ts +23 -12
- package/src/node-types/genesis-propose.ts +8 -0
- package/src/node-types/genesis-update-history.ts +12 -0
- package/src/ui/evolution-panel.tsx +96 -0
- package/src/ui/insights-widget.tsx +77 -0
|
@@ -22,7 +22,7 @@ function getManager(): BotManager {
|
|
|
22
22
|
return manager;
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
export function createAssistantExecutor(projectDir: string): ToolExecutor {
|
|
25
|
+
export function createAssistantExecutor(projectDir: string, steeringEngine?: import('./steering-engine.js').SteeringEngine): ToolExecutor {
|
|
26
26
|
const mgr = getManager();
|
|
27
27
|
|
|
28
28
|
return async (name: string, args: Record<string, unknown>) => {
|
|
@@ -38,7 +38,7 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
|
|
|
38
38
|
deadline: args.deadline as string | undefined,
|
|
39
39
|
branch: args.branch as string | undefined,
|
|
40
40
|
});
|
|
41
|
-
return { result:
|
|
41
|
+
return { result: `Bot "${botName}" started (pid ${bot.pid}). Add tasks with queue_add or check status with bot_status.`, isError: false };
|
|
42
42
|
}
|
|
43
43
|
case 'bot_list': {
|
|
44
44
|
const bots = mgr.list();
|
|
@@ -65,7 +65,7 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
|
|
|
65
65
|
if (failedTasks.length > 0) {
|
|
66
66
|
result += `\nFailed tasks:\n`;
|
|
67
67
|
for (const t of failedTasks) {
|
|
68
|
-
result += ` - ${t.instruction.slice(0,
|
|
68
|
+
result += ` - ${t.instruction.slice(0, 120)}\n`;
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
return { result, isError: false };
|
|
@@ -95,7 +95,7 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
|
|
|
95
95
|
targets: args.targets as string[] | undefined,
|
|
96
96
|
priority: 0,
|
|
97
97
|
});
|
|
98
|
-
if (duplicate) return { result: `
|
|
98
|
+
if (duplicate) return { result: `Skipped: similar task already exists (${id}).`, isError: false };
|
|
99
99
|
return { result: `Added task ${id} to "${args.bot}" queue.`, isError: false };
|
|
100
100
|
}
|
|
101
101
|
case 'queue_add_batch': {
|
|
@@ -113,7 +113,7 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
|
|
|
113
113
|
const queue = mgr.getQueue(String(args.bot));
|
|
114
114
|
const tasks = await queue.list();
|
|
115
115
|
if (tasks.length === 0) return { result: 'Queue is empty.', isError: false };
|
|
116
|
-
const lines = tasks.map(t => `[${t.status}] ${t.instruction.slice(0,
|
|
116
|
+
const lines = tasks.map(t => `[${t.status}] ${t.instruction.slice(0, 120)}`);
|
|
117
117
|
return { result: lines.join('\n'), isError: false };
|
|
118
118
|
}
|
|
119
119
|
case 'queue_retry': {
|
|
@@ -172,8 +172,9 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
|
|
|
172
172
|
case 'run_shell': {
|
|
173
173
|
const cmd = String(args.command);
|
|
174
174
|
// Safety: block destructive commands
|
|
175
|
-
|
|
176
|
-
|
|
175
|
+
const blockedPattern = isBlockedCommand(cmd);
|
|
176
|
+
if (blockedPattern) {
|
|
177
|
+
return { result: `Blocked: command matches disallowed pattern "${blockedPattern}".`, isError: true };
|
|
177
178
|
}
|
|
178
179
|
const output = execFileSync('sh', ['-c', cmd], {
|
|
179
180
|
encoding: 'utf-8', cwd: projectDir, timeout: 30_000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -181,6 +182,86 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
|
|
|
181
182
|
return { result: output.trim().slice(0, 5000) || '(no output)', isError: false };
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
case 'write_file': {
|
|
186
|
+
const filePath = path.isAbsolute(String(args.file)) ? String(args.file) : path.resolve(projectDir, String(args.file));
|
|
187
|
+
// Safety: must be within project directory
|
|
188
|
+
if (!filePath.startsWith(projectDir)) {
|
|
189
|
+
steeringEngine?.recordEvent('file_write_blocked');
|
|
190
|
+
return { result: 'Blocked: cannot write files outside project directory.', isError: true };
|
|
191
|
+
}
|
|
192
|
+
const content = String(args.content);
|
|
193
|
+
if (!content.trim()) {
|
|
194
|
+
steeringEngine?.recordEvent('file_write_blocked');
|
|
195
|
+
return { result: 'Blocked: cannot write empty file.', isError: true };
|
|
196
|
+
}
|
|
197
|
+
// Check shrink protection for existing files
|
|
198
|
+
if (fs.existsSync(filePath)) {
|
|
199
|
+
const existing = fs.readFileSync(filePath, 'utf-8');
|
|
200
|
+
if (existing.length > 0 && content.length < existing.length * 0.5) {
|
|
201
|
+
steeringEngine?.recordEvent('file_write_blocked');
|
|
202
|
+
return { result: `Blocked: write would shrink file by more than 50% (${existing.length} -> ${content.length} chars).`, isError: true };
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
const dir = path.dirname(filePath);
|
|
206
|
+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
207
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
208
|
+
steeringEngine?.recordEvent('file_write');
|
|
209
|
+
return { result: `Wrote ${content.length} chars to ${path.relative(projectDir, filePath)}`, isError: false };
|
|
210
|
+
}
|
|
211
|
+
case 'patch_file': {
|
|
212
|
+
const filePath = path.isAbsolute(String(args.file)) ? String(args.file) : path.resolve(projectDir, String(args.file));
|
|
213
|
+
if (!filePath.startsWith(projectDir)) {
|
|
214
|
+
return { result: 'Blocked: cannot patch files outside project directory.', isError: true };
|
|
215
|
+
}
|
|
216
|
+
if (!fs.existsSync(filePath)) {
|
|
217
|
+
return { result: `File not found: ${args.file}`, isError: true };
|
|
218
|
+
}
|
|
219
|
+
let content = fs.readFileSync(filePath, 'utf-8');
|
|
220
|
+
const patches = args.patches as Array<{ find: string; replace: string }>;
|
|
221
|
+
let applied = 0;
|
|
222
|
+
for (const p of patches) {
|
|
223
|
+
if (content.includes(p.find)) {
|
|
224
|
+
content = content.replace(p.find, p.replace);
|
|
225
|
+
applied++;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
if (applied === 0) {
|
|
229
|
+
steeringEngine?.recordEvent('file_patch_blocked');
|
|
230
|
+
return { result: `No patches matched in ${args.file}. Check exact strings.`, isError: true };
|
|
231
|
+
}
|
|
232
|
+
fs.writeFileSync(filePath, content, 'utf-8');
|
|
233
|
+
steeringEngine?.recordEvent('file_patch');
|
|
234
|
+
return { result: `Applied ${applied}/${patches.length} patches to ${path.relative(projectDir, filePath)}`, isError: false };
|
|
235
|
+
}
|
|
236
|
+
case 'tsc_check': {
|
|
237
|
+
try {
|
|
238
|
+
const output = execFileSync('npx', ['tsc', '--noEmit'], {
|
|
239
|
+
encoding: 'utf-8', cwd: projectDir, timeout: 60_000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
240
|
+
});
|
|
241
|
+
steeringEngine?.recordEvent('build_pass');
|
|
242
|
+
return { result: output.trim() || 'TypeScript check passed — no errors.', isError: false };
|
|
243
|
+
} catch (tscErr: any) {
|
|
244
|
+
steeringEngine?.recordEvent('build_fail');
|
|
245
|
+
const msg = tscErr instanceof Error ? tscErr.message : String(tscErr);
|
|
246
|
+
return { result: msg.length > 500 ? msg.slice(0, 497) + '...' : msg, isError: true };
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
case 'run_tests': {
|
|
250
|
+
const testArgs = ['vitest', 'run'];
|
|
251
|
+
if (args.pattern) testArgs.push(String(args.pattern));
|
|
252
|
+
try {
|
|
253
|
+
const output = execFileSync('npx', testArgs, {
|
|
254
|
+
encoding: 'utf-8', cwd: projectDir, timeout: 120_000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
255
|
+
});
|
|
256
|
+
steeringEngine?.recordEvent('test_pass');
|
|
257
|
+
return { result: output.trim().slice(-3000) || 'Tests completed.', isError: false };
|
|
258
|
+
} catch (testErr: any) {
|
|
259
|
+
steeringEngine?.recordEvent('test_fail');
|
|
260
|
+
const msg = testErr instanceof Error ? testErr.message : String(testErr);
|
|
261
|
+
return { result: msg.length > 3000 ? msg.slice(-3000) : msg, isError: true };
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
184
265
|
// Conversation management
|
|
185
266
|
case 'conversation_list': {
|
|
186
267
|
const { ConversationStore } = await import('./conversation-store.js');
|
|
@@ -200,7 +281,7 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
|
|
|
200
281
|
const cStore = new ConversationStore();
|
|
201
282
|
const existing = cStore.get(String(args.id));
|
|
202
283
|
if (!existing) return { result: `Conversation "${args.id}" not found.`, isError: true };
|
|
203
|
-
cStore.delete(String(args.id));
|
|
284
|
+
await cStore.delete(String(args.id));
|
|
204
285
|
return { result: `Deleted conversation "${args.id}" (${existing.title || 'untitled'}).`, isError: false };
|
|
205
286
|
}
|
|
206
287
|
case 'conversation_summary': {
|
|
@@ -307,12 +388,213 @@ export function createAssistantExecutor(projectDir: string): ToolExecutor {
|
|
|
307
388
|
return { result: entries.map(e => `${e.key}: ${e.value}`).join('\n'), isError: false };
|
|
308
389
|
}
|
|
309
390
|
|
|
391
|
+
// Overseer tools
|
|
392
|
+
case 'project_health': {
|
|
393
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
394
|
+
const pms = new ProjectModelStore(projectDir);
|
|
395
|
+
const model = await pms.getOrBuild();
|
|
396
|
+
const lines: string[] = [
|
|
397
|
+
`Overall health: ${model.health.overall}/100`,
|
|
398
|
+
'',
|
|
399
|
+
];
|
|
400
|
+
if (model.health.workflows.length > 0) {
|
|
401
|
+
lines.push('Workflows:');
|
|
402
|
+
for (const w of model.health.workflows) {
|
|
403
|
+
lines.push(` ${w.file}: ${w.score}/100 (${w.totalRuns} runs, ${Math.round(w.successRate * 100)}% success, trend: ${w.trend})`);
|
|
404
|
+
}
|
|
405
|
+
lines.push('');
|
|
406
|
+
}
|
|
407
|
+
if (model.bots.length > 0) {
|
|
408
|
+
lines.push('Bots:');
|
|
409
|
+
for (const b of model.bots) {
|
|
410
|
+
lines.push(` ${b.name}: ${b.ejected ? 'ejected' : 'pack'} (${b.totalTasksRun} tasks, ${Math.round(b.successRate * 100)}% success)`);
|
|
411
|
+
}
|
|
412
|
+
lines.push('');
|
|
413
|
+
}
|
|
414
|
+
if (model.failurePatterns.length > 0) {
|
|
415
|
+
lines.push('Failure patterns:');
|
|
416
|
+
for (const p of model.failurePatterns.slice(0, 5)) {
|
|
417
|
+
lines.push(` ${p.pattern} (${p.occurrences}x, ${p.transient ? 'transient' : 'persistent'})`);
|
|
418
|
+
}
|
|
419
|
+
lines.push('');
|
|
420
|
+
}
|
|
421
|
+
lines.push(`Cost (7d): $${model.cost.last7Days.toFixed(2)} (trend: ${model.cost.trend})`);
|
|
422
|
+
lines.push(`Trust: phase ${model.trust.phase} (score ${model.trust.score})`);
|
|
423
|
+
return { result: lines.join('\n'), isError: false };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
case 'project_insights': {
|
|
427
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
428
|
+
const { InsightEngine } = await import('./insight-engine.js');
|
|
429
|
+
const pms = new ProjectModelStore(projectDir);
|
|
430
|
+
const model = await pms.getOrBuild();
|
|
431
|
+
const engine = new InsightEngine();
|
|
432
|
+
const insights = engine.analyze(model);
|
|
433
|
+
const limit = (args.limit as number) ?? 5;
|
|
434
|
+
if (insights.length === 0) return { result: 'No actionable insights right now. Keep running workflows to build up data.', isError: false };
|
|
435
|
+
const lines = insights.slice(0, limit).map(i =>
|
|
436
|
+
`[${i.severity}] ${i.title} (confidence: ${Math.round(i.confidence * 100)}%)\n ${i.description}${i.suggestion ? `\n Suggestion: ${i.suggestion}` : ''}`
|
|
437
|
+
);
|
|
438
|
+
return { result: lines.join('\n\n'), isError: false };
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
case 'evolution_status': {
|
|
442
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
443
|
+
const pms = new ProjectModelStore(projectDir);
|
|
444
|
+
const model = await pms.getOrBuild();
|
|
445
|
+
const evo = model.evolution;
|
|
446
|
+
if (evo.totalCycles === 0) return { result: 'No genesis cycles recorded yet. Use /genesis or genesis_propose to start evolving bot workflows.', isError: false };
|
|
447
|
+
const lines = [
|
|
448
|
+
`Total cycles: ${evo.totalCycles} (${Math.round(evo.successRate * 100)}% success rate)`,
|
|
449
|
+
'',
|
|
450
|
+
'Operation effectiveness:',
|
|
451
|
+
...Object.entries(evo.byOperationType).map(([op, stats]) =>
|
|
452
|
+
` ${op}: ${Math.round(stats.effectiveness * 100)}% effective (${stats.applied}/${stats.proposed} applied)`
|
|
453
|
+
),
|
|
454
|
+
'',
|
|
455
|
+
'Recent cycles:',
|
|
456
|
+
...evo.recentCycles.slice(-5).map(c =>
|
|
457
|
+
` ${c.id} [${c.outcome}] ${c.proposal?.summary ?? 'no proposal'}`
|
|
458
|
+
),
|
|
459
|
+
];
|
|
460
|
+
return { result: lines.join('\n'), isError: false };
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
case 'improve_status': {
|
|
464
|
+
const summaryDir = path.join(os.homedir(), '.weaver', 'improve');
|
|
465
|
+
if (!fs.existsSync(summaryDir)) return { result: 'No improve runs found. Start one with: weaver improve', isError: false };
|
|
466
|
+
const files = fs.readdirSync(summaryDir).filter(f => f.endsWith('.json')).sort().reverse();
|
|
467
|
+
if (files.length === 0) return { result: 'No improve runs found.', isError: false };
|
|
468
|
+
const latest = JSON.parse(fs.readFileSync(path.join(summaryDir, files[0]!), 'utf-8'));
|
|
469
|
+
const duration = Math.round((new Date(latest.finishedAt).getTime() - new Date(latest.startedAt).getTime()) / 1000);
|
|
470
|
+
const lines: string[] = [
|
|
471
|
+
`Improve Run: ${latest.reason}`,
|
|
472
|
+
`Branch: ${latest.branch}`,
|
|
473
|
+
`Duration: ${duration}s`,
|
|
474
|
+
`Successes: ${latest.successes} Failures: ${latest.failures} Skips: ${latest.skips}`,
|
|
475
|
+
'',
|
|
476
|
+
];
|
|
477
|
+
for (const cy of latest.cycles) {
|
|
478
|
+
const icon = cy.outcome === 'success' ? '✓' : cy.outcome === 'failure' ? '✗' : '○';
|
|
479
|
+
lines.push(`${icon} Cycle ${cy.cycle}: [${cy.outcome}] ${cy.description.slice(0, 70)}`);
|
|
480
|
+
if (cy.commitHash) lines.push(` Commit: ${cy.commitHash}`);
|
|
481
|
+
}
|
|
482
|
+
// Check if worktree is active (run in progress)
|
|
483
|
+
try {
|
|
484
|
+
const worktrees = execFileSync('git', ['worktree', 'list'], { encoding: 'utf-8', cwd: projectDir });
|
|
485
|
+
if (worktrees.includes('weaver-improve')) {
|
|
486
|
+
lines.push('', 'LIVE: improve worktree active — run in progress');
|
|
487
|
+
}
|
|
488
|
+
} catch { /* git not available */ }
|
|
489
|
+
return { result: lines.join('\n'), isError: false };
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
case 'genesis_propose': {
|
|
493
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
494
|
+
const pms = new ProjectModelStore(projectDir);
|
|
495
|
+
const model = await pms.getOrBuild();
|
|
496
|
+
|
|
497
|
+
// Check if genesis config exists, create default if not
|
|
498
|
+
const genesisDir = path.join(projectDir, '.genesis');
|
|
499
|
+
const configPath = path.join(genesisDir, 'config.json');
|
|
500
|
+
if (!fs.existsSync(configPath)) {
|
|
501
|
+
fs.mkdirSync(genesisDir, { recursive: true });
|
|
502
|
+
const defaultConfig = {
|
|
503
|
+
intent: args.focus ? String(args.focus) : 'Improve bot workflow reliability and efficiency',
|
|
504
|
+
focus: args.focus ? [String(args.focus)] : [],
|
|
505
|
+
constraints: [],
|
|
506
|
+
approvalThreshold: 'MINOR',
|
|
507
|
+
budgetPerCycle: (args.budget as number) ?? 3,
|
|
508
|
+
stabilize: false,
|
|
509
|
+
targetWorkflow: '',
|
|
510
|
+
maxCyclesPerRun: 1,
|
|
511
|
+
};
|
|
512
|
+
|
|
513
|
+
// Find bot workflow — check ejected first, then pack
|
|
514
|
+
const botName = String(args.bot ?? 'weaver-bot');
|
|
515
|
+
const ejectedPath = path.join(projectDir, '.fw', 'bots', botName, 'weaver-bot.ts');
|
|
516
|
+
const packPath = path.resolve(projectDir, 'node_modules', '@synergenius', 'flow-weaver-pack-weaver', 'src', 'workflows', 'weaver-bot.ts');
|
|
517
|
+
|
|
518
|
+
if (fs.existsSync(ejectedPath)) {
|
|
519
|
+
defaultConfig.targetWorkflow = ejectedPath;
|
|
520
|
+
} else if (fs.existsSync(packPath)) {
|
|
521
|
+
// Auto-eject: copy from pack to project
|
|
522
|
+
const ejectDir = path.join(projectDir, '.fw', 'bots', botName);
|
|
523
|
+
fs.mkdirSync(ejectDir, { recursive: true });
|
|
524
|
+
fs.copyFileSync(packPath, path.join(ejectDir, 'weaver-bot.ts'));
|
|
525
|
+
defaultConfig.targetWorkflow = path.join(ejectDir, 'weaver-bot.ts');
|
|
526
|
+
// Also copy node types needed
|
|
527
|
+
} else {
|
|
528
|
+
return { result: `Could not find bot workflow for "${botName}". Ensure the weaver pack is installed.`, isError: true };
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
fs.writeFileSync(configPath, JSON.stringify(defaultConfig, null, 2), 'utf-8');
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// Build insight context
|
|
535
|
+
let insightSummary = '';
|
|
536
|
+
try {
|
|
537
|
+
const { getGenesisInsightContext } = await import('./genesis-prompt-context.js');
|
|
538
|
+
insightSummary = await getGenesisInsightContext(projectDir);
|
|
539
|
+
} catch { /* insights not available */ }
|
|
540
|
+
|
|
541
|
+
// Build proposal summary from model data
|
|
542
|
+
const lines: string[] = ['Genesis Proposal Context:', ''];
|
|
543
|
+
lines.push(`Project health: ${model.health.overall}/100`);
|
|
544
|
+
lines.push(`Trust phase: ${model.trust.phase}`);
|
|
545
|
+
if (model.failurePatterns.length > 0) {
|
|
546
|
+
lines.push('');
|
|
547
|
+
lines.push('Key failure patterns:');
|
|
548
|
+
for (const p of model.failurePatterns.slice(0, 3)) {
|
|
549
|
+
lines.push(` - ${p.pattern} (${p.occurrences}x, ${p.transient ? 'transient' : 'persistent'})`);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
if (model.bots.length > 0) {
|
|
553
|
+
lines.push('');
|
|
554
|
+
lines.push('Bot performance:');
|
|
555
|
+
for (const b of model.bots) {
|
|
556
|
+
lines.push(` - ${b.name}: ${Math.round(b.successRate * 100)}% success (${b.totalTasksRun} tasks, ${b.ejected ? 'ejected' : 'pack'})`);
|
|
557
|
+
}
|
|
558
|
+
}
|
|
559
|
+
if (insightSummary) {
|
|
560
|
+
lines.push('');
|
|
561
|
+
lines.push(insightSummary);
|
|
562
|
+
}
|
|
563
|
+
lines.push('');
|
|
564
|
+
lines.push('To generate and apply a full proposal, run: flow-weaver weaver genesis');
|
|
565
|
+
lines.push('Or use genesis_apply after reviewing this context.');
|
|
566
|
+
|
|
567
|
+
return { result: lines.join('\n'), isError: false };
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
case 'genesis_apply': {
|
|
571
|
+
const proposalId = String(args.proposal_id);
|
|
572
|
+
|
|
573
|
+
// Run genesis via CLI
|
|
574
|
+
try {
|
|
575
|
+
const output = execFileSync('npx', ['flow-weaver', 'weaver', 'genesis', '--max-cycles', '1'], {
|
|
576
|
+
encoding: 'utf-8', cwd: projectDir, timeout: 120_000, stdio: ['pipe', 'pipe', 'pipe'],
|
|
577
|
+
});
|
|
578
|
+
|
|
579
|
+
// Invalidate project model cache
|
|
580
|
+
try {
|
|
581
|
+
const { ProjectModelStore } = await import('./project-model.js');
|
|
582
|
+
new ProjectModelStore(projectDir).invalidate();
|
|
583
|
+
} catch { /* non-fatal */ }
|
|
584
|
+
|
|
585
|
+
return { result: output.trim() || 'Genesis cycle completed.', isError: false };
|
|
586
|
+
} catch (err: any) {
|
|
587
|
+
return { result: `Genesis cycle failed: ${(err.message ?? '').slice(0, 500)}`, isError: true };
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
|
|
310
591
|
default:
|
|
311
592
|
return { result: `Unknown tool: ${name}`, isError: true };
|
|
312
593
|
}
|
|
313
594
|
} catch (err: unknown) {
|
|
595
|
+
steeringEngine?.recordEvent('tool_error');
|
|
314
596
|
const msg = err instanceof Error ? err.message : String(err);
|
|
315
|
-
return { result: msg.slice(0,
|
|
597
|
+
return { result: msg.length > 500 ? msg.slice(0, 497) + '...' : msg, isError: true };
|
|
316
598
|
}
|
|
317
599
|
};
|
|
318
600
|
}
|
|
@@ -72,6 +72,7 @@ export class BotAgentChannel {
|
|
|
72
72
|
if (this.provider.decideWithTools) {
|
|
73
73
|
return this.provider.decideWithTools({ ...agentRequest, tools });
|
|
74
74
|
}
|
|
75
|
+
console.warn('[BotAgentChannel] Provider does not support decideWithTools — falling back to decide (tool results will be dropped)');
|
|
75
76
|
const result = await this.provider.decide(agentRequest);
|
|
76
77
|
return { result };
|
|
77
78
|
}
|
|
@@ -85,6 +86,7 @@ export class BotAgentChannel {
|
|
|
85
86
|
yield* this.provider.stream(agentRequest);
|
|
86
87
|
return;
|
|
87
88
|
}
|
|
89
|
+
console.warn('[BotAgentChannel] Provider does not support stream — falling back to decide (response will be non-streaming)');
|
|
88
90
|
const result = await this.provider.decide(agentRequest);
|
|
89
91
|
yield { type: 'text', text: JSON.stringify(result) };
|
|
90
92
|
yield { type: 'done' };
|
package/src/bot/bot-manager.ts
CHANGED
|
@@ -32,7 +32,7 @@ export interface SpawnOpts {
|
|
|
32
32
|
const BOTS_DIR = path.join(os.homedir(), '.weaver', 'bots');
|
|
33
33
|
|
|
34
34
|
export class BotManager {
|
|
35
|
-
private bots = new Map<string, { meta: ManagedBot; process: ChildProcess }>();
|
|
35
|
+
private bots = new Map<string, { meta: ManagedBot; process: ChildProcess | null }>();
|
|
36
36
|
|
|
37
37
|
constructor() {
|
|
38
38
|
// Ensure base dir exists
|
|
@@ -42,21 +42,33 @@ export class BotManager {
|
|
|
42
42
|
const cleanup = () => this.cleanup();
|
|
43
43
|
process.on('exit', cleanup);
|
|
44
44
|
process.on('SIGTERM', cleanup);
|
|
45
|
+
process.on('SIGINT', cleanup);
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
spawn(name: string, opts: SpawnOpts): ManagedBot {
|
|
48
49
|
if (this.bots.has(name)) {
|
|
49
|
-
throw new Error(`Bot "${name}" already exists.
|
|
50
|
+
throw new Error(`Bot "${name}" already exists. Use bot_stop("${name}") first, or choose a different name.`);
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const botDir = path.join(BOTS_DIR, name);
|
|
53
54
|
fs.mkdirSync(botDir, { recursive: true });
|
|
54
55
|
|
|
55
56
|
// Create git branch if specified (keeps main clean for overnight runs)
|
|
57
|
+
// WARNING: checkout -B mutates the user's working directory. Prefer git worktree
|
|
58
|
+
// for isolation when the project supports it.
|
|
56
59
|
if (opts.branch) {
|
|
57
60
|
try {
|
|
58
|
-
|
|
59
|
-
|
|
61
|
+
// Try worktree first for isolation (does not mutate the user's working dir)
|
|
62
|
+
const worktreePath = path.join(botDir, 'worktree');
|
|
63
|
+
execFileSync('git', ['worktree', 'add', '-B', opts.branch, worktreePath], { cwd: opts.projectDir, encoding: 'utf-8', stdio: 'pipe' });
|
|
64
|
+
opts.projectDir = worktreePath;
|
|
65
|
+
} catch {
|
|
66
|
+
// Worktree unavailable or failed — fall back to checkout but warn
|
|
67
|
+
process.stderr.write(`[weaver] WARNING: "git checkout -B ${opts.branch}" will switch the working directory in ${opts.projectDir}. Use git worktree for isolation.\n`);
|
|
68
|
+
try {
|
|
69
|
+
execFileSync('git', ['checkout', '-B', opts.branch], { cwd: opts.projectDir, encoding: 'utf-8', stdio: 'pipe' });
|
|
70
|
+
} catch { /* branch may already exist */ }
|
|
71
|
+
}
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
const logPath = path.join(botDir, 'output.log');
|
|
@@ -83,12 +95,19 @@ export class BotManager {
|
|
|
83
95
|
WEAVER_STEERING_DIR: botDir,
|
|
84
96
|
};
|
|
85
97
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
98
|
+
let child: ChildProcess;
|
|
99
|
+
try {
|
|
100
|
+
child = spawn(cmd, args, {
|
|
101
|
+
cwd: opts.projectDir,
|
|
102
|
+
env,
|
|
103
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
104
|
+
detached: false,
|
|
105
|
+
});
|
|
106
|
+
} catch (err) {
|
|
107
|
+
// Clean up logStream on spawn failure to prevent fd leak
|
|
108
|
+
logStream.destroy();
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
92
111
|
|
|
93
112
|
// Capture output to log file
|
|
94
113
|
child.stdout?.pipe(logStream);
|
|
@@ -135,7 +154,7 @@ export class BotManager {
|
|
|
135
154
|
}
|
|
136
155
|
|
|
137
156
|
get(name: string): ManagedBot | null {
|
|
138
|
-
const bot = this.
|
|
157
|
+
const bot = this.resolve(name);
|
|
139
158
|
if (!bot) return null;
|
|
140
159
|
// Health check on access
|
|
141
160
|
if (bot.meta.status === 'running' && !this.isAlive(bot.meta)) {
|
|
@@ -154,13 +173,13 @@ export class BotManager {
|
|
|
154
173
|
}
|
|
155
174
|
|
|
156
175
|
getQueue(name: string): TaskQueue {
|
|
157
|
-
const bot = this.
|
|
176
|
+
const bot = this.resolve(name);
|
|
158
177
|
if (!bot) throw new Error(`Bot "${name}" not found.`);
|
|
159
178
|
return new TaskQueue(bot.meta.botDir);
|
|
160
179
|
}
|
|
161
180
|
|
|
162
181
|
getSteering(name: string): SteeringController {
|
|
163
|
-
const bot = this.
|
|
182
|
+
const bot = this.resolve(name);
|
|
164
183
|
if (!bot) throw new Error(`Bot "${name}" not found.`);
|
|
165
184
|
return new SteeringController(bot.meta.botDir);
|
|
166
185
|
}
|
|
@@ -178,26 +197,30 @@ export class BotManager {
|
|
|
178
197
|
}
|
|
179
198
|
|
|
180
199
|
stop(name: string): void {
|
|
181
|
-
const bot = this.
|
|
200
|
+
const bot = this.resolve(name);
|
|
182
201
|
if (!bot) throw new Error(`Bot "${name}" not found.`);
|
|
183
|
-
|
|
184
|
-
if (bot.process.pid && !bot.process.killed) {
|
|
185
|
-
bot.process.kill('SIGTERM');
|
|
186
|
-
}
|
|
202
|
+
this.sendSignal(bot, 'SIGTERM');
|
|
187
203
|
bot.meta.status = 'stopped';
|
|
188
204
|
}
|
|
189
205
|
|
|
190
206
|
kill(name: string): void {
|
|
191
|
-
const bot = this.
|
|
207
|
+
const bot = this.resolve(name);
|
|
192
208
|
if (!bot) throw new Error(`Bot "${name}" not found.`);
|
|
193
|
-
|
|
194
|
-
bot.process.kill('SIGKILL');
|
|
195
|
-
}
|
|
209
|
+
this.sendSignal(bot, 'SIGKILL');
|
|
196
210
|
bot.meta.status = 'stopped';
|
|
197
211
|
}
|
|
198
212
|
|
|
213
|
+
/** Send a signal to a bot, using the process handle if available, otherwise the PID from meta. */
|
|
214
|
+
private sendSignal(bot: { meta: ManagedBot; process: ChildProcess | null }, signal: NodeJS.Signals): void {
|
|
215
|
+
if (bot.process?.pid && !bot.process.killed) {
|
|
216
|
+
bot.process.kill(signal);
|
|
217
|
+
} else if (bot.meta.pid > 0) {
|
|
218
|
+
try { process.kill(bot.meta.pid, signal); } catch { /* process already gone */ }
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
199
222
|
logs(name: string, lines = 50): string {
|
|
200
|
-
const bot = this.
|
|
223
|
+
const bot = this.resolve(name);
|
|
201
224
|
if (!bot) throw new Error(`Bot "${name}" not found.`);
|
|
202
225
|
const logPath = path.join(bot.meta.botDir, 'output.log');
|
|
203
226
|
if (!fs.existsSync(logPath)) return '(no logs yet)';
|
|
@@ -208,7 +231,7 @@ export class BotManager {
|
|
|
208
231
|
|
|
209
232
|
cleanup(): void {
|
|
210
233
|
for (const [, bot] of this.bots) {
|
|
211
|
-
if (bot.process
|
|
234
|
+
if (bot.process?.pid && !bot.process.killed) {
|
|
212
235
|
try { bot.process.kill('SIGTERM'); } catch (err) {
|
|
213
236
|
if (process.env.WEAVER_VERBOSE) process.stderr.write(`[weaver] SIGTERM failed for bot: ${err}\n`);
|
|
214
237
|
}
|
|
@@ -216,6 +239,16 @@ export class BotManager {
|
|
|
216
239
|
}
|
|
217
240
|
}
|
|
218
241
|
|
|
242
|
+
/** Look up a bot by name, lazily discovering bots from disk if not in memory. */
|
|
243
|
+
private resolve(name: string): { meta: ManagedBot; process: ChildProcess | null } | undefined {
|
|
244
|
+
let bot = this.bots.get(name);
|
|
245
|
+
if (!bot) {
|
|
246
|
+
this.discoverExistingBots();
|
|
247
|
+
bot = this.bots.get(name);
|
|
248
|
+
}
|
|
249
|
+
return bot;
|
|
250
|
+
}
|
|
251
|
+
|
|
219
252
|
/** Discover bots from disk that were spawned by a previous assistant session. */
|
|
220
253
|
private discoverExistingBots(): void {
|
|
221
254
|
if (!fs.existsSync(BOTS_DIR)) return;
|
|
@@ -235,7 +268,7 @@ export class BotManager {
|
|
|
235
268
|
}
|
|
236
269
|
}
|
|
237
270
|
// Store without a process handle (can only steer via file, not kill directly)
|
|
238
|
-
this.bots.set(name, { meta, process: null
|
|
271
|
+
this.bots.set(name, { meta, process: null });
|
|
239
272
|
} catch { /* corrupt meta */ }
|
|
240
273
|
}
|
|
241
274
|
}
|
|
@@ -277,15 +310,23 @@ function wrapWithSleepInhibitor(command: string, args: string[]): { cmd: string;
|
|
|
277
310
|
export function sendDesktopNotification(title: string, message: string): void {
|
|
278
311
|
try {
|
|
279
312
|
switch (process.platform) {
|
|
280
|
-
case 'darwin':
|
|
281
|
-
|
|
313
|
+
case 'darwin': {
|
|
314
|
+
// Escape backslashes and double-quotes for AppleScript string literals
|
|
315
|
+
const safeTitle = title.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
316
|
+
const safeMessage = message.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
317
|
+
execFileSync('osascript', ['-e', `display notification "${safeMessage}" with title "${safeTitle}"`], { stdio: 'ignore' });
|
|
282
318
|
break;
|
|
319
|
+
}
|
|
283
320
|
case 'linux':
|
|
284
321
|
execFileSync('notify-send', [title, message], { stdio: 'ignore' });
|
|
285
322
|
break;
|
|
286
|
-
case 'win32':
|
|
287
|
-
|
|
323
|
+
case 'win32': {
|
|
324
|
+
// Escape single-quotes for PowerShell single-quoted strings (double them)
|
|
325
|
+
const psTitle = title.replace(/'/g, "''");
|
|
326
|
+
const psMessage = message.replace(/'/g, "''");
|
|
327
|
+
execFileSync('powershell', ['-Command', `[System.Reflection.Assembly]::LoadWithPartialName('System.Windows.Forms'); [System.Windows.Forms.MessageBox]::Show('${psMessage}','${psTitle}')`], { stdio: 'ignore' });
|
|
288
328
|
break;
|
|
329
|
+
}
|
|
289
330
|
}
|
|
290
331
|
} catch {
|
|
291
332
|
// Non-fatal — notification is best-effort
|