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

462 lines 17.9 kB
/** * Settings.json Generator * Creates .claude/settings.json with V3-optimized hook configurations */ import { detectPlatform } from './types.js'; /** * Generate the complete settings.json content */ export function generateSettings(options) { const settings = {}; // Add hooks if enabled. CRITICAL (#1744 #3): only emit the hooks block when // the helpers directory will also be bundled. The hook commands point at // .claude/helpers/hook-handler.cjs; if that file isn't created (as in // --minimal where components.helpers=false), every hook fires and silently // fails to find its handler. Either bundle the helpers OR drop the hooks — // the option this fix takes is the latter (minimal stays minimal). if (options.components.settings && options.components.helpers) { settings.hooks = generateHooksConfig(options.hooks); } // Add statusLine configuration if enabled if (options.statusline.enabled) { settings.statusLine = generateStatusLineConfig(options); } // Add permissions settings.permissions = { allow: [ 'Bash(npx @claude-flow*)', 'Bash(npx claude-flow*)', 'Bash(node .claude/*)', 'mcp__claude-flow__:*', ], deny: [ 'Read(./.env)', 'Read(./.env.*)', ], }; // #1670 — RuFlo attribution (Co-Authored-By trailer + PR footer) is now // OPT-IN. Default behavior no longer injects a third-party Co-Authored-By // line into the user's commits — that pattern silently inflated GitHub // contributor graphs and was hard to undo without rewriting history. Pass // `--attribution` (or `attribution: true` in InitOptions) to enable. // // #2078 — when the user DOES opt in, write a no-reply bot email so GitHub // treats this as a tool, not a personal contribution. Personal emails get // added to user repos' contributor graphs even when the trailer is opt-in. // `ruflo-bot@users.noreply.github.com` is GitHub's no-reply convention and // is excluded from contributor graphs / mapped to a tool identity. if (options.attribution === true) { settings.attribution = { commit: 'Co-Authored-By: ruflo-bot <ruflo-bot@users.noreply.github.com>', pr: '🤖 Generated with [RuFlo](https://github.com/ruvnet/ruflo)', }; } // Note: Claude Code expects 'model' to be a string, not an object // Model preferences are stored in claudeFlow settings instead // settings.model = 'claude-sonnet-4-5-20250929'; // Uncomment if you want to set a default model // Add Agent Teams configuration (experimental feature) settings.env = { // Enable Claude Code Agent Teams for multi-agent coordination CLAUDE_CODE_EXPERIMENTAL_AGENT_TEAMS: '1', // Claude Flow specific environment CLAUDE_FLOW_V3_ENABLED: 'true', CLAUDE_FLOW_HOOKS_ENABLED: 'true', }; // Detect platform for platform-aware configuration const platform = detectPlatform(); // Add V3-specific settings settings.claudeFlow = { version: '3.0.0', enabled: true, platform: { os: platform.os, arch: platform.arch, shell: platform.shell, }, modelPreferences: { default: 'claude-opus-4-8', routing: 'claude-haiku-4-5-20251001', }, agentTeams: { enabled: true, teammateMode: 'auto', // 'auto' | 'in-process' | 'tmux' taskListEnabled: true, mailboxEnabled: true, coordination: { autoAssignOnIdle: true, // Auto-assign pending tasks when teammate is idle trainPatternsOnComplete: true, // Train neural patterns when tasks complete notifyLeadOnComplete: true, // Notify team lead when tasks complete sharedMemoryNamespace: 'agent-teams', // Memory namespace for team coordination }, hooks: { teammateIdle: { enabled: true, autoAssign: true, checkTaskList: true, }, taskCompleted: { enabled: true, trainPatterns: true, notifyLead: true, }, }, }, swarm: { topology: options.runtime.topology, maxAgents: options.runtime.maxAgents, }, memory: { backend: options.runtime.memoryBackend, enableHNSW: options.runtime.enableHNSW, learningBridge: { enabled: options.runtime.enableLearningBridge ?? true }, memoryGraph: { enabled: options.runtime.enableMemoryGraph ?? true }, agentScopes: { enabled: options.runtime.enableAgentScopes ?? true }, }, neural: { enabled: options.runtime.enableNeural, }, daemon: { autoStart: false, // Opt-in only — prevents unintended token consumption (#1427, #1330) workers: [ 'map', // Codebase mapping 'audit', // Security auditing (critical priority) 'optimize', // Performance optimization (high priority) ], schedules: { audit: { interval: '4h', priority: 'critical' }, optimize: { interval: '2h', priority: 'high' }, }, }, learning: { enabled: true, autoTrain: true, patterns: ['coordination', 'optimization', 'prediction'], retention: { shortTerm: '24h', longTerm: '30d', }, }, adr: { autoGenerate: true, directory: '/docs/adr', template: 'madr', }, ddd: { trackDomains: true, validateBoundedContexts: true, directory: '/docs/ddd', }, security: { autoScan: true, scanOnEdit: true, cveCheck: true, threatModel: true, }, }; return settings; } /** * Detect if we're on Windows for platform-aware hook commands. */ const IS_WINDOWS = process.platform === 'win32'; /** * Build a hook command that resolves to the right helpers dir on every * install layout. `ruflo init` can land helpers either project-locally * (`<project>/.claude/helpers/…`, when run from a project root) or globally * (`$HOME/.claude/helpers/…`, when settings.json gets merged into the * user-level Claude Code config). The earlier `${CLAUDE_PROJECT_DIR:-.}` * form assumed project-local — so any global-install user hit * `MODULE_NOT_FOUND` on every Bash/Edit/Session hook (#1943). * * The fix is a tiny POSIX `sh` probe: try `$CLAUDE_PROJECT_DIR/.claude/...` * first, fall back to `$HOME/.claude/...` if it's missing. Both modes work, * the global install never crashes, and project-local overrides still take * precedence when present. On Windows, the same probe via `cmd /c` (the %~% * fallback uses `IF EXIST`). */ function hookCmd(script, subcommand) { if (IS_WINDOWS) { // cmd.exe equivalent of the sh probe below. `IF EXIST` checks the // project-local path; falls back to %USERPROFILE% if missing. return `cmd /c "IF EXIST \"%CLAUDE_PROJECT_DIR%\\${script.replace(/\//g, '\\')}\" (node \"%CLAUDE_PROJECT_DIR%\\${script.replace(/\//g, '\\')}\" ${subcommand}) ELSE (node \"%USERPROFILE%\\${script.replace(/\//g, '\\')}\" ${subcommand})"`; } // POSIX sh: prefer project-local helpers, fall back to $HOME/.claude/. // The fallback handles `ruflo init`'s global-install path where helpers // live at `$HOME/.claude/helpers/` but Claude Code still sets // `CLAUDE_PROJECT_DIR` to the *project* root (which has no helpers). // eslint-disable-next-line no-template-curly-in-string const projVar = '${CLAUDE_PROJECT_DIR:-.}'; // eslint-disable-next-line no-template-curly-in-string const homeVar = '${HOME}'; return `sh -c 'D="${projVar}"; [ -f "$D/${script}" ] || D="${homeVar}"; exec node "$D/${script}" ${subcommand}'`; } /** Shorthand for CJS hook-handler commands */ function hookHandlerCmd(subcommand) { return hookCmd('.claude/helpers/hook-handler.cjs', subcommand); } /** Shorthand for ESM auto-memory-hook commands */ function autoMemoryCmd(subcommand) { return hookCmd('.claude/helpers/auto-memory-hook.mjs', subcommand); } /** * Generate statusLine configuration for Claude Code * Uses local helper script for cross-platform compatibility (no npx cold-start) */ function generateStatusLineConfig(_options) { // Claude Code pipes JSON session data to the script via stdin. // Valid fields: type, command, padding (optional). // The script runs after each assistant message (debounced 300ms). // // ruflo#1948 + #1973: the previous `sh -c 'D="${CLAUDE_PROJECT_DIR:-.}"; …'` // form requires a POSIX shell on PATH. On native Windows (no // Git-Bash / WSL), `sh` either isn't found or its quoting gets // mangled, producing weird artifacts like files named `0)` or // `toastr.error('ESD...` from misparsed tokens leaking back into // the file system. NEVER use `cmd /c` for statusline — Claude Code // manages stdin directly for statusline commands and `cmd /c` // blocks the stdin forwarding. // // Solution: emit a platform-appropriate command at init time. // POSIX: `sh -c 'D="…"; … exec node "$D/<script>"'` (existing) // Windows: a Node.js one-liner that resolves the path internally // using `process.env.CLAUDE_PROJECT_DIR` with a HOME // fallback — no shell-quoting hazards because the // resolution happens inside node, not in the shell. const script = '.claude/helpers/statusline.cjs'; if (process.platform === 'win32') { // The Node CLI's `-e` flag avoids all shell-quoting pitfalls. // We write the path resolution in JS: // const fs = require('fs'); const p = require('path'); // const d = process.env.CLAUDE_PROJECT_DIR || '.'; // const f = p.join(d, '.claude/helpers/statusline.cjs'); // const home = process.env.USERPROFILE || process.env.HOME || '.'; // const h = p.join(home, '.claude/helpers/statusline.cjs'); // require(fs.existsSync(f) ? f : h); // …compressed onto one line. Double-quotes around the -e arg are // safe on cmd.exe; the inner JS uses single-quotes for strings. const js = "const fs=require('fs'),p=require('path');" + `const d=process.env.CLAUDE_PROJECT_DIR||'.';` + `const f=p.join(d,'${script}');` + `const h=p.join(process.env.USERPROFILE||process.env.HOME||'.', '${script}');` + 'require(fs.existsSync(f)?f:h);'; return { type: 'command', command: `node -e "${js}"`, }; } // Same project-local / $HOME fallback as `hookCmd()` (see #1943). // eslint-disable-next-line no-template-curly-in-string const projVar = '${CLAUDE_PROJECT_DIR:-.}'; // eslint-disable-next-line no-template-curly-in-string const homeVar = '${HOME}'; return { type: 'command', command: `sh -c 'D="${projVar}"; [ -f "$D/${script}" ] || D="${homeVar}"; exec node "$D/${script}"'`, }; } /** * Generate hooks configuration * Uses local hook-handler.cjs for cross-platform compatibility. * All hooks invoke scripts directly via `node <script> <subcommand>`, * working identically on Windows, macOS, and Linux. */ function generateHooksConfig(config) { const hooks = {}; // Node.js scripts handle errors internally via try/catch. // No shell-level error suppression needed (2>/dev/null || true breaks Windows). // PreToolUse — validate commands and edits before execution if (config.preToolUse) { hooks.PreToolUse = [ { matcher: 'Bash', hooks: [ { type: 'command', command: hookHandlerCmd('pre-bash'), timeout: config.timeout, }, ], }, { matcher: 'Write|Edit|MultiEdit', hooks: [ { type: 'command', command: hookHandlerCmd('pre-edit'), timeout: config.timeout, }, ], }, ]; } // PostToolUse — record edits and commands for session metrics / learning if (config.postToolUse) { hooks.PostToolUse = [ { matcher: 'Write|Edit|MultiEdit', hooks: [ { type: 'command', command: hookHandlerCmd('post-edit'), timeout: 10000, }, ], }, { matcher: 'Bash', hooks: [ { type: 'command', command: hookHandlerCmd('post-bash'), timeout: config.timeout, }, ], }, ]; } // UserPromptSubmit — intelligent task routing if (config.userPromptSubmit) { hooks.UserPromptSubmit = [ { hooks: [ { type: 'command', command: hookHandlerCmd('route'), timeout: 10000, }, ], }, ]; } // SessionStart — restore session state + import auto memory if (config.sessionStart) { hooks.SessionStart = [ { hooks: [ { type: 'command', command: hookHandlerCmd('session-restore'), timeout: 15000, }, { type: 'command', command: autoMemoryCmd('import'), timeout: 8000, }, ], }, ]; } // SessionEnd — persist session state if (config.sessionStart) { hooks.SessionEnd = [ { hooks: [ { type: 'command', command: hookHandlerCmd('session-end'), timeout: 10000, }, ], }, ]; } // Stop — sync auto memory on exit if (config.stop) { hooks.Stop = [ { hooks: [ { type: 'command', command: autoMemoryCmd('sync'), timeout: 10000, }, ], }, ]; } // PreCompact — preserve context before compaction if (config.preCompact) { hooks.PreCompact = [ { matcher: 'manual', hooks: [ { type: 'command', command: hookHandlerCmd('compact-manual'), }, { type: 'command', command: hookHandlerCmd('session-end'), timeout: 5000, }, ], }, { matcher: 'auto', hooks: [ { type: 'command', command: hookHandlerCmd('compact-auto'), }, { type: 'command', command: hookHandlerCmd('session-end'), timeout: 6000, }, ], }, ]; } // SubagentStart — status update when a sub-agent is spawned hooks.SubagentStart = [ { hooks: [ { type: 'command', command: hookHandlerCmd('status'), timeout: 3000, }, ], }, ]; // SubagentStop — track agent completion for metrics // NOTE: The valid event is "SubagentStop" (not "SubagentEnd") hooks.SubagentStop = [ { hooks: [ { type: 'command', command: hookHandlerCmd('post-task'), timeout: 5000, }, ], }, ]; // Notification — capture Claude Code notifications for logging if (config.notification) { hooks.Notification = [ { hooks: [ { type: 'command', command: hookHandlerCmd('notify'), timeout: 3000, }, ], }, ]; } // NOTE: TeammateIdle, TaskCompleted, and PostCompact are NOT accepted by // Claude Code's settings.json validator (rejected as "Invalid key in record"). // Agent Teams coordination lives in claudeFlow.agentTeams.hooks instead. return hooks; } /** * Generate settings.json as formatted string */ export function generateSettingsJson(options) { const settings = generateSettings(options); return JSON.stringify(settings, null, 2); } //# sourceMappingURL=settings-generator.js.map