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

1,181 lines 78.9 kB
/** * Init Executor * Main execution logic for V3 initialization */ import * as fs from 'fs'; import * as path from 'path'; import { fileURLToPath } from 'url'; import { createRequire } from 'module'; import { dirname } from 'path'; // ESM-compatible __dirname const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); import { detectPlatform, DEFAULT_INIT_OPTIONS } from './types.js'; import { generateSettingsJson, generateSettings } from './settings-generator.js'; import { generateMCPJson } from './mcp-generator.js'; import { generateStatuslineScript } from './statusline-generator.js'; import { generatePreCommitHook, generatePostCommitHook, generateSessionManager, generateAgentRouter, generateMemoryHelper, generateHookHandler, generateIntelligenceStub, generateAutoMemoryHook, generateRufloHookCjs, } from './helpers-generator.js'; import { generateClaudeMd } from './claudemd-generator.js'; /** * Skills to copy based on configuration */ const SKILLS_MAP = { core: [ 'swarm-orchestration', 'swarm-advanced', 'sparc-methodology', 'hooks-automation', 'pair-programming', 'verification-quality', 'stream-chain', 'skill-builder', ], browser: ['browser'], // agent-browser integration dualMode: ['dual-mode'], // Claude Code + Codex hybrid execution agentdb: [ 'agentdb-advanced', 'agentdb-learning', 'agentdb-memory-patterns', 'agentdb-optimization', 'agentdb-vector-search', 'reasoningbank-agentdb', 'reasoningbank-intelligence', ], github: [ 'github-code-review', 'github-multi-repo', 'github-project-management', 'github-release-management', 'github-workflow-automation', ], flowNexus: [ 'flow-nexus-neural', 'flow-nexus-platform', 'flow-nexus-swarm', ], v3: [ 'v3-cli-modernization', 'v3-core-implementation', 'v3-ddd-architecture', 'v3-integration-deep', 'v3-mcp-optimization', 'v3-memory-unification', 'v3-performance-optimization', 'v3-security-overhaul', 'v3-swarm-coordination', ], }; /** * Commands to copy based on configuration * ADR-128 Phase 4: every subdirectory under .claude/commands/ now has a * corresponding key. The flow-nexus/ dir was deleted (belongs to the plugin). * New substrate keys default true; opt-in keys (pair, training, stream-chain, * truth, verify) default false per ADR-128 §Phase 3 opt-in rationale. */ const COMMANDS_MAP = { core: ['claude-flow-help.md', 'claude-flow-swarm.md', 'claude-flow-memory.md'], analysis: ['analysis'], automation: ['automation'], github: ['github'], hooks: ['hooks'], monitoring: ['monitoring'], optimization: ['optimization'], sparc: ['sparc'], // ADR-128 Phase 4 promotions (previously orphaned) agents: ['agents'], coordination: ['coordination'], hiveMind: ['hive-mind'], memory: ['memory'], swarm: ['swarm'], workflows: ['workflows'], // Opt-in categories (non-universal; default false in CommandsConfig) pair: ['pair'], training: ['training'], streamChain: ['stream-chain'], truth: ['truth'], verify: ['verify'], }; /** * Agents to copy based on configuration */ const AGENTS_MAP = { core: ['core'], consensus: ['consensus'], github: ['github'], hiveMind: ['hive-mind'], sparc: ['sparc'], swarm: ['swarm'], browser: ['browser'], // agent-browser integration dualMode: ['dual-mode'], // Claude Code + Codex hybrid execution // V3-specific agents v3: ['v3'], optimization: ['optimization'], templates: ['templates'], testing: ['testing'], sublinear: ['sublinear'], flowNexus: ['flow-nexus'], analysis: ['analysis'], architecture: ['architecture'], development: ['development'], devops: ['devops'], documentation: ['documentation'], specialized: ['specialized'], goal: ['goal'], sona: ['sona'], payments: ['payments'], data: ['data'], custom: ['custom'], }; /** * Directory structure to create */ const DIRECTORIES = { claude: [ '.claude', '.claude/skills', '.claude/commands', '.claude/agents', '.claude/helpers', ], runtime: [ '.claude-flow', '.claude-flow/data', '.claude-flow/logs', '.claude-flow/sessions', '.claude-flow/hooks', '.claude-flow/agents', '.claude-flow/workflows', ], }; /** * Execute initialization */ export async function executeInit(options) { // Detect platform const platform = detectPlatform(); const result = { success: true, platform, created: { directories: [], files: [], }, skipped: [], errors: [], summary: { skillsCount: 0, commandsCount: 0, agentsCount: 0, hooksEnabled: 0, }, }; const targetDir = options.targetDir; try { // Create directory structure await createDirectories(targetDir, options, result); // Generate and write settings.json if (options.components.settings) { await writeSettings(targetDir, options, result); } // Generate and write .mcp.json if (options.components.mcp) { await writeMCPConfig(targetDir, options, result); } // Copy skills if (options.components.skills) { await copySkills(targetDir, options, result); } // Copy commands if (options.components.commands) { await copyCommands(targetDir, options, result); } // Copy agents if (options.components.agents) { await copyAgents(targetDir, options, result); } // Generate helpers if (options.components.helpers) { await writeHelpers(targetDir, options, result); } // Generate statusline if (options.components.statusline) { await writeStatusline(targetDir, options, result); } // Generate runtime config if (options.components.runtime) { await writeRuntimeConfig(targetDir, options, result); } // Create initial metrics for statusline (prevents "all zeros" display) if (options.components.statusline) { await writeInitialMetrics(targetDir, options, result); } // Generate CLAUDE.md if (options.components.claudeMd) { await writeClaudeMd(targetDir, options, result); } // Count enabled hooks result.summary.hooksEnabled = countEnabledHooks(options); } catch (error) { result.success = false; result.errors.push(error instanceof Error ? error.message : String(error)); } return result; } /** * Merge new settings into existing settings.json * Preserves user customizations while adding new features like Agent Teams * Uses platform-specific commands for Mac, Linux, and Windows */ function mergeSettingsForUpgrade(existing) { const merged = { ...existing }; const platform = detectPlatform(); const isWindows = platform.os === 'windows'; // Platform-specific command wrappers // Windows: Use PowerShell-compatible commands // Mac/Linux: Use bash-compatible commands with 2>/dev/null // NOTE: teammateIdleCmd and taskCompletedCmd were removed. // TeammateIdle/TaskCompleted are not valid Claude Code hook events and caused warnings. // Agent Teams hook config lives in claudeFlow.agentTeams.hooks instead. // 1. Merge env vars (preserve existing, add new) const existingEnv = existing.env || {}; merged.env = { ...existingEnv, CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', CLAUDE_FLOW_V3_ENABLED: existingEnv.CLAUDE_FLOW_V3_ENABLED || 'true', CLAUDE_FLOW_HOOKS_ENABLED: existingEnv.CLAUDE_FLOW_HOOKS_ENABLED || 'true', }; // 2. Merge hooks (preserve existing, add new Agent Teams + auto-memory hooks) const existingHooks = existing.hooks || {}; merged.hooks = { ...existingHooks }; // Cross-platform auto-memory hook commands that resolve paths via git root. // Uses node -e with git rev-parse so hooks work regardless of CWD (#1259, #1284). const gitRootResolver = "var c=require('child_process'),p=require('path'),u=require('url'),r;" + "try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}" + 'catch(e){r=process.cwd()}'; const autoMemoryScript = '.claude/helpers/auto-memory-hook.mjs'; const autoMemoryImportCmd = `node -e "${gitRootResolver}var f=p.join(r,'${autoMemoryScript}');import(u.pathToFileURL(f).href)" import`; const autoMemorySyncCmd = `node -e "${gitRootResolver}var f=p.join(r,'${autoMemoryScript}');import(u.pathToFileURL(f).href)" sync`; // Add auto-memory import to SessionStart (if not already present) const sessionStartHooks = existingHooks.SessionStart; const hasAutoMemoryImport = sessionStartHooks?.some(group => group.hooks?.some(h => h.command?.includes('auto-memory-hook'))); if (!hasAutoMemoryImport) { const startHooks = merged.hooks; if (!startHooks.SessionStart) { startHooks.SessionStart = [{ hooks: [] }]; } const startGroup = startHooks.SessionStart[0]; if (!startGroup.hooks) startGroup.hooks = []; startGroup.hooks.push({ type: 'command', command: autoMemoryImportCmd, timeout: 6000, continueOnError: true, }); } // Add auto-memory sync to SessionEnd (if not already present) const sessionEndHooks = existingHooks.SessionEnd; const hasAutoMemorySync = sessionEndHooks?.some(group => group.hooks?.some(h => h.command?.includes('auto-memory-hook'))); if (!hasAutoMemorySync) { const endHooks = merged.hooks; if (!endHooks.SessionEnd) { endHooks.SessionEnd = [{ hooks: [] }]; } const endGroup = endHooks.SessionEnd[0]; if (!endGroup.hooks) endGroup.hooks = []; // Insert at beginning so sync runs before other cleanup endGroup.hooks.unshift({ type: 'command', command: autoMemorySyncCmd, timeout: 8000, continueOnError: true, }); } // NOTE: TeammateIdle and TaskCompleted are NOT valid Claude Code hook events. // They cause warnings when present in settings.json hooks. // Remove them if they exist from a previous init. delete merged.hooks.TeammateIdle; delete merged.hooks.TaskCompleted; // Their configuration lives in claudeFlow.agentTeams.hooks instead. // 3. Fix statusLine config (remove invalid fields, ensure correct format) // Claude Code only supports: type, command, padding const existingStatusLine = existing.statusLine; if (existingStatusLine) { merged.statusLine = { type: 'command', command: existingStatusLine.command || `node -e "var c=require('child_process'),p=require('path'),r;try{r=c.execSync('git rev-parse --show-toplevel',{encoding:'utf8'}).trim()}catch(e){r=process.cwd()}var s=p.join(r,'.claude/helpers/statusline.cjs');process.argv.splice(1,0,s);require(s)"`, // Remove invalid fields: refreshMs, enabled (not supported by Claude Code) }; } // 4. Merge claudeFlow settings (preserve existing, add agentTeams + memory) const existingClaudeFlow = existing.claudeFlow || {}; const existingMemory = existingClaudeFlow.memory || {}; merged.claudeFlow = { ...existingClaudeFlow, version: existingClaudeFlow.version || '3.0.0', enabled: existingClaudeFlow.enabled !== false, agentTeams: { enabled: true, teammateMode: 'auto', taskListEnabled: true, mailboxEnabled: true, coordination: { autoAssignOnIdle: true, trainPatternsOnComplete: true, notifyLeadOnComplete: true, sharedMemoryNamespace: 'agent-teams', }, hooks: { teammateIdle: { enabled: true, autoAssign: true, checkTaskList: true }, taskCompleted: { enabled: true, trainPatterns: true, notifyLead: true }, }, }, memory: { ...existingMemory, learningBridge: existingMemory.learningBridge ?? { enabled: true }, memoryGraph: existingMemory.memoryGraph ?? { enabled: true }, agentScopes: existingMemory.agentScopes ?? { enabled: true }, }, }; return merged; } /** * Execute upgrade - updates helpers and creates missing metrics without losing data * This is safe for existing users who want the latest statusline fixes * @param targetDir - Target directory * @param upgradeSettings - If true, merge new settings into existing settings.json */ export async function executeUpgrade(targetDir, upgradeSettings = false) { const result = { success: true, updated: [], created: [], preserved: [], errors: [], settingsUpdated: [], }; try { // Ensure required directories exist const dirs = [ '.claude/helpers', '.claude-flow/metrics', '.claude-flow/security', '.claude-flow/learning', ]; for (const dir of dirs) { const fullPath = path.join(targetDir, dir); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); } } // 0. ALWAYS update critical helpers (force overwrite) const sourceHelpersForUpgrade = findSourceHelpersDir(); if (sourceHelpersForUpgrade) { const criticalHelpers = ['auto-memory-hook.mjs', 'hook-handler.cjs', 'intelligence.cjs']; for (const helperName of criticalHelpers) { const targetPath = path.join(targetDir, '.claude', 'helpers', helperName); const sourcePath = path.join(sourceHelpersForUpgrade, helperName); if (fs.existsSync(sourcePath)) { if (fs.existsSync(targetPath)) { result.updated.push(`.claude/helpers/${helperName}`); } else { result.created.push(`.claude/helpers/${helperName}`); } fs.copyFileSync(sourcePath, targetPath); try { fs.chmodSync(targetPath, '755'); } catch { } } } } else { // Source not found (npx with broken paths) — use generated fallbacks const generatedCritical = { 'hook-handler.cjs': generateHookHandler(), 'intelligence.cjs': generateIntelligenceStub(), 'auto-memory-hook.mjs': generateAutoMemoryHook(), }; for (const [helperName, content] of Object.entries(generatedCritical)) { const targetPath = path.join(targetDir, '.claude', 'helpers', helperName); if (fs.existsSync(targetPath)) { result.updated.push(`.claude/helpers/${helperName}`); } else { result.created.push(`.claude/helpers/${helperName}`); } fs.writeFileSync(targetPath, content, 'utf-8'); try { fs.chmodSync(targetPath, '755'); } catch { } } } // 1. ALWAYS update statusline helper (force overwrite) const statuslinePath = path.join(targetDir, '.claude', 'helpers', 'statusline.cjs'); // Use default options with statusline config const upgradeOptions = { ...DEFAULT_INIT_OPTIONS, targetDir, force: true, statusline: { ...DEFAULT_INIT_OPTIONS.statusline, refreshInterval: 5000, }, }; const statuslineContent = generateStatuslineScript(upgradeOptions); if (fs.existsSync(statuslinePath)) { result.updated.push('.claude/helpers/statusline.cjs'); } else { result.created.push('.claude/helpers/statusline.cjs'); } fs.writeFileSync(statuslinePath, statuslineContent, 'utf-8'); // 2. Create MISSING metrics files only (preserve existing data) const metricsDir = path.join(targetDir, '.claude-flow', 'metrics'); const securityDir = path.join(targetDir, '.claude-flow', 'security'); // v3-progress.json const progressPath = path.join(metricsDir, 'v3-progress.json'); if (!fs.existsSync(progressPath)) { const progress = { version: '3.0.0', initialized: new Date().toISOString(), domains: { completed: 0, total: 5, status: 'INITIALIZING' }, ddd: { progress: 0, modules: 0, totalFiles: 0, totalLines: 0 }, swarm: { activeAgents: 0, maxAgents: 15, topology: 'hierarchical-mesh' }, learning: { status: 'READY', patternsLearned: 0, sessionsCompleted: 0 }, _note: 'Metrics will update as you use Ruflo' }; fs.writeFileSync(progressPath, JSON.stringify(progress, null, 2), 'utf-8'); result.created.push('.claude-flow/metrics/v3-progress.json'); } else { result.preserved.push('.claude-flow/metrics/v3-progress.json'); } // swarm-activity.json const activityPath = path.join(metricsDir, 'swarm-activity.json'); if (!fs.existsSync(activityPath)) { const activity = { timestamp: new Date().toISOString(), processes: { agentic_flow: 0, mcp_server: 0, estimated_agents: 0 }, swarm: { active: false, agent_count: 0, coordination_active: false }, integration: { agentic_flow_active: false, mcp_active: false }, _initialized: true }; fs.writeFileSync(activityPath, JSON.stringify(activity, null, 2), 'utf-8'); result.created.push('.claude-flow/metrics/swarm-activity.json'); } else { result.preserved.push('.claude-flow/metrics/swarm-activity.json'); } // learning.json const learningPath = path.join(metricsDir, 'learning.json'); if (!fs.existsSync(learningPath)) { const learning = { initialized: new Date().toISOString(), routing: { accuracy: 0, decisions: 0 }, patterns: { shortTerm: 0, longTerm: 0, quality: 0 }, sessions: { total: 0, current: null }, _note: 'Intelligence grows as you use Ruflo' }; fs.writeFileSync(learningPath, JSON.stringify(learning, null, 2), 'utf-8'); result.created.push('.claude-flow/metrics/learning.json'); } else { result.preserved.push('.claude-flow/metrics/learning.json'); } // audit-status.json const auditPath = path.join(securityDir, 'audit-status.json'); if (!fs.existsSync(auditPath)) { const audit = { initialized: new Date().toISOString(), status: 'PENDING', cvesFixed: 0, totalCves: 3, lastScan: null, _note: 'Run: npx @claude-flow/cli@latest security scan' }; fs.writeFileSync(auditPath, JSON.stringify(audit, null, 2), 'utf-8'); result.created.push('.claude-flow/security/audit-status.json'); } else { result.preserved.push('.claude-flow/security/audit-status.json'); } // 3. Merge settings if requested if (upgradeSettings) { const settingsPath = path.join(targetDir, '.claude', 'settings.json'); if (fs.existsSync(settingsPath)) { try { const existingSettings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); const mergedSettings = mergeSettingsForUpgrade(existingSettings); fs.writeFileSync(settingsPath, JSON.stringify(mergedSettings, null, 2), 'utf-8'); result.updated.push('.claude/settings.json'); result.settingsUpdated = [ 'env.CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS', 'hooks.SessionStart (auto-memory import)', 'hooks.SessionEnd (auto-memory sync)', 'hooks.TeammateIdle (removed — not a valid Claude Code hook)', 'hooks.TaskCompleted (removed — not a valid Claude Code hook)', 'claudeFlow.agentTeams', 'claudeFlow.memory (learningBridge, memoryGraph, agentScopes)', ]; } catch (settingsError) { result.errors.push(`Settings merge failed: ${settingsError instanceof Error ? settingsError.message : String(settingsError)}`); } } else { // Create new settings.json with defaults const defaultSettings = generateSettings(DEFAULT_INIT_OPTIONS); fs.writeFileSync(settingsPath, JSON.stringify(defaultSettings, null, 2), 'utf-8'); result.created.push('.claude/settings.json'); result.settingsUpdated = ['Created new settings.json with Agent Teams']; } } } catch (error) { result.success = false; result.errors.push(error instanceof Error ? error.message : String(error)); } return result; } /** * Execute upgrade with --add-missing flag * Adds any new skills, agents, and commands that don't exist yet * @param targetDir - Target directory * @param upgradeSettings - If true, merge new settings into existing settings.json */ export async function executeUpgradeWithMissing(targetDir, upgradeSettings = false) { // First do the normal upgrade (pass through upgradeSettings) const result = await executeUpgrade(targetDir, upgradeSettings); if (!result.success) { return result; } // Initialize tracking arrays result.addedSkills = []; result.addedAgents = []; result.addedCommands = []; try { // Ensure target directories exist const skillsDir = path.join(targetDir, '.claude', 'skills'); const agentsDir = path.join(targetDir, '.claude', 'agents'); const commandsDir = path.join(targetDir, '.claude', 'commands'); for (const dir of [skillsDir, agentsDir, commandsDir]) { if (!fs.existsSync(dir)) { fs.mkdirSync(dir, { recursive: true }); } } // Find source directories const sourceSkillsDir = findSourceDir('skills'); const sourceAgentsDir = findSourceDir('agents'); const sourceCommandsDir = findSourceDir('commands'); // Debug: Log source directories found if (process.env.DEBUG || process.env.CLAUDE_FLOW_DEBUG) { console.log('[DEBUG] Source directories:'); console.log(` Skills: ${sourceSkillsDir || 'NOT FOUND'}`); console.log(` Agents: ${sourceAgentsDir || 'NOT FOUND'}`); console.log(` Commands: ${sourceCommandsDir || 'NOT FOUND'}`); } // Add missing skills if (sourceSkillsDir) { const allSkills = Object.values(SKILLS_MAP).flat(); const debugMode = process.env.DEBUG || process.env.CLAUDE_FLOW_DEBUG; if (debugMode) { console.log(`[DEBUG] Checking ${allSkills.length} skills from SKILLS_MAP`); } for (const skillName of [...new Set(allSkills)]) { const sourcePath = path.join(sourceSkillsDir, skillName); const targetPath = path.join(skillsDir, skillName); const sourceExists = fs.existsSync(sourcePath); const targetExists = fs.existsSync(targetPath); if (debugMode) { console.log(`[DEBUG] Skill '${skillName}': source=${sourceExists}, target=${targetExists}`); } if (sourceExists && !targetExists) { copyDirRecursive(sourcePath, targetPath); result.addedSkills.push(skillName); result.created.push(`.claude/skills/${skillName}`); } } } // Add missing agents if (sourceAgentsDir) { const allAgents = Object.values(AGENTS_MAP).flat(); for (const agentCategory of [...new Set(allAgents)]) { const sourcePath = path.join(sourceAgentsDir, agentCategory); const targetPath = path.join(agentsDir, agentCategory); if (fs.existsSync(sourcePath) && !fs.existsSync(targetPath)) { copyDirRecursive(sourcePath, targetPath); result.addedAgents.push(agentCategory); result.created.push(`.claude/agents/${agentCategory}`); } } } // Add missing commands if (sourceCommandsDir) { const allCommands = Object.values(COMMANDS_MAP).flat(); for (const cmdName of [...new Set(allCommands)]) { const sourcePath = path.join(sourceCommandsDir, cmdName); const targetPath = path.join(commandsDir, cmdName); if (fs.existsSync(sourcePath) && !fs.existsSync(targetPath)) { if (fs.statSync(sourcePath).isDirectory()) { copyDirRecursive(sourcePath, targetPath); } else { fs.copyFileSync(sourcePath, targetPath); } result.addedCommands.push(cmdName); result.created.push(`.claude/commands/${cmdName}`); } } } } catch (error) { result.errors.push(`Add missing failed: ${error instanceof Error ? error.message : String(error)}`); } return result; } /** * Create directory structure */ async function createDirectories(targetDir, options, result) { const dirs = [ ...DIRECTORIES.claude, ...(options.components.runtime ? DIRECTORIES.runtime : []), ]; for (const dir of dirs) { const fullPath = path.join(targetDir, dir); if (!fs.existsSync(fullPath)) { fs.mkdirSync(fullPath, { recursive: true }); result.created.directories.push(dir); } } } /** * Write settings.json */ async function writeSettings(targetDir, options, result) { const settingsPath = path.join(targetDir, '.claude', 'settings.json'); const generated = JSON.parse(generateSettingsJson(options)); if (fs.existsSync(settingsPath) && !options.force) { // Merge hooks/env/permissions into existing settings instead of skipping try { const existing = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')); let merged = false; // Merge hooks (the critical missing piece — #1484) if (generated.hooks && !existing.hooks) { existing.hooks = generated.hooks; merged = true; } // Merge env vars (for CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS etc.) if (generated.env) { existing.env = { ...(existing.env || {}), ...generated.env }; merged = true; } // Merge permissions (add ruflo allow rules) if (generated.permissions?.allow) { const existingAllow = existing.permissions?.allow || []; const newRules = generated.permissions.allow.filter((r) => !existingAllow.includes(r)); if (newRules.length > 0) { existing.permissions = existing.permissions || {}; existing.permissions.allow = [...existingAllow, ...newRules]; merged = true; } } if (merged) { fs.writeFileSync(settingsPath, JSON.stringify(existing, null, 2), 'utf-8'); result.created.files.push('.claude/settings.json (merged hooks)'); } else { result.skipped.push('.claude/settings.json'); } } catch { // Existing file is corrupt — overwrite fs.writeFileSync(settingsPath, JSON.stringify(generated, null, 2), 'utf-8'); result.created.files.push('.claude/settings.json'); } return; } fs.writeFileSync(settingsPath, JSON.stringify(generated, null, 2), 'utf-8'); result.created.files.push('.claude/settings.json'); } /** * #1779 — Walk parents of `targetDir` plus the user-global Claude Code * config locations, looking for any `.mcp.json` (or `~/.claude.json`) * that already declares a `ruflo`-keyed MCP server. We use this to skip * writing our own `claude-flow`-keyed entry when the user has already * registered the same binary under the new name — that's exactly the * "same MCP server twice under two different prefixes" duplication the * issue describes. * * Returns the path of the file that already declares `ruflo` (so we can * surface it in the skipped-message), or null if none found. */ function detectExistingRufloMCP(targetDir) { const home = (process.env.HOME ?? process.env.USERPROFILE) ?? ''; const candidates = new Set(); // User-global Claude Code config locations if (home) { candidates.add(path.join(home, '.claude.json')); candidates.add(path.join(home, '.claude', 'mcp.json')); } // Walk parents of targetDir up to root, checking for .mcp.json at each const targetResolved = path.resolve(targetDir); let dir = targetResolved; const targetAncestors = new Set(); while (true) { candidates.add(path.join(dir, '.mcp.json')); targetAncestors.add(normalizeProjectKey(dir)); const parent = path.dirname(dir); if (parent === dir) break; dir = parent; } // Skip the targetDir itself — that's the one we're about to write candidates.delete(path.join(targetResolved, '.mcp.json')); for (const candidate of candidates) { if (!fs.existsSync(candidate)) continue; try { const parsed = JSON.parse(fs.readFileSync(candidate, 'utf-8')); if (!parsed || typeof parsed !== 'object') continue; // (a) Top-level mcpServers (legacy / global form). // #2207: accept BOTH the old 'ruflo' key AND the new 'claude-flow' key so that // a prior install with either key is correctly detected as already-initialized. // This also avoids the reverse problem: after #2206 fixed the generator to write // 'claude-flow', a second `ruflo init` must still recognise the existing install. if (parsed.mcpServers && typeof parsed.mcpServers === 'object') { const servers = parsed.mcpServers; if ('claude-flow' in servers || 'ruflo' in servers) return candidate; } // (b) #1840: Claude Code project-scoped registrations under // parsed.projects[<projectPath>].mcpServers. Match by // normalized path against targetDir or any of its ancestors so // a `claude mcp add claude-flow` (or legacy `ruflo`) in this repo is // detected even when Claude stored the key with different casing/slash style. // #2207: accept both keys here too. if (parsed.projects && typeof parsed.projects === 'object') { for (const [projectKey, projectVal] of Object.entries(parsed.projects)) { if (!projectVal || typeof projectVal !== 'object') continue; const projectMcp = projectVal.mcpServers; if (!projectMcp || typeof projectMcp !== 'object') continue; const mcp = projectMcp; if (!('claude-flow' in mcp) && !('ruflo' in mcp)) continue; if (targetAncestors.has(normalizeProjectKey(projectKey))) { return `${candidate} (projects[${projectKey}])`; } } } } catch { /* malformed JSON — ignore */ } } return null; } /** * Normalize a project path key for cross-platform comparison. * Claude Code stores Windows paths like "C:/Users/.../Project" while * Node's `path.resolve()` may emit "C:\Users\...\Project". Lowercase + * forward-slash gives a stable comparison key on both platforms. */ function normalizeProjectKey(p) { return path.resolve(p).replace(/\\/g, '/').toLowerCase(); } /** * Write .mcp.json */ async function writeMCPConfig(targetDir, options, result) { const mcpPath = path.join(targetDir, '.mcp.json'); if (fs.existsSync(mcpPath) && !options.force) { result.skipped.push('.mcp.json'); return; } // #1779 — Skip writing if the user already has a `ruflo`-keyed MCP // server registered elsewhere (parent .mcp.json, ~/.claude.json, etc). // Writing our `claude-flow`-keyed entry on top of that produces the // duplicate-registration the issue describes (~250 duplicate tools). // Force-mode (`--force`) bypasses this guard for users who actually // want both registrations. if (!options.force) { const existingRufloPath = detectExistingRufloMCP(targetDir); if (existingRufloPath) { result.skipped.push(`.mcp.json (existing 'ruflo' MCP registration found at ${existingRufloPath} — would create duplicate; pass --force to write anyway)`); return; } } const content = generateMCPJson(options); fs.writeFileSync(mcpPath, content, 'utf-8'); result.created.files.push('.mcp.json'); } /** * Copy skills from source */ async function copySkills(targetDir, options, result) { const skillsConfig = options.skills; const targetSkillsDir = path.join(targetDir, '.claude', 'skills'); // Determine which skills to copy const skillsToCopy = []; if (skillsConfig.all) { // Copy all available skills Object.values(SKILLS_MAP).forEach(skills => skillsToCopy.push(...skills)); } else { if (skillsConfig.core) skillsToCopy.push(...SKILLS_MAP.core); if (skillsConfig.agentdb) skillsToCopy.push(...SKILLS_MAP.agentdb); if (skillsConfig.github) skillsToCopy.push(...SKILLS_MAP.github); if (skillsConfig.flowNexus) skillsToCopy.push(...SKILLS_MAP.flowNexus); if (skillsConfig.browser) skillsToCopy.push(...SKILLS_MAP.browser); if (skillsConfig.v3) skillsToCopy.push(...SKILLS_MAP.v3); if (skillsConfig.dualMode) skillsToCopy.push(...SKILLS_MAP.dualMode); } // Find source skills directory const sourceSkillsDir = findSourceDir('skills', options.sourceBaseDir); if (!sourceSkillsDir) { result.errors.push('Could not find source skills directory'); return; } // Copy each skill for (const skillName of [...new Set(skillsToCopy)]) { const sourcePath = path.join(sourceSkillsDir, skillName); const targetPath = path.join(targetSkillsDir, skillName); if (fs.existsSync(sourcePath)) { if (!fs.existsSync(targetPath) || options.force) { copyDirRecursive(sourcePath, targetPath); result.created.files.push(`.claude/skills/${skillName}`); result.summary.skillsCount++; } else { result.skipped.push(`.claude/skills/${skillName}`); } } } } /** * Copy commands from source */ async function copyCommands(targetDir, options, result) { const commandsConfig = options.commands; const targetCommandsDir = path.join(targetDir, '.claude', 'commands'); // Determine which commands to copy const commandsToCopy = []; if (commandsConfig.all) { Object.values(COMMANDS_MAP).forEach(cmds => commandsToCopy.push(...cmds)); } else { if (commandsConfig.core) commandsToCopy.push(...COMMANDS_MAP.core); if (commandsConfig.analysis) commandsToCopy.push(...COMMANDS_MAP.analysis); if (commandsConfig.automation) commandsToCopy.push(...COMMANDS_MAP.automation); if (commandsConfig.github) commandsToCopy.push(...COMMANDS_MAP.github); if (commandsConfig.hooks) commandsToCopy.push(...COMMANDS_MAP.hooks); if (commandsConfig.monitoring) commandsToCopy.push(...COMMANDS_MAP.monitoring); if (commandsConfig.optimization) commandsToCopy.push(...COMMANDS_MAP.optimization); if (commandsConfig.sparc) commandsToCopy.push(...COMMANDS_MAP.sparc); // ADR-128 Phase 4 substrate promotions if (commandsConfig.agents) commandsToCopy.push(...(COMMANDS_MAP.agents || [])); if (commandsConfig.coordination) commandsToCopy.push(...(COMMANDS_MAP.coordination || [])); if (commandsConfig.hiveMind) commandsToCopy.push(...(COMMANDS_MAP.hiveMind || [])); if (commandsConfig.memory) commandsToCopy.push(...(COMMANDS_MAP.memory || [])); if (commandsConfig.swarm) commandsToCopy.push(...(COMMANDS_MAP.swarm || [])); if (commandsConfig.workflows) commandsToCopy.push(...(COMMANDS_MAP.workflows || [])); // ADR-128 Phase 4 opt-in categories if (commandsConfig.pair) commandsToCopy.push(...(COMMANDS_MAP.pair || [])); if (commandsConfig.training) commandsToCopy.push(...(COMMANDS_MAP.training || [])); if (commandsConfig.streamChain) commandsToCopy.push(...(COMMANDS_MAP.streamChain || [])); if (commandsConfig.truth) commandsToCopy.push(...(COMMANDS_MAP.truth || [])); if (commandsConfig.verify) commandsToCopy.push(...(COMMANDS_MAP.verify || [])); } // Find source commands directory const sourceCommandsDir = findSourceDir('commands', options.sourceBaseDir); if (!sourceCommandsDir) { result.errors.push('Could not find source commands directory'); return; } // Copy each command/directory for (const cmdName of [...new Set(commandsToCopy)]) { const sourcePath = path.join(sourceCommandsDir, cmdName); const targetPath = path.join(targetCommandsDir, cmdName); if (fs.existsSync(sourcePath)) { if (!fs.existsSync(targetPath) || options.force) { if (fs.statSync(sourcePath).isDirectory()) { copyDirRecursive(sourcePath, targetPath); } else { fs.copyFileSync(sourcePath, targetPath); } result.created.files.push(`.claude/commands/${cmdName}`); result.summary.commandsCount++; } else { result.skipped.push(`.claude/commands/${cmdName}`); } } } } /** * Copy agents from source */ async function copyAgents(targetDir, options, result) { const agentsConfig = options.agents; const targetAgentsDir = path.join(targetDir, '.claude', 'agents'); // Determine which agents to copy const agentsToCopy = []; if (agentsConfig.all) { Object.values(AGENTS_MAP).forEach(agents => agentsToCopy.push(...agents)); } else { if (agentsConfig.core) agentsToCopy.push(...AGENTS_MAP.core); if (agentsConfig.consensus) agentsToCopy.push(...AGENTS_MAP.consensus); if (agentsConfig.github) agentsToCopy.push(...AGENTS_MAP.github); if (agentsConfig.hiveMind) agentsToCopy.push(...AGENTS_MAP.hiveMind); if (agentsConfig.sparc) agentsToCopy.push(...AGENTS_MAP.sparc); if (agentsConfig.swarm) agentsToCopy.push(...AGENTS_MAP.swarm); if (agentsConfig.browser) agentsToCopy.push(...AGENTS_MAP.browser); // V3-specific agent categories if (agentsConfig.v3) agentsToCopy.push(...(AGENTS_MAP.v3 || [])); if (agentsConfig.optimization) agentsToCopy.push(...(AGENTS_MAP.optimization || [])); if (agentsConfig.testing) agentsToCopy.push(...(AGENTS_MAP.testing || [])); // Dual-mode agents (Claude Code + Codex hybrid) if (agentsConfig.dualMode) agentsToCopy.push(...(AGENTS_MAP.dualMode || [])); } // Find source agents directory const sourceAgentsDir = findSourceDir('agents', options.sourceBaseDir); if (!sourceAgentsDir) { result.errors.push('Could not find source agents directory'); return; } // Copy each agent category for (const agentCategory of [...new Set(agentsToCopy)]) { const sourcePath = path.join(sourceAgentsDir, agentCategory); const targetPath = path.join(targetAgentsDir, agentCategory); if (fs.existsSync(sourcePath)) { if (!fs.existsSync(targetPath) || options.force) { copyDirRecursive(sourcePath, targetPath); // Count agent files (.md only — .yaml agents were migrated to .md) const mdFiles = countFiles(sourcePath, '.md'); result.summary.agentsCount += mdFiles; result.created.files.push(`.claude/agents/${agentCategory}`); } else { result.skipped.push(`.claude/agents/${agentCategory}`); } } } } /** * Find source helpers directory. * Validates that the directory contains hook-handler.cjs to avoid * returning the target directory or an incomplete source. */ function findSourceHelpersDir(sourceBaseDir) { const possiblePaths = []; const SENTINEL_FILE = 'hook-handler.cjs'; // Must exist in valid source // If explicit source base directory is provided, check it first if (sourceBaseDir) { possiblePaths.push(path.join(sourceBaseDir, '.claude', 'helpers')); } // Strategy 1: require.resolve to find package root (most reliable for npx) try { const esmRequire = createRequire(import.meta.url); const pkgJsonPath = esmRequire.resolve('@claude-flow/cli/package.json'); const pkgRoot = path.dirname(pkgJsonPath); possiblePaths.push(path.join(pkgRoot, '.claude', 'helpers')); } catch { // Not installed as a package — skip } // Strategy 2: __dirname-based (dist/src/init -> package root) const packageRoot = path.resolve(__dirname, '..', '..', '..'); const packageHelpers = path.join(packageRoot, '.claude', 'helpers'); possiblePaths.push(packageHelpers); // Strategy 3: Walk up from __dirname looking for package root let currentDir = __dirname; for (let i = 0; i < 10; i++) { const parentDir = path.dirname(currentDir); if (parentDir === currentDir) break; // hit filesystem root const helpersPath = path.join(parentDir, '.claude', 'helpers'); possiblePaths.push(helpersPath); currentDir = parentDir; } // Strategy 4: Check cwd-relative paths (for local dev) const cwdBased = [ path.join(process.cwd(), '.claude', 'helpers'), path.join(process.cwd(), '..', '.claude', 'helpers'), path.join(process.cwd(), '..', '..', '.claude', 'helpers'), ]; possiblePaths.push(...cwdBased); // Return first path that exists AND contains the sentinel file for (const p of possiblePaths) { if (fs.existsSync(p) && fs.existsSync(path.join(p, SENTINEL_FILE))) { return p; } } return null; } /** * Write helper scripts */ async function writeHelpers(targetDir, options, result) { const helpersDir = path.join(targetDir, '.claude', 'helpers'); // Find source helpers directory (works for npm package and local dev) const sourceHelpersDir = findSourceHelpersDir(options.sourceBaseDir); // On Windows: emit a notice before writing helpers — the settings.json // hooks will use node-based commands instead of bash shims (#2132). if (process.platform === 'win32') { console.log('Detected Windows — adding cross-platform hook overrides to .claude/settings.json (#2132)'); } // Try to copy existing helpers from source first if (sourceHelpersDir && fs.existsSync(sourceHelpersDir)) { const helperFiles = fs.readdirSync(sourceHelpersDir); let copiedCount = 0; for (const file of helperFiles) { const sourcePath = path.join(sourceHelpersDir, file); const destPath = path.join(helpersDir, file); // Skip directories and only copy files if (!fs.statSync(sourcePath).isFile()) continue; if (!fs.existsSync(destPath) || options.force) { fs.copyFileSync(sourcePath, destPath); // Make shell scripts and mjs files executable if (file.endsWith('.sh') || file.endsWith('.mjs')) { fs.chmodSync(destPath, '755'); } result.created.files.push(`.claude/helpers/${file}`); copiedCount++; } else { result.skipped.push(`.claude/helpers/${file}`); } } // #2132: Always generate ruflo-hook.cjs regardless of source copy path. // The source helpers dir may not contain this file (it lives in // plugins/ruflo-core/scripts/, not .claude/helpers/), but it must // always be present so Windows users can use the node-based shim. const rufloHookDest = path.join(helpersDir, 'ruflo-hook.cjs'); if (!fs.existsSync(rufloHookDest) || options.force) { fs.writeFileSync(rufloHookDest, generateRufloHookCjs(), 'utf-8'); result.created.files.push('.claude/helpers/ruflo-hook.cjs'); } else { result.skipped.push('.claude/helpers/ruflo-hook.cjs'); } if (copiedCount > 0) { return; // Skip generating if we copied from source } } // Fall back to generating helpers if source not available const helpers = { 'pre-commit': generatePreCommitHook(), 'post-commit': generatePostCommitHook(), 'session.js': generateSessionManager(), 'router.js': generateAgentRouter(), 'memory.js': generateMemoryHelper(), 'hook-handler.cjs': generateHookHandler(), 'intelligence.cjs': generateIntelligenceStub(), 'auto-memory-hook.mjs': generateAutoMemoryHook(), // #2132: cross-platform Node.js port of ruflo-hook.sh — always deployed so // Windows users have a working shim even if the plugin's hooks.json bash // commands are overridden via settings.json. 'ruflo-hook.cjs': generateRufloHookCjs(), }; for (const [name, content] of Object.entries(helpers)) { const filePath = path.join(helpersDir, name); if (!fs.existsSync(filePath) || options.force) { fs.writeFileSync(filePath, content, 'utf-8'); // Make shell scripts executable if (!name.endsWith('.js')) { fs.chmodSync(filePath, '755'); } result.created.files.push(`.claude/helpers/${name}`); } else { result.skipped.push(`.claude/helpers/${name}`); } } } /** * Find source .claude directory for statusline files */ function findSourceClaudeDir(sourceBaseDir) { const possiblePaths = []; // If explicit source base directory is provided, check it first if (sourceBaseDir) { possiblePaths.push(path.join(sourceBaseDir, '.claude')); } // IMPORTANT: Check the package's own .claude directory // Go up 3 levels: dist/src/init -> dist/src -> dist -> root const packageRoot = path.resolve(__dirname, '..', '..', '..'); const packageClaude = path.join(packageRoot, '.claude'); if (fs.existsSync(packageClaude)) { possiblePaths.unshift(packageClaude); // Add to beginning (highest priority) } // From dist/src/init -> go up to project root let currentDir = __dirname; for (let i = 0; i < 10; i++) { const parentDir = path.dirname(currentDir); const claudePath = path.join(parentDir, '.claude'); if (fs.existsSync(claudePath)) { possiblePaths.push(claudePath); } currentDir = parentDir; } for (const p of possiblePaths) { if (fs.existsSync(p)) { return p; } } return null; } /** * Write statusline configuration */ async function writeStatusline(targetDir, options, result) { const claudeDir = path.join(targetDir, '.claude'); const helpersDir = path.join(targetDir, '.claude', 'helpers'); // Find source .claude directory (works for npm package and local dev) const sourceClaudeDir = findSourceClaudeDir(options.sourceBaseDir