fix(codex): ship stable Windows-safe Ruflo integration

This commit is contained in:
ruvnet
2026-07-16 18:48:10 -04:00
parent a0c1ac4b4f
commit d20f1323b1
20 changed files with 517 additions and 192 deletions
@@ -1,7 +1,7 @@
{
"name": "ruflo-core",
"description": "Foundation plugin — registers the ruflo MCP server (300+ tools across memory/agentdb/embeddings/hooks/aidefence/neural/autopilot/browser/agent/swarm), provides 4 generalist agents (coder/researcher/reviewer/witness-curator), 4 first-run skills (discover-plugins/init-project/ruflo-doctor/witness), and a curated plugin-discovery catalog",
"version": "0.2.2",
"version": "0.2.3",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+4 -4
View File
@@ -1,5 +1,5 @@
#!/usr/bin/env bash
# Structural smoke test for ruflo-core v0.2.2 (ADR-0001).
# Structural smoke test for ruflo-core v0.2.3 (ADR-0001).
set -u
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
PASS=0
@@ -8,10 +8,10 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.2.2 with new keywords"
step "1. plugin.json declares 0.2.3 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.2.2" ]]; then
bad "expected 0.2.2, got '$v'"
if [[ "$v" != "0.2.3" ]]; then
bad "expected 0.2.3, got '$v'"
else
miss=""
for k in foundation mcp-server plugin-catalog discovery; do
@@ -1,7 +1,7 @@
{
"name": "ruflo-cost-tracker",
"description": "Token usage tracking, model cost attribution per agent, budget alerts, and optimization recommendations — uses memory_* (namespace-routed) for cost-tracking and cost-patterns; pairs with federation budget circuit breaker (ADR-097)",
"version": "0.26.0",
"version": "0.26.1",
"author": {
"name": "ruvnet",
"url": "https://github.com/ruvnet"
+3 -3
View File
@@ -8,10 +8,10 @@ step() { printf "→ %s ... " "$1"; }
ok() { printf "PASS\n"; PASS=$((PASS+1)); }
bad() { printf "FAIL: %s\n" "$1"; FAIL=$((FAIL+1)); }
step "1. plugin.json declares 0.26.0 with new keywords"
step "1. plugin.json declares 0.26.1 with new keywords"
v=$(grep -E '"version"' "$ROOT/.claude-plugin/plugin.json" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)
if [[ "$v" != "0.26.0" ]]; then
bad "expected 0.26.0, got '$v'"
if [[ "$v" != "0.26.1" ]]; then
bad "expected 0.26.1, got '$v'"
else
miss=""
for k in namespace-routing mcp agentic-flow agent-booster tier1-routing model-routing benchmarking verified telemetry budget projection forecast counterfactual drift-detection trend-alert anomaly-detection outlier-detection health-check composite-gate auto-track stop-hook snapshot-diff pr-regression git-context traceability drill-down per-message; do
+8 -3
View File
@@ -104,9 +104,14 @@ check(/version: "\$\{version\}"/.test(skillGen) && /author: \$\{author\}/.test(s
// ── 8. config.toml generator emits a working `ruflo` MCP server ────────────
section('config.toml generator MCP default:');
const cfgGen = read('v3/@claude-flow/codex/src/generators/config-toml.ts');
check(/name:\s*'ruflo'/.test(cfgGen) && /args:\s*\['-y',\s*'ruflo@latest',\s*'mcp',\s*'start'\]/.test(cfgGen),
`default MCP server is \`ruflo\` with \`mcp start\` subcommand`,
`default MCP server must be \`{ name: 'ruflo', args: ['-y','ruflo@latest','mcp','start'] }\` (was \`claude-flow\` w/o \`mcp start\`)`);
const mcpConfig = read('v3/@claude-flow/codex/src/mcp-config.ts');
check(/getRufloMcpServerConfig/.test(cfgGen)
&& /RUFLO_MCP_SERVER_NAME\s*=\s*'ruflo'/.test(mcpConfig)
&& /RUFLO_MCP_PACKAGE\s*=\s*'ruflo@latest'/.test(mcpConfig)
&& /args:\s*\['\/c',\s*'npx',\s*\.\.\.args\]/.test(mcpConfig)
&& /RUFLO_MCP_STARTUP_TIMEOUT_SEC\s*=\s*120/.test(mcpConfig),
`default MCP server is Windows-safe \`ruflo@latest mcp start\` with 120s startup timeout`,
`default MCP server must use the shared platform-aware Ruflo definition (cmd /c npx on Windows)`);
console.log('\n' + '─'.repeat(48));
if (failures > 0) {
@@ -94,6 +94,16 @@ let posixWindowsPathMissing = false;
for (const file of walkForHooksJson(REPO_ROOT)) {
const text = readFileSync(file, 'utf8');
if (text.charCodeAt(0) === 0xFEFF) {
violations.push({
file: relative(REPO_ROOT, file),
line: 1,
label: 'UTF-8 BOM forbidden (Codex reports line 1 column 1)',
cmd: 'hooks.json starts with bytes EF BB BF',
hint: 'Save hooks.json as UTF-8 without BOM before publishing the plugin.',
});
continue;
}
let json;
try { json = JSON.parse(text); } catch (err) {
violations.push({ file: relative(REPO_ROOT, file), line: 0, label: 'invalid JSON', cmd: err.message, hint: '' });
+11 -11
View File
@@ -65,13 +65,13 @@ Transform OpenAI Codex CLI into a **self-improving AI development system**. Whil
```bash
# Initialize for Codex (recommended)
npx claude-flow@alpha init --codex
npx ruflo@latest init --codex
# Full setup with all 137+ skills
npx claude-flow@alpha init --codex --full
npx ruflo@latest init --codex --full
# Dual mode (both Claude Code and Codex)
npx claude-flow@alpha init --dual
npx ruflo@latest init --dual
```
**That's it!** The MCP server is auto-registered, skills are installed, and your project is ready for self-learning development.
@@ -276,13 +276,13 @@ project/
```bash
# Minimal (fastest init)
npx claude-flow@alpha init --codex --minimal
npx ruflo@latest init --codex --minimal
# Default
npx claude-flow@alpha init --codex
npx ruflo@latest init --codex
# Full (all skills)
npx claude-flow@alpha init --codex --full
npx ruflo@latest init --codex --full
```
### Template Contents
@@ -326,7 +326,7 @@ npx claude-flow@alpha init --codex --full
Run `init --dual` to set up both platforms:
```bash
npx claude-flow@alpha init --dual
npx ruflo@latest init --dual
```
This creates:
@@ -365,7 +365,7 @@ $performance-optimization
|-------|--------|-------------|
| V3 Security Overhaul | `$v3-security-overhaul` | Complete security architecture with CVE remediation |
| V3 Memory Unification | `$v3-memory-unification` | Unify 6+ memory systems into AgentDB with HNSW |
| V3 Integration Deep | `$v3-integration-deep` | Deep agentic-flow@alpha integration (ADR-001) |
| V3 Integration Deep | `$v3-integration-deep` | Deep agentic-flow integration (ADR-001) |
| V3 Performance Optimization | `$v3-performance-optimization` | Achieve 2.49x-7.47x speedup targets |
| V3 Swarm Coordination | `$v3-swarm-coordination` | 15-agent hierarchical mesh coordination |
| V3 DDD Architecture | `$v3-ddd-architecture` | Domain-Driven Design architecture |
@@ -550,7 +550,7 @@ Run Claude Code for interactive development and spawn headless Codex workers for
```bash
# Initialize dual-mode
npx claude-flow@alpha init --dual
npx ruflo@latest init --dual
# Creates both:
# - CLAUDE.md (Claude Code configuration)
@@ -959,7 +959,7 @@ console.log(`Skills generated: ${result.skillsGenerated.length}`);
Instead of migrating, use dual mode to support both:
```bash
npx claude-flow@alpha init --dual
npx ruflo@latest init --dual
```
This keeps both `CLAUDE.md` and `AGENTS.md` in sync.
@@ -1008,7 +1008,7 @@ ls -la .agents/skills/
cat .agents/config.toml | grep skills
# Rebuild skills
npx claude-flow@alpha init --codex --force
npx ruflo@latest init --codex --force
```
### Vector Search Slow
+10 -5
View File
@@ -1,12 +1,12 @@
{
"name": "@claude-flow/codex",
"version": "3.0.0-alpha.9",
"version": "3.0.0",
"description": "Codex CLI integration for Ruflo (claude-flow) - OpenAI Codex platform adapter",
"type": "module",
"main": "./dist/index.js",
"types": "./dist/index.d.ts",
"bin": {
"claude-flow-codex": "./dist/cli.js"
"claude-flow-codex": "dist/cli.js"
},
"exports": {
".": {
@@ -42,7 +42,8 @@
}
},
"files": [
"dist"
"dist",
".agents/skills"
],
"scripts": {
"build": "tsc",
@@ -65,7 +66,7 @@
"license": "MIT",
"repository": {
"type": "git",
"url": "https://github.com/ruvnet/ruflo.git",
"url": "git+https://github.com/ruvnet/ruflo.git",
"directory": "v3/@claude-flow/codex"
},
"homepage": "https://github.com/ruvnet/ruflo#readme",
@@ -76,7 +77,7 @@
"node": ">=18"
},
"peerDependencies": {
"@claude-flow/cli": "^3.0.0-alpha.1"
"@claude-flow/cli": "^3.0.0"
},
"peerDependenciesMeta": {
"@claude-flow/cli": {
@@ -98,5 +99,9 @@
"typescript": "^5.4.0",
"vitest": "^4.1.0",
"eslint": "^8.57.0"
},
"publishConfig": {
"access": "public",
"tag": "latest"
}
}
+2 -2
View File
@@ -187,7 +187,7 @@ function createStatusCommand(): Command {
const { spawn } = await import('child_process');
const proc = spawn('npx', [
'ruflo@alpha', 'memory', 'list',
'ruflo@latest', 'memory', 'list',
'--namespace', options.namespace
], { stdio: 'inherit' });
@@ -288,5 +288,5 @@ function printResults(result: CollaborationResult): void {
}
console.log();
console.log(chalk.gray('View shared memory: npx ruflo@alpha memory list --namespace collaboration'));
console.log(chalk.gray('View shared memory: npx ruflo@latest memory list --namespace collaboration'));
}
@@ -77,7 +77,7 @@ export class DualModeOrchestrator extends EventEmitter {
// Initialize memory database
await this.runCommand(
'npx',
['ruflo@alpha', 'memory', 'init', '--force'],
['ruflo@latest', 'memory', 'init', '--force'],
projectPath
);
@@ -85,7 +85,7 @@ export class DualModeOrchestrator extends EventEmitter {
await this.runCommand(
'npx',
[
'ruflo@alpha', 'memory', 'store',
'ruflo@latest', 'memory', 'store',
'--key', 'task-context',
'--value', taskContext,
'--namespace', sharedNamespace
@@ -219,9 +219,9 @@ Working Directory: ${projectPath}
Shared Memory Namespace: ${sharedNamespace}
COLLABORATION PROTOCOL:
1. Search shared memory for context: npx ruflo@alpha memory search --query "<relevant terms>" --namespace ${sharedNamespace}
1. Search shared memory for context: npx ruflo@latest memory search --query "<relevant terms>" --namespace ${sharedNamespace}
2. Complete your assigned task
3. Store your results: npx ruflo@alpha memory store --key "${config.id}-result" --value "<your summary>" --namespace ${sharedNamespace}
3. Store your results: npx ruflo@latest memory store --key "${config.id}-result" --value "<your summary>" --namespace ${sharedNamespace}
YOUR TASK:
${config.prompt}
@@ -338,7 +338,7 @@ Remember: Other agents depend on your results in shared memory. Be concise and s
try {
const output = await this.runCommand(
'npx',
['ruflo@alpha', 'memory', 'list', '--namespace', sharedNamespace, '--format', 'json'],
['ruflo@latest', 'memory', 'list', '--namespace', sharedNamespace, '--format', 'json'],
projectPath
);
return JSON.parse(output);
@@ -5,6 +5,7 @@
*/
import type { ConfigTomlOptions, McpServerConfig, SkillConfig, ConfigProfile } from '../types.js';
import { getRufloMcpServerConfig, renderMcpServerToml } from '../mcp-config.js';
/**
* Security configuration options
@@ -70,6 +71,7 @@ export async function generateConfigToml(options: ExtendedConfigTomlOptions = {}
security = {},
performance = {},
logging = {},
platform = process.platform,
} = options;
const lines: string[] = [];
@@ -158,18 +160,12 @@ export async function generateConfigToml(options: ExtendedConfigTomlOptions = {}
// Default claude-flow server
const hasRuflo = mcpServers.some(s => s.name === 'ruflo' || s.name === 'claude-flow');
if (!hasRuflo) {
lines.push(...generateMcpServer({
name: 'ruflo',
command: 'npx',
args: ['-y', 'ruflo@latest', 'mcp', 'start'],
enabled: true,
toolTimeout: 120,
}));
lines.push(...renderMcpServerToml(getRufloMcpServerConfig(platform)));
lines.push('');
}
for (const server of mcpServers) {
lines.push(...generateMcpServer(server));
lines.push(...renderMcpServerToml(server));
lines.push('');
}
}
@@ -452,33 +448,6 @@ function escapeTomlString(str: string): string {
/**
* Generate MCP server configuration lines
*/
function generateMcpServer(server: McpServerConfig): string[] {
const lines: string[] = [];
lines.push(`[mcp_servers.${server.name}]`);
lines.push(`command = "${server.command}"`);
if (server.args && server.args.length > 0) {
const argsStr = server.args.map(a => `"${a}"`).join(', ');
lines.push(`args = [${argsStr}]`);
}
lines.push(`enabled = ${server.enabled ?? true}`);
if (server.toolTimeout) {
lines.push(`tool_timeout_sec = ${server.toolTimeout}`);
}
if (server.env && Object.keys(server.env).length > 0) {
lines.push('');
lines.push(`[mcp_servers.${server.name}.env]`);
for (const [key, value] of Object.entries(server.env)) {
lines.push(`${key} = "${value}"`);
}
}
return lines;
}
/**
* Generate skill configuration lines
*/
@@ -524,6 +493,7 @@ export async function generateMinimalConfigToml(options: ConfigTomlOptions = {})
model = 'gpt-5.3-codex',
approvalPolicy = 'on-request',
sandboxMode = 'workspace-write',
platform = process.platform,
} = options;
return `# Claude Flow V3 - Minimal Codex Configuration
@@ -532,17 +502,14 @@ model = "${model}"
approval_policy = "${approvalPolicy}"
sandbox_mode = "${sandboxMode}"
[mcp_servers.ruflo]
command = "npx"
args = ["-y", "ruflo@latest", "mcp", "start"]
enabled = true
${renderMcpServerToml(getRufloMcpServerConfig(platform)).join('\n')}
`;
}
/**
* Generate CI/CD config.toml
*/
export async function generateCIConfigToml(): Promise<string> {
export async function generateCIConfigToml(platform: NodeJS.Platform = process.platform): Promise<string> {
return `# =============================================================================
# Claude Flow V3 - CI/CD Pipeline Configuration
# =============================================================================
@@ -565,11 +532,7 @@ remote_compaction = false
child_agents_md = true
request_rule = false
[mcp_servers.ruflo]
command = "npx"
args = ["-y", "ruflo@latest", "mcp", "start"]
enabled = true
tool_timeout_sec = 300
${renderMcpServerToml(getRufloMcpServerConfig(platform, 300)).join('\n')}
[history]
persistence = "none"
@@ -611,7 +574,7 @@ train_on_edit = false
/**
* Generate enterprise config.toml with full governance
*/
export async function generateEnterpriseConfigToml(): Promise<string> {
export async function generateEnterpriseConfigToml(platform: NodeJS.Platform = process.platform): Promise<string> {
return `# =============================================================================
# Claude Flow V3 - Enterprise Configuration
# =============================================================================
@@ -643,14 +606,10 @@ remote_compaction = true
# MCP Servers
# =============================================================================
[mcp_servers.ruflo]
command = "npx"
args = ["-y", "ruflo@latest", "mcp", "start"]
enabled = true
tool_timeout_sec = 120
[mcp_servers.ruflo.env]
CLAUDE_FLOW_LOG_LEVEL = "info"
${renderMcpServerToml({
...getRufloMcpServerConfig(platform),
env: { CLAUDE_FLOW_LOG_LEVEL: 'info' },
}).join('\n')}
# =============================================================================
# Profiles
@@ -826,7 +785,7 @@ hipaa = false
/**
* Generate development config.toml with permissive settings
*/
export async function generateDevConfigToml(): Promise<string> {
export async function generateDevConfigToml(platform: NodeJS.Platform = process.platform): Promise<string> {
return `# =============================================================================
# Claude Flow V3 - Development Configuration
# =============================================================================
@@ -848,11 +807,7 @@ shell_snapshot = true
request_rule = false
remote_compaction = true
[mcp_servers.ruflo]
command = "npx"
args = ["-y", "ruflo@latest", "mcp", "start"]
enabled = true
tool_timeout_sec = 120
${renderMcpServerToml(getRufloMcpServerConfig(platform)).join('\n')}
[history]
persistence = "save-all"
@@ -907,7 +862,7 @@ enabled = true
/**
* Generate security-focused config.toml
*/
export async function generateSecureConfigToml(): Promise<string> {
export async function generateSecureConfigToml(platform: NodeJS.Platform = process.platform): Promise<string> {
return `# =============================================================================
# Claude Flow V3 - Security-Focused Configuration
# =============================================================================
@@ -929,11 +884,7 @@ shell_snapshot = false
request_rule = true
remote_compaction = false
[mcp_servers.ruflo]
command = "npx"
args = ["-y", "ruflo@latest", "mcp", "start"]
enabled = true
tool_timeout_sec = 60
${renderMcpServerToml(getRufloMcpServerConfig(platform, 60)).join('\n')}
[history]
persistence = "save-all"
+1 -1
View File
@@ -75,7 +75,7 @@ export {
/**
* Package version
*/
export const VERSION = '3.0.0-alpha.9';
export const VERSION = '3.0.0';
/**
* Package metadata
+76 -40
View File
@@ -5,7 +5,9 @@
*/
import fs from 'fs-extra';
import os from 'os';
import path from 'path';
import { fileURLToPath } from 'url';
import type {
CodexInitOptions,
CodexInitResult,
@@ -16,11 +18,20 @@ import { generateAgentsMd } from './generators/agents-md.js';
import { generateSkillMd, generateBuiltInSkill } from './generators/skill-md.js';
import { generateConfigToml } from './generators/config-toml.js';
import { DEFAULT_SKILLS_BY_TEMPLATE, AGENTS_OVERRIDE_TEMPLATE, GITIGNORE_ENTRIES, ALL_AVAILABLE_SKILLS } from './templates/index.js';
import {
getRufloMcpAddCommand,
getCodexCliInvocation,
getRufloMcpServerConfig,
hasExpectedRufloMcpTransport,
hasExpectedRufloMcpTimeout,
upsertMcpServerStartupTimeout,
type CodexMcpRegistration,
} from './mcp-config.js';
/**
* Bundled skills source directory (relative to package)
*/
const BUNDLED_SKILLS_DIR = '../../../../.agents/skills';
const MONOREPO_SKILLS_DIR = '../../../../.agents/skills';
/**
* Main initializer for Codex projects
@@ -43,11 +54,14 @@ export class CodexInitializer {
this.force = options.force ?? false;
this.dual = options.dual ?? false;
// Resolve bundled skills path (relative to this file's location)
this.bundledSkillsPath = path.resolve(
path.dirname(new URL(import.meta.url).pathname),
BUNDLED_SKILLS_DIR
);
// Published packages carry their built-in skills beside dist/. The
// monorepo fallback keeps source checkouts compatible.
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
const packagedSkillsPath = path.resolve(moduleDir, '..', '.agents', 'skills');
const monorepoSkillsPath = path.resolve(moduleDir, MONOREPO_SKILLS_DIR);
this.bundledSkillsPath = await fs.pathExists(packagedSkillsPath)
? packagedSkillsPath
: monorepoSkillsPath;
const filesCreated: string[] = [];
const skillsGenerated: string[] = [];
@@ -61,13 +75,7 @@ export class CodexInitializer {
// Check if already initialized
const alreadyInitialized = await this.isAlreadyInitialized();
if (alreadyInitialized && !this.force) {
return {
success: false,
filesCreated,
skillsGenerated,
warnings: ['Project already initialized. Use --force to overwrite.'],
errors: ['Project already initialized'],
};
warnings.push('Project already initialized - preserving existing project files and repairing Codex MCP registration');
}
if (alreadyInitialized && this.force) {
@@ -317,72 +325,100 @@ export class CodexInitializer {
* Register claude-flow as MCP server with Codex
*/
private async registerMCPServer(): Promise<{ registered: boolean; warning?: string }> {
const manualCommand = getRufloMcpAddCommand(process.platform);
try {
const { execSync } = await import('child_process');
const { execFileSync } = await import('child_process');
// Check if codex CLI is available
let codex: ReturnType<typeof getCodexCliInvocation>;
try {
execSync('which codex', { stdio: 'pipe' });
const output = process.platform === 'win32'
? execFileSync('where.exe', ['codex'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] })
: execFileSync('which', ['codex'], { encoding: 'utf-8', stdio: ['ignore', 'pipe', 'pipe'] });
codex = getCodexCliInvocation(output, process.platform);
} catch {
return {
registered: false,
warning: 'Codex CLI not found. Run: codex mcp add ruflo -- npx ruflo mcp start',
warning: `Codex CLI not found. Run: ${manualCommand}`,
};
}
// Check if already registered. Prefer the structured `--json` output
// (each entry has a `name` field — confirmed current as of the 2026
// `codex mcp` CLI) over a plain substring match against the human
// -readable table, which false-positives on any server whose name or
// command merely contains "ruflo" and breaks silently if the table
// formatting changes.
let existing: CodexMcpRegistration | undefined;
try {
const listJson = execSync('codex mcp list --json 2>&1', { encoding: 'utf-8' });
const listJson = execFileSync(codex.command, [...codex.prefixArgs, 'mcp', 'list', '--json'], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
const parsed = JSON.parse(listJson);
// Confirmed shape (2026 `codex mcp` CLI) is a bare array; tolerate a
// future `{ servers: [...] }` wrapper but otherwise treat an
// unrecognized shape as "unknown" rather than silently concluding
// not-registered — falls through to the safe text-based fallback.
const servers = Array.isArray(parsed) ? parsed : Array.isArray(parsed?.servers) ? parsed.servers : null;
if (!servers) throw new Error('unrecognized `codex mcp list --json` shape');
if (servers.some((s: unknown) => s && typeof s === 'object' && (s as { name?: unknown }).name === 'ruflo')) {
return { registered: true }; // Already registered
}
existing = servers.find((server: unknown): server is CodexMcpRegistration =>
Boolean(server && typeof server === 'object' && (server as CodexMcpRegistration).name === 'ruflo'));
} catch {
// --json unsupported (older codex CLI) or unparsable — fall back to
// the plain-text listing so registration still no-ops idempotently.
// Treat a plain-text match as stale because its transport cannot be validated.
try {
const list = execSync('codex mcp list 2>&1', { encoding: 'utf-8' });
const list = execFileSync(codex.command, [...codex.prefixArgs, 'mcp', 'list'], {
encoding: 'utf-8',
stdio: ['ignore', 'pipe', 'pipe'],
});
if (list.includes('ruflo')) {
return { registered: true };
existing = { name: 'ruflo' };
}
} catch {
// Ignore list errors — fall through to (re-)register below.
// Ignore list errors and attempt registration below.
}
}
// Register the MCP server
if (existing && hasExpectedRufloMcpTransport(existing, process.platform)) {
await this.ensureGlobalMcpStartupTimeout();
return {
registered: true,
...(!hasExpectedRufloMcpTimeout(existing)
? { warning: 'Updated Ruflo MCP startup timeout to 120 seconds' }
: {}),
};
}
try {
execSync('codex mcp add ruflo -- npx ruflo mcp start', {
stdio: 'pipe',
if (existing) {
execFileSync(codex.command, [...codex.prefixArgs, 'mcp', 'remove', 'ruflo'], {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 10000,
});
}
const server = getRufloMcpServerConfig(process.platform);
execFileSync(codex.command, [...codex.prefixArgs, 'mcp', 'add', 'ruflo', '--', server.command, ...(server.args ?? [])], {
stdio: ['ignore', 'pipe', 'pipe'],
timeout: 10000,
});
await this.ensureGlobalMcpStartupTimeout();
return { registered: true };
} catch (err) {
const errorMessage = err instanceof Error ? err.message : String(err);
return {
registered: false,
warning: `Failed to register MCP server: ${errorMessage}. Run manually: codex mcp add ruflo -- npx ruflo mcp start`,
warning: `Failed to register MCP server: ${errorMessage}. Run manually: ${manualCommand}`,
};
}
} catch {
return {
registered: false,
warning: 'Could not register MCP server. Run manually: codex mcp add ruflo -- npx ruflo mcp start',
warning: `Could not register MCP server. Run manually: ${manualCommand}`,
};
}
}
private async ensureGlobalMcpStartupTimeout(): Promise<void> {
const codexHome = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
const configPath = path.join(codexHome, 'config.toml');
const config = await fs.readFile(configPath, 'utf-8');
const updated = upsertMcpServerStartupTimeout(config);
if (updated !== config) {
await fs.writeFile(configPath, updated, 'utf-8');
}
}
/**
* Generate AGENTS.md content
*/
+171
View File
@@ -0,0 +1,171 @@
/**
* Shared Ruflo MCP configuration for Codex generators, migrations, and init.
*/
import type { McpServerConfig } from './types.js';
export const RUFLO_MCP_SERVER_NAME = 'ruflo';
export const RUFLO_MCP_PACKAGE = 'ruflo@latest';
export const RUFLO_MCP_STARTUP_TIMEOUT_SEC = 120;
export interface CodexMcpRegistration {
name?: unknown;
transport?: {
type?: unknown;
command?: unknown;
args?: unknown;
} | null;
startup_timeout_sec?: unknown;
}
export interface CodexCliInvocation {
command: string;
prefixArgs: string[];
}
export function getCodexCliInvocation(
lookupOutput: string,
platform: NodeJS.Platform = process.platform,
commandShell = process.env.ComSpec || 'cmd.exe',
): CodexCliInvocation {
const matches = lookupOutput.split(/\r?\n/).map(value => value.trim()).filter(Boolean);
if (matches.length === 0) {
throw new Error('Codex CLI path not found');
}
if (platform !== 'win32') {
return { command: matches[0]!, prefixArgs: [] };
}
const executable = matches.find(match => /\.exe$/i.test(match));
if (executable) {
return { command: executable, prefixArgs: [] };
}
// npm installs expose extensionless and .cmd shims, neither of which can
// be launched reliably with execFileSync on Windows. Resolve the shim via
// cmd.exe without interpolating any user-controlled arguments.
return { command: commandShell, prefixArgs: ['/d', '/s', '/c', 'codex'] };
}
export function getRufloMcpServerConfig(
platform: NodeJS.Platform = process.platform,
toolTimeout = 120,
): McpServerConfig {
const args = ['-y', RUFLO_MCP_PACKAGE, 'mcp', 'start'];
return platform === 'win32'
? {
name: RUFLO_MCP_SERVER_NAME,
command: 'cmd',
args: ['/c', 'npx', ...args],
enabled: true,
startupTimeout: RUFLO_MCP_STARTUP_TIMEOUT_SEC,
toolTimeout,
}
: {
name: RUFLO_MCP_SERVER_NAME,
command: 'npx',
args,
enabled: true,
startupTimeout: RUFLO_MCP_STARTUP_TIMEOUT_SEC,
toolTimeout,
};
}
export function renderMcpServerToml(server: McpServerConfig): string[] {
const lines = [
`[mcp_servers.${server.name}]`,
`command = ${tomlString(server.command)}`,
];
if (server.args && server.args.length > 0) {
lines.push(`args = [${server.args.map(tomlString).join(', ')}]`);
}
lines.push(`enabled = ${server.enabled ?? true}`);
if (server.startupTimeout !== undefined) {
lines.push(`startup_timeout_sec = ${server.startupTimeout}`);
}
if (server.toolTimeout !== undefined) {
lines.push(`tool_timeout_sec = ${server.toolTimeout}`);
}
if (server.env && Object.keys(server.env).length > 0) {
lines.push('', `[mcp_servers.${server.name}.env]`);
for (const [key, value] of Object.entries(server.env)) {
lines.push(`${key} = ${tomlString(value)}`);
}
}
return lines;
}
export function getRufloMcpAddCommand(platform: NodeJS.Platform = process.platform): string {
const server = getRufloMcpServerConfig(platform);
return ['codex', 'mcp', 'add', RUFLO_MCP_SERVER_NAME, '--', server.command, ...(server.args ?? [])].join(' ');
}
export function hasExpectedRufloMcpTransport(
registration: CodexMcpRegistration,
platform: NodeJS.Platform = process.platform,
): boolean {
const expected = getRufloMcpServerConfig(platform);
const transport = registration.transport;
if (!transport || transport.type !== 'stdio' || transport.command !== expected.command) {
return false;
}
return Array.isArray(transport.args)
&& transport.args.length === expected.args?.length
&& transport.args.every((arg, index) => arg === expected.args?.[index]);
}
export function hasExpectedRufloMcpTimeout(registration: CodexMcpRegistration): boolean {
return typeof registration.startup_timeout_sec === 'number'
&& registration.startup_timeout_sec >= RUFLO_MCP_STARTUP_TIMEOUT_SEC;
}
export function upsertMcpServerStartupTimeout(
config: string,
serverName = RUFLO_MCP_SERVER_NAME,
timeoutSec = RUFLO_MCP_STARTUP_TIMEOUT_SEC,
): string {
const eol = config.includes('\r\n') ? '\r\n' : '\n';
const lines = config.split(/\r?\n/);
const header = `[mcp_servers.${serverName}]`;
const start = lines.findIndex(line => line.trim() === header);
if (start < 0) {
throw new Error(`${header} not found in Codex config`);
}
let end = lines.length;
for (let index = start + 1; index < lines.length; index += 1) {
if (lines[index]?.trim().startsWith('[')) {
end = index;
break;
}
}
const timeoutPattern = /^\s*startup_timeout_sec\s*=/;
for (let index = start + 1; index < end; index += 1) {
if (timeoutPattern.test(lines[index] ?? '')) {
const currentValue = Number((lines[index] ?? '').split('=', 2)[1]?.trim());
if (Number.isFinite(currentValue) && currentValue >= timeoutSec) {
return config;
}
lines[index] = `startup_timeout_sec = ${timeoutSec}`;
return lines.join(eol);
}
}
lines.splice(end, 0, `startup_timeout_sec = ${timeoutSec}`);
return lines.join(eol);
}
function tomlString(value: string): string {
return `"${value.replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
}
+56 -37
View File
@@ -16,6 +16,7 @@ import type {
ApprovalPolicy,
SandboxMode,
} from '../types.js';
import { getRufloMcpServerConfig, renderMcpServerToml } from '../mcp-config.js';
/**
* Parsed CLAUDE.md structure
@@ -62,6 +63,15 @@ export interface CodeBlock {
line: number;
}
function isRufloMcpServer(name: string, args: string[] | undefined): boolean {
if (name === 'ruflo' || name === 'claude-flow' || name === 'claude_flow') {
return true;
}
const commandLine = (args ?? []).join(' ');
return /(?:^|\s)(?:ruflo|claude-flow)(?:@[^\s]+)?\s+mcp\s+start(?:\s|$)/.test(commandLine);
}
/**
* Parsed settings from CLAUDE.md content
*/
@@ -702,7 +712,10 @@ export function generateAgentsMdFromParsed(parsed: ParsedClaudeMd): string {
/**
* Convert settings.json to config.toml format
*/
export function convertSettingsToToml(settings: Record<string, unknown>): string {
export function convertSettingsToToml(
settings: Record<string, unknown>,
platform: NodeJS.Platform = process.platform,
): string {
const lines: string[] = [];
lines.push('# Migrated from settings.json');
lines.push('# Generated by @claude-flow/codex');
@@ -752,34 +765,35 @@ export function convertSettingsToToml(settings: Record<string, unknown>): string
lines.push('');
// MCP servers
let hasRuflo = false;
if (settings.mcpServers && typeof settings.mcpServers === 'object') {
for (const [name, config] of Object.entries(settings.mcpServers as Record<string, unknown>)) {
const mcpConfig = config as { command?: string; args?: string[]; env?: Record<string, string> };
lines.push(`[mcp_servers.${name}]`);
if (mcpConfig.command) {
lines.push(`command = "${mcpConfig.command}"`);
}
if (mcpConfig.args && mcpConfig.args.length > 0) {
const argsStr = mcpConfig.args.map((a) => `"${a}"`).join(', ');
lines.push(`args = [${argsStr}]`);
}
lines.push('enabled = true');
if (mcpConfig.env && Object.keys(mcpConfig.env).length > 0) {
lines.push('');
lines.push(`[mcp_servers.${name}.env]`);
for (const [key, value] of Object.entries(mcpConfig.env)) {
lines.push(`${key} = "${value}"`);
if (isRufloMcpServer(name, mcpConfig.args)) {
if (!hasRuflo) {
lines.push(...renderMcpServerToml({
...getRufloMcpServerConfig(platform),
...(mcpConfig.env ? { env: mcpConfig.env } : {}),
}));
lines.push('');
hasRuflo = true;
}
continue;
}
lines.push(...renderMcpServerToml({
name,
command: mcpConfig.command || 'npx',
enabled: true,
...(mcpConfig.args ? { args: mcpConfig.args } : {}),
...(mcpConfig.env ? { env: mcpConfig.env } : {}),
}));
lines.push('');
}
} else {
// Add default claude-flow server
lines.push('[mcp_servers.ruflo]');
lines.push('command = "npx"');
lines.push('args = ["-y", "ruflo@latest", "mcp", "start"]');
lines.push('enabled = true');
}
if (!hasRuflo) {
lines.push(...renderMcpServerToml(getRufloMcpServerConfig(platform)));
lines.push('');
}
@@ -807,7 +821,10 @@ export function convertSettingsToToml(settings: Record<string, unknown>): string
/**
* Generate config.toml from parsed CLAUDE.md
*/
export function generateConfigTomlFromParsed(parsed: ParsedClaudeMd): string {
export function generateConfigTomlFromParsed(
parsed: ParsedClaudeMd,
platform: NodeJS.Platform = process.platform,
): string {
const lines: string[] = [];
lines.push('# Migrated from CLAUDE.md');
lines.push('# Generated by @claude-flow/codex');
@@ -839,23 +856,25 @@ export function generateConfigTomlFromParsed(parsed: ParsedClaudeMd): string {
lines.push('');
// MCP servers
if (parsed.mcpServers.length > 0) {
for (const server of parsed.mcpServers) {
lines.push(`[mcp_servers.${server.name.replace(/-/g, '_')}]`);
lines.push(`command = "${server.command}"`);
if (server.args && server.args.length > 0) {
const argsStr = server.args.map((a) => `"${a}"`).join(', ');
lines.push(`args = [${argsStr}]`);
let hasRuflo = false;
for (const server of parsed.mcpServers) {
if (isRufloMcpServer(server.name, server.args)) {
if (!hasRuflo) {
lines.push(...renderMcpServerToml(getRufloMcpServerConfig(platform)));
lines.push('');
hasRuflo = true;
}
lines.push(`enabled = ${server.enabled ?? true}`);
} else {
lines.push(...renderMcpServerToml({
...server,
name: server.name.replace(/-/g, '_'),
}));
lines.push('');
}
} else {
// Default claude-flow server
lines.push('[mcp_servers.ruflo]');
lines.push('command = "npx"');
lines.push('args = ["-y", "ruflo@latest", "mcp", "start"]');
lines.push('enabled = true');
}
if (!hasRuflo) {
lines.push(...renderMcpServerToml(getRufloMcpServerConfig(platform)));
lines.push('');
}
+2
View File
@@ -95,6 +95,7 @@ export interface McpServerConfig {
command: string;
args?: string[];
enabled?: boolean;
startupTimeout?: number;
toolTimeout?: number;
env?: Record<string, string>;
}
@@ -111,6 +112,7 @@ export interface SkillConfig {
* Configuration options for config.toml generation
*/
export interface ConfigTomlOptions {
platform?: NodeJS.Platform;
model?: string;
approvalPolicy?: ApprovalPolicy;
sandboxMode?: SandboxMode;
+12 -3
View File
@@ -189,7 +189,7 @@ describe('generateAgentsMd', () => {
const result = await generateAgentsMd(options);
expect(result).toContain('Co-Authored-By: claude-flow');
expect(result).toContain('Co-Authored-By: ruflo-bot');
expect(result).toContain('feat');
expect(result).toContain('fix');
});
@@ -609,7 +609,7 @@ describe('generateBuiltInSkill', () => {
describe('generateConfigToml', () => {
describe('default configuration', () => {
it('should generate valid TOML with header', async () => {
const result = await generateConfigToml();
const result = await generateConfigToml({ platform: 'linux' });
expect(result).toContain('# Claude Flow V3 - Codex Configuration');
expect(result).toContain('# Generated by: @claude-flow/codex');
@@ -646,7 +646,7 @@ describe('generateConfigToml', () => {
});
it('should include default ruflo MCP server', async () => {
const result = await generateConfigToml();
const result = await generateConfigToml({ platform: 'linux' });
expect(result).toContain('[mcp_servers.ruflo]');
expect(result).toContain('command = "npx"');
@@ -925,4 +925,13 @@ describe('generateCIConfigToml', () => {
expect(result).toContain('persistence = "none"');
});
it('should generate a Windows-safe Ruflo MCP command with startup headroom', async () => {
const result = await generateCIConfigToml('win32');
expect(result).toContain('command = "cmd"');
expect(result).toContain('args = ["/c", "npx", "-y", "ruflo@latest", "mcp", "start"]');
expect(result).toContain('startup_timeout_sec = 120');
expect(result).toContain('tool_timeout_sec = 300');
});
});
@@ -0,0 +1,105 @@
import { describe, expect, it } from 'vitest';
import {
getRufloMcpAddCommand,
getCodexCliInvocation,
getRufloMcpServerConfig,
hasExpectedRufloMcpTimeout,
hasExpectedRufloMcpTransport,
renderMcpServerToml,
upsertMcpServerStartupTimeout,
} from '../src/mcp-config.js';
describe('Ruflo Codex MCP configuration', () => {
it('uses cmd /c to resolve npx on Windows', () => {
expect(getRufloMcpServerConfig('win32')).toMatchObject({
command: 'cmd',
args: ['/c', 'npx', '-y', 'ruflo@latest', 'mcp', 'start'],
startupTimeout: 120,
});
expect(getRufloMcpAddCommand('win32')).toBe(
'codex mcp add ruflo -- cmd /c npx -y ruflo@latest mcp start',
);
});
it('uses npx directly on POSIX systems', () => {
expect(getRufloMcpServerConfig('linux')).toMatchObject({
command: 'npx',
args: ['-y', 'ruflo@latest', 'mcp', 'start'],
startupTimeout: 120,
});
});
it('launches npm Codex shims through cmd.exe on Windows', () => {
expect(getCodexCliInvocation(
'C:\\Users\\dev\\AppData\\Roaming\\npm\\codex\r\nC:\\Users\\dev\\AppData\\Roaming\\npm\\codex.cmd\r\n',
'win32',
'C:\\Windows\\System32\\cmd.exe',
)).toEqual({
command: 'C:\\Windows\\System32\\cmd.exe',
prefixArgs: ['/d', '/s', '/c', 'codex'],
});
});
it('prefers a native Codex executable on Windows', () => {
expect(getCodexCliInvocation(
'C:\\Tools\\codex.exe\r\nC:\\Users\\dev\\npm\\codex.cmd\r\n',
'win32',
)).toEqual({ command: 'C:\\Tools\\codex.exe', prefixArgs: [] });
});
it('renders both startup and tool timeouts', () => {
const toml = renderMcpServerToml(getRufloMcpServerConfig('win32', 300)).join('\n');
expect(toml).toContain('command = "cmd"');
expect(toml).toContain('startup_timeout_sec = 120');
expect(toml).toContain('tool_timeout_sec = 300');
});
it('detects stale and current Codex registrations', () => {
const current = {
name: 'ruflo',
transport: {
type: 'stdio',
command: 'cmd',
args: ['/c', 'npx', '-y', 'ruflo@latest', 'mcp', 'start'],
},
startup_timeout_sec: 120,
};
expect(hasExpectedRufloMcpTransport(current, 'win32')).toBe(true);
expect(hasExpectedRufloMcpTimeout(current)).toBe(true);
expect(hasExpectedRufloMcpTransport({
...current,
transport: { type: 'stdio', command: 'npx', args: ['ruflo', 'mcp', 'start'] },
}, 'win32')).toBe(false);
});
it('updates only the Ruflo timeout while preserving the rest of config.toml', () => {
const source = [
'# user comment',
'[mcp_servers.ruflo]',
'command = "cmd"',
'startup_timeout_sec = 30',
'',
'[mcp_servers.other]',
'command = "node"',
'startup_timeout_sec = 45',
'',
].join('\r\n');
const updated = upsertMcpServerStartupTimeout(source);
expect(updated).toContain('# user comment\r\n');
expect(updated).toContain('[mcp_servers.ruflo]\r\ncommand = "cmd"\r\nstartup_timeout_sec = 120');
expect(updated).toContain('[mcp_servers.other]\r\ncommand = "node"\r\nstartup_timeout_sec = 45');
});
it('inserts a missing timeout before the next TOML table', () => {
const source = '[mcp_servers.ruflo]\ncommand = "npx"\n\n[history]\npersistence = "save-all"\n';
expect(upsertMcpServerStartupTimeout(source)).toBe(
'[mcp_servers.ruflo]\ncommand = "npx"\n\nstartup_timeout_sec = 120\n[history]\npersistence = "save-all"\n',
);
});
it('preserves a user timeout that is already above the minimum', () => {
const source = '[mcp_servers.ruflo]\ncommand = "npx"\nstartup_timeout_sec = 300\n';
expect(upsertMcpServerStartupTimeout(source)).toBe(source);
});
});
+18 -7
View File
@@ -422,7 +422,7 @@ describe('convertSettingsToToml', () => {
model: 'claude-3-opus',
};
const result = convertSettingsToToml(settings);
const result = convertSettingsToToml(settings, 'linux');
expect(result).toContain('model = "claude-3-opus"');
});
@@ -434,7 +434,7 @@ describe('convertSettingsToToml', () => {
},
};
const result = convertSettingsToToml(settings);
const result = convertSettingsToToml(settings, 'linux');
expect(result).toContain('approval_policy = "never"');
});
@@ -478,21 +478,32 @@ describe('convertSettingsToToml', () => {
},
};
const result = convertSettingsToToml(settings);
const result = convertSettingsToToml(settings, 'linux');
expect(result).toContain('[mcp_servers.claude-flow]');
expect(result).toContain('command = "npx"');
expect(result).toContain('args = ["-y", "@claude-flow/cli"]');
expect(result).toContain('[mcp_servers.ruflo]');
expect(result).toContain('args = ["-y", "ruflo@latest", "mcp", "start"]');
expect(result).toContain('startup_timeout_sec = 120');
expect(result).toContain('[mcp_servers.custom-server]');
expect(result).toContain('command = "node"');
});
it('adds Ruflo alongside unrelated custom MCP servers', () => {
const result = convertSettingsToToml({
mcpServers: { custom: { command: 'node', args: ['server.js'] } },
}, 'win32');
expect(result).toContain('[mcp_servers.custom]');
expect(result).toContain('[mcp_servers.ruflo]');
expect(result).toContain('command = "cmd"');
expect(result).toContain('args = ["/c", "npx", "-y", "ruflo@latest", "mcp", "start"]');
});
it('should add default ruflo server when no mcpServers', () => {
const settings = {
model: 'gpt-4',
};
const result = convertSettingsToToml(settings);
const result = convertSettingsToToml(settings, 'linux');
// The implementation adds a default ruflo server when none specified
expect(result).toContain('[mcp_servers.ruflo]');
+4 -3
View File
@@ -169,7 +169,7 @@ importers:
'@claude-flow/codex':
dependencies:
'@claude-flow/cli':
specifier: ^3.0.0-alpha.1
specifier: ^3.0.0
version: link:../cli
'@iarna/toml':
specifier: ^2.2.5
@@ -6358,7 +6358,7 @@ packages:
'@vitest/spy': 4.1.8
estree-walker: 3.0.3
magic-string: 0.30.21
vite: 7.3.0(@types/node@20.19.27)(yaml@2.8.2)
vite: 7.3.0(@types/node@20.19.27)(tsx@4.21.0)
dev: true
/@vitest/pretty-format@4.1.8:
@@ -12568,6 +12568,7 @@ packages:
yaml: 2.8.2
optionalDependencies:
fsevents: 2.3.3
dev: false
/vitest@4.1.8(@opentelemetry/api@1.9.0)(@types/node@20.19.27)(@vitest/coverage-v8@4.1.9)(vite@7.3.0):
resolution: {integrity: sha512-flY6ScbCIt9HThs+C5HS7jvGOB560DJtk/Z15IQROTA6zEy49Nh8T/dofWTQL+n3vswqn87sbJNiuqw1SDp5Ig==}
@@ -12699,7 +12700,7 @@ packages:
tinyexec: 1.0.2
tinyglobby: 0.2.15
tinyrainbow: 3.1.0
vite: 7.3.0(@types/node@20.19.27)(yaml@2.8.2)
vite: 7.3.0(@types/node@20.19.27)(tsx@4.21.0)
why-is-node-running: 2.3.0
transitivePeerDependencies:
- msw