UNPKG

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

593 lines 26.9 kB
/** * 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 { spawn } 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'] }, ], 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; const projectRoot = process.cwd(); const isDaemonProcess = process.env.CLAUDE_FLOW_DAEMON === '1'; // 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})`); } return { success: true }; } } // Background mode (default): fork a detached process if (!foreground) { return startBackgroundDaemon(projectRoot, quiet); } // 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 }); } // Write PID file for foreground mode fs.writeFileSync(pidFile, String(process.pid)); // 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); }); if (!quiet) { const spinner = output.createSpinner({ text: 'Starting worker daemon...', spinner: 'dots' }); spinner.start(); const daemon = await startDaemon(projectRoot); 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}`, ].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 await new Promise(() => { }); // Never resolves - daemon runs until killed } else { await startDaemon(projectRoot); 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`); } } } /** * Start daemon as a detached background process */ async function startBackgroundDaemon(projectRoot, quiet) { // 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 }; } // Use spawn with explicit arguments instead of shell string interpolation // This prevents command injection via paths const child = spawn(process.execPath, [ cliPath, 'daemon', 'start', '--foreground', '--quiet' ], { cwd: resolvedRoot, detached: true, stdio: ['ignore', fs.openSync(logFile, 'a'), fs.openSync(logFile, 'a')], env: { ...process.env, CLAUDE_FLOW_DAEMON: '1' }, }); // 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 }; } // Save PID 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`); } // Unref so parent can exit immediately child.unref(); 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); spinner.succeed(killed ? 'Worker daemon stopped' : 'Worker daemon was not running'); } else { await stopDaemon(); await killBackgroundDaemon(projectRoot); } 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; } } /** * 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}`, ].filter(Boolean).join('\n'), 'Worker 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'), 'Worker 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`; } // 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, ], 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 () => { output.writeln(); output.writeln(output.bold('Worker Daemon - Background Task Management')); output.writeln(); output.writeln('Node.js-based background worker system that auto-runs like shell daemons.'); output.writeln('Manages 12 specialized workers for continuous optimization and monitoring.'); output.writeln(); output.writeln(output.bold('Headless Mode')); output.writeln('Workers can run in headless mode using E2B sandboxes for isolated execution.'); output.writeln('Use --headless flag with start/trigger commands. Sandbox modes: strict, permissive, disabled.'); output.writeln(); output.writeln(output.bold('Available Workers')); output.printList([ `${output.highlight('map')} - Codebase mapping (5 min interval)`, `${output.highlight('audit')} - Security analysis (10 min interval)`, `${output.highlight('optimize')} - Performance optimization (15 min interval)`, `${output.highlight('consolidate')} - Memory consolidation (30 min interval)`, `${output.highlight('testgaps')} - Test coverage analysis (20 min interval)`, `${output.highlight('predict')} - Predictive preloading (2 min, disabled by default)`, `${output.highlight('document')} - Auto-documentation (60 min, disabled by default)`, `${output.highlight('ultralearn')} - Deep knowledge acquisition (manual trigger)`, `${output.highlight('refactor')} - Code refactoring suggestions (manual trigger)`, `${output.highlight('benchmark')} - Performance benchmarking (manual trigger)`, `${output.highlight('deepdive')} - Deep code analysis (manual trigger)`, `${output.highlight('preload')} - Resource preloading (manual trigger)`, ]); output.writeln(); output.writeln(output.bold('Subcommands')); output.printList([ `${output.highlight('start')} - Start the daemon`, `${output.highlight('stop')} - Stop the daemon`, `${output.highlight('status')} - Show daemon status`, `${output.highlight('trigger')} - Manually run a worker`, `${output.highlight('enable')} - Enable/disable a worker`, ]); output.writeln(); output.writeln('Run "claude-flow daemon <subcommand> --help" for details'); return { success: true }; }, }; export default daemonCommand; //# sourceMappingURL=daemon.js.map