feat: remove codex support

This commit is contained in:
Kacper
2025-12-13 20:13:53 +01:00
parent 83fbf55781
commit 37f45ee89b
32 changed files with 925 additions and 7065 deletions

View File

@@ -1,408 +0,0 @@
/**
* Codex CLI Detector - Checks if OpenAI Codex CLI is installed
*
* Codex CLI is OpenAI's agent CLI tool that allows users to use
* GPT-5.1/5.2 Codex models for code generation and agentic tasks.
*/
import { execSync } from "child_process";
import fs from "fs";
import path from "path";
import os from "os";
import type { InstallationStatus } from "./types.js";
export class CodexCliDetector {
/**
* Get the path to Codex config directory
*/
static getConfigDir(): string {
return path.join(os.homedir(), ".codex");
}
/**
* Get the path to Codex auth file
*/
static getAuthPath(): string {
return path.join(this.getConfigDir(), "auth.json");
}
/**
* Check Codex authentication status
*/
static checkAuth(): {
authenticated: boolean;
method: string;
hasAuthFile?: boolean;
hasEnvKey?: boolean;
authPath?: string;
error?: string;
} {
try {
const authPath = this.getAuthPath();
const envApiKey = process.env.OPENAI_API_KEY;
// Try to verify authentication using codex CLI command if available
try {
const detection = this.detectCodexInstallation();
if (detection.installed && detection.path) {
try {
// Use 2>&1 to capture both stdout and stderr
const statusOutput = execSync(
`"${detection.path}" login status 2>&1`,
{
encoding: "utf-8",
timeout: 5000,
}
).trim();
// Check if the output indicates logged in status
if (
statusOutput &&
(statusOutput.includes("Logged in") || statusOutput.includes("Authenticated"))
) {
return {
authenticated: true,
method: "cli_verified",
hasAuthFile: fs.existsSync(authPath),
hasEnvKey: !!envApiKey,
authPath,
};
}
} catch (statusError) {
// status command failed, continue with file-based check
}
}
} catch (verifyError) {
// CLI verification failed, continue with file-based check
}
// Check if auth file exists
if (fs.existsSync(authPath)) {
try {
const content = fs.readFileSync(authPath, "utf-8");
const auth: any = JSON.parse(content);
// Check for token object structure
if (auth.token && typeof auth.token === "object") {
const token = auth.token;
if (
token.Id_token ||
token.access_token ||
token.refresh_token ||
token.id_token
) {
return {
authenticated: true,
method: "cli_tokens",
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath,
};
}
}
// Check for tokens at root level
if (
auth.access_token ||
auth.refresh_token ||
auth.Id_token ||
auth.id_token
) {
return {
authenticated: true,
method: "cli_tokens",
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath,
};
}
// Check for API key fields
if (auth.api_key || auth.openai_api_key || auth.apiKey) {
return {
authenticated: true,
method: "auth_file",
hasAuthFile: true,
hasEnvKey: !!envApiKey,
authPath,
};
}
} catch (error) {
return {
authenticated: false,
method: "none",
hasAuthFile: false,
hasEnvKey: !!envApiKey,
authPath,
};
}
}
// Environment variable override
if (envApiKey) {
return {
authenticated: true,
method: "env",
hasAuthFile: fs.existsSync(authPath),
hasEnvKey: true,
authPath,
};
}
return {
authenticated: false,
method: "none",
hasAuthFile: fs.existsSync(authPath),
hasEnvKey: false,
authPath,
};
} catch (error) {
return {
authenticated: false,
method: "none",
error: (error as Error).message,
};
}
}
/**
* Check if Codex CLI is installed and accessible
*/
static detectCodexInstallation(): InstallationStatus & {
hasApiKey?: boolean;
} {
try {
// Method 1: Check if 'codex' command is in PATH
try {
const codexPath = execSync("which codex 2>/dev/null", {
encoding: "utf-8",
}).trim();
if (codexPath) {
const version = this.getCodexVersion(codexPath);
return {
installed: true,
path: codexPath,
version: version || undefined,
method: "cli",
};
}
} catch (error) {
// CLI not in PATH, continue checking other methods
}
// Method 2: Check for npm global installation
try {
const npmListOutput = execSync(
"npm list -g @openai/codex --depth=0 2>/dev/null",
{ encoding: "utf-8" }
);
if (npmListOutput && npmListOutput.includes("@openai/codex")) {
// Get the path from npm bin
const npmBinPath = execSync("npm bin -g", {
encoding: "utf-8",
}).trim();
const codexPath = path.join(npmBinPath, "codex");
const version = this.getCodexVersion(codexPath);
return {
installed: true,
path: codexPath,
version: version || undefined,
method: "npm",
};
}
} catch (error) {
// npm global not found
}
// Method 3: Check for Homebrew installation on macOS
if (process.platform === "darwin") {
try {
const brewList = execSync("brew list --formula 2>/dev/null", {
encoding: "utf-8",
});
if (brewList.includes("codex")) {
const brewPrefixOutput = execSync("brew --prefix codex 2>/dev/null", {
encoding: "utf-8",
}).trim();
const codexPath = path.join(brewPrefixOutput, "bin", "codex");
const version = this.getCodexVersion(codexPath);
return {
installed: true,
path: codexPath,
version: version || undefined,
method: "brew",
};
}
} catch (error) {
// Homebrew not found or codex not installed via brew
}
}
// Method 4: Check Windows path
if (process.platform === "win32") {
try {
const codexPath = execSync("where codex 2>nul", {
encoding: "utf-8",
})
.trim()
.split("\n")[0];
if (codexPath) {
const version = this.getCodexVersion(codexPath);
return {
installed: true,
path: codexPath,
version: version || undefined,
method: "cli",
};
}
} catch (error) {
// Not found on Windows
}
}
// Method 5: Check common installation paths
const commonPaths = [
path.join(os.homedir(), ".local", "bin", "codex"),
path.join(os.homedir(), ".npm-global", "bin", "codex"),
"/usr/local/bin/codex",
"/opt/homebrew/bin/codex",
];
for (const checkPath of commonPaths) {
if (fs.existsSync(checkPath)) {
const version = this.getCodexVersion(checkPath);
return {
installed: true,
path: checkPath,
version: version || undefined,
method: "cli",
};
}
}
// Method 6: Check if OPENAI_API_KEY is set (can use Codex API directly)
if (process.env.OPENAI_API_KEY) {
return {
installed: false,
hasApiKey: true,
};
}
return {
installed: false,
};
} catch (error) {
return {
installed: false,
error: (error as Error).message,
};
}
}
/**
* Get Codex CLI version from executable path
*/
static getCodexVersion(codexPath: string): string | null {
try {
const version = execSync(`"${codexPath}" --version 2>/dev/null`, {
encoding: "utf-8",
}).trim();
return version || null;
} catch (error) {
return null;
}
}
/**
* Get installation info and recommendations
*/
static getInstallationInfo(): {
status: string;
method?: string;
version?: string | null;
path?: string | null;
recommendation: string;
installCommands?: Record<string, string>;
} {
const detection = this.detectCodexInstallation();
if (detection.installed) {
return {
status: "installed",
method: detection.method,
version: detection.version,
path: detection.path,
recommendation:
detection.method === "cli"
? "Using Codex CLI - ready for GPT-5.1/5.2 Codex models"
: `Using Codex CLI via ${detection.method} - ready for GPT-5.1/5.2 Codex models`,
};
}
// Not installed but has API key
if (detection.hasApiKey) {
return {
status: "api_key_only",
method: "api-key-only",
recommendation:
"OPENAI_API_KEY detected but Codex CLI not installed. Install Codex CLI for full agentic capabilities.",
installCommands: this.getInstallCommands(),
};
}
return {
status: "not_installed",
recommendation:
"Install OpenAI Codex CLI to use GPT-5.1/5.2 Codex models for agentic tasks",
installCommands: this.getInstallCommands(),
};
}
/**
* Get installation commands for different platforms
*/
static getInstallCommands(): Record<string, string> {
return {
npm: "npm install -g @openai/codex@latest",
macos: "brew install codex",
linux: "npm install -g @openai/codex@latest",
windows: "npm install -g @openai/codex@latest",
};
}
/**
* Check if Codex CLI supports a specific model
*/
static isModelSupported(model: string): boolean {
const supportedModels = [
"gpt-5.1-codex-max",
"gpt-5.1-codex",
"gpt-5.1-codex-mini",
"gpt-5.1",
"gpt-5.2",
];
return supportedModels.includes(model);
}
/**
* Get default model for Codex CLI
*/
static getDefaultModel(): string {
return "gpt-5.2";
}
/**
* Get comprehensive installation info including auth status
*/
static getFullStatus() {
const installation = this.detectCodexInstallation();
const auth = this.checkAuth();
const info = this.getInstallationInfo();
return {
...info,
auth,
installation,
};
}
}

View File

@@ -1,355 +0,0 @@
/**
* Codex TOML Configuration Manager
*
* Manages Codex CLI's TOML configuration file to add/update MCP server settings.
* Codex CLI looks for config at:
* - ~/.codex/config.toml (user-level)
* - .codex/config.toml (project-level, takes precedence)
*/
import fs from "fs/promises";
import path from "path";
import os from "os";
interface McpServerConfig {
command: string;
args?: string[];
env?: Record<string, string>;
startup_timeout_sec?: number;
tool_timeout_sec?: number;
enabled_tools?: string[];
}
interface CodexConfig {
experimental_use_rmcp_client?: boolean;
mcp_servers?: Record<string, McpServerConfig>;
[key: string]: any;
}
export class CodexConfigManager {
private userConfigPath: string;
private projectConfigPath: string | null = null;
constructor() {
this.userConfigPath = path.join(os.homedir(), ".codex", "config.toml");
}
/**
* Set the project path for project-level config
*/
setProjectPath(projectPath: string): void {
this.projectConfigPath = path.join(projectPath, ".codex", "config.toml");
}
/**
* Get the effective config path (project-level if exists, otherwise user-level)
*/
async getConfigPath(): Promise<string> {
if (this.projectConfigPath) {
try {
await fs.access(this.projectConfigPath);
return this.projectConfigPath;
} catch (e) {
// Project config doesn't exist, fall back to user config
}
}
// Ensure user config directory exists
const userConfigDir = path.dirname(this.userConfigPath);
try {
await fs.mkdir(userConfigDir, { recursive: true });
} catch (e) {
// Directory might already exist
}
return this.userConfigPath;
}
/**
* Read existing TOML config (simple parser for our needs)
*/
async readConfig(configPath: string): Promise<CodexConfig> {
try {
const content = await fs.readFile(configPath, "utf-8");
return this.parseToml(content);
} catch (e: any) {
if (e.code === "ENOENT") {
return {};
}
throw e;
}
}
/**
* Simple TOML parser for our specific use case
* This is a minimal parser that handles the MCP server config structure
*/
parseToml(content: string): CodexConfig {
const config: CodexConfig = {};
let currentSection: string | null = null;
let currentSubsection: string | null = null;
const lines = content.split("\n");
for (const line of lines) {
const trimmed = line.trim();
// Skip comments and empty lines
if (!trimmed || trimmed.startsWith("#")) {
continue;
}
// Section header: [section]
const sectionMatch = trimmed.match(/^\[([^\]]+)\]$/);
if (sectionMatch) {
const sectionName = sectionMatch[1];
const parts = sectionName.split(".");
if (parts.length === 1) {
currentSection = parts[0];
currentSubsection = null;
if (!config[currentSection]) {
config[currentSection] = {};
}
} else if (parts.length === 2) {
currentSection = parts[0];
currentSubsection = parts[1];
if (!config[currentSection]) {
config[currentSection] = {};
}
if (!config[currentSection][currentSubsection]) {
config[currentSection][currentSubsection] = {};
}
}
continue;
}
// Key-value pair: key = value
const kvMatch = trimmed.match(/^([^=]+)=(.+)$/);
if (kvMatch) {
const key = kvMatch[1].trim();
let value: any = kvMatch[2].trim();
// Remove quotes if present
if (
(value.startsWith('"') && value.endsWith('"')) ||
(value.startsWith("'") && value.endsWith("'"))
) {
value = value.slice(1, -1);
}
// Parse boolean
if (value === "true") value = true;
else if (value === "false") value = false;
// Parse number
else if (/^-?\d+$/.test(value)) value = parseInt(value, 10);
else if (/^-?\d+\.\d+$/.test(value)) value = parseFloat(value);
if (currentSubsection && currentSection) {
if (!config[currentSection][currentSubsection]) {
config[currentSection][currentSubsection] = {};
}
config[currentSection][currentSubsection][key] = value;
} else if (currentSection) {
if (!config[currentSection]) {
config[currentSection] = {};
}
config[currentSection][key] = value;
} else {
config[key] = value;
}
}
}
return config;
}
/**
* Configure the automaker-tools MCP server
*/
async configureMcpServer(
projectPath: string,
mcpServerScriptPath: string
): Promise<string> {
this.setProjectPath(projectPath);
const configPath = await this.getConfigPath();
// Read existing config
const config = await this.readConfig(configPath);
// Ensure mcp_servers section exists
if (!config.mcp_servers) {
config.mcp_servers = {};
}
// Configure automaker-tools server
config.mcp_servers["automaker-tools"] = {
command: "node",
args: [mcpServerScriptPath],
env: {
AUTOMAKER_PROJECT_PATH: projectPath,
},
startup_timeout_sec: 10,
tool_timeout_sec: 60,
enabled_tools: ["UpdateFeatureStatus"],
};
// Ensure experimental_use_rmcp_client is enabled (if needed)
if (!config.experimental_use_rmcp_client) {
config.experimental_use_rmcp_client = true;
}
// Write config back
await this.writeConfig(configPath, config);
console.log(
`[CodexConfigManager] Configured automaker-tools MCP server in ${configPath}`
);
return configPath;
}
/**
* Write config to TOML file
*/
async writeConfig(configPath: string, config: CodexConfig): Promise<void> {
let content = "";
// Write top-level keys first (preserve existing non-MCP config)
for (const [key, value] of Object.entries(config)) {
if (key === "mcp_servers" || key === "experimental_use_rmcp_client") {
continue; // Handle these separately
}
if (typeof value !== "object") {
content += `${key} = ${this.formatValue(value)}\n`;
}
}
// Write experimental flag if enabled
if (config.experimental_use_rmcp_client) {
if (content && !content.endsWith("\n\n")) {
content += "\n";
}
content += `experimental_use_rmcp_client = true\n`;
}
// Write mcp_servers section
if (config.mcp_servers && Object.keys(config.mcp_servers).length > 0) {
if (content && !content.endsWith("\n\n")) {
content += "\n";
}
for (const [serverName, serverConfig] of Object.entries(
config.mcp_servers
)) {
content += `\n[mcp_servers.${serverName}]\n`;
// Write command first
if (serverConfig.command) {
content += `command = "${this.escapeTomlString(serverConfig.command)}"\n`;
}
// Write args
if (serverConfig.args && Array.isArray(serverConfig.args)) {
const argsStr = serverConfig.args
.map((a) => `"${this.escapeTomlString(a)}"`)
.join(", ");
content += `args = [${argsStr}]\n`;
}
// Write timeouts (must be before env subsection)
if (serverConfig.startup_timeout_sec !== undefined) {
content += `startup_timeout_sec = ${serverConfig.startup_timeout_sec}\n`;
}
if (serverConfig.tool_timeout_sec !== undefined) {
content += `tool_timeout_sec = ${serverConfig.tool_timeout_sec}\n`;
}
// Write enabled_tools (must be before env subsection - at server level, not env level)
if (serverConfig.enabled_tools && Array.isArray(serverConfig.enabled_tools)) {
const toolsStr = serverConfig.enabled_tools
.map((t) => `"${this.escapeTomlString(t)}"`)
.join(", ");
content += `enabled_tools = [${toolsStr}]\n`;
}
// Write env section last (as a separate subsection)
if (
serverConfig.env &&
typeof serverConfig.env === "object" &&
Object.keys(serverConfig.env).length > 0
) {
content += `\n[mcp_servers.${serverName}.env]\n`;
for (const [envKey, envValue] of Object.entries(serverConfig.env)) {
content += `${envKey} = "${this.escapeTomlString(String(envValue))}"\n`;
}
}
}
}
// Ensure directory exists
const configDir = path.dirname(configPath);
await fs.mkdir(configDir, { recursive: true });
// Write file
await fs.writeFile(configPath, content, "utf-8");
}
/**
* Escape special characters in TOML strings
*/
escapeTomlString(str: string): string {
return str
.replace(/\\/g, "\\\\")
.replace(/"/g, '\\"')
.replace(/\n/g, "\\n")
.replace(/\r/g, "\\r")
.replace(/\t/g, "\\t");
}
/**
* Format a value for TOML output
*/
formatValue(value: any): string {
if (typeof value === "string") {
const escaped = value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
return `"${escaped}"`;
} else if (typeof value === "boolean") {
return value.toString();
} else if (typeof value === "number") {
return value.toString();
}
return `"${String(value)}"`;
}
/**
* Remove automaker-tools MCP server configuration
*/
async removeMcpServer(projectPath: string): Promise<void> {
this.setProjectPath(projectPath);
const configPath = await this.getConfigPath();
try {
const config = await this.readConfig(configPath);
if (config.mcp_servers && config.mcp_servers["automaker-tools"]) {
delete config.mcp_servers["automaker-tools"];
// If no more MCP servers, remove the section
if (Object.keys(config.mcp_servers).length === 0) {
delete config.mcp_servers;
}
await this.writeConfig(configPath, config);
console.log(
`[CodexConfigManager] Removed automaker-tools MCP server from ${configPath}`
);
}
} catch (e) {
console.error(`[CodexConfigManager] Error removing MCP server config:`, e);
}
}
}
// Export singleton instance
export const codexConfigManager = new CodexConfigManager();

View File

@@ -1,569 +0,0 @@
/**
* Codex Provider - Executes queries using OpenAI Codex CLI
*
* Spawns Codex CLI as a subprocess and converts JSONL output to
* Claude SDK-compatible message format for seamless integration.
*/
import { BaseProvider } from "./base-provider.js";
import { CodexCliDetector } from "./codex-cli-detector.js";
import { codexConfigManager } from "./codex-config-manager.js";
import { spawnJSONLProcess } from "../lib/subprocess-manager.js";
import { formatHistoryAsText } from "../lib/conversation-utils.js";
import type {
ExecuteOptions,
ProviderMessage,
InstallationStatus,
ModelDefinition,
ContentBlock,
} from "./types.js";
// Codex event types
const CODEX_EVENT_TYPES = {
THREAD_STARTED: "thread.started",
THREAD_COMPLETED: "thread.completed",
ITEM_STARTED: "item.started",
ITEM_COMPLETED: "item.completed",
TURN_STARTED: "turn.started",
ERROR: "error",
};
interface CodexEvent {
type: string;
data?: any;
item?: any;
thread_id?: string;
message?: string;
}
export class CodexProvider extends BaseProvider {
getName(): string {
return "codex";
}
/**
* Execute a query using Codex CLI
*/
async *executeQuery(options: ExecuteOptions): AsyncGenerator<ProviderMessage> {
const {
prompt,
model = "gpt-5.2",
cwd,
systemPrompt,
mcpServers,
abortController,
conversationHistory,
} = options;
// Find Codex CLI path
const codexPath = this.findCodexPath();
if (!codexPath) {
yield {
type: "error",
error:
"Codex CLI not found. Please install it with: npm install -g @openai/codex@latest",
};
return;
}
// Configure MCP server if provided
if (mcpServers && mcpServers["automaker-tools"]) {
try {
const mcpServerScriptPath = await this.getMcpServerPath();
if (mcpServerScriptPath) {
await codexConfigManager.configureMcpServer(cwd, mcpServerScriptPath);
}
} catch (error) {
console.error("[CodexProvider] Failed to configure MCP server:", error);
// Continue execution even if MCP config fails
}
}
// Build combined prompt with conversation history
// Codex CLI doesn't support native conversation history or images, so we extract text
let combinedPrompt = "";
if (typeof prompt === "string") {
combinedPrompt = prompt;
} else if (Array.isArray(prompt)) {
// Extract text from content blocks (ignore images - Codex CLI doesn't support vision)
combinedPrompt = prompt
.filter(block => block.type === "text")
.map(block => block.text || "")
.join("\n");
}
// Add system prompt first
if (systemPrompt) {
combinedPrompt = `${systemPrompt}\n\n---\n\n${combinedPrompt}`;
}
// Add conversation history
if (conversationHistory && conversationHistory.length > 0) {
const historyText = formatHistoryAsText(conversationHistory);
combinedPrompt = `${historyText}Current request:\n${combinedPrompt}`;
}
// Build command arguments
const args = this.buildArgs({ prompt: combinedPrompt, model });
// Check authentication - either API key or CLI login
const auth = CodexCliDetector.checkAuth();
const hasApiKey = this.config.apiKey || process.env.OPENAI_API_KEY;
if (!auth.authenticated && !hasApiKey) {
yield {
type: "error",
error:
"Codex CLI is not authenticated. Please run 'codex login' or set OPENAI_API_KEY environment variable.",
};
return;
}
// Prepare environment variables (API key is optional if using CLI auth)
const env = {
...this.config.env,
...(hasApiKey && { OPENAI_API_KEY: hasApiKey }),
};
// Spawn the Codex process and stream JSONL output
try {
const stream = spawnJSONLProcess({
command: codexPath,
args,
cwd,
env,
abortController,
timeout: 30000, // 30s timeout for no output
});
for await (const event of stream) {
const converted = this.convertToProviderFormat(event as CodexEvent);
if (converted) {
yield converted;
}
}
// Yield completion event
yield {
type: "result",
subtype: "success",
result: "",
};
} catch (error) {
console.error("[CodexProvider] Execution error:", error);
yield {
type: "error",
error: (error as Error).message,
};
}
}
/**
* Convert Codex JSONL event to Provider message format (Claude SDK compatible)
*/
private convertToProviderFormat(event: CodexEvent): ProviderMessage | null {
const { type, data, item, thread_id } = event;
switch (type) {
case CODEX_EVENT_TYPES.THREAD_STARTED:
case "thread.started":
// Session initialization - not needed for provider format
return null;
case CODEX_EVENT_TYPES.ITEM_COMPLETED:
case "item.completed":
return this.convertItemCompleted(item || data);
case CODEX_EVENT_TYPES.ITEM_STARTED:
case "item.started":
// Item started events can show tool usage
const startedItem = item || data;
if (
startedItem?.type === "command_execution" &&
startedItem?.command
) {
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "tool_use",
name: "bash",
input: { command: startedItem.command },
},
],
},
};
}
// Handle todo_list started
if (startedItem?.type === "todo_list" && startedItem?.items) {
const todos = startedItem.items || [];
const todoText = todos
.map((t: any, i: number) => `${i + 1}. ${t.text || t}`)
.join("\n");
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "text",
text: `**Todo List:**\n${todoText}`,
},
],
},
};
}
return null;
case "item.updated":
// Handle updated items (like todo list updates)
const updatedItem = item || data;
if (updatedItem?.type === "todo_list" && updatedItem?.items) {
const todos = updatedItem.items || [];
const todoText = todos
.map((t: any, i: number) => {
const status = t.status === "completed" ? "✓" : " ";
return `${i + 1}. [${status}] ${t.text || t}`;
})
.join("\n");
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "text",
text: `**Updated Todo List:**\n${todoText}`,
},
],
},
};
}
return null;
case CODEX_EVENT_TYPES.THREAD_COMPLETED:
case "thread.completed":
return {
type: "result",
subtype: "success",
result: "",
};
case CODEX_EVENT_TYPES.ERROR:
case "error":
return {
type: "error",
error:
data?.message ||
item?.message ||
event.message ||
"Unknown error from Codex CLI",
};
case "turn.started":
case "turn.completed":
// Turn markers - not needed for provider format
return null;
default:
return null;
}
}
/**
* Convert item.completed event to Provider format
*/
private convertItemCompleted(item: any): ProviderMessage | null {
if (!item) {
return null;
}
const itemType = item.type || item.item_type;
switch (itemType) {
case "reasoning":
// Thinking/reasoning output
const reasoningText = item.text || item.content || "";
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "thinking",
thinking: reasoningText,
},
],
},
};
case "agent_message":
case "message":
// Assistant text message
const messageText = item.content || item.text || "";
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "text",
text: messageText,
},
],
},
};
case "command_execution":
// Command execution - show both the command and its output
const command = item.command || "";
const output = item.aggregated_output || item.output || "";
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "text",
text: `\`\`\`bash\n${command}\n\`\`\`\n\n${output}`,
},
],
},
};
case "tool_use":
// Tool use
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "tool_use",
name: item.tool || item.command || "unknown",
input: item.input || item.args || {},
},
],
},
};
case "tool_result":
// Tool result
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "tool_result",
tool_use_id: item.tool_use_id,
content: item.output || item.result,
},
],
},
};
case "todo_list":
// Todo list - convert to text format
const todos = item.items || [];
const todoText = todos
.map((t: any, i: number) => `${i + 1}. ${t.text || t}`)
.join("\n");
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "text",
text: `**Todo List:**\n${todoText}`,
},
],
},
};
case "file_change":
// File changes - show what files were modified
const changes = item.changes || [];
const changeText = changes
.map((c: any) => `- Modified: ${c.path}`)
.join("\n");
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "text",
text: `**File Changes:**\n${changeText}`,
},
],
},
};
default:
// Generic text output
const text = item.text || item.content || item.aggregated_output;
if (text) {
return {
type: "assistant",
message: {
role: "assistant",
content: [
{
type: "text",
text: String(text),
},
],
},
};
}
return null;
}
}
/**
* Build command arguments for Codex CLI
*/
private buildArgs(options: {
prompt: string;
model: string;
}): string[] {
const { prompt, model } = options;
return [
"exec",
"--model",
model,
"--json", // JSONL output format
"--full-auto", // Non-interactive mode
prompt, // Prompt as the last argument
];
}
/**
* Find Codex CLI executable path
*/
private findCodexPath(): string | null {
// Check config override
if (this.config.cliPath) {
return this.config.cliPath;
}
// Check environment variable override
if (process.env.CODEX_CLI_PATH) {
return process.env.CODEX_CLI_PATH;
}
// Auto-detect
const detection = CodexCliDetector.detectCodexInstallation();
return detection.path || "codex";
}
/**
* Get MCP server script path
*/
private async getMcpServerPath(): Promise<string | null> {
// TODO: Implement MCP server path resolution
// For now, return null - MCP support is optional
return null;
}
/**
* Detect Codex CLI installation
*/
async detectInstallation(): Promise<InstallationStatus> {
const detection = CodexCliDetector.detectCodexInstallation();
const auth = CodexCliDetector.checkAuth();
return {
installed: detection.installed,
path: detection.path,
version: detection.version,
method: detection.method,
hasApiKey: auth.hasEnvKey || auth.authenticated,
authenticated: auth.authenticated,
};
}
/**
* Get available Codex models
*/
getAvailableModels(): ModelDefinition[] {
return [
{
id: "gpt-5.2",
name: "GPT-5.2 (Codex)",
modelString: "gpt-5.2",
provider: "openai-codex",
description: "Latest Codex model for agentic code generation",
contextWindow: 256000,
maxOutputTokens: 32768,
supportsVision: true,
supportsTools: true,
tier: "premium",
default: true,
},
{
id: "gpt-5.1-codex-max",
name: "GPT-5.1 Codex Max",
modelString: "gpt-5.1-codex-max",
provider: "openai-codex",
description: "Maximum capability Codex model",
contextWindow: 256000,
maxOutputTokens: 32768,
supportsVision: true,
supportsTools: true,
tier: "premium",
},
{
id: "gpt-5.1-codex",
name: "GPT-5.1 Codex",
modelString: "gpt-5.1-codex",
provider: "openai-codex",
description: "Standard Codex model",
contextWindow: 256000,
maxOutputTokens: 32768,
supportsVision: true,
supportsTools: true,
tier: "standard",
},
{
id: "gpt-5.1-codex-mini",
name: "GPT-5.1 Codex Mini",
modelString: "gpt-5.1-codex-mini",
provider: "openai-codex",
description: "Faster, lightweight Codex model",
contextWindow: 256000,
maxOutputTokens: 16384,
supportsVision: false,
supportsTools: true,
tier: "basic",
},
{
id: "gpt-5.1",
name: "GPT-5.1",
modelString: "gpt-5.1",
provider: "openai-codex",
description: "General-purpose GPT-5.1 model",
contextWindow: 256000,
maxOutputTokens: 32768,
supportsVision: true,
supportsTools: true,
tier: "standard",
},
];
}
/**
* Check if the provider supports a specific feature
*/
supportsFeature(feature: string): boolean {
const supportedFeatures = ["tools", "text", "vision", "mcp", "cli"];
return supportedFeatures.includes(feature);
}
}

View File

@@ -8,7 +8,6 @@
import { BaseProvider } from "./base-provider.js";
import { ClaudeProvider } from "./claude-provider.js";
import { CodexProvider } from "./codex-provider.js";
import type { InstallationStatus } from "./types.js";
export class ProviderFactory {
@@ -21,12 +20,6 @@ export class ProviderFactory {
static getProviderForModel(modelId: string): BaseProvider {
const lowerModel = modelId.toLowerCase();
// OpenAI/Codex models (gpt-*)
// Note: o1/o3 models are not supported by Codex CLI
if (lowerModel.startsWith("gpt-")) {
return new CodexProvider();
}
// Claude models (claude-*, opus, sonnet, haiku)
if (
lowerModel.startsWith("claude-") ||
@@ -56,7 +49,6 @@ export class ProviderFactory {
static getAllProviders(): BaseProvider[] {
return [
new ClaudeProvider(),
new CodexProvider(),
// Future providers...
];
}
@@ -95,10 +87,6 @@ export class ProviderFactory {
case "anthropic":
return new ClaudeProvider();
case "codex":
case "openai":
return new CodexProvider();
// Future providers:
// case "cursor":
// return new CursorProvider();