claude-flow 3.7.0-alpha.3 → 3.7.0-alpha.30
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/.claude/agents/dual-mode/codex-coordinator.md +34 -29
- package/.claude/agents/dual-mode/codex-worker.md +39 -32
- package/.claude/helpers/hook-handler.cjs +4 -1
- package/.claude/helpers/statusline.cjs +26 -5
- package/.claude/scheduled_tasks.lock +1 -0
- package/.claude/settings.json +2 -0
- package/.claude/settings.local.json +10 -1
- package/.claude/skills/browser/SKILL.md +204 -0
- package/.claude/skills/github-project-management/SKILL.md +13 -0
- package/.claude/skills/verification-quality/SKILL.md +117 -73
- package/.claude-plugin/hooks/hooks.json +6 -5
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/scripts/ruflo-hook.sh +33 -0
- package/README.md +43 -39
- package/package.json +8 -2
- package/v3/@claude-flow/cli/README.md +43 -39
- package/v3/@claude-flow/cli/bin/cli.js +15 -2
- package/v3/@claude-flow/cli/bin/mcp-server.js +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/agent-wasm.js +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/daemon.d.ts +20 -0
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +338 -3
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +224 -46
- package/v3/@claude-flow/cli/dist/src/commands/hive-mind.js +25 -7
- package/v3/@claude-flow/cli/dist/src/commands/hooks.js +56 -29
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +90 -3
- package/v3/@claude-flow/cli/dist/src/commands/start.js +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/swarm.js +1 -1
- package/v3/@claude-flow/cli/dist/src/commands/task.js +8 -4
- package/v3/@claude-flow/cli/dist/src/config-adapter.js +1 -1
- package/v3/@claude-flow/cli/dist/src/index.d.ts +5 -1
- package/v3/@claude-flow/cli/dist/src/index.js +61 -18
- package/v3/@claude-flow/cli/dist/src/init/executor.js +92 -0
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +6 -2
- package/v3/@claude-flow/cli/dist/src/init/mcp-generator.js +4 -4
- package/v3/@claude-flow/cli/dist/src/init/settings-generator.js +44 -17
- package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +25 -14
- package/v3/@claude-flow/cli/dist/src/init/types.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +12 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.d.ts +3 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +16 -9
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +88 -11
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +206 -21
- package/v3/@claude-flow/cli/dist/src/mcp-tools/analyze-tools.js +6 -6
- package/v3/@claude-flow/cli/dist/src/mcp-tools/autopilot-tools.js +10 -10
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-session-tools.js +5 -5
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +23 -23
- package/v3/@claude-flow/cli/dist/src/mcp-tools/claims-tools.js +12 -12
- package/v3/@claude-flow/cli/dist/src/mcp-tools/config-tools.js +6 -6
- package/v3/@claude-flow/cli/dist/src/mcp-tools/coordination-tools.js +7 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/daa-tools.js +8 -8
- package/v3/@claude-flow/cli/dist/src/mcp-tools/embeddings-tools.js +10 -10
- package/v3/@claude-flow/cli/dist/src/mcp-tools/github-tools.js +5 -5
- package/v3/@claude-flow/cli/dist/src/mcp-tools/guidance-tools.js +21 -21
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hive-mind-tools.js +53 -9
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +183 -48
- package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.js +357 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +400 -63
- package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +20 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/performance-tools.js +6 -6
- package/v3/@claude-flow/cli/dist/src/mcp-tools/progress-tools.js +4 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/ruvllm-tools.js +10 -10
- package/v3/@claude-flow/cli/dist/src/mcp-tools/security-tools.js +34 -9
- package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +130 -6
- package/v3/@claude-flow/cli/dist/src/mcp-tools/swarm-tools.js +76 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/system-tools.js +91 -18
- package/v3/@claude-flow/cli/dist/src/mcp-tools/task-tools.js +55 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/terminal-tools.js +5 -5
- package/v3/@claude-flow/cli/dist/src/mcp-tools/transfer-tools.js +11 -11
- package/v3/@claude-flow/cli/dist/src/mcp-tools/wasm-agent-tools.js +11 -11
- package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +118 -10
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +28 -3
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +69 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +282 -5
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +8 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +80 -16
- package/v3/@claude-flow/cli/dist/src/memory/neural-package-bridge.d.ts +48 -0
- package/v3/@claude-flow/cli/dist/src/memory/neural-package-bridge.js +87 -0
- package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.js +3 -0
- package/v3/@claude-flow/cli/dist/src/parser.d.ts +9 -0
- package/v3/@claude-flow/cli/dist/src/parser.js +11 -0
- package/v3/@claude-flow/cli/dist/src/plugins/store/discovery.d.ts +6 -3
- package/v3/@claude-flow/cli/dist/src/plugins/store/discovery.js +11 -8
- package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.js +4 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/coverage-tools.js +6 -6
- package/v3/@claude-flow/cli/dist/src/ruvector/index.d.ts +0 -2
- package/v3/@claude-flow/cli/dist/src/ruvector/index.js +8 -2
- package/v3/@claude-flow/cli/dist/src/ruvector/model-router.d.ts +22 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/model-router.js +125 -5
- package/v3/@claude-flow/cli/dist/src/services/headless-worker-executor.d.ts +6 -0
- package/v3/@claude-flow/cli/dist/src/services/headless-worker-executor.js +37 -3
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +80 -2
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +372 -11
- package/v3/@claude-flow/cli/package.json +7 -5
- package/v3/@claude-flow/guidance/dist/analyzer.js +9 -0
- package/v3/@claude-flow/guidance/package.json +1 -1
- package/v3/@claude-flow/shared/dist/events/event-store.js +20 -9
- package/v3/@claude-flow/shared/dist/hooks/executor.js +7 -4
- package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.js +6 -6
- package/v3/@claude-flow/shared/dist/mcp/server.js +3 -6
- package/v3/@claude-flow/shared/dist/mcp/types.d.ts +4 -6
- package/v3/@claude-flow/shared/package.json +1 -1
|
@@ -21,6 +21,11 @@ const startCommand = {
|
|
|
21
21
|
{ name: 'sandbox', type: 'string', description: 'Default sandbox mode for headless workers', choices: ['strict', 'permissive', 'disabled'] },
|
|
22
22
|
{ name: 'max-cpu-load', type: 'string', description: 'Override maxCpuLoad resource threshold (e.g. 4.0)' },
|
|
23
23
|
{ name: 'min-free-memory', type: 'string', description: 'Override minFreeMemoryPercent resource threshold (e.g. 15)' },
|
|
24
|
+
// #1914: workspace root for this daemon. Set automatically when the
|
|
25
|
+
// background launcher forks the foreground child so the daemon process
|
|
26
|
+
// carries its workspace path in argv — `killStaleDaemons` then only
|
|
27
|
+
// reaps daemons belonging to the current workspace (ADR-014 scope).
|
|
28
|
+
{ name: 'workspace', type: 'string', description: 'Workspace root for this daemon (internal — set automatically when forking)' },
|
|
24
29
|
],
|
|
25
30
|
examples: [
|
|
26
31
|
{ command: 'claude-flow daemon start', description: 'Start daemon in background (default)' },
|
|
@@ -31,7 +36,9 @@ const startCommand = {
|
|
|
31
36
|
action: async (ctx) => {
|
|
32
37
|
const quiet = ctx.flags.quiet;
|
|
33
38
|
const foreground = ctx.flags.foreground;
|
|
34
|
-
|
|
39
|
+
// #1914: a forked daemon child receives --workspace <root>; the launcher
|
|
40
|
+
// and interactive invocations have no flag and fall back to cwd.
|
|
41
|
+
const projectRoot = resolveWorkspaceFlag(ctx.flags.workspace) ?? process.cwd();
|
|
35
42
|
const isDaemonProcess = process.env.CLAUDE_FLOW_DAEMON === '1';
|
|
36
43
|
// Parse resource threshold overrides from CLI flags
|
|
37
44
|
const config = {};
|
|
@@ -195,6 +202,37 @@ function validatePath(path, label) {
|
|
|
195
202
|
}
|
|
196
203
|
}
|
|
197
204
|
}
|
|
205
|
+
/**
|
|
206
|
+
* #1914: Resolve the `--workspace` flag to an absolute path, or return null
|
|
207
|
+
* if it is absent / not a usable string. Rejects values with null bytes or
|
|
208
|
+
* shell metacharacters (defence-in-depth — the value is later embedded in a
|
|
209
|
+
* forked child's argv and compared against `ps`/`tasklist` output).
|
|
210
|
+
*/
|
|
211
|
+
export function resolveWorkspaceFlag(raw) {
|
|
212
|
+
if (typeof raw !== 'string')
|
|
213
|
+
return null;
|
|
214
|
+
const trimmed = raw.trim();
|
|
215
|
+
if (!trimmed)
|
|
216
|
+
return null;
|
|
217
|
+
if (trimmed.includes('\0') || /[;&|`$<>]/.test(trimmed))
|
|
218
|
+
return null;
|
|
219
|
+
return resolve(trimmed);
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* #1914: True when a process command line (from `ps -eo command` on POSIX or
|
|
223
|
+
* the tasklist Window Title column on Windows) belongs to a daemon started
|
|
224
|
+
* for `workspaceRoot`. The launcher (`startBackgroundDaemon`) always appends
|
|
225
|
+
* `--workspace <root>` as the FINAL argv entry, so an exact trailing match
|
|
226
|
+
* after stripping trailing whitespace/quotes is unambiguous — even for
|
|
227
|
+
* workspace paths containing spaces — and never a bare path-prefix match,
|
|
228
|
+
* so workspace `/a/proj` does not reap `/a/proj-other`'s daemon. A daemon
|
|
229
|
+
* whose argv puts `--workspace` mid-list (only possible via a hand-rolled
|
|
230
|
+
* invocation) simply won't be auto-reaped — `daemon stop` still handles it
|
|
231
|
+
* via the PID file.
|
|
232
|
+
*/
|
|
233
|
+
export function daemonCommandLineBelongsToWorkspace(commandLine, workspaceRoot) {
|
|
234
|
+
return commandLine.replace(/[\s"']+$/u, '').endsWith(`--workspace ${workspaceRoot}`);
|
|
235
|
+
}
|
|
198
236
|
/**
|
|
199
237
|
* Start daemon as a detached background process
|
|
200
238
|
*/
|
|
@@ -266,6 +304,10 @@ async function startBackgroundDaemon(projectRoot, quiet, maxCpuLoad, minFreeMemo
|
|
|
266
304
|
if (minFreeMemory && SPAWN_NUMERIC_RE.test(minFreeMemory)) {
|
|
267
305
|
forkArgs.push('--min-free-memory', minFreeMemory);
|
|
268
306
|
}
|
|
307
|
+
// #1914: stamp the workspace into argv (kept LAST) so the foreground daemon
|
|
308
|
+
// process is self-identifying and `killStaleDaemons` only reaps daemons
|
|
309
|
+
// belonging to this workspace. resolvedRoot was validatePath()'d above.
|
|
310
|
+
forkArgs.push('--workspace', resolvedRoot);
|
|
269
311
|
const child = fork(cliPath, forkArgs, forkOpts);
|
|
270
312
|
// Get PID from spawned process directly (no shell echo needed)
|
|
271
313
|
const pid = child.pid;
|
|
@@ -391,22 +433,35 @@ async function killBackgroundDaemon(projectRoot) {
|
|
|
391
433
|
}
|
|
392
434
|
}
|
|
393
435
|
/**
|
|
394
|
-
* Kill stale daemon processes not tracked by the PID file (#1551).
|
|
395
|
-
* Uses `ps`
|
|
436
|
+
* Kill stale daemon processes not tracked by the PID file (#1551, #1857).
|
|
437
|
+
* Uses `ps` on POSIX and `tasklist` on Windows to find all daemon
|
|
438
|
+
* processes for this project and kill them.
|
|
396
439
|
*/
|
|
397
440
|
async function killStaleDaemons(projectRoot, quiet) {
|
|
441
|
+
if (process.platform === 'win32') {
|
|
442
|
+
return killStaleDaemonsWindows(projectRoot, quiet);
|
|
443
|
+
}
|
|
444
|
+
return killStaleDaemonsPosix(projectRoot, quiet);
|
|
445
|
+
}
|
|
446
|
+
async function killStaleDaemonsPosix(projectRoot, quiet) {
|
|
398
447
|
try {
|
|
399
448
|
const { execFileSync } = await import('child_process');
|
|
400
449
|
const psOutput = execFileSync('ps', ['-eo', 'pid,command'], { encoding: 'utf-8', timeout: 5000 });
|
|
401
450
|
const lines = psOutput.split('\n');
|
|
402
451
|
const currentPid = process.pid;
|
|
403
452
|
const trackedPid = getBackgroundDaemonPid(projectRoot);
|
|
453
|
+
// #1914: only ever reap daemons belonging to THIS workspace (ADR-014).
|
|
454
|
+
const resolvedRoot = resolve(projectRoot);
|
|
404
455
|
let killed = 0;
|
|
405
456
|
for (const line of lines) {
|
|
406
457
|
if (!line.includes('daemon start --foreground'))
|
|
407
458
|
continue;
|
|
408
459
|
if (!line.includes('claude-flow') && !line.includes('@claude-flow/cli'))
|
|
409
460
|
continue;
|
|
461
|
+
// #1914: skip daemons from other workspaces (or pre-#1914 versions that
|
|
462
|
+
// didn't stamp --workspace — let `daemon stop` handle those via PID file).
|
|
463
|
+
if (!daemonCommandLineBelongsToWorkspace(line, resolvedRoot))
|
|
464
|
+
continue;
|
|
410
465
|
const pidStr = line.trim().split(/\s+/)[0];
|
|
411
466
|
const pid = parseInt(pidStr, 10);
|
|
412
467
|
if (isNaN(pid) || pid === currentPid || pid === trackedPid)
|
|
@@ -430,6 +485,68 @@ async function killStaleDaemons(projectRoot, quiet) {
|
|
|
430
485
|
// ps not available or failed — skip stale cleanup
|
|
431
486
|
}
|
|
432
487
|
}
|
|
488
|
+
/**
|
|
489
|
+
* #1857: Windows replacement for the POSIX `ps -eo pid,command` path.
|
|
490
|
+
* Uses `tasklist /v /fo csv` which returns CSV with the full Window
|
|
491
|
+
* Title column (last field) — Node-spawned daemon processes carry
|
|
492
|
+
* their command line there. Best-effort like the POSIX path: any
|
|
493
|
+
* tooling failure (tasklist missing, parse error, etc.) is swallowed
|
|
494
|
+
* silently so cleanup doesn't break daemon start.
|
|
495
|
+
*/
|
|
496
|
+
async function killStaleDaemonsWindows(projectRoot, quiet) {
|
|
497
|
+
try {
|
|
498
|
+
const { execFileSync } = await import('child_process');
|
|
499
|
+
// /v includes the Window Title; /fo csv uses comma-separated quoted fields
|
|
500
|
+
const out = execFileSync('tasklist', ['/v', '/fo', 'csv', '/nh'], { encoding: 'utf-8', timeout: 5000 });
|
|
501
|
+
const lines = out.split(/\r?\n/);
|
|
502
|
+
const currentPid = process.pid;
|
|
503
|
+
const trackedPid = getBackgroundDaemonPid(projectRoot);
|
|
504
|
+
// #1914: only ever reap daemons belonging to THIS workspace (ADR-014).
|
|
505
|
+
const resolvedRoot = resolve(projectRoot);
|
|
506
|
+
let killed = 0;
|
|
507
|
+
for (const line of lines) {
|
|
508
|
+
if (!line.trim())
|
|
509
|
+
continue;
|
|
510
|
+
// Match daemon command line markers — the Window Title field
|
|
511
|
+
// typically holds the full invocation. Skip rows that aren't ours.
|
|
512
|
+
if (!line.includes('daemon start --foreground'))
|
|
513
|
+
continue;
|
|
514
|
+
if (!line.includes('claude-flow') && !line.includes('@claude-flow/cli'))
|
|
515
|
+
continue;
|
|
516
|
+
// #1914: skip daemons from other workspaces (or pre-#1914 versions).
|
|
517
|
+
if (!daemonCommandLineBelongsToWorkspace(line, resolvedRoot))
|
|
518
|
+
continue;
|
|
519
|
+
// Parse CSV: tasklist quotes each field, so split on `","`
|
|
520
|
+
const fields = line.split(/","/).map(f => f.replace(/^"|"$/g, ''));
|
|
521
|
+
// fields[0] = Image Name, fields[1] = PID, …
|
|
522
|
+
const pidStr = fields[1];
|
|
523
|
+
const pid = parseInt(pidStr ?? '', 10);
|
|
524
|
+
if (isNaN(pid) || pid === currentPid || pid === trackedPid)
|
|
525
|
+
continue;
|
|
526
|
+
if (!isProcessRunning(pid))
|
|
527
|
+
continue;
|
|
528
|
+
try {
|
|
529
|
+
// taskkill is the Windows equivalent of kill — /pid <n> /f forces.
|
|
530
|
+
// Use SIGTERM-equivalent (no /f) first; the daemon's signal handler
|
|
531
|
+
// catches and cleans up; force-kill is the next start's job.
|
|
532
|
+
execFileSync('taskkill', ['/pid', String(pid), '/t'], { encoding: 'utf-8', timeout: 5000 });
|
|
533
|
+
killed++;
|
|
534
|
+
if (!quiet) {
|
|
535
|
+
output.printWarning(`Killed stale daemon process (PID: ${pid})`);
|
|
536
|
+
}
|
|
537
|
+
}
|
|
538
|
+
catch { /* taskkill failed — process may have exited; ignore */ }
|
|
539
|
+
}
|
|
540
|
+
if (killed > 0 && !quiet) {
|
|
541
|
+
output.printInfo(`Cleaned up ${killed} stale daemon process(es)`);
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
catch {
|
|
545
|
+
// tasklist not available or failed — skip stale cleanup. Defensive
|
|
546
|
+
// shape matches the POSIX path. Not tested on Windows by the
|
|
547
|
+
// maintainer; please report regressions on the issue tracker.
|
|
548
|
+
}
|
|
549
|
+
}
|
|
433
550
|
/**
|
|
434
551
|
* Get PID of background daemon from PID file
|
|
435
552
|
*/
|
|
@@ -671,6 +788,222 @@ function formatTimeUntil(date) {
|
|
|
671
788
|
return `in ${Math.floor(seconds / 3600)}h`;
|
|
672
789
|
return `in ${Math.floor(seconds / 86400)}d`;
|
|
673
790
|
}
|
|
791
|
+
// #1565: Supervisor installer subcommand. Writes a native auto-restart
|
|
792
|
+
// unit (launchd plist on macOS, systemd-user .service on Linux) so the
|
|
793
|
+
// daemon survives crashes and reboots without requiring the operator
|
|
794
|
+
// to manually run `daemon start` after every failure.
|
|
795
|
+
const installSupervisorCommand = {
|
|
796
|
+
name: 'install-supervisor',
|
|
797
|
+
description: 'Install OS-level auto-restart supervisor (launchd on macOS, systemd-user on Linux)',
|
|
798
|
+
options: [
|
|
799
|
+
{ name: 'force', short: 'f', type: 'boolean', description: 'Overwrite existing unit file', default: 'false' },
|
|
800
|
+
{ name: 'load', type: 'boolean', description: 'Load/enable the unit immediately', default: 'true' },
|
|
801
|
+
{ name: 'dry-run', type: 'boolean', description: 'Print the unit file content without writing', default: 'false' },
|
|
802
|
+
],
|
|
803
|
+
examples: [
|
|
804
|
+
{ command: 'claude-flow daemon install-supervisor', description: 'Install + load (auto-restart enabled)' },
|
|
805
|
+
{ command: 'claude-flow daemon install-supervisor --no-load', description: 'Write unit file but do not enable yet' },
|
|
806
|
+
{ command: 'claude-flow daemon install-supervisor --dry-run', description: 'Preview the unit file' },
|
|
807
|
+
],
|
|
808
|
+
action: async (ctx) => {
|
|
809
|
+
const force = ctx.flags.force === true;
|
|
810
|
+
const load = ctx.flags.load !== false;
|
|
811
|
+
const dryRun = ctx.flags['dry-run'] === true || ctx.flags.dryRun === true;
|
|
812
|
+
const projectRoot = process.cwd();
|
|
813
|
+
const platform = process.platform;
|
|
814
|
+
if (platform === 'win32') {
|
|
815
|
+
output.printError('Windows scheduled-task installer is not yet implemented.');
|
|
816
|
+
output.printInfo('Use Task Scheduler manually, or follow this issue: https://github.com/ruvnet/ruflo/issues/1565');
|
|
817
|
+
return { success: false, exitCode: 1 };
|
|
818
|
+
}
|
|
819
|
+
if (platform !== 'darwin' && platform !== 'linux') {
|
|
820
|
+
output.printError(`Unsupported platform: ${platform}. Supported: darwin (launchd), linux (systemd-user).`);
|
|
821
|
+
return { success: false, exitCode: 1 };
|
|
822
|
+
}
|
|
823
|
+
// Resolve absolute paths the unit file will reference.
|
|
824
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
825
|
+
if (!home) {
|
|
826
|
+
output.printError('HOME/USERPROFILE not set; cannot resolve user unit path.');
|
|
827
|
+
return { success: false, exitCode: 1 };
|
|
828
|
+
}
|
|
829
|
+
const nodeBin = process.execPath;
|
|
830
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
831
|
+
const __dirname = dirname(__filename);
|
|
832
|
+
const cliJs = resolve(join(__dirname, '..', '..', '..', 'bin', 'cli.js'));
|
|
833
|
+
if (!fs.existsSync(cliJs)) {
|
|
834
|
+
output.printError(`CLI not found at: ${cliJs}`);
|
|
835
|
+
return { success: false, exitCode: 1 };
|
|
836
|
+
}
|
|
837
|
+
if (platform === 'darwin') {
|
|
838
|
+
const plistDir = join(home, 'Library', 'LaunchAgents');
|
|
839
|
+
const plistPath = join(plistDir, 'io.ruv.ruflo.daemon.plist');
|
|
840
|
+
const logDir = join(projectRoot, '.claude-flow', 'logs');
|
|
841
|
+
const plist = `<?xml version="1.0" encoding="UTF-8"?>
|
|
842
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
843
|
+
<plist version="1.0">
|
|
844
|
+
<dict>
|
|
845
|
+
<key>Label</key><string>io.ruv.ruflo.daemon</string>
|
|
846
|
+
<key>ProgramArguments</key>
|
|
847
|
+
<array>
|
|
848
|
+
<string>${nodeBin}</string>
|
|
849
|
+
<string>${cliJs}</string>
|
|
850
|
+
<string>daemon</string><string>start</string><string>--foreground</string><string>--quiet</string>
|
|
851
|
+
</array>
|
|
852
|
+
<key>WorkingDirectory</key><string>${projectRoot}</string>
|
|
853
|
+
<key>RunAtLoad</key><true/>
|
|
854
|
+
<key>KeepAlive</key>
|
|
855
|
+
<dict>
|
|
856
|
+
<key>SuccessfulExit</key><false/>
|
|
857
|
+
<key>Crashed</key><true/>
|
|
858
|
+
</dict>
|
|
859
|
+
<key>ThrottleInterval</key><integer>10</integer>
|
|
860
|
+
<key>StandardOutPath</key><string>${logDir}/supervisor.out.log</string>
|
|
861
|
+
<key>StandardErrorPath</key><string>${logDir}/supervisor.err.log</string>
|
|
862
|
+
<key>EnvironmentVariables</key>
|
|
863
|
+
<dict>
|
|
864
|
+
<key>CLAUDE_FLOW_DAEMON</key><string>1</string>
|
|
865
|
+
</dict>
|
|
866
|
+
</dict>
|
|
867
|
+
</plist>
|
|
868
|
+
`;
|
|
869
|
+
if (dryRun) {
|
|
870
|
+
output.writeln(plist);
|
|
871
|
+
return { success: true };
|
|
872
|
+
}
|
|
873
|
+
if (fs.existsSync(plistPath) && !force) {
|
|
874
|
+
output.printWarning(`Already installed: ${plistPath}`);
|
|
875
|
+
output.printInfo('Use --force to overwrite.');
|
|
876
|
+
return { success: false, exitCode: 1 };
|
|
877
|
+
}
|
|
878
|
+
if (!fs.existsSync(plistDir))
|
|
879
|
+
fs.mkdirSync(plistDir, { recursive: true });
|
|
880
|
+
if (!fs.existsSync(logDir))
|
|
881
|
+
fs.mkdirSync(logDir, { recursive: true });
|
|
882
|
+
fs.writeFileSync(plistPath, plist, 'utf-8');
|
|
883
|
+
output.printSuccess(`Wrote ${plistPath}`);
|
|
884
|
+
if (load) {
|
|
885
|
+
try {
|
|
886
|
+
const { execFileSync } = await import('child_process');
|
|
887
|
+
// unload first in case a previous version is loaded
|
|
888
|
+
try {
|
|
889
|
+
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', timeout: 5000 });
|
|
890
|
+
}
|
|
891
|
+
catch { /* ok */ }
|
|
892
|
+
execFileSync('launchctl', ['load', '-w', plistPath], { encoding: 'utf-8', timeout: 5000 });
|
|
893
|
+
output.printSuccess('Supervisor loaded — daemon will auto-restart on crash and survive reboot.');
|
|
894
|
+
}
|
|
895
|
+
catch (err) {
|
|
896
|
+
output.printWarning(`launchctl load failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
897
|
+
output.printInfo(`Run manually: launchctl load -w ${plistPath}`);
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
else {
|
|
901
|
+
output.printInfo(`Run when ready: launchctl load -w ${plistPath}`);
|
|
902
|
+
}
|
|
903
|
+
return { success: true };
|
|
904
|
+
}
|
|
905
|
+
// Linux: systemd-user
|
|
906
|
+
const unitDir = join(home, '.config', 'systemd', 'user');
|
|
907
|
+
const unitPath = join(unitDir, 'ruflo-daemon.service');
|
|
908
|
+
const unit = `[Unit]
|
|
909
|
+
Description=RuFlo background worker daemon
|
|
910
|
+
After=default.target
|
|
911
|
+
|
|
912
|
+
[Service]
|
|
913
|
+
Type=simple
|
|
914
|
+
WorkingDirectory=${projectRoot}
|
|
915
|
+
Environment=CLAUDE_FLOW_DAEMON=1
|
|
916
|
+
ExecStart=${nodeBin} ${cliJs} daemon start --foreground --quiet
|
|
917
|
+
Restart=on-failure
|
|
918
|
+
RestartSec=10
|
|
919
|
+
# Restart on Crashed (signal) too
|
|
920
|
+
StartLimitIntervalSec=300
|
|
921
|
+
StartLimitBurst=5
|
|
922
|
+
|
|
923
|
+
[Install]
|
|
924
|
+
WantedBy=default.target
|
|
925
|
+
`;
|
|
926
|
+
if (dryRun) {
|
|
927
|
+
output.writeln(unit);
|
|
928
|
+
return { success: true };
|
|
929
|
+
}
|
|
930
|
+
if (fs.existsSync(unitPath) && !force) {
|
|
931
|
+
output.printWarning(`Already installed: ${unitPath}`);
|
|
932
|
+
output.printInfo('Use --force to overwrite.');
|
|
933
|
+
return { success: false, exitCode: 1 };
|
|
934
|
+
}
|
|
935
|
+
if (!fs.existsSync(unitDir))
|
|
936
|
+
fs.mkdirSync(unitDir, { recursive: true });
|
|
937
|
+
fs.writeFileSync(unitPath, unit, 'utf-8');
|
|
938
|
+
output.printSuccess(`Wrote ${unitPath}`);
|
|
939
|
+
if (load) {
|
|
940
|
+
try {
|
|
941
|
+
const { execFileSync } = await import('child_process');
|
|
942
|
+
execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8', timeout: 5000 });
|
|
943
|
+
execFileSync('systemctl', ['--user', 'enable', '--now', 'ruflo-daemon.service'], { encoding: 'utf-8', timeout: 10000 });
|
|
944
|
+
output.printSuccess('Supervisor enabled — daemon will auto-restart on crash and survive reboot.');
|
|
945
|
+
output.printInfo('Note: requires `loginctl enable-linger $USER` for restart-after-logout on some distros.');
|
|
946
|
+
}
|
|
947
|
+
catch (err) {
|
|
948
|
+
output.printWarning(`systemctl --user enable failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
949
|
+
output.printInfo(`Run manually: systemctl --user daemon-reload && systemctl --user enable --now ruflo-daemon.service`);
|
|
950
|
+
}
|
|
951
|
+
}
|
|
952
|
+
else {
|
|
953
|
+
output.printInfo(`Run when ready: systemctl --user daemon-reload && systemctl --user enable --now ruflo-daemon.service`);
|
|
954
|
+
}
|
|
955
|
+
return { success: true };
|
|
956
|
+
},
|
|
957
|
+
};
|
|
958
|
+
const uninstallSupervisorCommand = {
|
|
959
|
+
name: 'uninstall-supervisor',
|
|
960
|
+
description: 'Remove the auto-restart supervisor unit (launchd on macOS, systemd-user on Linux)',
|
|
961
|
+
options: [],
|
|
962
|
+
action: async () => {
|
|
963
|
+
const platform = process.platform;
|
|
964
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
|
|
965
|
+
if (platform === 'darwin') {
|
|
966
|
+
const plistPath = join(home, 'Library', 'LaunchAgents', 'io.ruv.ruflo.daemon.plist');
|
|
967
|
+
try {
|
|
968
|
+
const { execFileSync } = await import('child_process');
|
|
969
|
+
try {
|
|
970
|
+
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', timeout: 5000 });
|
|
971
|
+
}
|
|
972
|
+
catch { /* ok */ }
|
|
973
|
+
}
|
|
974
|
+
catch { /* ignore */ }
|
|
975
|
+
if (fs.existsSync(plistPath)) {
|
|
976
|
+
fs.unlinkSync(plistPath);
|
|
977
|
+
output.printSuccess(`Removed ${plistPath}`);
|
|
978
|
+
}
|
|
979
|
+
else {
|
|
980
|
+
output.printInfo(`Not installed: ${plistPath}`);
|
|
981
|
+
}
|
|
982
|
+
return { success: true };
|
|
983
|
+
}
|
|
984
|
+
if (platform === 'linux') {
|
|
985
|
+
const unitPath = join(home, '.config', 'systemd', 'user', 'ruflo-daemon.service');
|
|
986
|
+
try {
|
|
987
|
+
const { execFileSync } = await import('child_process');
|
|
988
|
+
try {
|
|
989
|
+
execFileSync('systemctl', ['--user', 'disable', '--now', 'ruflo-daemon.service'], { encoding: 'utf-8', timeout: 5000 });
|
|
990
|
+
}
|
|
991
|
+
catch { /* ok */ }
|
|
992
|
+
}
|
|
993
|
+
catch { /* ignore */ }
|
|
994
|
+
if (fs.existsSync(unitPath)) {
|
|
995
|
+
fs.unlinkSync(unitPath);
|
|
996
|
+
output.printSuccess(`Removed ${unitPath}`);
|
|
997
|
+
}
|
|
998
|
+
else {
|
|
999
|
+
output.printInfo(`Not installed: ${unitPath}`);
|
|
1000
|
+
}
|
|
1001
|
+
return { success: true };
|
|
1002
|
+
}
|
|
1003
|
+
output.printError(`Unsupported platform: ${platform}`);
|
|
1004
|
+
return { success: false, exitCode: 1 };
|
|
1005
|
+
},
|
|
1006
|
+
};
|
|
674
1007
|
// Main daemon command
|
|
675
1008
|
export const daemonCommand = {
|
|
676
1009
|
name: 'daemon',
|
|
@@ -681,6 +1014,8 @@ export const daemonCommand = {
|
|
|
681
1014
|
statusCommand,
|
|
682
1015
|
triggerCommand,
|
|
683
1016
|
enableCommand,
|
|
1017
|
+
installSupervisorCommand,
|
|
1018
|
+
uninstallSupervisorCommand,
|
|
684
1019
|
],
|
|
685
1020
|
options: [],
|
|
686
1021
|
examples: [
|