/** * @claude-flow/codex - CodexInitializer * * Main initialization class for setting up Codex projects */ import fs from 'fs-extra'; import path from 'path'; 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 } from './templates/index.js'; /** * Bundled skills source directory (relative to package) */ const BUNDLED_SKILLS_DIR = '../../../../.agents/skills'; /** * Main initializer for Codex projects */ export class CodexInitializer { projectPath = ''; template = 'default'; skills = []; force = false; dual = false; bundledSkillsPath = ''; /** * Initialize a new Codex project */ async initialize(options) { this.projectPath = path.resolve(options.projectPath); this.template = options.template ?? 'default'; this.skills = options.skills ?? DEFAULT_SKILLS_BY_TEMPLATE[this.template]; 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); const filesCreated = []; const skillsGenerated = []; const warnings = []; const errors = []; try { // Validate project path await this.validateProjectPath(); // 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'], }; } if (alreadyInitialized && this.force) { warnings.push('Overwriting existing configuration files'); } // Create directory structure await this.createDirectoryStructure(); // Generate AGENTS.md const agentsMd = await this.generateAgentsMd(); const agentsMdPath = path.join(this.projectPath, 'AGENTS.md'); if (await this.shouldWriteFile(agentsMdPath)) { await fs.writeFile(agentsMdPath, agentsMd, 'utf-8'); filesCreated.push('AGENTS.md'); } else { warnings.push('AGENTS.md already exists - skipped'); } // Generate config.toml const configToml = await this.generateConfigToml(); const configTomlPath = path.join(this.projectPath, '.agents', 'config.toml'); if (await this.shouldWriteFile(configTomlPath)) { await fs.writeFile(configTomlPath, configToml, 'utf-8'); filesCreated.push('.agents/config.toml'); } else { warnings.push('.agents/config.toml already exists - skipped'); } // Copy bundled skills first (for full/enterprise templates or specific skills) const bundledResult = await this.copyBundledSkills(); skillsGenerated.push(...bundledResult.copied); warnings.push(...bundledResult.warnings); // For skills not bundled, generate from templates for (const skillName of this.skills) { // Skip if already copied as bundled skill if (bundledResult.copied.includes(skillName)) { filesCreated.push(`.agents/skills/${skillName}/SKILL.md`); continue; } try { const skillResult = await this.generateSkill(skillName); if (skillResult.created) { skillsGenerated.push(skillName); filesCreated.push(skillResult.path); } else if (skillResult.skipped) { // Only warn if not already in bundled warnings if (!bundledResult.warnings.some(w => w.includes(skillName))) { warnings.push(`Skill ${skillName} already exists - skipped`); } } } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); warnings.push(`Failed to generate skill ${skillName}: ${errorMessage}`); } } // Generate local overrides template const overridePath = path.join(this.projectPath, '.codex', 'AGENTS.override.md'); if (await this.shouldWriteFile(overridePath)) { await fs.writeFile(overridePath, AGENTS_OVERRIDE_TEMPLATE, 'utf-8'); filesCreated.push('.codex/AGENTS.override.md'); } // Generate local config.toml const localConfigPath = path.join(this.projectPath, '.codex', 'config.toml'); if (await this.shouldWriteFile(localConfigPath)) { await fs.writeFile(localConfigPath, await this.generateLocalConfigToml(), 'utf-8'); filesCreated.push('.codex/config.toml'); } // Update .gitignore const gitignoreUpdated = await this.updateGitignore(); if (gitignoreUpdated) { filesCreated.push('.gitignore (updated)'); } // Register MCP server with Codex const mcpResult = await this.registerMCPServer(); if (mcpResult.registered) { filesCreated.push('MCP server (claude-flow) registered'); } if (mcpResult.warning) { warnings.push(mcpResult.warning); } // If dual mode, also generate Claude Code files if (this.dual) { const dualResult = await this.generateDualPlatformFiles(); filesCreated.push(...dualResult.files); if (dualResult.warnings) { warnings.push(...dualResult.warnings); } } // Create a README for the .agents directory const agentsReadmePath = path.join(this.projectPath, '.agents', 'README.md'); if (await this.shouldWriteFile(agentsReadmePath)) { await fs.writeFile(agentsReadmePath, this.generateAgentsReadme(), 'utf-8'); filesCreated.push('.agents/README.md'); } const result = { success: true, filesCreated, skillsGenerated, }; if (warnings.length > 0) { result.warnings = warnings; } return result; } catch (error) { const errorMessage = error instanceof Error ? error.message : 'Unknown error occurred'; errors.push(errorMessage); const result = { success: false, filesCreated, skillsGenerated, errors, }; if (warnings.length > 0) { result.warnings = warnings; } return result; } } /** * Validate that the project path is valid and writable */ async validateProjectPath() { try { await fs.ensureDir(this.projectPath); // Check write permissions by attempting to create a temp file const tempFile = path.join(this.projectPath, '.codex-init-test'); await fs.writeFile(tempFile, 'test', 'utf-8'); await fs.remove(tempFile); } catch (error) { throw new Error(`Cannot write to project path: ${this.projectPath}`); } } /** * Check if project is already initialized */ async isAlreadyInitialized() { const agentsMdExists = await fs.pathExists(path.join(this.projectPath, 'AGENTS.md')); const agentsConfigExists = await fs.pathExists(path.join(this.projectPath, '.agents', 'config.toml')); return agentsMdExists || agentsConfigExists; } /** * Check if we should write a file (force mode or doesn't exist) */ async shouldWriteFile(filePath) { if (this.force) { return true; } return !(await fs.pathExists(filePath)); } /** * Create the directory structure */ async createDirectoryStructure() { const dirs = [ '.agents', '.agents/skills', '.codex', '.claude-flow', '.claude-flow/data', '.claude-flow/logs', ]; for (const dir of dirs) { const fullPath = path.join(this.projectPath, dir); await fs.ensureDir(fullPath); } } /** * Copy bundled skills from the package or source directory * Returns the list of skills copied */ async copyBundledSkills() { const copied = []; const warnings = []; // Check if bundled skills directory exists if (!await fs.pathExists(this.bundledSkillsPath)) { warnings.push(`Bundled skills directory not found: ${this.bundledSkillsPath}`); return { copied, warnings }; } const destSkillsDir = path.join(this.projectPath, '.agents', 'skills'); // Get all skill directories const skillDirs = await fs.readdir(this.bundledSkillsPath, { withFileTypes: true }); for (const dirent of skillDirs) { if (!dirent.isDirectory()) continue; const skillName = dirent.name; const srcPath = path.join(this.bundledSkillsPath, skillName); const destPath = path.join(destSkillsDir, skillName); // Skip if skill should be filtered (based on template) // For 'full' and 'enterprise' templates, include all skills const includeAll = this.template === 'full' || this.template === 'enterprise'; if (!includeAll && !this.skills.includes(skillName)) { continue; } try { // Check if skill already exists and we're not forcing if (!this.force && await fs.pathExists(destPath)) { warnings.push(`Skill ${skillName} already exists - skipped`); continue; } // Copy the entire skill directory await fs.copy(srcPath, destPath, { overwrite: this.force }); copied.push(skillName); } catch (err) { const errorMessage = err instanceof Error ? err.message : String(err); warnings.push(`Failed to copy skill ${skillName}: ${errorMessage}`); } } return { copied, warnings }; } /** * Check if a skill is bundled (exists in source directory) */ async isBundledSkill(skillName) { const skillPath = path.join(this.bundledSkillsPath, skillName); return fs.pathExists(skillPath); } /** * Register claude-flow as MCP server with Codex */ async registerMCPServer() { try { const { execSync } = await import('child_process'); // Check if codex CLI is available try { execSync('which codex', { stdio: 'pipe' }); } catch { return { registered: false, warning: 'Codex CLI not found. Run: codex mcp add claude-flow -- npx claude-flow mcp start', }; } // Check if already registered try { const list = execSync('codex mcp list 2>&1', { encoding: 'utf-8' }); if (list.includes('claude-flow')) { return { registered: true }; // Already registered } } catch { // Ignore list errors } // Register the MCP server try { execSync('codex mcp add claude-flow -- npx claude-flow mcp start', { stdio: 'pipe', timeout: 10000, }); 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 claude-flow -- npx claude-flow mcp start`, }; } } catch { return { registered: false, warning: 'Could not register MCP server. Run manually: codex mcp add claude-flow -- npx claude-flow mcp start', }; } } /** * Generate AGENTS.md content */ async generateAgentsMd() { const projectName = path.basename(this.projectPath); return generateAgentsMd({ projectName, template: this.template, skills: this.skills, }); } /** * Generate config.toml content */ async generateConfigToml() { return generateConfigToml({ skills: this.skills.map(skill => ({ path: `.agents/skills/${skill}`, enabled: true, })), }); } /** * Generate local config.toml for .codex directory */ async generateLocalConfigToml() { return `# Local Codex Configuration # This file overrides .agents/config.toml for local development # DO NOT commit this file to version control # Development profile - more permissive approval_policy = "never" sandbox_mode = "danger-full-access" web_search = "live" # Debug settings # Uncomment to enable debug logging # CODEX_LOG_LEVEL = "debug" # Local MCP server overrides # [mcp_servers.local] # command = "node" # args = ["./local-mcp-server.js"] # enabled = true # Environment-specific settings # [env] # ANTHROPIC_API_KEY = "your-local-key" `; } /** * Generate a skill */ async generateSkill(skillName) { const skillDir = path.join(this.projectPath, '.agents', 'skills', skillName); const skillPath = path.join(skillDir, 'SKILL.md'); // Check if skill already exists if (!this.force && await fs.pathExists(skillPath)) { return { created: false, skipped: true, path: `.agents/skills/${skillName}/SKILL.md` }; } await fs.ensureDir(skillDir); // Check if it's a built-in skill const builtInSkills = [ 'swarm-orchestration', 'memory-management', 'sparc-methodology', 'security-audit', 'performance-analysis', 'github-automation', ]; let skillMd; if (builtInSkills.includes(skillName)) { const result = await generateBuiltInSkill(skillName); skillMd = result.skillMd; // Also write any associated scripts or references if (Object.keys(result.scripts).length > 0) { const scriptsDir = path.join(skillDir, 'scripts'); await fs.ensureDir(scriptsDir); for (const [scriptName, scriptContent] of Object.entries(result.scripts)) { await fs.writeFile(path.join(scriptsDir, scriptName), scriptContent, 'utf-8'); } } if (Object.keys(result.references).length > 0) { const refsDir = path.join(skillDir, 'docs'); await fs.ensureDir(refsDir); for (const [refName, refContent] of Object.entries(result.references)) { await fs.writeFile(path.join(refsDir, refName), refContent, 'utf-8'); } } } else { // Generate a custom skill template skillMd = await generateSkillMd({ name: skillName, description: `Custom skill: ${skillName}`, triggers: ['Define when to trigger this skill'], skipWhen: ['Define when to skip this skill'], }); } await fs.writeFile(skillPath, skillMd, 'utf-8'); return { created: true, skipped: false, path: `.agents/skills/${skillName}/SKILL.md` }; } /** * Update .gitignore with Codex entries */ async updateGitignore() { const gitignorePath = path.join(this.projectPath, '.gitignore'); let content = ''; if (await fs.pathExists(gitignorePath)) { content = await fs.readFile(gitignorePath, 'utf-8'); } // Check if Codex entries already exist if (content.includes('.codex/')) { return false; // Already has entries } // Add entries with proper spacing const separator = content.length > 0 && !content.endsWith('\n') ? '\n\n' : '\n'; const newContent = content + separator + GITIGNORE_ENTRIES.join('\n') + '\n'; await fs.writeFile(gitignorePath, newContent, 'utf-8'); return true; } /** * Generate README for .agents directory */ generateAgentsReadme() { return `# .agents Directory This directory contains agent configuration and skills for OpenAI Codex CLI. ## Structure \`\`\` .agents/ config.toml # Main configuration file skills/ # Skill definitions skill-name/ SKILL.md # Skill instructions scripts/ # Optional scripts docs/ # Optional documentation README.md # This file \`\`\` ## Configuration The \`config.toml\` file controls: - Model selection - Approval policies - Sandbox modes - MCP server connections - Skills configuration ## Skills Skills are invoked using \`$skill-name\` syntax. Each skill has: - YAML frontmatter with metadata - Trigger and skip conditions - Commands and examples ## Documentation - Main instructions: \`AGENTS.md\` (project root) - Local overrides: \`.codex/AGENTS.override.md\` (gitignored) - Claude Flow: https://github.com/ruvnet/claude-flow `; } /** * Generate dual-platform files (Claude Code + Codex) */ async generateDualPlatformFiles() { const files = []; const warnings = []; // Check if CLAUDE.md already exists const claudeMdPath = path.join(this.projectPath, 'CLAUDE.md'); const claudeMdExists = await fs.pathExists(claudeMdPath); if (claudeMdExists && !this.force) { warnings.push('CLAUDE.md already exists - not overwriting. Use --force to replace.'); return { files, warnings }; } const projectName = path.basename(this.projectPath); // Generate a CLAUDE.md that references AGENTS.md const claudeMd = `# ${projectName} > This project supports both Claude Code and OpenAI Codex. ## Platform Compatibility | Platform | Config File | Skill Syntax | |----------|-------------|--------------| | Claude Code | CLAUDE.md | /skill-name | | OpenAI Codex | AGENTS.md | $skill-name | ## Instructions **Primary instructions are in \`AGENTS.md\`** (Agentic AI Foundation standard). This file provides compatibility for Claude Code users. ## Quick Start \`\`\`bash # Install dependencies npm install # Build the project npm run build # Run tests npm test \`\`\` ## Available Skills Both platforms share the same skills in \`.agents/skills/\`: ${this.skills.map(s => `- \`$${s}\` (Codex) / \`/${s}\` (Claude Code)`).join('\n')} ## Configuration ### Codex Configuration - Main: \`.agents/config.toml\` - Local: \`.codex/config.toml\` (gitignored) ### Claude Code Configuration - This file: \`CLAUDE.md\` - Local: \`CLAUDE.local.md\` (gitignored) ## MCP Integration \`\`\`bash # Start MCP server npx @claude-flow/cli mcp start \`\`\` ## Swarm Orchestration This project uses hierarchical swarm coordination: | Setting | Value | |---------|-------| | Topology | hierarchical | | Max Agents | 8 | | Strategy | specialized | ## Code Standards - Files under 500 lines - No hardcoded secrets - Input validation at boundaries - Typed interfaces for APIs ## Security - NEVER commit .env files or secrets - Always validate user input - Use parameterized queries for SQL ## Full Documentation For complete instructions, see \`AGENTS.md\`. --- *Generated by @claude-flow/codex - Dual platform mode* `; await fs.writeFile(claudeMdPath, claudeMd, 'utf-8'); files.push('CLAUDE.md'); // Generate CLAUDE.local.md template const claudeLocalPath = path.join(this.projectPath, 'CLAUDE.local.md'); if (await this.shouldWriteFile(claudeLocalPath)) { const claudeLocal = `# Local Development Configuration ## Environment \`\`\`bash # Development settings CLAUDE_FLOW_LOG_LEVEL=debug \`\`\` ## Personal Preferences [Add your preferences here] ## Debug Settings Enable verbose logging for development. --- *This file is gitignored and contains local-only settings.* `; await fs.writeFile(claudeLocalPath, claudeLocal, 'utf-8'); files.push('CLAUDE.local.md'); } // Update .gitignore for CLAUDE.local.md const gitignorePath = path.join(this.projectPath, '.gitignore'); if (await fs.pathExists(gitignorePath)) { let content = await fs.readFile(gitignorePath, 'utf-8'); if (!content.includes('CLAUDE.local.md')) { content += '\n# Claude Code local config\nCLAUDE.local.md\n'; await fs.writeFile(gitignorePath, content, 'utf-8'); } } warnings.push('Generated dual-platform setup. AGENTS.md is the canonical source.'); return { files, warnings }; } /** * Get the list of files that would be created (dry-run) */ async dryRun(options) { const files = [ 'AGENTS.md', '.agents/config.toml', '.agents/README.md', '.codex/AGENTS.override.md', '.codex/config.toml', '.gitignore (updated)', ]; const skills = options.skills ?? DEFAULT_SKILLS_BY_TEMPLATE[options.template ?? 'default']; for (const skill of skills) { files.push(`.agents/skills/${skill}/SKILL.md`); } if (options.dual) { files.push('CLAUDE.md'); files.push('CLAUDE.local.md'); } return files; } } /** * Quick initialization function for programmatic use */ export async function initializeCodexProject(projectPath, options) { const initializer = new CodexInitializer(); const initOptions = { projectPath, template: options?.template ?? 'default', force: options?.force ?? false, dual: options?.dual ?? false, }; if (options?.skills) { initOptions.skills = options.skills; } return initializer.initialize(initOptions); } //# sourceMappingURL=initializer.js.map