/** * Codex Discovery Provider * * Loads configuration from OpenAI Codex format: * - System Instructions: AGENTS.md (user-level only at ~/.codex/AGENTS.md) * * User directory: ~/.codex */ import * as path from "node:path"; import { logger, parseFrontmatter } from "@oh-my-pi/pi-utils"; import { registerProvider } from "../capability"; import type { ContextFile } from "../capability/context-file"; import { contextFileCapability } from "../capability/context-file"; import { type ExtensionModule, extensionModuleCapability } from "../capability/extension-module"; import { readFile } from "../capability/fs"; import type { Hook } from "../capability/hook"; import { hookCapability } from "../capability/hook"; import type { MCPServer } from "../capability/mcp"; import { mcpCapability } from "../capability/mcp"; import type { Prompt } from "../capability/prompt"; import { promptCapability } from "../capability/prompt"; import type { Settings } from "../capability/settings"; import { settingsCapability } from "../capability/settings"; import type { Skill } from "../capability/skill"; import { skillCapability } from "../capability/skill"; import type { SlashCommand } from "../capability/slash-command"; import { slashCommandCapability } from "../capability/slash-command"; import type { CustomTool } from "../capability/tool"; import { toolCapability } from "../capability/tool"; import type { LoadContext, LoadResult, SourceMeta } from "../capability/types"; import { buildExtensionModuleItems, createSourceMeta, discoverExtensionModulePaths, loadFilesFromDir, SOURCE_PATHS, scanSkillsFromDir, } from "./helpers"; const PROVIDER_ID = "codex"; const DISPLAY_NAME = "OpenAI Codex"; const PRIORITY = 70; function getProjectCodexDir(ctx: LoadContext): string { return path.join(ctx.cwd, ".codex"); } // ============================================================================= // Context Files (AGENTS.md) // ============================================================================= async function loadContextFiles(ctx: LoadContext): Promise> { const items: ContextFile[] = []; const warnings: string[] = []; // User level only: ~/.codex/AGENTS.md const agentsMd = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "AGENTS.md"); const agentsContent = await readFile(agentsMd); if (agentsContent) { items.push({ path: agentsMd, content: agentsContent, level: "user", _source: createSourceMeta(PROVIDER_ID, agentsMd, "user"), }); } return { items, warnings }; } // ============================================================================= // MCP Servers (config.toml) // ============================================================================= async function loadMCPServers(ctx: LoadContext): Promise> { const warnings: string[] = []; const userConfigPath = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "config.toml"); const codexDir = getProjectCodexDir(ctx); const projectConfigPath = path.join(codexDir, "config.toml"); const [userConfig, projectConfig] = await Promise.all([ loadTomlConfig(ctx, userConfigPath), loadTomlConfig(ctx, projectConfigPath), ]); const items: MCPServer[] = []; if (userConfig) { const servers = extractMCPServersFromToml(userConfig); for (const [name, config] of Object.entries(servers)) { items.push({ name, ...config, _source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"), }); } } if (projectConfig) { const servers = extractMCPServersFromToml(projectConfig); for (const [name, config] of Object.entries(servers)) { items.push({ name, ...config, _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"), }); } } return { items, warnings }; } async function loadTomlConfig(_ctx: LoadContext, path: string): Promise | null> { const content = await readFile(path); if (!content) return null; try { return Bun.TOML.parse(content) as Record; } catch (error) { logger.warn("Failed to parse TOML config", { path, error: String(error) }); return null; } } /** Codex MCP server config format (from config.toml) */ interface CodexMCPConfig { command?: string; args?: string[]; env?: Record; env_vars?: string[]; // Environment variable names to forward from parent url?: string; http_headers?: Record; env_http_headers?: Record; // Header name -> env var name bearer_token_env_var?: string; cwd?: string; startup_timeout_sec?: number; tool_timeout_sec?: number; enabled_tools?: string[]; disabled_tools?: string[]; } function extractMCPServersFromToml(toml: Record): Record> { // Check for [mcp_servers.*] sections (Codex format) if (!toml.mcp_servers || typeof toml.mcp_servers !== "object") { return {}; } const codexServers = toml.mcp_servers as Record; const result: Record> = {}; for (const [name, config] of Object.entries(codexServers)) { const server: Partial = { command: config.command, args: config.args, url: config.url, }; // Build env by merging explicit env and forwarded env_vars const env: Record = { ...config.env }; if (config.env_vars) { for (const varName of config.env_vars) { const value = Bun.env[varName]; if (value !== undefined) { env[varName] = value; } } } if (Object.keys(env).length > 0) { server.env = env; } // Build headers from http_headers, env_http_headers, and bearer_token_env_var const headers: Record = { ...config.http_headers }; if (config.env_http_headers) { for (const [headerName, envVarName] of Object.entries(config.env_http_headers)) { const value = Bun.env[envVarName]; if (value !== undefined) { headers[headerName] = value; } } } if (config.bearer_token_env_var) { const token = Bun.env[config.bearer_token_env_var]; if (token) { headers.Authorization = `Bearer ${token}`; } } if (Object.keys(headers).length > 0) { server.headers = headers; } // Determine transport type (infer from config if not explicit) if (config.url) { server.transport = "http"; } else if (config.command) { server.transport = "stdio"; } // Note: validation of transport vs endpoint is handled by mcpCapability.validate() // Map Codex tool_timeout_sec (seconds) to MCPServer timeout (milliseconds) if (typeof config.tool_timeout_sec === "number" && config.tool_timeout_sec > 0) { server.timeout = config.tool_timeout_sec * 1000; } result[name] = server; } return result; } // ============================================================================= // Skills (skills/) // ============================================================================= async function loadSkills(ctx: LoadContext): Promise> { const userSkillsDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "skills"); const codexDir = getProjectCodexDir(ctx); const projectSkillsDir = path.join(codexDir, "skills"); const results = await Promise.all([ scanSkillsFromDir(ctx, { dir: userSkillsDir, providerId: PROVIDER_ID, level: "user", }), scanSkillsFromDir(ctx, { dir: projectSkillsDir, providerId: PROVIDER_ID, level: "project", }), ]); const items = results.flatMap(r => r.items); const warnings = results.flatMap(r => r.warnings || []); return { items, warnings }; } // ============================================================================= // Extension Modules (extensions/) // ============================================================================= async function loadExtensionModules(ctx: LoadContext): Promise> { const warnings: string[] = []; const userExtensionsDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "extensions"); const codexDir = getProjectCodexDir(ctx); const projectExtensionsDir = path.join(codexDir, "extensions"); const [userPaths, projectPaths] = await Promise.all([ discoverExtensionModulePaths(ctx, userExtensionsDir), discoverExtensionModulePaths(ctx, projectExtensionsDir), ]); const items = buildExtensionModuleItems(PROVIDER_ID, userPaths, projectPaths); return { items, warnings }; } // ============================================================================= // Slash Commands (commands/) // ============================================================================= async function loadSlashCommands(ctx: LoadContext): Promise> { const userCommandsDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "commands"); const codexDir = getProjectCodexDir(ctx); const projectCommandsDir = path.join(codexDir, "commands"); const transformCommand = (level: "user" | "project") => (name: string, content: string, path: string, source: SourceMeta) => { const { frontmatter, body } = parseFrontmatter(content, { source: path }); const commandName = frontmatter.name || name.replace(/\.md$/, ""); return { name: String(commandName), path, content: body, level, _source: source, }; }; const results = await Promise.all([ loadFilesFromDir(ctx, userCommandsDir, PROVIDER_ID, "user", { extensions: ["md"], transform: transformCommand("user"), }), loadFilesFromDir(ctx, projectCommandsDir, PROVIDER_ID, "project", { extensions: ["md"], transform: transformCommand("project"), }), ]); const items = results.flatMap(r => r.items); const warnings = results.flatMap(r => r.warnings || []); return { items, warnings }; } // ============================================================================= // Prompts (prompts/*.md) // ============================================================================= async function loadPrompts(ctx: LoadContext): Promise> { const userPromptsDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "prompts"); const codexDir = getProjectCodexDir(ctx); const projectPromptsDir = path.join(codexDir, "prompts"); const transformPrompt = (name: string, content: string, path: string, source: SourceMeta) => { const { frontmatter, body } = parseFrontmatter(content, { source: path }); const promptName = frontmatter.name || name.replace(/\.md$/, ""); return { name: String(promptName), path, content: body, description: frontmatter.description ? String(frontmatter.description) : undefined, _source: source, }; }; const results = await Promise.all([ loadFilesFromDir(ctx, userPromptsDir, PROVIDER_ID, "user", { extensions: ["md"], transform: transformPrompt, }), loadFilesFromDir(ctx, projectPromptsDir, PROVIDER_ID, "project", { extensions: ["md"], transform: transformPrompt, }), ]); const items = results.flatMap(r => r.items); const warnings = results.flatMap(r => r.warnings || []); return { items, warnings }; } // ============================================================================= // Hooks (hooks/) // ============================================================================= async function loadHooks(ctx: LoadContext): Promise> { const userHooksDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "hooks"); const codexDir = getProjectCodexDir(ctx); const projectHooksDir = path.join(codexDir, "hooks"); const transformHook = (level: "user" | "project") => (name: string, _content: string, path: string, source: SourceMeta) => { const baseName = name.replace(/\.(ts|js)$/, ""); const match = baseName.match(/^(pre|post)-(.+)$/); const hookType = (match?.[1] as "pre" | "post") || "pre"; const toolName = match?.[2] || baseName; return { name, path, type: hookType, tool: toolName, level, _source: source, }; }; const results = await Promise.all([ loadFilesFromDir(ctx, userHooksDir, PROVIDER_ID, "user", { extensions: ["ts", "js"], transform: transformHook("user"), }), loadFilesFromDir(ctx, projectHooksDir, PROVIDER_ID, "project", { extensions: ["ts", "js"], transform: transformHook("project"), }), ]); const items = results.flatMap(r => r.items); const warnings = results.flatMap(r => r.warnings || []); return { items, warnings }; } // ============================================================================= // Tools (tools/) // ============================================================================= async function loadTools(ctx: LoadContext): Promise> { const userToolsDir = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "tools"); const codexDir = getProjectCodexDir(ctx); const projectToolsDir = path.join(codexDir, "tools"); const transformTool = (level: "user" | "project") => (name: string, _content: string, path: string, source: SourceMeta) => { const toolName = name.replace(/\.(ts|js)$/, ""); return { name: toolName, path, level, _source: source, } as CustomTool; }; const results = await Promise.all([ loadFilesFromDir(ctx, userToolsDir, PROVIDER_ID, "user", { extensions: ["ts", "js"], transform: transformTool("user"), }), loadFilesFromDir(ctx, projectToolsDir, PROVIDER_ID, "project", { extensions: ["ts", "js"], transform: transformTool("project"), }), ]); const items = results.flatMap(r => r.items); const warnings = results.flatMap(r => r.warnings || []); return { items, warnings }; } // ============================================================================= // Settings (config.toml) // ============================================================================= async function loadSettings(ctx: LoadContext): Promise> { const warnings: string[] = []; const userConfigPath = path.join(ctx.home, SOURCE_PATHS.codex.userBase, "config.toml"); const codexDir = getProjectCodexDir(ctx); const projectConfigPath = path.join(codexDir, "config.toml"); const [userConfig, projectConfig] = await Promise.all([ loadTomlConfig(ctx, userConfigPath), loadTomlConfig(ctx, projectConfigPath), ]); const items: Settings[] = []; if (userConfig) { items.push({ ...userConfig, _source: createSourceMeta(PROVIDER_ID, userConfigPath, "user"), } as Settings); } if (projectConfig) { items.push({ ...projectConfig, _source: createSourceMeta(PROVIDER_ID, projectConfigPath, "project"), } as Settings); } return { items, warnings }; } // ============================================================================= // Provider Registration (executes on module import) // ============================================================================= registerProvider(contextFileCapability.id, { id: PROVIDER_ID, displayName: DISPLAY_NAME, description: "Load context files from ~/.codex/AGENTS.md (user-level only)", priority: PRIORITY, load: loadContextFiles, }); registerProvider(mcpCapability.id, { id: PROVIDER_ID, displayName: DISPLAY_NAME, description: "Load MCP servers from config.toml [mcp_servers.*] sections", priority: PRIORITY, load: loadMCPServers, }); registerProvider(skillCapability.id, { id: PROVIDER_ID, displayName: DISPLAY_NAME, description: "Load skills from ~/.codex/skills and .codex/skills/", priority: PRIORITY, load: loadSkills, }); registerProvider(extensionModuleCapability.id, { id: PROVIDER_ID, displayName: DISPLAY_NAME, description: "Load extension modules from ~/.codex/extensions and .codex/extensions/", priority: PRIORITY, load: loadExtensionModules, }); registerProvider(slashCommandCapability.id, { id: PROVIDER_ID, displayName: DISPLAY_NAME, description: "Load slash commands from ~/.codex/commands and .codex/commands/", priority: PRIORITY, load: loadSlashCommands, }); registerProvider(promptCapability.id, { id: PROVIDER_ID, displayName: DISPLAY_NAME, description: "Load prompts from ~/.codex/prompts and .codex/prompts/", priority: PRIORITY, load: loadPrompts, }); registerProvider(hookCapability.id, { id: PROVIDER_ID, displayName: DISPLAY_NAME, description: "Load hooks from ~/.codex/hooks and .codex/hooks/", priority: PRIORITY, load: loadHooks, }); registerProvider(toolCapability.id, { id: PROVIDER_ID, displayName: DISPLAY_NAME, description: "Load custom tools from ~/.codex/tools and .codex/tools/", priority: PRIORITY, load: loadTools, }); registerProvider(settingsCapability.id, { id: PROVIDER_ID, displayName: DISPLAY_NAME, description: "Load settings from config.toml", priority: PRIORITY, load: loadSettings, });