#!/usr/bin/env node import "dotenv/config"; // Hack to suppress deprecation warnings (punycode) // eslint-disable-next-line @typescript-eslint/no-explicit-any (process as any).noDeprecation = true; import type { AppRollout } from "./app"; import type { ApprovalPolicy } from "./approvals"; import type { CommandConfirmation } from "./utils/agent/agent-loop"; import type { AppConfig } from "./utils/config"; import type { ResponseItem } from "openai/resources/responses/responses"; import App from "./app"; import { runSinglePass } from "./cli-singlepass"; import { AgentLoop } from "./utils/agent/agent-loop"; import { ReviewDecision } from "./utils/agent/review"; import { AutoApprovalMode } from "./utils/auto-approval-mode"; import { checkForUpdates } from "./utils/check-updates"; import { getApiKey, loadConfig, PRETTY_PRINT, INSTRUCTIONS_FILEPATH, } from "./utils/config"; import { createInputItem } from "./utils/input-utils"; import { initLogger } from "./utils/logger/log"; import { isModelSupportedForResponses } from "./utils/model-utils.js"; import { parseToolCall } from "./utils/parsers"; import { onExit, setInkRenderer } from "./utils/terminal"; import chalk from "chalk"; import { spawnSync } from "child_process"; import fs from "fs"; import { render } from "ink"; import meow from "meow"; import path from "path"; import React from "react"; // Call this early so `tail -F "$TMPDIR/oai-codex/codex-cli-latest.log"` works // immediately. This must be run with DEBUG=1 for logging to work. initLogger(); // TODO: migrate to new versions of quiet mode // // -q, --quiet Non-interactive quiet mode that only prints final message // -j, --json Non-interactive JSON output mode that prints JSON messages const cli = meow( ` Usage $ codex [options] $ codex completion Options -h, --help Show usage and exit -m, --model Model to use for completions (default: o4-mini) -i, --image Path(s) to image files to include as input -v, --view Inspect a previously saved rollout instead of starting a session -q, --quiet Non-interactive mode that only prints the assistant's final output -c, --config Open the instructions file in your editor -w, --writable-root Writable folder for sandbox in full-auto mode (can be specified multiple times) -a, --approval-mode Override the approval policy: 'suggest', 'auto-edit', or 'full-auto' --auto-edit Automatically approve file edits; still prompt for commands --full-auto Automatically approve edits and commands when executed in the sandbox --no-project-doc Do not automatically include the repository's 'codex.md' --project-doc Include an additional markdown file at as context --full-stdout Do not truncate stdout/stderr from command outputs --notify Enable desktop notifications for responses --flex-mode Use "flex-mode" processing mode for the request (only supported with models o3 and o4-mini) Dangerous options --dangerously-auto-approve-everything Skip all confirmation prompts and execute commands without sandboxing. Intended solely for ephemeral local testing. Experimental options -f, --full-context Launch in "full-context" mode which loads the entire repository into context and applies a batch of edits in one go. Incompatible with all other flags, except for --model. Examples $ codex "Write and run a python program that prints ASCII art" $ codex -q "fix build issues" $ codex completion bash `, { importMeta: import.meta, autoHelp: true, flags: { // misc help: { type: "boolean", aliases: ["h"] }, view: { type: "string" }, model: { type: "string", aliases: ["m"] }, provider: { type: "string", aliases: ["p"] }, image: { type: "string", isMultiple: true, aliases: ["i"] }, quiet: { type: "boolean", aliases: ["q"], description: "Non-interactive quiet mode", }, config: { type: "boolean", aliases: ["c"], description: "Open the instructions file in your editor", }, dangerouslyAutoApproveEverything: { type: "boolean", description: "Automatically approve all commands without prompting. This is EXTREMELY DANGEROUS and should only be used in trusted environments.", }, autoEdit: { type: "boolean", description: "Automatically approve edits; prompt for commands.", }, fullAuto: { type: "boolean", description: "Automatically run commands in a sandbox; only prompt for failures.", }, approvalMode: { type: "string", aliases: ["a"], description: "Determine the approval mode for Codex (default: suggest) Values: suggest, auto-edit, full-auto", }, writableRoot: { type: "string", isMultiple: true, aliases: ["w"], description: "Writable folder for sandbox in full-auto mode (can be specified multiple times)", }, noProjectDoc: { type: "boolean", description: "Disable automatic inclusion of project-level codex.md", }, projectDoc: { type: "string", description: "Path to a markdown file to include as project doc", }, flexMode: { type: "boolean", description: "Enable the flex-mode service tier (only supported by models o3 and o4-mini)", }, fullStdout: { type: "boolean", description: "Disable truncation of command stdout/stderr messages (show everything)", aliases: ["no-truncate"], }, // Notification notify: { type: "boolean", description: "Enable desktop notifications for responses", }, // Experimental mode where whole directory is loaded in context and model is requested // to make code edits in a single pass. fullContext: { type: "boolean", aliases: ["f"], description: `Run in full-context editing approach. The model is given the whole code directory as context and performs changes in one go without acting.`, }, }, }, ); // Handle 'completion' subcommand before any prompting or API calls if (cli.input[0] === "completion") { const shell = cli.input[1] || "bash"; const scripts: Record = { bash: `# bash completion for codex _codex_completion() { local cur cur="\${COMP_WORDS[COMP_CWORD]}" COMPREPLY=( $(compgen -o default -o filenames -- "\${cur}") ) } complete -F _codex_completion codex`, zsh: `# zsh completion for codex #compdef codex _codex() { _arguments '*:filename:_files' } _codex`, fish: `# fish completion for codex complete -c codex -a '(_fish_complete_path)' -d 'file path'`, }; const script = scripts[shell]; if (!script) { // eslint-disable-next-line no-console console.error(`Unsupported shell: ${shell}`); process.exit(1); } // eslint-disable-next-line no-console console.log(script); process.exit(0); } // For --help, show help and exit. if (cli.flags.help) { cli.showHelp(); } // For --config, open custom instructions file in editor and exit. if (cli.flags.config) { try { loadConfig(); // Ensures the file is created if it doesn't already exit. } catch { // ignore errors } const filePath = INSTRUCTIONS_FILEPATH; const editor = process.env["EDITOR"] || (process.platform === "win32" ? "notepad" : "vi"); spawnSync(editor, [filePath], { stdio: "inherit" }); process.exit(0); } // --------------------------------------------------------------------------- // API key handling // --------------------------------------------------------------------------- const fullContextMode = Boolean(cli.flags.fullContext); let config = loadConfig(undefined, undefined, { cwd: process.cwd(), disableProjectDoc: Boolean(cli.flags.noProjectDoc), projectDocPath: cli.flags.projectDoc, isFullContext: fullContextMode, }); const prompt = cli.input[0]; const model = cli.flags.model ?? config.model; const imagePaths = cli.flags.image; const provider = cli.flags.provider ?? config.provider ?? "openai"; const apiKey = getApiKey(provider); if (!apiKey) { // eslint-disable-next-line no-console console.error( `\n${chalk.red(`Missing ${provider} API key.`)}\n\n` + `Set the environment variable ${chalk.bold("OPENAI_API_KEY")} ` + `and re-run this command.\n` + `You can create a key here: ${chalk.bold( chalk.underline("https://platform.openai.com/account/api-keys"), )}\n`, ); process.exit(1); } config = { apiKey, ...config, model: model ?? config.model, notify: Boolean(cli.flags.notify), flexMode: Boolean(cli.flags.flexMode), provider, }; // Check for updates after loading config. This is important because we write state file in // the config dir. await checkForUpdates().catch(); // For --flex-mode, validate and exit if incorrect. if (cli.flags.flexMode) { const allowedFlexModels = new Set(["o3", "o4-mini"]); if (!allowedFlexModels.has(config.model)) { // eslint-disable-next-line no-console console.error( `The --flex-mode option is only supported when using the 'o3' or 'o4-mini' models. ` + `Current model: '${config.model}'.`, ); process.exit(1); } } if ( !(await isModelSupportedForResponses(provider, config.model)) && (!provider || provider.toLowerCase() === "openai") ) { // eslint-disable-next-line no-console console.error( `The model "${config.model}" does not appear in the list of models ` + `available to your account. Double-check the spelling (use\n` + ` openai models list\n` + `to see the full list) or choose another model with the --model flag.`, ); process.exit(1); } let rollout: AppRollout | undefined; // For --view, optionally load an existing rollout from disk, display it and exit. if (cli.flags.view) { const viewPath = cli.flags.view; const absolutePath = path.isAbsolute(viewPath) ? viewPath : path.join(process.cwd(), viewPath); try { const content = fs.readFileSync(absolutePath, "utf-8"); rollout = JSON.parse(content) as AppRollout; } catch (error) { // eslint-disable-next-line no-console console.error("Error reading rollout file:", error); process.exit(1); } } // For --fullcontext, run the separate cli entrypoint and exit. if (fullContextMode) { await runSinglePass({ originalPrompt: prompt, config, rootPath: process.cwd(), }); onExit(); process.exit(0); } // Ensure that all values in additionalWritableRoots are absolute paths. const additionalWritableRoots: ReadonlyArray = ( cli.flags.writableRoot ?? [] ).map((p) => path.resolve(p)); // For --quiet, run the cli without user interactions and exit. if (cli.flags.quiet) { process.env["CODEX_QUIET_MODE"] = "1"; if (!prompt || prompt.trim() === "") { // eslint-disable-next-line no-console console.error( 'Quiet mode requires a prompt string, e.g.,: codex -q "Fix bug #123 in the foobar project"', ); process.exit(1); } // Determine approval policy for quiet mode based on flags const quietApprovalPolicy: ApprovalPolicy = cli.flags.fullAuto || cli.flags.approvalMode === "full-auto" ? AutoApprovalMode.FULL_AUTO : cli.flags.autoEdit || cli.flags.approvalMode === "auto-edit" ? AutoApprovalMode.AUTO_EDIT : config.approvalMode || AutoApprovalMode.SUGGEST; await runQuietMode({ prompt, imagePaths: imagePaths || [], approvalPolicy: quietApprovalPolicy, additionalWritableRoots, config, }); onExit(); process.exit(0); } // Default to the "suggest" policy. // Determine the approval policy to use in interactive mode. // // Priority (highest → lowest): // 1. --fullAuto – run everything automatically in a sandbox. // 2. --dangerouslyAutoApproveEverything – run everything **without** a sandbox // or prompts. This is intended for completely trusted environments. Since // it is more dangerous than --fullAuto we deliberately give it lower // priority so a user specifying both flags still gets the safer behaviour. // 3. --autoEdit – automatically approve edits, but prompt for commands. // 4. config.approvalMode - use the approvalMode setting from ~/.codex/config.json. // 5. Default – suggest mode (prompt for everything). const approvalPolicy: ApprovalPolicy = cli.flags.fullAuto || cli.flags.approvalMode === "full-auto" ? AutoApprovalMode.FULL_AUTO : cli.flags.autoEdit || cli.flags.approvalMode === "auto-edit" ? AutoApprovalMode.AUTO_EDIT : config.approvalMode || AutoApprovalMode.SUGGEST; const instance = render( , { patchConsole: process.env["DEBUG"] ? false : true, }, ); setInkRenderer(instance); function formatResponseItemForQuietMode(item: ResponseItem): string { if (!PRETTY_PRINT) { return JSON.stringify(item); } switch (item.type) { case "message": { const role = item.role === "assistant" ? "assistant" : item.role; const txt = item.content .map((c) => { if (c.type === "output_text" || c.type === "input_text") { return c.text; } if (c.type === "input_image") { return ""; } if (c.type === "input_file") { return c.filename; } if (c.type === "refusal") { return c.refusal; } return "?"; }) .join(" "); return `${role}: ${txt}`; } case "function_call": { const details = parseToolCall(item); return `$ ${details?.cmdReadableText ?? item.name}`; } case "function_call_output": { // @ts-expect-error metadata unknown on ResponseFunctionToolCallOutputItem const meta = item.metadata as ExecOutputMetadata; const parts: Array = []; if (typeof meta?.exit_code === "number") { parts.push(`code: ${meta.exit_code}`); } if (typeof meta?.duration_seconds === "number") { parts.push(`duration: ${meta.duration_seconds}s`); } const header = parts.length > 0 ? ` (${parts.join(", ")})` : ""; return `command.stdout${header}\n${item.output}`; } default: { return JSON.stringify(item); } } } async function runQuietMode({ prompt, imagePaths, approvalPolicy, additionalWritableRoots, config, }: { prompt: string; imagePaths: Array; approvalPolicy: ApprovalPolicy; additionalWritableRoots: ReadonlyArray; config: AppConfig; }): Promise { const agent = new AgentLoop({ model: config.model, config: config, instructions: config.instructions, approvalPolicy, additionalWritableRoots, onItem: (item: ResponseItem) => { // eslint-disable-next-line no-console console.log(formatResponseItemForQuietMode(item)); }, onLoading: () => { /* intentionally ignored in quiet mode */ }, getCommandConfirmation: ( _command: Array, ): Promise => { // In quiet mode, default to NO_CONTINUE, except when in full-auto mode const reviewDecision = approvalPolicy === AutoApprovalMode.FULL_AUTO ? ReviewDecision.YES : ReviewDecision.NO_CONTINUE; return Promise.resolve({ review: reviewDecision }); }, onLastResponseId: () => { /* intentionally ignored in quiet mode */ }, }); const inputItem = await createInputItem(prompt, imagePaths); await agent.run([inputItem]); } const exit = () => { onExit(); process.exit(0); }; process.on("SIGINT", exit); process.on("SIGQUIT", exit); process.on("SIGTERM", exit); // --------------------------------------------------------------------------- // Fallback for Ctrl-C when stdin is in raw-mode // --------------------------------------------------------------------------- if (process.stdin.isTTY) { // Ensure we do not leave the terminal in raw mode if the user presses // Ctrl-C while some other component has focus and Ink is intercepting // input. Node does *not* emit a SIGINT in raw-mode, so we listen for the // corresponding byte (0x03) ourselves and trigger a graceful shutdown. const onRawData = (data: Buffer | string): void => { const str = Buffer.isBuffer(data) ? data.toString("utf8") : data; if (str === "\u0003") { exit(); } }; process.stdin.on("data", onRawData); } // Ensure terminal clean-up always runs, even when other code calls // `process.exit()` directly. process.once("exit", onExit);