mirror of
https://github.com/AutoMaker-Org/automaker.git
synced 2026-02-01 20:23:36 +00:00
feat: remove codex support
This commit is contained in:
@@ -3,7 +3,6 @@
|
||||
*
|
||||
* Provides centralized model resolution logic:
|
||||
* - Maps Claude model aliases to full model strings
|
||||
* - Detects and passes through OpenAI/Codex models
|
||||
* - Provides default models per provider
|
||||
* - Handles multiple model sources with priority
|
||||
*/
|
||||
@@ -22,7 +21,6 @@ export const CLAUDE_MODEL_MAP: Record<string, string> = {
|
||||
*/
|
||||
export const DEFAULT_MODELS = {
|
||||
claude: "claude-opus-4-5-20251101",
|
||||
openai: "gpt-5.2",
|
||||
} as const;
|
||||
|
||||
/**
|
||||
@@ -41,13 +39,6 @@ export function resolveModelString(
|
||||
return defaultModel;
|
||||
}
|
||||
|
||||
// OpenAI/Codex models - pass through unchanged
|
||||
// Only check for gpt-* models (Codex CLI doesn't support o1/o3)
|
||||
if (modelKey.startsWith("gpt-")) {
|
||||
console.log(`[ModelResolver] Using OpenAI/Codex model: ${modelKey}`);
|
||||
return modelKey;
|
||||
}
|
||||
|
||||
// Full Claude model string - pass through unchanged
|
||||
if (modelKey.includes("claude-")) {
|
||||
console.log(`[ModelResolver] Using full Claude model string: ${modelKey}`);
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -64,78 +64,6 @@ export function createModelsRoutes(): Router {
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-4o",
|
||||
name: "GPT-4o",
|
||||
provider: "openai",
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 16384,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-4o-mini",
|
||||
name: "GPT-4o Mini",
|
||||
provider: "openai",
|
||||
contextWindow: 128000,
|
||||
maxOutputTokens: 16384,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "o1",
|
||||
name: "o1",
|
||||
provider: "openai",
|
||||
contextWindow: 200000,
|
||||
maxOutputTokens: 100000,
|
||||
supportsVision: true,
|
||||
supportsTools: false,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.2",
|
||||
name: "GPT-5.2 (Codex)",
|
||||
provider: "openai-codex",
|
||||
contextWindow: 256000,
|
||||
maxOutputTokens: 32768,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.1-codex-max",
|
||||
name: "GPT-5.1 Codex Max",
|
||||
provider: "openai-codex",
|
||||
contextWindow: 256000,
|
||||
maxOutputTokens: 32768,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.1-codex",
|
||||
name: "GPT-5.1 Codex",
|
||||
provider: "openai-codex",
|
||||
contextWindow: 256000,
|
||||
maxOutputTokens: 32768,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.1-codex-mini",
|
||||
name: "GPT-5.1 Codex Mini",
|
||||
provider: "openai-codex",
|
||||
contextWindow: 256000,
|
||||
maxOutputTokens: 16384,
|
||||
supportsVision: false,
|
||||
supportsTools: true,
|
||||
},
|
||||
{
|
||||
id: "gpt-5.1",
|
||||
name: "GPT-5.1",
|
||||
provider: "openai-codex",
|
||||
contextWindow: 256000,
|
||||
maxOutputTokens: 32768,
|
||||
supportsVision: true,
|
||||
supportsTools: true,
|
||||
},
|
||||
];
|
||||
|
||||
res.json({ success: true, models });
|
||||
@@ -156,17 +84,6 @@ export function createModelsRoutes(): Router {
|
||||
available: statuses.claude?.installed || false,
|
||||
hasApiKey: !!process.env.ANTHROPIC_API_KEY || !!process.env.CLAUDE_CODE_OAUTH_TOKEN,
|
||||
},
|
||||
openai: {
|
||||
available: !!process.env.OPENAI_API_KEY,
|
||||
hasApiKey: !!process.env.OPENAI_API_KEY,
|
||||
},
|
||||
"openai-codex": {
|
||||
available: statuses.codex?.installed || false,
|
||||
hasApiKey: !!process.env.OPENAI_API_KEY,
|
||||
cliInstalled: statuses.codex?.installed,
|
||||
cliVersion: statuses.codex?.version,
|
||||
cliPath: statuses.codex?.path,
|
||||
},
|
||||
google: {
|
||||
available: !!process.env.GOOGLE_API_KEY,
|
||||
hasApiKey: !!process.env.GOOGLE_API_KEY,
|
||||
|
||||
@@ -230,84 +230,6 @@ export function createSetupRoutes(): Router {
|
||||
}
|
||||
});
|
||||
|
||||
// Get Codex CLI status
|
||||
router.get("/codex-status", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
let installed = false;
|
||||
let version = "";
|
||||
let cliPath = "";
|
||||
let method = "none";
|
||||
|
||||
// Try to find Codex CLI
|
||||
try {
|
||||
const { stdout } = await execAsync("which codex || where codex 2>/dev/null");
|
||||
cliPath = stdout.trim();
|
||||
installed = true;
|
||||
method = "path";
|
||||
|
||||
try {
|
||||
const { stdout: versionOut } = await execAsync("codex --version");
|
||||
version = versionOut.trim();
|
||||
} catch {
|
||||
version = "unknown";
|
||||
}
|
||||
} catch {
|
||||
// Not found
|
||||
}
|
||||
|
||||
// Check for OpenAI/Codex authentication
|
||||
// Simplified: only check via CLI command, no file parsing
|
||||
let auth = {
|
||||
authenticated: false,
|
||||
method: "none" as string,
|
||||
hasEnvKey: !!process.env.OPENAI_API_KEY,
|
||||
hasStoredApiKey: !!apiKeys.openai,
|
||||
};
|
||||
|
||||
// Try to verify authentication using codex CLI command if CLI is installed
|
||||
if (installed && cliPath) {
|
||||
try {
|
||||
const { stdout: statusOutput } = await execAsync(`"${cliPath}" login status 2>&1`, {
|
||||
timeout: 5000,
|
||||
});
|
||||
|
||||
// Check if the output indicates logged in status
|
||||
if (statusOutput && (statusOutput.includes('Logged in') || statusOutput.includes('Authenticated'))) {
|
||||
auth.authenticated = true;
|
||||
auth.method = "cli_verified"; // CLI verified via login status command
|
||||
}
|
||||
} catch (error) {
|
||||
// CLI check failed - user needs to login manually
|
||||
console.log("[Setup] Codex login status check failed:", error);
|
||||
}
|
||||
}
|
||||
|
||||
// Environment variable override
|
||||
if (process.env.OPENAI_API_KEY) {
|
||||
auth.authenticated = true;
|
||||
auth.method = "env"; // OPENAI_API_KEY environment variable
|
||||
}
|
||||
|
||||
// In-memory stored API key (from settings UI)
|
||||
if (!auth.authenticated && apiKeys.openai) {
|
||||
auth.authenticated = true;
|
||||
auth.method = "api_key"; // Manually stored API key
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
status: installed ? "installed" : "not_installed",
|
||||
method,
|
||||
version,
|
||||
path: cliPath,
|
||||
auth,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Install Claude CLI
|
||||
router.post("/install-claude", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -324,20 +246,6 @@ export function createSetupRoutes(): Router {
|
||||
}
|
||||
});
|
||||
|
||||
// Install Codex CLI
|
||||
router.post("/install-codex", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
res.json({
|
||||
success: false,
|
||||
error:
|
||||
"CLI installation requires terminal access. Please install manually using: npm install -g @openai/codex",
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Auth Claude
|
||||
router.post("/auth-claude", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -353,28 +261,6 @@ export function createSetupRoutes(): Router {
|
||||
}
|
||||
});
|
||||
|
||||
// Auth Codex
|
||||
router.post("/auth-codex", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { apiKey } = req.body as { apiKey?: string };
|
||||
|
||||
if (apiKey) {
|
||||
apiKeys.openai = apiKey;
|
||||
process.env.OPENAI_API_KEY = apiKey;
|
||||
res.json({ success: true });
|
||||
} else {
|
||||
res.json({
|
||||
success: true,
|
||||
requiresManualAuth: true,
|
||||
command: "codex auth login",
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Store API key
|
||||
router.post("/store-api-key", async (req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -401,9 +287,6 @@ export function createSetupRoutes(): Router {
|
||||
process.env.ANTHROPIC_API_KEY = apiKey;
|
||||
await persistApiKeyToEnv("ANTHROPIC_API_KEY", apiKey);
|
||||
console.log("[Setup] Stored API key as ANTHROPIC_API_KEY");
|
||||
} else if (provider === "openai") {
|
||||
process.env.OPENAI_API_KEY = apiKey;
|
||||
await persistApiKeyToEnv("OPENAI_API_KEY", apiKey);
|
||||
} else if (provider === "google") {
|
||||
process.env.GOOGLE_API_KEY = apiKey;
|
||||
await persistApiKeyToEnv("GOOGLE_API_KEY", apiKey);
|
||||
@@ -422,7 +305,6 @@ export function createSetupRoutes(): Router {
|
||||
res.json({
|
||||
success: true,
|
||||
hasAnthropicKey: !!apiKeys.anthropic || !!process.env.ANTHROPIC_API_KEY,
|
||||
hasOpenAIKey: !!apiKeys.openai || !!process.env.OPENAI_API_KEY,
|
||||
hasGoogleKey: !!apiKeys.google || !!process.env.GOOGLE_API_KEY,
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -431,34 +313,6 @@ export function createSetupRoutes(): Router {
|
||||
}
|
||||
});
|
||||
|
||||
// Configure Codex MCP
|
||||
router.post("/configure-codex-mcp", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { projectPath } = req.body as { projectPath: string };
|
||||
|
||||
if (!projectPath) {
|
||||
res.status(400).json({ success: false, error: "projectPath required" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Create .codex directory and config
|
||||
const codexDir = path.join(projectPath, ".codex");
|
||||
await fs.mkdir(codexDir, { recursive: true });
|
||||
|
||||
const configPath = path.join(codexDir, "config.toml");
|
||||
const config = `# Codex configuration
|
||||
[mcp]
|
||||
enabled = true
|
||||
`;
|
||||
await fs.writeFile(configPath, config);
|
||||
|
||||
res.json({ success: true, configPath });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
// Get platform info
|
||||
router.get("/platform", async (_req: Request, res: Response) => {
|
||||
try {
|
||||
@@ -478,29 +332,5 @@ enabled = true
|
||||
}
|
||||
});
|
||||
|
||||
// Test OpenAI connection
|
||||
router.post("/test-openai", async (req: Request, res: Response) => {
|
||||
try {
|
||||
const { apiKey } = req.body as { apiKey?: string };
|
||||
const key = apiKey || apiKeys.openai || process.env.OPENAI_API_KEY;
|
||||
|
||||
if (!key) {
|
||||
res.json({ success: false, error: "No OpenAI API key provided" });
|
||||
return;
|
||||
}
|
||||
|
||||
// Simple test - just verify the key format
|
||||
if (!key.startsWith("sk-")) {
|
||||
res.json({ success: false, error: "Invalid OpenAI API key format" });
|
||||
return;
|
||||
}
|
||||
|
||||
res.json({ success: true, message: "API key format is valid" });
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "Unknown error";
|
||||
res.status(500).json({ success: false, error: message });
|
||||
}
|
||||
});
|
||||
|
||||
return router;
|
||||
}
|
||||
|
||||
@@ -1092,13 +1092,10 @@ When done, summarize what you implemented and any notes for the developer.`;
|
||||
if (block.text && (block.text.includes("Invalid API key") ||
|
||||
block.text.includes("authentication_failed") ||
|
||||
block.text.includes("Fix external API key"))) {
|
||||
const isCodex = finalModel.startsWith("gpt-")
|
||||
const errorMsg = isCodex
|
||||
? "Authentication failed: Invalid or expired API key. " +
|
||||
"Please check your OPENAI_API_KEY or run 'codex login' to re-authenticate."
|
||||
: "Authentication failed: Invalid or expired API key. " +
|
||||
"Please check your ANTHROPIC_API_KEY or run 'claude login' to re-authenticate.";
|
||||
throw new Error(errorMsg);
|
||||
throw new Error(
|
||||
"Authentication failed: Invalid or expired API key. " +
|
||||
"Please check your ANTHROPIC_API_KEY or run 'claude login' to re-authenticate."
|
||||
);
|
||||
}
|
||||
|
||||
this.emitAutoModeEvent("auto_mode_progress", {
|
||||
|
||||
Reference in New Issue
Block a user