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

131 lines (121 loc) 5.28 kB
import { Command } from 'commander'; import chalk from 'chalk'; import path from 'node:path'; import { loadLoopState, requestLoopStop, resolveLoopPaths, runCodexLoop, type LoopState, } from './index.js'; export function createLoopCommand(): Command { const loop = new Command('loop') .description('Run Codex in a bounded /loop-compatible iteration cycle'); loop .command('run') .description('Start a Codex loop') .argument('[prompt...]', 'Prompt for Codex. Omit when using --command.') .option('-n, --name <name>', 'Loop name', 'default') .option('-p, --path <path>', 'Project path', process.cwd()) .option('-i, --interval <seconds>', 'Seconds between iterations', '270') .option('-m, --max-iterations <count>', 'Maximum iterations; 0 means unbounded', '10') .option('--timeout <ms>', 'Per-iteration timeout in milliseconds', '1800000') .option('--until-file <path>', 'Stop when this file exists. Defaults to .codex/loop/<name>.complete') .option('--command <command>', 'Run a shell command each iteration instead of codex exec') .option('--codex-command <command>', 'Codex executable', 'codex') .option('--model <model>', 'Model passed to codex exec') .option('--sandbox <mode>', 'Sandbox passed to codex exec', 'workspace-write') .option('--no-skip-git-repo-check', 'Do not pass --skip-git-repo-check to codex exec') .option('--dry-run', 'Write planned state and print what would run without executing') .action(async (promptParts: string[], options) => { try { const prompt = promptParts.join(' ').trim(); const state = await runCodexLoop({ name: options.name, projectPath: options.path, prompt, command: options.command, codexCommand: options.codexCommand, model: options.model, sandbox: options.sandbox, skipGitRepoCheck: options.skipGitRepoCheck, intervalSeconds: parseInteger(options.interval, 270), maxIterations: parseInteger(options.maxIterations, 10), timeoutMs: parseInteger(options.timeout, 1_800_000), untilFile: options.untilFile, dryRun: options.dryRun, onEvent: event => { if (event.type === 'iteration-start') { console.log(chalk.cyan(`iteration ${event.state.iteration} starting`)); } else if (event.type === 'iteration-complete') { console.log(chalk.gray(event.message ?? `iteration ${event.state.iteration} complete`)); } else if (event.type === 'sleep') { console.log(chalk.gray(event.message)); } else if (event.type === 'error') { console.log(chalk.red(event.message)); } }, }); printState(state); if (state.status === 'failed') process.exitCode = 1; } catch (error) { console.error(chalk.red(error instanceof Error ? error.message : String(error))); process.exitCode = 1; } }); loop .command('status') .description('Show loop state') .option('-n, --name <name>', 'Loop name', 'default') .option('-p, --path <path>', 'Project path', process.cwd()) .option('--json', 'Print raw JSON state') .action(async (options) => { const projectPath = path.resolve(options.path); const state = await loadLoopState(projectPath, options.name); if (!state) { const paths = resolveLoopPaths(projectPath, options.name); console.log(chalk.yellow(`No loop state found at ${paths.statePath}`)); return; } if (options.json) { console.log(JSON.stringify(state, null, 2)); return; } printState(state); }); loop .command('stop') .description('Request a running loop to stop after the current iteration') .option('-n, --name <name>', 'Loop name', 'default') .option('-p, --path <path>', 'Project path', process.cwd()) .action(async (options) => { const projectPath = path.resolve(options.path); const paths = await requestLoopStop(projectPath, options.name); console.log(chalk.green(`Stop requested: ${paths.stopPath}`)); }); return loop; } function printState(state: LoopState): void { const color = state.status === 'failed' ? chalk.red : state.status === 'running' ? chalk.cyan : chalk.green; console.log(color(`Loop ${state.name}: ${state.status}`)); console.log(chalk.gray(` mode: ${state.mode}`)); console.log(chalk.gray(` iteration: ${state.iteration}/${state.maxIterations === 0 ? 'unbounded' : state.maxIterations}`)); console.log(chalk.gray(` interval: ${state.intervalSeconds}s`)); console.log(chalk.gray(` until file: ${state.untilFile}`)); if (state.lastExitCode !== undefined) { console.log(chalk.gray(` last exit: ${state.lastExitCode}`)); } if (state.lastError) { console.log(chalk.red(` last error: ${state.lastError.split('\n')[0]}`)); } } function parseInteger(value: string | number | undefined, fallback: number): number { if (typeof value === 'number') return value; if (!value) return fallback; const parsed = Number.parseInt(value, 10); return Number.isFinite(parsed) ? parsed : fallback; }