claude-flow
Version:
Ruflo - Enterprise AI agent orchestration for Claude Code. Deploy 60+ specialized agents in coordinated swarms with self-learning, fault-tolerant consensus, vector memory, and MCP integration
1,053 lines (1,050 loc) • 52.6 kB
JavaScript
/**
* V3 CLI Daemon Command
* Manages background worker daemon (Node.js-based, similar to shell helpers)
*/
import { output } from '../output.js';
import { getDaemon, startDaemon, stopDaemon } from '../services/worker-daemon.js';
import { fork } from 'child_process';
import { fileURLToPath } from 'url';
import { dirname, join, resolve } from 'path';
import * as fs from 'fs';
// Start daemon subcommand
const startCommand = {
name: 'start',
description: 'Start the worker daemon with all enabled background workers',
options: [
{ name: 'workers', short: 'w', type: 'string', description: 'Comma-separated list of workers to enable (default: map,audit,optimize,consolidate,testgaps)' },
{ name: 'quiet', short: 'Q', type: 'boolean', description: 'Suppress output' },
{ name: 'background', short: 'b', type: 'boolean', description: 'Run daemon in background (detached process)', default: true },
{ name: 'foreground', short: 'f', type: 'boolean', description: 'Run daemon in foreground (blocks terminal)' },
{ name: 'headless', type: 'boolean', description: 'Enable headless worker execution (E2B sandbox)' },
{ name: 'sandbox', type: 'string', description: 'Default sandbox mode for headless workers', choices: ['strict', 'permissive', 'disabled'] },
{ name: 'max-cpu-load', type: 'string', description: 'Override maxCpuLoad resource threshold (e.g. 4.0)' },
{ name: 'min-free-memory', type: 'string', description: 'Override minFreeMemoryPercent resource threshold (e.g. 15)' },
// #1914: workspace root for this daemon. Set automatically when the
// background launcher forks the foreground child so the daemon process
// carries its workspace path in argv — `killStaleDaemons` then only
// reaps daemons belonging to the current workspace (ADR-014 scope).
{ name: 'workspace', type: 'string', description: 'Workspace root for this daemon (internal — set automatically when forking)' },
],
examples: [
{ command: 'claude-flow daemon start', description: 'Start daemon in background (default)' },
{ command: 'claude-flow daemon start --foreground', description: 'Start in foreground (blocks terminal)' },
{ command: 'claude-flow daemon start -w map,audit,optimize', description: 'Start with specific workers' },
{ command: 'claude-flow daemon start --headless --sandbox strict', description: 'Start with headless workers in strict sandbox' },
],
action: async (ctx) => {
const quiet = ctx.flags.quiet;
const foreground = ctx.flags.foreground;
// #1914: a forked daemon child receives --workspace <root>; the launcher
// and interactive invocations have no flag and fall back to cwd.
const projectRoot = resolveWorkspaceFlag(ctx.flags.workspace) ?? process.cwd();
const isDaemonProcess = process.env.CLAUDE_FLOW_DAEMON === '1';
// Parse resource threshold overrides from CLI flags
const config = {};
const rawMaxCpu = ctx.flags['max-cpu-load'];
const rawMinMem = ctx.flags['min-free-memory'];
// Strict numeric pattern to prevent command injection when forwarding to subprocess (S1)
const NUMERIC_RE = /^\d+(\.\d+)?$/;
const sanitize = (s) => s.replace(/[\x00-\x1f\x7f-\x9f]/g, '');
if (rawMaxCpu || rawMinMem) {
const thresholds = {};
if (rawMaxCpu) {
const val = parseFloat(rawMaxCpu);
if (NUMERIC_RE.test(rawMaxCpu) && isFinite(val) && val > 0 && val <= 1000) {
thresholds.maxCpuLoad = val;
}
else if (!quiet) {
output.printWarning(`Ignoring invalid --max-cpu-load value: ${sanitize(rawMaxCpu)}`);
}
}
if (rawMinMem) {
const val = parseFloat(rawMinMem);
if (NUMERIC_RE.test(rawMinMem) && isFinite(val) && val >= 0 && val <= 100) {
thresholds.minFreeMemoryPercent = val;
}
else if (!quiet) {
output.printWarning(`Ignoring invalid --min-free-memory value: ${sanitize(rawMinMem)}`);
}
}
if (thresholds.maxCpuLoad !== undefined || thresholds.minFreeMemoryPercent !== undefined) {
config.resourceThresholds = thresholds;
}
}
// Check if background daemon already running (skip if we ARE the daemon process)
if (!isDaemonProcess) {
const bgPid = getBackgroundDaemonPid(projectRoot);
if (bgPid && isProcessRunning(bgPid)) {
if (!quiet) {
output.printWarning(`Daemon already running in background (PID: ${bgPid}). Stop it first with: daemon stop`);
}
return { success: true };
}
// #1551: Kill any stale daemon processes that weren't tracked by PID file
await killStaleDaemons(projectRoot, quiet);
}
// Background mode (default): fork a detached process.
// #1968: previously only forwarded resource thresholds — `--workers`,
// `--headless`, and `--sandbox` were dropped on the floor when the
// launcher forked the foreground child, so `daemon start --workers map`
// got the full default worker set instead.
if (!foreground) {
return startBackgroundDaemon(projectRoot, quiet, {
maxCpuLoad: rawMaxCpu,
minFreeMemory: rawMinMem,
workers: ctx.flags.workers,
headless: ctx.flags.headless,
sandbox: ctx.flags.sandbox,
});
}
// Foreground mode: run in current process (blocks terminal)
try {
const stateDir = join(projectRoot, '.claude-flow');
const pidFile = join(stateDir, 'daemon.pid');
// Ensure state directory exists
if (!fs.existsSync(stateDir)) {
fs.mkdirSync(stateDir, { recursive: true });
}
// NOTE: Do NOT write PID file here — startDaemon() writes it internally.
// Writing it before startDaemon() causes checkExistingDaemon() to detect
// our own PID and return early, leaving no workers scheduled (#1478 Bug 1).
// Clean up PID file on exit
const cleanup = () => {
try {
if (fs.existsSync(pidFile)) {
fs.unlinkSync(pidFile);
}
}
catch { /* ignore */ }
};
process.on('exit', cleanup);
process.on('SIGINT', () => { cleanup(); process.exit(0); });
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
// Ignore SIGHUP on macOS/Linux — prevents daemon death when terminal closes (#1283)
if (process.platform !== 'win32') {
process.on('SIGHUP', () => { });
}
if (!quiet) {
const spinner = output.createSpinner({ text: 'Starting worker daemon...', spinner: 'dots' });
spinner.start();
const daemon = await startDaemon(projectRoot, config);
const status = daemon.getStatus();
spinner.succeed('Worker daemon started (foreground mode)');
output.writeln();
output.printBox([
`PID: ${status.pid}`,
`Started: ${status.startedAt?.toISOString()}`,
`Workers: ${status.config.workers.filter(w => w.enabled).length} enabled`,
`Max Concurrent: ${status.config.maxConcurrent}`,
`Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
`Min Free Memory: ${status.config.resourceThresholds.minFreeMemoryPercent}%`,
].join('\n'), 'Daemon Status');
output.writeln();
output.writeln(output.bold('Scheduled Workers'));
output.printTable({
columns: [
{ key: 'type', header: 'Worker', width: 15 },
{ key: 'interval', header: 'Interval', width: 12 },
{ key: 'priority', header: 'Priority', width: 10 },
{ key: 'description', header: 'Description', width: 30 },
],
data: status.config.workers
.filter(w => w.enabled)
.map(w => ({
type: output.highlight(w.type),
interval: `${Math.round(w.intervalMs / 60000)}min`,
priority: w.priority === 'critical' ? output.error(w.priority) :
w.priority === 'high' ? output.warning(w.priority) :
output.dim(w.priority),
description: w.description,
})),
});
output.writeln();
output.writeln(output.dim('Press Ctrl+C to stop daemon'));
// Listen for worker events
daemon.on('worker:start', ({ type }) => {
output.writeln(output.dim(`[daemon] Worker starting: ${type}`));
});
daemon.on('worker:complete', ({ type, durationMs }) => {
output.writeln(output.success(`[daemon] Worker completed: ${type} (${durationMs}ms)`));
});
daemon.on('worker:error', ({ type, error }) => {
output.writeln(output.error(`[daemon] Worker failed: ${type} - ${error}`));
});
// Keep process alive — setInterval creates a ref'd handle that prevents
// Node.js from exiting even when startDaemon's timers are unref'd (#1478 Bug 2).
setInterval(() => { }, 60_000);
await new Promise(() => { }); // Never resolves - daemon runs until killed
}
else {
await startDaemon(projectRoot, config);
setInterval(() => { }, 60_000); // Keep alive with ref'd handle (#1478)
await new Promise(() => { }); // Keep alive
}
return { success: true };
}
catch (error) {
output.printError(`Failed to start daemon: ${error instanceof Error ? error.message : String(error)}`);
return { success: false, exitCode: 1 };
}
},
};
/**
* Validate path for security - prevents path traversal and injection
*/
function validatePath(path, label) {
// Must be absolute after resolution
const resolved = resolve(path);
// Check for null bytes (injection attack)
if (path.includes('\0')) {
throw new Error(`${label} contains null bytes`);
}
// Check for shell metacharacters in path components
if (/[;&|`$<>]/.test(path)) {
throw new Error(`${label} contains shell metacharacters`);
}
// Prevent path traversal outside expected directories
if (!resolved.includes('.claude-flow') && !resolved.includes('bin')) {
// Allow only paths within project structure
const cwd = process.cwd();
if (!resolved.startsWith(cwd)) {
throw new Error(`${label} escapes project directory`);
}
}
}
/**
* #1914: Resolve the `--workspace` flag to an absolute path, or return null
* if it is absent / not a usable string. Rejects values with null bytes or
* shell metacharacters (defence-in-depth — the value is later embedded in a
* forked child's argv and compared against `ps`/`tasklist` output).
*/
export function resolveWorkspaceFlag(raw) {
if (typeof raw !== 'string')
return null;
const trimmed = raw.trim();
if (!trimmed)
return null;
if (trimmed.includes('\0') || /[;&|`$<>]/.test(trimmed))
return null;
return resolve(trimmed);
}
/**
* #1914: True when a process command line (from `ps -eo command` on POSIX or
* the tasklist Window Title column on Windows) belongs to a daemon started
* for `workspaceRoot`. The launcher (`startBackgroundDaemon`) always appends
* `--workspace <root>` as the FINAL argv entry, so an exact trailing match
* after stripping trailing whitespace/quotes is unambiguous — even for
* workspace paths containing spaces — and never a bare path-prefix match,
* so workspace `/a/proj` does not reap `/a/proj-other`'s daemon. A daemon
* whose argv puts `--workspace` mid-list (only possible via a hand-rolled
* invocation) simply won't be auto-reaped — `daemon stop` still handles it
* via the PID file.
*/
export function daemonCommandLineBelongsToWorkspace(commandLine, workspaceRoot) {
return commandLine.replace(/[\s"']+$/u, '').endsWith(`--workspace ${workspaceRoot}`);
}
async function startBackgroundDaemon(projectRoot, quiet, forwarded = {}) {
const { maxCpuLoad, minFreeMemory, workers, headless, sandbox } = forwarded;
// Validate and resolve project root
const resolvedRoot = resolve(projectRoot);
validatePath(resolvedRoot, 'Project root');
const stateDir = join(resolvedRoot, '.claude-flow');
const pidFile = join(stateDir, 'daemon.pid');
const logFile = join(stateDir, 'daemon.log');
// Validate all paths
validatePath(stateDir, 'State directory');
validatePath(pidFile, 'PID file');
validatePath(logFile, 'Log file');
// Ensure state directory exists
if (!fs.existsSync(stateDir)) {
fs.mkdirSync(stateDir, { recursive: true });
}
// Get path to CLI (from dist/src/commands/daemon.js -> bin/cli.js)
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// dist/src/commands -> dist/src -> dist -> package root -> bin/cli.js
const cliPath = resolve(join(__dirname, '..', '..', '..', 'bin', 'cli.js'));
validatePath(cliPath, 'CLI path');
// Verify CLI path exists
if (!fs.existsSync(cliPath)) {
output.printError(`CLI not found at: ${cliPath}`);
return { success: false, exitCode: 1 };
}
// Platform-aware spawn flags. We use child_process.fork() because the daemon
// child is itself a Node script — fork() spawns Node directly and skips the
// cmd.exe interpretation pass that broke Windows + Node 25 when
// process.execPath contained a space (#1691). It also avoids the [DEP0190]
// shell:true security warning.
const isWin = process.platform === 'win32';
const forkOpts = {
cwd: resolvedRoot,
// detached: true on every platform (#1766). On Windows, leaving detached:false
// kept the child in the parent's process group AND the IPC pipe held the
// child to npx — when npx exited, the IPC pipe tore down and the daemon
// died within ~1s. detached:true + child.disconnect() (below) gives the
// child its own session/pgid and breaks the IPC pipe so the daemon
// genuinely survives parent exit. On POSIX, detached:true was already the
// path; this just makes Windows match.
detached: true,
// Use 'ignore' for all stdio + 'ignore' for the IPC channel via silent:true off.
// fork() defaults to creating an IPC channel; we don't need it here, so we
// pass stdio explicitly. Passing fs.openSync() FDs causes the child to die
// on Windows when the parent exits and closes the FDs (#1478 Bug 3) — the
// daemon writes its own logs via appendFileSync to .claude-flow/logs/.
stdio: ['ignore', 'ignore', 'ignore', 'ipc'],
windowsHide: true,
env: {
...process.env,
CLAUDE_FLOW_DAEMON: '1',
// Prevent macOS SIGHUP kill when terminal closes
...(process.platform === 'darwin' ? { NOHUP: '1' } : {}),
},
};
// Forward args to the foreground child. fork() resolves the script path
// via Node's normal module resolution, so cliPath does not need to be
// shell-quoted even when it contains spaces.
const forkArgs = ['daemon', 'start', '--foreground', '--quiet'];
// Validate with strict numeric pattern to prevent injection via crafted flags.
const SPAWN_NUMERIC_RE = /^\d+(\.\d+)?$/;
if (maxCpuLoad && SPAWN_NUMERIC_RE.test(maxCpuLoad)) {
forkArgs.push('--max-cpu-load', maxCpuLoad);
}
if (minFreeMemory && SPAWN_NUMERIC_RE.test(minFreeMemory)) {
forkArgs.push('--min-free-memory', minFreeMemory);
}
// #1968: forward worker-selection / sandbox flags. The previous launcher
// dropped these, so `daemon start --workers map` ran with the default
// five-worker set instead of just `map`. Validate each before passing
// through — argv goes straight to a forked process so reject anything
// that doesn't look like a comma-separated worker-name list or one of
// the allowed sandbox modes.
const WORKERS_RE = /^[a-z][a-z0-9_-]*(,[a-z][a-z0-9_-]*)*$/;
if (typeof workers === 'string' && workers.length > 0 && WORKERS_RE.test(workers)) {
forkArgs.push('--workers', workers);
}
if (headless === true) {
forkArgs.push('--headless');
}
if (typeof sandbox === 'string' && (sandbox === 'strict' || sandbox === 'permissive' || sandbox === 'disabled')) {
forkArgs.push('--sandbox', sandbox);
}
// #1914: stamp the workspace into argv (kept LAST) so the foreground daemon
// process is self-identifying and `killStaleDaemons` only reaps daemons
// belonging to this workspace. resolvedRoot was validatePath()'d above.
forkArgs.push('--workspace', resolvedRoot);
const child = fork(cliPath, forkArgs, forkOpts);
// Get PID from spawned process directly (no shell echo needed)
const pid = child.pid;
if (!pid || pid <= 0) {
output.printError('Failed to get daemon PID');
return { success: false, exitCode: 1 };
}
// Unref BEFORE writing PID file — prevents race where parent exits
// but child hasn't fully detached yet (fixes macOS daemon death #1283).
child.unref();
// #1766: also break the IPC pipe explicitly. unref() releases the libuv
// handle but does NOT close the IPC channel; on Windows the open IPC
// pipe keeps the daemon tied to its parent npx, and when npx exits the
// pipe is torn down and the daemon exits with it. disconnect() severs
// the IPC pipe so the daemon truly stands on its own. Wrapped in try
// because disconnect() throws if the IPC channel is already gone.
try {
child.disconnect();
}
catch { /* IPC channel already closed */ }
// Longer delay to let the child process start and write its own PID file.
// 100ms was too short on Windows; the child's checkExistingDaemon() would
// find the parent-written PID and return early (#1478 Bug 1).
await new Promise(resolve => setTimeout(resolve, 500));
// Write PID file only if the child hasn't already written its own.
// The foreground child calls writePidFile() internally, but on some platforms
// it may not have started yet, so we write as a fallback.
if (!fs.existsSync(pidFile)) {
fs.writeFileSync(pidFile, String(pid));
}
if (!quiet) {
output.printSuccess(`Daemon started in background (PID: ${pid})`);
output.printInfo(`Logs: ${logFile}`);
output.printInfo(`Stop with: claude-flow daemon stop`);
}
return { success: true };
}
// Stop daemon subcommand
const stopCommand = {
name: 'stop',
description: 'Stop the worker daemon and all background workers',
options: [
{ name: 'quiet', short: 'Q', type: 'boolean', description: 'Suppress output' },
],
examples: [
{ command: 'claude-flow daemon stop', description: 'Stop the daemon' },
],
action: async (ctx) => {
const quiet = ctx.flags.quiet;
const projectRoot = process.cwd();
try {
if (!quiet) {
const spinner = output.createSpinner({ text: 'Stopping worker daemon...', spinner: 'dots' });
spinner.start();
// Try to stop in-process daemon first
await stopDaemon();
// Also kill any background daemon by PID
const killed = await killBackgroundDaemon(projectRoot);
// #1551: Also kill stale daemon processes not tracked by PID file
await killStaleDaemons(projectRoot, true);
spinner.succeed(killed ? 'Worker daemon stopped' : 'Worker daemon was not running');
}
else {
await stopDaemon();
await killBackgroundDaemon(projectRoot);
await killStaleDaemons(projectRoot, true);
}
return { success: true };
}
catch (error) {
output.printError(`Failed to stop daemon: ${error instanceof Error ? error.message : String(error)}`);
return { success: false, exitCode: 1 };
}
},
};
/**
* Kill background daemon process using PID file
*/
async function killBackgroundDaemon(projectRoot) {
const pidFile = join(projectRoot, '.claude-flow', 'daemon.pid');
if (!fs.existsSync(pidFile)) {
return false;
}
try {
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
if (isNaN(pid)) {
fs.unlinkSync(pidFile);
return false;
}
// Check if process is running
try {
process.kill(pid, 0); // Signal 0 = check if alive
}
catch {
// Process not running, clean up stale PID file
fs.unlinkSync(pidFile);
return false;
}
// Kill the process
process.kill(pid, 'SIGTERM');
// Wait a moment then force kill if needed
await new Promise(resolve => setTimeout(resolve, 1000));
try {
process.kill(pid, 0);
// Still alive, force kill
process.kill(pid, 'SIGKILL');
}
catch {
// Process terminated
}
// Clean up PID file
if (fs.existsSync(pidFile)) {
fs.unlinkSync(pidFile);
}
return true;
}
catch (error) {
// Clean up PID file on any error
if (fs.existsSync(pidFile)) {
fs.unlinkSync(pidFile);
}
return false;
}
}
/**
* Kill stale daemon processes not tracked by the PID file (#1551, #1857).
* Uses `ps` on POSIX and `tasklist` on Windows to find all daemon
* processes for this project and kill them.
*/
async function killStaleDaemons(projectRoot, quiet) {
if (process.platform === 'win32') {
return killStaleDaemonsWindows(projectRoot, quiet);
}
return killStaleDaemonsPosix(projectRoot, quiet);
}
async function killStaleDaemonsPosix(projectRoot, quiet) {
try {
const { execFileSync } = await import('child_process');
const psOutput = execFileSync('ps', ['-eo', 'pid,command'], { encoding: 'utf-8', timeout: 5000 });
const lines = psOutput.split('\n');
const currentPid = process.pid;
const trackedPid = getBackgroundDaemonPid(projectRoot);
// #1914: only ever reap daemons belonging to THIS workspace (ADR-014).
const resolvedRoot = resolve(projectRoot);
let killed = 0;
for (const line of lines) {
if (!line.includes('daemon start --foreground'))
continue;
if (!line.includes('claude-flow') && !line.includes('@claude-flow/cli'))
continue;
// #1914: skip daemons from other workspaces (or pre-#1914 versions that
// didn't stamp --workspace — let `daemon stop` handle those via PID file).
if (!daemonCommandLineBelongsToWorkspace(line, resolvedRoot))
continue;
const pidStr = line.trim().split(/\s+/)[0];
const pid = parseInt(pidStr, 10);
if (isNaN(pid) || pid === currentPid || pid === trackedPid)
continue;
if (!isProcessRunning(pid))
continue;
try {
process.kill(pid, 'SIGTERM');
killed++;
if (!quiet) {
output.printWarning(`Killed stale daemon process (PID: ${pid})`);
}
}
catch { /* ignore — may have exited between check and kill */ }
}
if (killed > 0 && !quiet) {
output.printInfo(`Cleaned up ${killed} stale daemon process(es)`);
}
}
catch {
// ps not available or failed — skip stale cleanup
}
}
/**
* #1857: Windows replacement for the POSIX `ps -eo pid,command` path.
* Uses `tasklist /v /fo csv` which returns CSV with the full Window
* Title column (last field) — Node-spawned daemon processes carry
* their command line there. Best-effort like the POSIX path: any
* tooling failure (tasklist missing, parse error, etc.) is swallowed
* silently so cleanup doesn't break daemon start.
*/
async function killStaleDaemonsWindows(projectRoot, quiet) {
try {
const { execFileSync } = await import('child_process');
// /v includes the Window Title; /fo csv uses comma-separated quoted fields
const out = execFileSync('tasklist', ['/v', '/fo', 'csv', '/nh'], { encoding: 'utf-8', timeout: 5000 });
const lines = out.split(/\r?\n/);
const currentPid = process.pid;
const trackedPid = getBackgroundDaemonPid(projectRoot);
// #1914: only ever reap daemons belonging to THIS workspace (ADR-014).
const resolvedRoot = resolve(projectRoot);
let killed = 0;
for (const line of lines) {
if (!line.trim())
continue;
// Match daemon command line markers — the Window Title field
// typically holds the full invocation. Skip rows that aren't ours.
if (!line.includes('daemon start --foreground'))
continue;
if (!line.includes('claude-flow') && !line.includes('@claude-flow/cli'))
continue;
// #1914: skip daemons from other workspaces (or pre-#1914 versions).
if (!daemonCommandLineBelongsToWorkspace(line, resolvedRoot))
continue;
// Parse CSV: tasklist quotes each field, so split on `","`
const fields = line.split(/","/).map(f => f.replace(/^"|"$/g, ''));
// fields[0] = Image Name, fields[1] = PID, …
const pidStr = fields[1];
const pid = parseInt(pidStr ?? '', 10);
if (isNaN(pid) || pid === currentPid || pid === trackedPid)
continue;
if (!isProcessRunning(pid))
continue;
try {
// taskkill is the Windows equivalent of kill — /pid <n> /f forces.
// Use SIGTERM-equivalent (no /f) first; the daemon's signal handler
// catches and cleans up; force-kill is the next start's job.
execFileSync('taskkill', ['/pid', String(pid), '/t'], { encoding: 'utf-8', timeout: 5000 });
killed++;
if (!quiet) {
output.printWarning(`Killed stale daemon process (PID: ${pid})`);
}
}
catch { /* taskkill failed — process may have exited; ignore */ }
}
if (killed > 0 && !quiet) {
output.printInfo(`Cleaned up ${killed} stale daemon process(es)`);
}
}
catch {
// tasklist not available or failed — skip stale cleanup. Defensive
// shape matches the POSIX path. Not tested on Windows by the
// maintainer; please report regressions on the issue tracker.
}
}
/**
* Get PID of background daemon from PID file
*/
function getBackgroundDaemonPid(projectRoot) {
const pidFile = join(projectRoot, '.claude-flow', 'daemon.pid');
if (!fs.existsSync(pidFile)) {
return null;
}
try {
const pid = parseInt(fs.readFileSync(pidFile, 'utf-8').trim(), 10);
return isNaN(pid) ? null : pid;
}
catch {
return null;
}
}
/**
* Check if a process is running
*/
function isProcessRunning(pid) {
try {
process.kill(pid, 0); // Signal 0 = check if alive
return true;
}
catch {
return false;
}
}
// Status subcommand
const statusCommand = {
name: 'status',
description: 'Show daemon and worker status',
options: [
{ name: 'verbose', short: 'v', type: 'boolean', description: 'Show detailed worker statistics' },
{ name: 'show-modes', type: 'boolean', description: 'Show worker execution modes (local/headless) and sandbox settings' },
],
examples: [
{ command: 'claude-flow daemon status', description: 'Show daemon status' },
{ command: 'claude-flow daemon status -v', description: 'Show detailed status' },
{ command: 'claude-flow daemon status --show-modes', description: 'Show worker execution modes' },
],
action: async (ctx) => {
const verbose = ctx.flags.verbose;
const showModes = ctx.flags['show-modes'];
const projectRoot = process.cwd();
try {
const daemon = getDaemon(projectRoot);
const status = daemon.getStatus();
// Also check for background daemon
const bgPid = getBackgroundDaemonPid(projectRoot);
const bgRunning = bgPid ? isProcessRunning(bgPid) : false;
const isRunning = status.running || bgRunning;
const displayPid = bgPid || status.pid;
output.writeln();
// Daemon status box
const statusIcon = isRunning ? output.success('●') : output.error('○');
const statusText = isRunning ? output.success('RUNNING') : output.error('STOPPED');
const mode = bgRunning ? output.dim(' (background)') : status.running ? output.dim(' (foreground)') : '';
output.printBox([
`Status: ${statusIcon} ${statusText}${mode}`,
`PID: ${displayPid}`,
status.startedAt ? `Started: ${status.startedAt.toISOString()}` : '',
`Workers Enabled: ${status.config.workers.filter(w => w.enabled).length}`,
`Max Concurrent: ${status.config.maxConcurrent}`,
`Max CPU Load: ${status.config.resourceThresholds.maxCpuLoad}`,
`Min Free Memory: ${status.config.resourceThresholds.minFreeMemoryPercent}%`,
].filter(Boolean).join('\n'), 'RuFlo Daemon');
output.writeln();
output.writeln(output.bold('Worker Status'));
const workerData = status.config.workers.map(w => {
const state = status.workers.get(w.type);
// Check for headless mode from worker config or state
const isHeadless = w.headless || state?.headless || false;
const sandboxMode = w.sandbox || state?.sandbox || null;
return {
type: w.enabled ? output.highlight(w.type) : output.dim(w.type),
enabled: w.enabled ? output.success('✓') : output.dim('○'),
status: state?.isRunning ? output.warning('running') :
w.enabled ? output.success('idle') : output.dim('disabled'),
runs: state?.runCount ?? 0,
success: state ? `${Math.round((state.successCount / Math.max(state.runCount, 1)) * 100)}%` : '-',
lastRun: state?.lastRun ? formatTimeAgo(state.lastRun) : output.dim('never'),
nextRun: state?.nextRun && w.enabled ? formatTimeUntil(state.nextRun) : output.dim('-'),
mode: isHeadless ? output.highlight('headless') : output.dim('local'),
sandbox: isHeadless ? (sandboxMode || 'strict') : output.dim('-'),
};
});
// Build columns based on --show-modes flag
const baseColumns = [
{ key: 'type', header: 'Worker', width: 12 },
{ key: 'enabled', header: 'On', width: 4 },
{ key: 'status', header: 'Status', width: 10 },
{ key: 'runs', header: 'Runs', width: 6 },
{ key: 'success', header: 'Success', width: 8 },
{ key: 'lastRun', header: 'Last Run', width: 12 },
{ key: 'nextRun', header: 'Next Run', width: 12 },
];
const modeColumns = showModes ? [
{ key: 'mode', header: 'Mode', width: 10 },
{ key: 'sandbox', header: 'Sandbox', width: 12 },
] : [];
output.printTable({
columns: [...baseColumns, ...modeColumns],
data: workerData,
});
if (verbose) {
output.writeln();
output.writeln(output.bold('Worker Configuration'));
output.printTable({
columns: [
{ key: 'type', header: 'Worker', width: 12 },
{ key: 'interval', header: 'Interval', width: 10 },
{ key: 'priority', header: 'Priority', width: 10 },
{ key: 'avgDuration', header: 'Avg Duration', width: 12 },
{ key: 'description', header: 'Description', width: 30 },
],
data: status.config.workers.map(w => {
const state = status.workers.get(w.type);
return {
type: w.type,
interval: `${Math.round(w.intervalMs / 60000)}min`,
priority: w.priority,
avgDuration: state?.averageDurationMs ? `${Math.round(state.averageDurationMs)}ms` : '-',
description: w.description,
};
}),
});
}
return { success: true, data: status };
}
catch (error) {
// Daemon not initialized
output.writeln();
output.printBox([
`Status: ${output.error('○')} ${output.error('NOT INITIALIZED')}`,
'',
'Run "claude-flow daemon start" to start the daemon',
].join('\n'), 'RuFlo Daemon');
return { success: true };
}
},
};
// Trigger subcommand - manually run a worker
const triggerCommand = {
name: 'trigger',
description: 'Manually trigger a specific worker',
options: [
{ name: 'worker', short: 'w', type: 'string', description: 'Worker type to trigger', required: true },
{ name: 'headless', type: 'boolean', description: 'Run triggered worker in headless mode (E2B sandbox)' },
],
examples: [
{ command: 'claude-flow daemon trigger -w map', description: 'Trigger the map worker' },
{ command: 'claude-flow daemon trigger -w audit', description: 'Trigger security audit' },
{ command: 'claude-flow daemon trigger -w audit --headless', description: 'Trigger audit in headless sandbox' },
],
action: async (ctx) => {
const workerType = ctx.flags.worker;
if (!workerType) {
output.printError('Worker type is required. Use --worker or -w flag.');
output.writeln();
output.writeln('Available workers: map, audit, optimize, consolidate, testgaps, predict, document, ultralearn, refactor, benchmark, deepdive, preload');
return { success: false, exitCode: 1 };
}
try {
const daemon = getDaemon(process.cwd());
const spinner = output.createSpinner({ text: `Running ${workerType} worker...`, spinner: 'dots' });
spinner.start();
const result = await daemon.triggerWorker(workerType);
if (result.success) {
spinner.succeed(`Worker ${workerType} completed in ${result.durationMs}ms`);
if (result.output) {
output.writeln();
output.writeln(output.bold('Output'));
output.printJson(result.output);
}
}
else {
spinner.fail(`Worker ${workerType} failed: ${result.error}`);
}
return { success: result.success, data: result };
}
catch (error) {
output.printError(`Failed to trigger worker: ${error instanceof Error ? error.message : String(error)}`);
return { success: false, exitCode: 1 };
}
},
};
// Enable/disable worker subcommand
const enableCommand = {
name: 'enable',
description: 'Enable or disable a specific worker',
options: [
{ name: 'worker', short: 'w', type: 'string', description: 'Worker type', required: true },
{ name: 'disable', short: 'd', type: 'boolean', description: 'Disable instead of enable' },
],
examples: [
{ command: 'claude-flow daemon enable -w predict', description: 'Enable predict worker' },
{ command: 'claude-flow daemon enable -w document --disable', description: 'Disable document worker' },
],
action: async (ctx) => {
const workerType = ctx.flags.worker;
const disable = ctx.flags.disable;
if (!workerType) {
output.printError('Worker type is required. Use --worker or -w flag.');
return { success: false, exitCode: 1 };
}
try {
const daemon = getDaemon(process.cwd());
daemon.setWorkerEnabled(workerType, !disable);
output.printSuccess(`Worker ${workerType} ${disable ? 'disabled' : 'enabled'}`);
return { success: true };
}
catch (error) {
output.printError(`Failed to ${disable ? 'disable' : 'enable'} worker: ${error instanceof Error ? error.message : String(error)}`);
return { success: false, exitCode: 1 };
}
},
};
// Helper functions for time formatting
function formatTimeAgo(date) {
const seconds = Math.floor((Date.now() - date.getTime()) / 1000);
if (seconds < 60)
return `${seconds}s ago`;
if (seconds < 3600)
return `${Math.floor(seconds / 60)}m ago`;
if (seconds < 86400)
return `${Math.floor(seconds / 3600)}h ago`;
return `${Math.floor(seconds / 86400)}d ago`;
}
function formatTimeUntil(date) {
const seconds = Math.floor((date.getTime() - Date.now()) / 1000);
if (seconds < 0)
return 'now';
if (seconds < 60)
return `in ${seconds}s`;
if (seconds < 3600)
return `in ${Math.floor(seconds / 60)}m`;
if (seconds < 86400)
return `in ${Math.floor(seconds / 3600)}h`;
return `in ${Math.floor(seconds / 86400)}d`;
}
// #1565: Supervisor installer subcommand. Writes a native auto-restart
// unit (launchd plist on macOS, systemd-user .service on Linux) so the
// daemon survives crashes and reboots without requiring the operator
// to manually run `daemon start` after every failure.
const installSupervisorCommand = {
name: 'install-supervisor',
description: 'Install OS-level auto-restart supervisor (launchd on macOS, systemd-user on Linux)',
options: [
{ name: 'force', short: 'f', type: 'boolean', description: 'Overwrite existing unit file', default: 'false' },
{ name: 'load', type: 'boolean', description: 'Load/enable the unit immediately', default: 'true' },
{ name: 'dry-run', type: 'boolean', description: 'Print the unit file content without writing', default: 'false' },
],
examples: [
{ command: 'claude-flow daemon install-supervisor', description: 'Install + load (auto-restart enabled)' },
{ command: 'claude-flow daemon install-supervisor --no-load', description: 'Write unit file but do not enable yet' },
{ command: 'claude-flow daemon install-supervisor --dry-run', description: 'Preview the unit file' },
],
action: async (ctx) => {
const force = ctx.flags.force === true;
const load = ctx.flags.load !== false;
const dryRun = ctx.flags['dry-run'] === true || ctx.flags.dryRun === true;
const projectRoot = process.cwd();
const platform = process.platform;
if (platform === 'win32') {
output.printError('Windows scheduled-task installer is not yet implemented.');
output.printInfo('Use Task Scheduler manually, or follow this issue: https://github.com/ruvnet/ruflo/issues/1565');
return { success: false, exitCode: 1 };
}
if (platform !== 'darwin' && platform !== 'linux') {
output.printError(`Unsupported platform: ${platform}. Supported: darwin (launchd), linux (systemd-user).`);
return { success: false, exitCode: 1 };
}
// Resolve absolute paths the unit file will reference.
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
if (!home) {
output.printError('HOME/USERPROFILE not set; cannot resolve user unit path.');
return { success: false, exitCode: 1 };
}
const nodeBin = process.execPath;
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
const cliJs = resolve(join(__dirname, '..', '..', '..', 'bin', 'cli.js'));
if (!fs.existsSync(cliJs)) {
output.printError(`CLI not found at: ${cliJs}`);
return { success: false, exitCode: 1 };
}
if (platform === 'darwin') {
const plistDir = join(home, 'Library', 'LaunchAgents');
const plistPath = join(plistDir, 'io.ruv.ruflo.daemon.plist');
const logDir = join(projectRoot, '.claude-flow', 'logs');
const plist = `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key><string>io.ruv.ruflo.daemon</string>
<key>ProgramArguments</key>
<array>
<string>${nodeBin}</string>
<string>${cliJs}</string>
<string>daemon</string><string>start</string><string>--foreground</string><string>--quiet</string>
</array>
<key>WorkingDirectory</key><string>${projectRoot}</string>
<key>RunAtLoad</key><true/>
<key>KeepAlive</key>
<dict>
<key>SuccessfulExit</key><false/>
<key>Crashed</key><true/>
</dict>
<key>ThrottleInterval</key><integer>10</integer>
<key>StandardOutPath</key><string>${logDir}/supervisor.out.log</string>
<key>StandardErrorPath</key><string>${logDir}/supervisor.err.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>CLAUDE_FLOW_DAEMON</key><string>1</string>
</dict>
</dict>
</plist>
`;
if (dryRun) {
output.writeln(plist);
return { success: true };
}
if (fs.existsSync(plistPath) && !force) {
output.printWarning(`Already installed: ${plistPath}`);
output.printInfo('Use --force to overwrite.');
return { success: false, exitCode: 1 };
}
if (!fs.existsSync(plistDir))
fs.mkdirSync(plistDir, { recursive: true });
if (!fs.existsSync(logDir))
fs.mkdirSync(logDir, { recursive: true });
fs.writeFileSync(plistPath, plist, 'utf-8');
output.printSuccess(`Wrote ${plistPath}`);
if (load) {
try {
const { execFileSync } = await import('child_process');
// unload first in case a previous version is loaded
try {
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', timeout: 5000 });
}
catch { /* ok */ }
execFileSync('launchctl', ['load', '-w', plistPath], { encoding: 'utf-8', timeout: 5000 });
output.printSuccess('Supervisor loaded — daemon will auto-restart on crash and survive reboot.');
}
catch (err) {
output.printWarning(`launchctl load failed: ${err instanceof Error ? err.message : String(err)}`);
output.printInfo(`Run manually: launchctl load -w ${plistPath}`);
}
}
else {
output.printInfo(`Run when ready: launchctl load -w ${plistPath}`);
}
return { success: true };
}
// Linux: systemd-user
const unitDir = join(home, '.config', 'systemd', 'user');
const unitPath = join(unitDir, 'ruflo-daemon.service');
const unit = `[Unit]
Description=RuFlo background worker daemon
After=default.target
[Service]
Type=simple
WorkingDirectory=${projectRoot}
Environment=CLAUDE_FLOW_DAEMON=1
ExecStart=${nodeBin} ${cliJs} daemon start --foreground --quiet
Restart=on-failure
RestartSec=10
# Restart on Crashed (signal) too
StartLimitIntervalSec=300
StartLimitBurst=5
[Install]
WantedBy=default.target
`;
if (dryRun) {
output.writeln(unit);
return { success: true };
}
if (fs.existsSync(unitPath) && !force) {
output.printWarning(`Already installed: ${unitPath}`);
output.printInfo('Use --force to overwrite.');
return { success: false, exitCode: 1 };
}
if (!fs.existsSync(unitDir))
fs.mkdirSync(unitDir, { recursive: true });
fs.writeFileSync(unitPath, unit, 'utf-8');
output.printSuccess(`Wrote ${unitPath}`);
if (load) {
try {
const { execFileSync } = await import('child_process');
execFileSync('systemctl', ['--user', 'daemon-reload'], { encoding: 'utf-8', timeout: 5000 });
execFileSync('systemctl', ['--user', 'enable', '--now', 'ruflo-daemon.service'], { encoding: 'utf-8', timeout: 10000 });
output.printSuccess('Supervisor enabled — daemon will auto-restart on crash and survive reboot.');
output.printInfo('Note: requires `loginctl enable-linger $USER` for restart-after-logout on some distros.');
}
catch (err) {
output.printWarning(`systemctl --user enable failed: ${err instanceof Error ? err.message : String(err)}`);
output.printInfo(`Run manually: systemctl --user daemon-reload && systemctl --user enable --now ruflo-daemon.service`);
}
}
else {
output.printInfo(`Run when ready: systemctl --user daemon-reload && systemctl --user enable --now ruflo-daemon.service`);
}
return { success: true };
},
};
const uninstallSupervisorCommand = {
name: 'uninstall-supervisor',
description: 'Remove the auto-restart supervisor unit (launchd on macOS, systemd-user on Linux)',
options: [],
action: async () => {
const platform = process.platform;
const home = process.env.HOME ?? process.env.USERPROFILE ?? '';
if (platform === 'darwin') {
const plistPath = join(home, 'Library', 'LaunchAgents', 'io.ruv.ruflo.daemon.plist');
try {
const { execFileSync } = await import('child_process');
try {
execFileSync('launchctl', ['unload', plistPath], { encoding: 'utf-8', timeout: 5000 });
}
catch { /* ok */ }
}
catch { /* ignore */ }
if (fs.existsSync(plistPath)) {
fs.unlinkSync(plistPath);
output.printSuccess(`Removed ${plistPath}`);
}
else {
output.printInfo(`Not installed: ${plistPath}`);
}
return { success: true };
}
if (platform === 'linux') {
const unitPath = join(home, '.config', 'systemd', 'user', 'ruflo-daemon.service');
try {
const { execFileSync } = await import('child_process');
try {
execFileSync('systemctl', ['--user', 'disable', '--now', 'ruflo-daemon.service'], { encoding: 'utf-8', timeout: 5000 });
}
catch { /* ok */ }
}
catch { /* ignore */ }
if (fs.existsSync(unitPath)) {
fs.unlinkSync(unitPath);
output.printSuccess(`Removed ${unitPath}`);
}
else {
output.printInfo(`Not installed: ${unitPath}`);
}
return { success: true };
}
output.printError(`Unsupported platform: ${platform}`);
return { success: false, exitCode: 1 };
},
};
// Main daemon command
export const daemonCommand = {
name: 'daemon',
description: 'Manage background worker daemon (Node.js-based, auto-runs like shell helpers)',
subcommands: [
startCommand,
stopCommand,
statusCommand,
triggerCommand,
enableCommand,
installSupervisorCommand,
uninstallSupervisorCommand,
],
options: [],
examples: [
{ command: 'claude-flow daemon start', description: 'Start the daemon' },
{ command: 'claude-flow daemon start --headless', description: 'Start with headless workers (E2B sandbox)' },
{ command: 'claude-flow daemon status', description: 'Check daemon status' },
{ command: 'claude-flow daemon stop', description: 'Stop the daemon' },
{ command: 'claude-flow daemon trigger -w audit', description: 'Run security audit' },
],
action: async () => {