mirror of
https://github.com/ruvnet/ruflo.git
synced 2026-09-14 14:01:28 +08:00
fix(codex): ship stable Windows-safe Ruflo integration
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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: '' });
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -75,7 +75,7 @@ export {
|
||||
/**
|
||||
* Package version
|
||||
*/
|
||||
export const VERSION = '3.0.0-alpha.9';
|
||||
export const VERSION = '3.0.0';
|
||||
|
||||
/**
|
||||
* Package metadata
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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, '\\"')}"`;
|
||||
}
|
||||
@@ -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('');
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
@@ -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]');
|
||||
|
||||
Generated
+4
-3
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user