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
740 lines (653 loc) • 24.8 kB
text/typescript
/**
* V3 CLI Main Entry Point
* Modernized CLI for RuFlo V3
*
* Created with ❤️ by ruv.io
*/
// MUST be the first import — installs console filter for the cosmetic
// "[AgentDB Patch] Controller index not found" warning before any
// agentic-flow / agentdb code can load. ES module imports are evaluated
// in source order, so this file runs its side effects before any other
// import in this module's import graph (including transitive imports of
// agentic-flow via commands/index.js).
import './log-filters.js';
import { readFileSync } from 'fs';
import { fileURLToPath } from 'url';
import { dirname, join } from 'path';
import type { Command, CommandContext, CommandResult, V3Config, CLIError } from './types.js';
import { CommandParser, commandParser } from './parser.js';
import { OutputFormatter, output } from './output.js';
import { commands, commandsByCategory, getCommandsByCategory, commandRegistry, getCommand, getCommandAsync, getCommandNames, getLazyCommandNames, hasCommand } from './commands/index.js';
import { suggestCommand } from './suggest.js';
import { runStartupUpdateCheck } from './update/index.js';
// Read version from package.json at runtime
function getPackageVersion(): string {
try {
const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);
// Navigate from dist/src to package root
const pkgPath = join(__dirname, '..', '..', 'package.json');
const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
return pkg.version || '3.0.0';
} catch {
return '3.0.0';
}
}
export const VERSION = getPackageVersion();
export interface CLIOptions {
name?: string;
description?: string;
version?: string;
interactive?: boolean;
}
/**
* V3 CLI Application
*/
export class CLI {
private name: string;
private description: string;
private version: string;
private parser: CommandParser;
private output: OutputFormatter;
private interactive: boolean;
constructor(options: CLIOptions = {}) {
this.name = options.name || 'ruflo';
this.description = options.description || 'RuFlo V3 - AI Agent Orchestration Platform';
this.version = options.version || VERSION;
this.parser = commandParser;
this.output = output;
this.interactive = options.interactive ?? process.stdin.isTTY ?? false;
// Register all core (synchronously loaded) commands with full definitions
for (const cmd of commands) {
this.parser.registerCommand(cmd);
}
// Register lazy command names so the parser can recognize them during
// argument resolution without importing their modules. Fix for #1596:
// prevents `daemon start` from being mis-routed to the `start` command.
for (const name of getLazyCommandNames()) {
this.parser.registerLazyCommandName(name);
}
}
/**
* Run the CLI with given arguments
*/
async run(args: string[] = process.argv.slice(2)): Promise<void> {
try {
// #1791.2 — If the user invoked a lazy command (e.g. `hive-mind task`),
// pre-load it BEFORE parsing so the parser can build scoped flag
// aliases for its subcommands. Without this, short flags defined on
// the lazy command's subcommand options (`-d` for description, etc.)
// never get into the alias map and silently fall through to global
// resolution — the user sees `[ERROR] Task description is required`
// even though they passed `-d "smoke"`.
for (const arg of args) {
if (arg.startsWith('-')) continue;
if (this.parser.isLazyOnly(arg)) {
const cmd = await getCommandAsync(arg);
if (cmd) this.parser.registerCommand(cmd);
}
break; // only the first non-flag positional is the command name
}
// Parse arguments
const parseResult = this.parser.parse(args);
const { command: commandPath, flags, positional } = parseResult;
// Handle global flags
if (flags.version || flags.V) {
this.showVersion();
return;
}
if (flags.noColor) {
this.output.setColorEnabled(false);
}
// Set verbosity level based on flags
if (flags.quiet) {
this.output.setVerbosity('quiet');
} else if (flags.verbose) {
this.output.setVerbosity(process.env.DEBUG ? 'debug' : 'verbose');
}
// Verbose mode: show parsed arguments
if (this.output.isVerbose()) {
this.output.printDebug(`Command: ${commandPath.join(' ') || '(none)'}`);
this.output.printDebug(`Positional: [${positional.join(', ')}]`);
this.output.printDebug(`Flags: ${JSON.stringify(Object.fromEntries(Object.entries(flags).filter(([k]) => k !== '_')))}`);
this.output.printDebug(`CWD: ${process.cwd()}`);
}
// Run startup update check (non-blocking, silent on skip)
if (!flags.noUpdate && commandPath[0] !== 'update') {
this.checkForUpdatesOnStartup().catch(() => {/* silent */});
}
// Handle lazy-loaded commands that weren't recognized by the parser
// If commandPath is empty but positional has a command name, check if it's lazy-loadable
if (commandPath.length === 0 && positional.length > 0 && !positional[0].startsWith('-')) {
const potentialCommand = positional[0];
if (hasCommand(potentialCommand)) {
// This is a lazy-loaded command, treat it as the command
commandPath.push(potentialCommand);
positional.shift();
}
}
// No command - show help or suggest correction
if (commandPath.length === 0 || flags.help || flags.h) {
if (commandPath.length > 0) {
// #1791.4 — pass the FULL command path so subcommands like
// `hive-mind spawn --help` render spawn's own options/examples
// instead of falling back to the parent's SUBCOMMANDS list.
await this.showCommandHelp(commandPath);
} else if (positional.length > 0 && !positional[0].startsWith('-')) {
// First positional looks like an attempted command - suggest correction
const attemptedCommand = positional[0];
this.output.printError(`Unknown command: ${attemptedCommand}`);
const availableCommands = Array.from(new Set([...commands.map(c => c.name), ...getCommandNames()]));
const { message } = suggestCommand(attemptedCommand, availableCommands);
this.output.writeln(this.output.dim(` ${message}`));
process.exit(1);
} else {
await this.showHelp();
}
return;
}
// Find and execute command
const commandName = commandPath[0];
// First check the parser's registry (for dynamically registered commands)
// Then fall back to the static registry, then try lazy loading
let command = this.parser.getCommand(commandName) || getCommand(commandName);
// If not found in sync registry, try lazy loading
if (!command && hasCommand(commandName)) {
command = await getCommandAsync(commandName);
}
if (!command) {
this.output.printError(`Unknown command: ${commandName}`);
// Smart suggestions - include lazy-loadable commands in suggestions
const availableCommands = Array.from(new Set([...commands.map(c => c.name), ...getCommandNames()]));
const { message } = suggestCommand(commandName, availableCommands);
this.output.writeln(this.output.dim(` ${message}`));
process.exit(1);
}
// Handle subcommand (supports nested subcommands)
let targetCommand = command;
let subcommandArgs = positional;
// Process command path (e.g., ['hooks', 'worker', 'list'])
// Note: When parser includes subcommand in commandPath, positional already excludes it
if (commandPath.length > 1 && command.subcommands) {
const subcommandName = commandPath[1];
const subcommand = command.subcommands.find(
sc => sc.name === subcommandName || sc.aliases?.includes(subcommandName)
);
if (subcommand) {
targetCommand = subcommand;
// Parser already extracted subcommand from positional, so use as-is
subcommandArgs = positional;
// Check for nested subcommand (level 2)
if (commandPath.length > 2 && subcommand.subcommands) {
const nestedName = commandPath[2];
const nestedSubcommand = subcommand.subcommands.find(
sc => sc.name === nestedName || sc.aliases?.includes(nestedName)
);
if (nestedSubcommand) {
targetCommand = nestedSubcommand;
// Parser already extracted nested subcommand too
subcommandArgs = positional;
}
}
}
} else if (positional.length > 0 && command.subcommands) {
// Check if first positional is a subcommand
const subcommandName = positional[0];
const subcommand = command.subcommands.find(
sc => sc.name === subcommandName || sc.aliases?.includes(subcommandName)
);
if (subcommand) {
targetCommand = subcommand;
subcommandArgs = positional.slice(1);
// Check for nested subcommand (level 2 from positional)
if (subcommandArgs.length > 0 && subcommand.subcommands) {
const nestedName = subcommandArgs[0];
const nestedSubcommand = subcommand.subcommands.find(
sc => sc.name === nestedName || sc.aliases?.includes(nestedName)
);
if (nestedSubcommand) {
targetCommand = nestedSubcommand;
subcommandArgs = subcommandArgs.slice(1);
}
}
}
}
// Validate flags
const validationErrors = this.parser.validateFlags(flags, targetCommand);
if (validationErrors.length > 0) {
for (const error of validationErrors) {
this.output.printError(error);
}
process.exit(1);
}
// Build context
const ctx: CommandContext = {
args: subcommandArgs,
flags,
config: await this.loadConfig(flags.config as string),
cwd: process.cwd(),
interactive: this.interactive && !flags.quiet
};
// Execute command
if (targetCommand.action) {
if (this.output.isVerbose()) {
this.output.printDebug(`Executing: ${targetCommand.name}`);
}
const startTime = Date.now();
const result = await targetCommand.action(ctx);
if (this.output.isVerbose()) {
this.output.printDebug(`Completed in ${Date.now() - startTime}ms`);
}
if (result && !result.success) {
process.exit(result.exitCode || 1);
}
} else {
// No action - show command help (full path so nested subcommands work)
await this.showCommandHelp(commandPath);
}
} catch (error) {
// Don't re-handle if this is a process.exit error (from mocked tests)
const errorMessage = (error as Error).message;
if (errorMessage && errorMessage.startsWith('process.exit:')) {
throw error; // Re-throw so tests can capture the exit code
}
this.handleError(error as Error);
}
}
/**
* Show main help
*/
private async showHelp(): Promise<void> {
this.output.writeln();
this.output.writeln(this.output.bold(`${this.name} v${this.version}`));
this.output.writeln(this.output.dim(this.description));
this.output.writeln();
this.output.writeln(this.output.bold('USAGE:'));
this.output.writeln(` ${this.name} <command> [subcommand] [options]`);
this.output.writeln();
// PERF-03: Load all commands by category (lazy-loaded on demand)
const categories = await getCommandsByCategory();
// Primary Commands
this.output.writeln(this.output.bold('PRIMARY COMMANDS:'));
for (const cmd of categories.primary) {
if (cmd.hidden) continue;
const name = cmd.name.padEnd(12);
this.output.writeln(` ${this.output.highlight(name)} ${cmd.description}`);
}
this.output.writeln();
// Advanced Commands
if (categories.advanced.length > 0) {
this.output.writeln(this.output.bold('ADVANCED COMMANDS:'));
for (const cmd of categories.advanced) {
if (cmd.hidden) continue;
const name = cmd.name.padEnd(12);
this.output.writeln(` ${this.output.highlight(name)} ${cmd.description}`);
}
this.output.writeln();
}
// Utility Commands
if (categories.utility.length > 0) {
this.output.writeln(this.output.bold('UTILITY COMMANDS:'));
for (const cmd of categories.utility) {
if (cmd.hidden) continue;
const name = cmd.name.padEnd(12);
this.output.writeln(` ${this.output.highlight(name)} ${cmd.description}`);
}
this.output.writeln();
}
// Analysis Commands
if (categories.analysis.length > 0) {
this.output.writeln(this.output.bold('ANALYSIS COMMANDS:'));
for (const cmd of categories.analysis) {
if (cmd.hidden) continue;
const name = cmd.name.padEnd(12);
this.output.writeln(` ${this.output.highlight(name)} ${cmd.description}`);
}
this.output.writeln();
}
// Management Commands
if (categories.management.length > 0) {
this.output.writeln(this.output.bold('MANAGEMENT COMMANDS:'));
for (const cmd of categories.management) {
if (cmd.hidden) continue;
const name = cmd.name.padEnd(12);
this.output.writeln(` ${this.output.highlight(name)} ${cmd.description}`);
}
this.output.writeln();
}
this.output.writeln(this.output.bold('GLOBAL OPTIONS:'));
for (const opt of this.parser.getGlobalOptions()) {
const flags = opt.short ? `-${opt.short}, --${opt.name}` : ` --${opt.name}`;
this.output.writeln(` ${flags.padEnd(25)} ${opt.description}`);
}
this.output.writeln();
this.output.writeln(this.output.bold('V3 FEATURES:'));
this.output.printList([
'15-agent hierarchical mesh coordination',
'AgentDB with HNSW indexing (150x-12,500x faster)',
'Flash Attention (2.49x-7.47x speedup)',
'Unified SwarmCoordinator engine',
'Event-sourced state management',
'Domain-Driven Design architecture'
]);
this.output.writeln();
this.output.writeln(this.output.bold('EXAMPLES:'));
this.output.writeln(` ${this.name} agent spawn -t coder # Spawn a coder agent`);
this.output.writeln(` ${this.name} swarm init --v3-mode # Initialize V3 swarm`);
this.output.writeln(` ${this.name} memory search -q "auth patterns" # Semantic search`);
this.output.writeln(` ${this.name} mcp start # Start MCP server`);
this.output.writeln();
this.output.writeln(this.output.dim(`Run "${this.name} <command> --help" for command help`));
this.output.writeln();
this.output.writeln(this.output.dim('Created with ❤️ by ruv.io'));
this.output.writeln();
}
/**
* Show command-specific help.
*
* #1791.4 — accepts a FULL command path (e.g. ['hive-mind', 'spawn']) and
* walks subcommands so nested invocations show the leaf's own options /
* examples instead of always rendering the parent's SUBCOMMANDS list.
*/
private async showCommandHelp(commandPathOrName: string | string[]): Promise<void> {
const commandPath = Array.isArray(commandPathOrName) ? commandPathOrName : [commandPathOrName];
if (commandPath.length === 0) {
await this.showHelp();
return;
}
const rootName = commandPath[0];
// Try sync first, then lazy load
let command: Command | undefined = getCommand(rootName);
if (!command && hasCommand(rootName)) {
command = await getCommandAsync(rootName);
}
if (!command) {
this.output.printError(`Unknown command: ${rootName}`);
return;
}
// Walk into subcommands following the path so `hive-mind spawn --help`
// renders spawn's help, not hive-mind's parent help. We use a non-null
// local (`current`) instead of reassigning the optional `command` so
// TS can prove the value is defined for the rest of the function.
let current: Command = command;
const titleParts: string[] = [current.name];
for (let i = 1; i < commandPath.length; i++) {
const subName = commandPath[i];
const sub = current.subcommands?.find(sc => sc.name === subName || sc.aliases?.includes(subName));
if (!sub) break; // unknown leaf — fall back to last known
current = sub;
titleParts.push(sub.name);
}
this.output.writeln();
this.output.writeln(this.output.bold(`${this.name} ${titleParts.join(' ')}`));
this.output.writeln(current.description);
this.output.writeln();
// Subcommands
if (current.subcommands && current.subcommands.length > 0) {
this.output.writeln(this.output.bold('SUBCOMMANDS:'));
for (const sub of current.subcommands) {
if (sub.hidden) continue;
const name = sub.name.padEnd(15);
const aliases = sub.aliases ? this.output.dim(` (${sub.aliases.join(', ')})`) : '';
this.output.writeln(` ${this.output.highlight(name)} ${sub.description}${aliases}`);
}
this.output.writeln();
}
// Options
if (current.options && current.options.length > 0) {
this.output.writeln(this.output.bold('OPTIONS:'));
for (const opt of current.options) {
const flags = opt.short ? `-${opt.short}, --${opt.name}` : ` --${opt.name}`;
const required = opt.required ? this.output.error(' (required)') : '';
const defaultVal = opt.default !== undefined ? this.output.dim(` [default: ${opt.default}]`) : '';
this.output.writeln(` ${flags.padEnd(25)} ${opt.description}${required}${defaultVal}`);
}
this.output.writeln();
}
// Examples
if (current.examples && current.examples.length > 0) {
this.output.writeln(this.output.bold('EXAMPLES:'));
for (const example of current.examples) {
this.output.writeln(` ${this.output.dim('$')} ${example.command}`);
this.output.writeln(` ${this.output.dim(example.description)}`);
}
this.output.writeln();
}
}
/**
* Show version
*/
private showVersion(): void {
this.output.writeln(`${this.name} v${this.version}`);
}
/**
* Check for updates on startup (non-blocking)
* Shows notification if updates are available
*/
private async checkForUpdatesOnStartup(): Promise<void> {
try {
const result = await runStartupUpdateCheck({ autoUpdate: true });
// Show notifications for available updates that weren't auto-applied
if (result.checked && result.updatesAvailable.length > 0) {
const nonAutoUpdates = result.updatesAvailable.filter(u => !u.shouldAutoUpdate);
if (result.updatesApplied.length > 0) {
this.output.writeln(
this.output.dim(`Auto-updated: ${result.updatesApplied.join(', ')}`)
);
}
if (nonAutoUpdates.length > 0) {
this.output.writeln(
this.output.dim(`Updates available: ${nonAutoUpdates.map(u => `${u.package}@${u.latestVersion}`).join(', ')}`)
);
this.output.writeln(
this.output.dim(`Run '${this.name} update check' for details`)
);
}
}
} catch {
// Silently fail - don't interrupt CLI usage
}
}
/**
* Load configuration file
*/
private async loadConfig(configPath?: string): Promise<V3Config | undefined> {
try {
// Import config utilities
const { loadConfig: loadSystemConfig } = await import('@claude-flow/shared');
const { systemConfigToV3Config } = await import('./config-adapter.js');
// Load configuration
const loaded = await loadSystemConfig({
file: configPath,
paths: configPath ? undefined : [process.cwd()],
});
// Convert to V3Config format
const v3Config = systemConfigToV3Config(loaded.config);
// Log warnings if any
if (loaded.warnings && loaded.warnings.length > 0) {
for (const warning of loaded.warnings) {
this.output.printWarning(warning);
}
}
return v3Config;
} catch (error) {
// Config loading is optional - don't fail if it doesn't exist
if (process.env.DEBUG) {
this.output.writeln(
this.output.dim(`Config loading failed: ${(error as Error).message}`)
);
}
return undefined;
}
}
/**
* Handle errors
*/
private handleError(error: Error): void {
if ('code' in error) {
// CLIError
const cliError = error as CLIError;
this.output.printError(cliError.message);
if (cliError.details) {
this.output.writeln(this.output.dim(JSON.stringify(cliError.details, null, 2)));
}
process.exit(cliError.exitCode);
} else {
// Generic error
this.output.printError(error.message);
if (process.env.DEBUG) {
this.output.writeln();
this.output.writeln(this.output.dim(error.stack || ''));
}
process.exit(1);
}
}
}
// =============================================================================
// Module Exports
// =============================================================================
// Types
export * from './types.js';
// Parser
export { CommandParser, commandParser } from './parser.js';
// Output
export { OutputFormatter, output, Progress, Spinner, type VerbosityLevel } from './output.js';
// Prompt
export * from './prompt.js';
// Commands (internal use)
export * from './commands/index.js';
// MCP Server management
export {
MCPServerManager,
createMCPServerManager,
getServerManager,
startMCPServer,
stopMCPServer,
getMCPServerStatus,
type MCPServerOptions,
type MCPServerStatus,
} from './mcp-server.js';
// Memory & Intelligence (V3 Performance Features)
export {
initializeMemoryDatabase,
generateEmbedding,
generateBatchEmbeddings,
storeEntry,
searchEntries,
getHNSWIndex,
addToHNSWIndex,
searchHNSWIndex,
getHNSWStatus,
clearHNSWIndex,
quantizeInt8,
dequantizeInt8,
quantizedCosineSim,
getQuantizationStats,
// Flash Attention-style batch operations
batchCosineSim,
softmaxAttention,
topKIndices,
flashAttentionSearch,
type MemoryInitResult,
} from './memory/memory-initializer.js';
export {
initializeIntelligence,
recordStep,
recordTrajectory,
findSimilarPatterns,
getIntelligenceStats,
getSonaCoordinator,
getReasoningBank,
clearIntelligence,
benchmarkAdaptation,
// RL loop API
endTrajectoryWithVerdict,
distillLearning,
// Pattern persistence API
getAllPatterns,
getPatternsByType,
flushPatterns,
deletePattern,
clearAllPatterns,
getNeuralDataDir,
getPersistenceStatus,
type SonaConfig,
type TrajectoryStep,
type Pattern,
type IntelligenceStats,
} from './memory/intelligence.js';
// EWC++ Consolidation (Prevents Catastrophic Forgetting)
export {
EWCConsolidator,
getEWCConsolidator,
resetEWCConsolidator,
consolidatePatterns,
recordPatternOutcome,
getEWCStats,
type PatternWeights,
type EWCConfig,
type ConsolidationResult,
type EWCStats,
} from './memory/ewc-consolidation.js';
// SONA Optimizer (Adaptive Routing via Trajectory Learning)
export {
SONAOptimizer,
getSONAOptimizer,
resetSONAOptimizer,
processTrajectory,
getSuggestion,
getSONAStats,
type TrajectoryOutcome,
type LearnedPattern,
type RoutingSuggestion,
type SONAStats,
} from './memory/sona-optimizer.js';
// Production Hardening
export {
ErrorHandler,
withErrorHandling,
} from './production/error-handler.js';
export type {
ErrorContext,
ErrorHandlerConfig,
} from './production/error-handler.js';
export {
RateLimiter,
createRateLimiter,
} from './production/rate-limiter.js';
export type {
RateLimiterConfig,
RateLimitResult,
} from './production/rate-limiter.js';
export {
withRetry,
makeRetryable,
} from './production/retry.js';
export type {
RetryConfig,
RetryResult,
RetryStrategy,
} from './production/retry.js';
export {
CircuitBreaker,
getCircuitBreaker,
getAllCircuitStats,
resetAllCircuits,
} from './production/circuit-breaker.js';
export type {
CircuitBreakerConfig,
CircuitState,
} from './production/circuit-breaker.js';
export {
MonitoringHooks,
createMonitor,
getMonitor,
} from './production/monitoring.js';
export type {
MonitorConfig,
MetricEvent,
HealthStatus,
PerformanceMetrics,
} from './production/monitoring.js';
// Default export
export default CLI;