#!/usr/bin/env node import type { AppRollout } from "./app"; import type { CommandConfirmation } from "./utils/agent/agent-loop"; import type { AppConfig } from "./utils/config"; import type { ApprovalPolicy } from "@lib/approvals"; 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 { initLogger } from "./utils/agent/log"; import { ReviewDecision } from "./utils/agent/review"; import { AutoApprovalMode } from "./utils/auto-approval-mode"; import { loadConfig, PRETTY_PRINT } from "./utils/config"; import { createInputItem } from "./utils/input-utils"; import { isModelSupportedForResponses, preloadModels, } from "./utils/model-utils.js"; import { parseToolCall } from "./utils/parsers"; import { onExit, setInkRenderer } from "./utils/terminal"; import chalk from "chalk"; 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] 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 -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 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" `, { importMeta: import.meta, autoHelp: true, flags: { // misc help: { type: "boolean", aliases: ["h"] }, view: { type: "string" }, model: { type: "string", aliases: ["m"] }, image: { type: "string", isMultiple: true, aliases: ["i"] }, quiet: { type: "boolean", aliases: ["q"], description: "Non-interactive quiet mode", }, 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", }, 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", }, fullStdout: { type: "boolean", description: "Disable truncation of command stdout/stderr messages (show everything)", aliases: ["no-truncate"], }, // 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.`, }, }, }, ); if (cli.flags.help) { cli.showHelp(); } // --------------------------------------------------------------------------- // API key handling // --------------------------------------------------------------------------- const apiKey = process.env["OPENAI_API_KEY"]; if (!apiKey) { // eslint-disable-next-line no-console console.error( `\n${chalk.red("Missing OpenAI 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); } const fullContextMode = Boolean(cli.flags.fullContext); let config = loadConfig(undefined, undefined, { cwd: process.cwd(), disableProjectDoc: Boolean(cli.flags.noProjectDoc), projectDocPath: cli.flags.projectDoc as string | undefined, isFullContext: fullContextMode, }); const prompt = cli.input[0]; const model = cli.flags.model; const imagePaths = cli.flags.image as Array | undefined; config = { apiKey, ...config, model: model ?? config.model, }; if (!(await isModelSupportedForResponses(config.model))) { // 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; 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); } } // If we are running in --fullcontext mode, do that and exit. if (fullContextMode) { await runSinglePass({ originalPrompt: prompt, config, rootPath: process.cwd(), }); onExit(); process.exit(0); } // If we are running in --quiet mode, do that and exit. const quietMode = Boolean(cli.flags.quiet); const autoApproveEverything = Boolean( cli.flags.dangerouslyAutoApproveEverything, ); const fullStdout = Boolean(cli.flags.fullStdout); if (quietMode) { 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); } await runQuietMode({ prompt: prompt as string, imagePaths: imagePaths || [], approvalPolicy: autoApproveEverything ? AutoApprovalMode.FULL_AUTO : AutoApprovalMode.SUGGEST, 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. Default – suggest mode (prompt for everything). const approvalPolicy: ApprovalPolicy = cli.flags.fullAuto || cli.flags.approvalMode === "full-auto" ? AutoApprovalMode.FULL_AUTO : cli.flags.autoEdit ? AutoApprovalMode.AUTO_EDIT : AutoApprovalMode.SUGGEST; preloadModels(); 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, config, }: { prompt: string; imagePaths: Array; approvalPolicy: ApprovalPolicy; config: AppConfig; }): Promise { const agent = new AgentLoop({ model: config.model, config: config, instructions: config.instructions, approvalPolicy, onItem: (item: ResponseItem) => { // eslint-disable-next-line no-console console.log(formatResponseItemForQuietMode(item)); }, onLoading: () => { /* intentionally ignored in quiet mode */ }, getCommandConfirmation: ( _command: Array, ): Promise => { return Promise.resolve({ review: ReviewDecision.NO_CONTINUE }); }, 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);