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.
Files changed (104) hide show
  1. package/.claude/agents/dual-mode/codex-coordinator.md +34 -29
  2. package/.claude/agents/dual-mode/codex-worker.md +39 -32
  3. package/.claude/helpers/hook-handler.cjs +4 -1
  4. package/.claude/helpers/statusline.cjs +26 -5
  5. package/.claude/scheduled_tasks.lock +1 -0
  6. package/.claude/settings.json +2 -0
  7. package/.claude/settings.local.json +10 -1
  8. package/.claude/skills/browser/SKILL.md +204 -0
  9. package/.claude/skills/github-project-management/SKILL.md +13 -0
  10. package/.claude/skills/verification-quality/SKILL.md +117 -73
  11. package/.claude-plugin/hooks/hooks.json +6 -5
  12. package/.claude-plugin/marketplace.json +3 -3
  13. package/.claude-plugin/scripts/ruflo-hook.sh +33 -0
  14. package/README.md +43 -39
  15. package/package.json +8 -2
  16. package/v3/@claude-flow/cli/README.md +43 -39
  17. package/v3/@claude-flow/cli/bin/cli.js +15 -2
  18. package/v3/@claude-flow/cli/bin/mcp-server.js +1 -1
  19. package/v3/@claude-flow/cli/dist/src/commands/agent-wasm.js +2 -2
  20. package/v3/@claude-flow/cli/dist/src/commands/daemon.d.ts +20 -0
  21. package/v3/@claude-flow/cli/dist/src/commands/daemon.js +338 -3
  22. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +224 -46
  23. package/v3/@claude-flow/cli/dist/src/commands/hive-mind.js +25 -7
  24. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +56 -29
  25. package/v3/@claude-flow/cli/dist/src/commands/memory.js +90 -3
  26. package/v3/@claude-flow/cli/dist/src/commands/start.js +1 -1
  27. package/v3/@claude-flow/cli/dist/src/commands/swarm.js +1 -1
  28. package/v3/@claude-flow/cli/dist/src/commands/task.js +8 -4
  29. package/v3/@claude-flow/cli/dist/src/config-adapter.js +1 -1
  30. package/v3/@claude-flow/cli/dist/src/index.d.ts +5 -1
  31. package/v3/@claude-flow/cli/dist/src/index.js +61 -18
  32. package/v3/@claude-flow/cli/dist/src/init/executor.js +92 -0
  33. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +6 -2
  34. package/v3/@claude-flow/cli/dist/src/init/mcp-generator.js +4 -4
  35. package/v3/@claude-flow/cli/dist/src/init/settings-generator.js +44 -17
  36. package/v3/@claude-flow/cli/dist/src/init/statusline-generator.js +25 -14
  37. package/v3/@claude-flow/cli/dist/src/init/types.d.ts +7 -0
  38. package/v3/@claude-flow/cli/dist/src/mcp-client.js +12 -0
  39. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.d.ts +3 -2
  40. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +16 -9
  41. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +88 -11
  42. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.d.ts +3 -0
  43. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +206 -21
  44. package/v3/@claude-flow/cli/dist/src/mcp-tools/analyze-tools.js +6 -6
  45. package/v3/@claude-flow/cli/dist/src/mcp-tools/autopilot-tools.js +10 -10
  46. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-session-tools.js +5 -5
  47. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-tools.js +23 -23
  48. package/v3/@claude-flow/cli/dist/src/mcp-tools/claims-tools.js +12 -12
  49. package/v3/@claude-flow/cli/dist/src/mcp-tools/config-tools.js +6 -6
  50. package/v3/@claude-flow/cli/dist/src/mcp-tools/coordination-tools.js +7 -7
  51. package/v3/@claude-flow/cli/dist/src/mcp-tools/daa-tools.js +8 -8
  52. package/v3/@claude-flow/cli/dist/src/mcp-tools/embeddings-tools.js +10 -10
  53. package/v3/@claude-flow/cli/dist/src/mcp-tools/github-tools.js +5 -5
  54. package/v3/@claude-flow/cli/dist/src/mcp-tools/guidance-tools.js +21 -21
  55. package/v3/@claude-flow/cli/dist/src/mcp-tools/hive-mind-tools.js +53 -9
  56. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.d.ts +2 -0
  57. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +183 -48
  58. package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.d.ts +22 -0
  59. package/v3/@claude-flow/cli/dist/src/mcp-tools/managed-agent-tools.js +357 -0
  60. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +400 -63
  61. package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +20 -7
  62. package/v3/@claude-flow/cli/dist/src/mcp-tools/performance-tools.js +6 -6
  63. package/v3/@claude-flow/cli/dist/src/mcp-tools/progress-tools.js +4 -4
  64. package/v3/@claude-flow/cli/dist/src/mcp-tools/ruvllm-tools.js +10 -10
  65. package/v3/@claude-flow/cli/dist/src/mcp-tools/security-tools.js +34 -9
  66. package/v3/@claude-flow/cli/dist/src/mcp-tools/session-tools.js +130 -6
  67. package/v3/@claude-flow/cli/dist/src/mcp-tools/swarm-tools.js +76 -7
  68. package/v3/@claude-flow/cli/dist/src/mcp-tools/system-tools.js +91 -18
  69. package/v3/@claude-flow/cli/dist/src/mcp-tools/task-tools.js +55 -7
  70. package/v3/@claude-flow/cli/dist/src/mcp-tools/terminal-tools.js +5 -5
  71. package/v3/@claude-flow/cli/dist/src/mcp-tools/transfer-tools.js +11 -11
  72. package/v3/@claude-flow/cli/dist/src/mcp-tools/wasm-agent-tools.js +11 -11
  73. package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +118 -10
  74. package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +28 -3
  75. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +69 -0
  76. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +282 -5
  77. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +8 -0
  78. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +80 -16
  79. package/v3/@claude-flow/cli/dist/src/memory/neural-package-bridge.d.ts +48 -0
  80. package/v3/@claude-flow/cli/dist/src/memory/neural-package-bridge.js +87 -0
  81. package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.js +3 -0
  82. package/v3/@claude-flow/cli/dist/src/parser.d.ts +9 -0
  83. package/v3/@claude-flow/cli/dist/src/parser.js +11 -0
  84. package/v3/@claude-flow/cli/dist/src/plugins/store/discovery.d.ts +6 -3
  85. package/v3/@claude-flow/cli/dist/src/plugins/store/discovery.js +11 -8
  86. package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.js +4 -1
  87. package/v3/@claude-flow/cli/dist/src/ruvector/coverage-tools.js +6 -6
  88. package/v3/@claude-flow/cli/dist/src/ruvector/index.d.ts +0 -2
  89. package/v3/@claude-flow/cli/dist/src/ruvector/index.js +8 -2
  90. package/v3/@claude-flow/cli/dist/src/ruvector/model-router.d.ts +22 -1
  91. package/v3/@claude-flow/cli/dist/src/ruvector/model-router.js +125 -5
  92. package/v3/@claude-flow/cli/dist/src/services/headless-worker-executor.d.ts +6 -0
  93. package/v3/@claude-flow/cli/dist/src/services/headless-worker-executor.js +37 -3
  94. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +80 -2
  95. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +372 -11
  96. package/v3/@claude-flow/cli/package.json +7 -5
  97. package/v3/@claude-flow/guidance/dist/analyzer.js +9 -0
  98. package/v3/@claude-flow/guidance/package.json +1 -1
  99. package/v3/@claude-flow/shared/dist/events/event-store.js +20 -9
  100. package/v3/@claude-flow/shared/dist/hooks/executor.js +7 -4
  101. package/v3/@claude-flow/shared/dist/hooks/verify-exports.test.js +6 -6
  102. package/v3/@claude-flow/shared/dist/mcp/server.js +3 -6
  103. package/v3/@claude-flow/shared/dist/mcp/types.d.ts +4 -6
  104. 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
- const projectRoot = process.cwd();
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` to find all daemon processes for this project and kills them.
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: [