Skip to main content
Glama

AI Code Toolkit

by AgiFlow
CodexService.ts13 kB
/** * CodexService * * DESIGN PATTERNS: * - Class-based service pattern for encapsulating business logic * - Interface implementation for dependency injection and testing * - Single Responsibility: Manages Codex CLI interactions * - Method-based API: Public methods expose service capabilities * * CODING STANDARDS: * - Service class names use PascalCase with 'Service' suffix * - Method names use camelCase with descriptive verbs * - Return types should be explicit (never use implicit any) * - Use async/await for asynchronous operations * - Handle errors with try-catch and throw descriptive Error objects * - Document public methods with JSDoc comments * * AVOID: * - Side effects in constructors (keep them lightweight) * - Mixing concerns (keep services focused on single domain) * - Direct coupling to other services (use dependency injection) * - Exposing internal implementation details */ import { execa } from 'execa'; import * as fs from 'fs-extra'; import * as os from 'node:os'; import * as path from 'node:path'; import * as readline from 'node:readline'; import type { CodingAgentService, LlmInvocationParams, LlmInvocationResponse, McpSettings, PromptConfig, } from '../types'; import { appendUniqueToFile, appendUniqueWithMarkers, writeFileEnsureDir } from '../utils/file'; /** * Internal message types for parsing JSONL output from Codex CLI */ interface CodexStreamEvent { type: | 'thread.started' | 'turn.started' | 'turn.completed' | 'item.started' | 'item.updated' | 'item.completed'; data?: { item?: { type?: 'agent_message' | 'reasoning' | 'command_execution'; content?: string; message?: { content?: string; }; }; turn?: { usage?: { input_tokens?: number; output_tokens?: number; }; }; }; } /** * Service for interacting with Codex CLI as a coding agent * Provides standard LLM interface using Codex's exec mode with JSON output */ export class CodexService implements CodingAgentService { private mcpSettings: McpSettings = {}; private promptConfig: PromptConfig = {}; private readonly workspaceRoot: string; private readonly codexPath: string; private readonly defaultTimeout: number; private readonly defaultModel: string; private readonly defaultEnv: Record<string, string>; constructor(options?: { workspaceRoot?: string; codexPath?: string; defaultTimeout?: number; defaultModel?: string; defaultEnv?: Record<string, string>; }) { this.workspaceRoot = options?.workspaceRoot || process.cwd(); this.codexPath = options?.codexPath || 'codex'; this.defaultTimeout = options?.defaultTimeout || 60000; // 1 minute default this.defaultModel = options?.defaultModel || 'gpt-5-codex'; this.defaultEnv = options?.defaultEnv || { CODEX_API_KEY: process.env.CODEX_API_KEY || '', }; } /** * Check if the Codex service is enabled * Detects Codex by checking for .codex file in workspace root (project-level only) */ async isEnabled(): Promise<boolean> { const codexWorkspaceFile = path.join(this.workspaceRoot, '.codex'); return fs.pathExists(codexWorkspaceFile); } /** * Update MCP (Model Context Protocol) settings for Codex * Writes MCP server configuration to ~/.codex/config.toml * Converts standardized McpServerConfig to Codex TOML format */ async updateMcpSettings(settings: McpSettings): Promise<void> { this.mcpSettings = { ...this.mcpSettings, ...settings }; // Codex uses config.toml in ~/.codex directory const configDir = path.join(os.homedir(), '.codex'); const configPath = path.join(configDir, 'config.toml'); // Ensure config directory exists await fs.ensureDir(configDir); // Read existing config or create new let configContent = ''; if (await fs.pathExists(configPath)) { configContent = await fs.readFile(configPath, 'utf-8'); } // Parse TOML (simple approach - append MCP servers section) // For production, consider using a TOML parser library like @iarna/toml if (settings.servers) { // Remove existing [mcp_servers] section if present configContent = configContent.replace(/\[mcp_servers\][\s\S]*?(?=\n\[|\n*$)/, ''); // Build MCP servers TOML section let mcpSection = '\n[mcp_servers]\n'; for (const [serverName, serverConfig] of Object.entries(settings.servers)) { mcpSection += `\n[mcp_servers.${serverName}]\n`; mcpSection += `disabled = ${serverConfig.disabled ?? false}\n`; if (serverConfig.type === 'stdio') { mcpSection += 'type = "stdio"\n'; mcpSection += `command = "${serverConfig.command}"\n`; if (serverConfig.args && serverConfig.args.length > 0) { mcpSection += `args = [${serverConfig.args.map((arg) => `"${arg}"`).join(', ')}]\n`; } if (serverConfig.env) { mcpSection += `[mcp_servers.${serverName}.env]\n`; for (const [key, value] of Object.entries(serverConfig.env)) { mcpSection += `${key} = "${value}"\n`; } } } else if (serverConfig.type === 'http' || serverConfig.type === 'sse') { mcpSection += `type = "${serverConfig.type}"\n`; mcpSection += `url = "${serverConfig.url}"\n`; } } // Append MCP section to config configContent = configContent.trim() + mcpSection; } // Write config back await fs.writeFile(configPath, configContent); } /** * Update prompt configuration for Codex * * If customInstructionFile is provided, writes the prompt to that file and references it * using @file syntax in AGENTS.md (workspace) and instructions.md (global ~/.codex). * * If marker is true, wraps the content with AICODE tracking markers * (<!-- AICODE:START --> and <!-- AICODE:END -->). * * Otherwise, appends the prompt directly to AGENTS.md and instructions.md. */ async updatePrompt(config: PromptConfig): Promise<void> { this.promptConfig = { ...this.promptConfig, ...config }; if (!config.systemPrompt) { return; } // Codex uses AGENTS.md in workspace root (similar to Claude) const agentsMdPath = path.join(this.workspaceRoot, 'AGENTS.md'); // Codex uses instructions.md in ~/.codex directory for global context const codexDir = path.join(os.homedir(), '.codex'); const instructionsMdPath = path.join(codexDir, 'instructions.md'); if (config.customInstructionFile) { // Write prompt to custom instruction file const customFilePath = path.join(this.workspaceRoot, config.customInstructionFile); await writeFileEnsureDir(customFilePath, config.systemPrompt); // Reference the file in AGENTS.md and instructions.md using @ syntax (without curly braces) const reference = `@${config.customInstructionFile}`; if (config.marker) { // Use AICODE markers to track the reference in AGENTS.md await appendUniqueWithMarkers( agentsMdPath, reference, reference, `# Codex Instructions\n\n<!-- AICODE:START -->\n${reference}\n<!-- AICODE:END -->\n`, ); // Append reference to instructions.md (global) await appendUniqueWithMarkers(instructionsMdPath, reference, reference); } else { // Append reference without markers const referenceContent = `\n\n${reference}\n`; await appendUniqueToFile( agentsMdPath, referenceContent, reference, `# Codex Instructions\n${referenceContent}`, ); await appendUniqueToFile(instructionsMdPath, referenceContent, reference); } } else { // Append prompt directly to AGENTS.md and instructions.md if (config.marker) { // Use AICODE markers to track the prompt content await appendUniqueWithMarkers( agentsMdPath, config.systemPrompt, config.systemPrompt, `# Codex Instructions\n\n<!-- AICODE:START -->\n${config.systemPrompt}\n<!-- AICODE:END -->\n`, ); // Append to instructions.md (global) await appendUniqueWithMarkers(instructionsMdPath, config.systemPrompt, config.systemPrompt); } else { // Append prompt without markers const promptContent = `\n\n${config.systemPrompt}\n`; await appendUniqueToFile( agentsMdPath, promptContent, config.systemPrompt, `# Codex Instructions\n${promptContent}`, ); await appendUniqueToFile(instructionsMdPath, promptContent, config.systemPrompt); } } } /** * Invoke Codex as an LLM * Executes Codex CLI with exec mode and JSON output format */ async invokeAsLlm(params: LlmInvocationParams): Promise<LlmInvocationResponse> { // Build the prompt with optional system prompt let fullPrompt = params.prompt; const systemPrompt = this.promptConfig.systemPrompt; if (systemPrompt) { fullPrompt = `${systemPrompt}\n\n${params.prompt}`; } // Build command arguments for non-interactive LLM invocation const args = [ 'exec', '--json', // Enable JSON output '--skip-git-repo-check', // Allow running outside git repos fullPrompt, ]; if (params.model) { args.push('--model', params.model); } // Build environment with API key and custom env vars const env = { ...process.env, ...this.defaultEnv, }; // Execute Codex CLI const child = execa(this.codexPath, args, { stdin: 'ignore', stdout: 'pipe', stderr: 'pipe', timeout: params.maxTokens ? params.maxTokens * 100 : this.defaultTimeout, maxBuffer: 1024 * 1024 * 100, // 100MB buffer env, cwd: this.workspaceRoot, }); // Create readline interface for streaming output const rl = readline.createInterface({ input: child.stdout, }); // Collect response data let responseContent = ''; const model = params.model || this.defaultModel; const usage = { inputTokens: 0, outputTokens: 0, }; let partialData = ''; try { // Process streaming JSONL output for await (const line of rl) { if (!line.trim()) continue; let event: CodexStreamEvent; try { event = JSON.parse(line); } catch { // Handle partial JSON by accumulating partialData += line; try { event = JSON.parse(partialData); partialData = ''; } catch { continue; } } // Process different event types if (event.type === 'item.completed' && event.data?.item) { const item = event.data.item; // Extract text content from agent messages if (item.type === 'agent_message') { const content = item.content || item.message?.content || ''; if (content) { responseContent += content; } } } else if (event.type === 'turn.completed' && event.data?.turn?.usage) { // Extract usage statistics const turnUsage = event.data.turn.usage; usage.inputTokens = turnUsage.input_tokens || 0; usage.outputTokens = turnUsage.output_tokens || 0; } } // Wait for process to complete const { exitCode } = await child; if (exitCode !== 0) { throw new Error(`Codex process exited with code ${exitCode}`); } // Return standard LLM response return { content: responseContent.trim(), model, usage: { inputTokens: usage.inputTokens, outputTokens: usage.outputTokens, }, }; } catch (error) { // Clean up on error rl.close(); if (!child.killed) { child.kill(); } // Provide descriptive error messages based on error type if (error instanceof Error) { if (error.message.includes('ETIMEDOUT') || error.message.includes('timed out')) { throw new Error( `Codex invocation timed out after ${params.maxTokens ? params.maxTokens * 100 : this.defaultTimeout}ms. Consider increasing the timeout or reducing maxTokens.`, ); } if (error.message.includes('ENOENT')) { throw new Error( `Codex CLI not found at path: ${this.codexPath}. Ensure Codex is installed and the path is correct.`, ); } if (error.message.includes('exited with code')) { throw new Error(`Codex process failed: ${error.message}. Check Codex logs for details.`); } throw new Error(`Failed to invoke Codex: ${error.message}`); } throw new Error(`Failed to invoke Codex: ${String(error)}`); } finally { rl.close(); } } }

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/AgiFlow/aicode-toolkit'

If you have feedback or need assistance with the MCP directory API, please join our Discord server