## Description This PR addresses the following improvements: **Unify Prettier Version**: Currently, the Prettier version used in `/package.json` and `/codex-cli/package.json` are different. In this PR, we're updating both to use Prettier v3. - Prettier v3 introduces improved support for JavaScript and TypeScript. (e.g. the formatting scenario shown in the image below. This is more aligned with the TypeScript indentation standard). <img width="1126" alt="image" src="https://github.com/user-attachments/assets/6e237eb8-4553-4574-b336-ed9561c55370" /> **Add Prettier Auto-Formatting in lint-staged**: We've added a step to automatically run prettier --write on JavaScript and TypeScript files as part of the lint-staged process, before the ESLint checks. - This will help ensure that all committed code is properly formatted according to the project's Prettier configuration.
560 lines
18 KiB
JavaScript
560 lines
18 KiB
JavaScript
#!/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] <prompt>
|
||
$ codex completion <bash|zsh|fish>
|
||
|
||
Options
|
||
--version Print version and exit
|
||
|
||
-h, --help Show usage and exit
|
||
-m, --model <model> Model to use for completions (default: o4-mini)
|
||
-p, --provider <provider> Provider to use for completions (default: openai)
|
||
-i, --image <path> Path(s) to image files to include as input
|
||
-v, --view <rollout> 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 <path> Writable folder for sandbox in full-auto mode (can be specified multiple times)
|
||
-a, --approval-mode <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 <file> Include an additional markdown file at <file> as context
|
||
--full-stdout Do not truncate stdout/stderr from command outputs
|
||
--notify Enable desktop notifications for responses
|
||
|
||
--disable-response-storage Disable server‑side response storage (sends the
|
||
full conversation context with every request)
|
||
|
||
--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"] },
|
||
version: { type: "boolean", description: "Print version and exit" },
|
||
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",
|
||
},
|
||
|
||
disableResponseStorage: {
|
||
type: "boolean",
|
||
description:
|
||
"Disable server-side response storage (sends full conversation context with every request)",
|
||
},
|
||
|
||
// 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<string, string> = {
|
||
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);
|
||
|
||
// Set of providers that don't require API keys
|
||
const NO_API_KEY_REQUIRED = new Set(["ollama"]);
|
||
|
||
// Skip API key validation for providers that don't require an API key
|
||
if (!apiKey && !NO_API_KEY_REQUIRED.has(provider.toLowerCase())) {
|
||
// eslint-disable-next-line no-console
|
||
console.error(
|
||
`\n${chalk.red(`Missing ${provider} API key.`)}\n\n` +
|
||
`Set the environment variable ${chalk.bold(
|
||
`${provider.toUpperCase()}_API_KEY`,
|
||
)} ` +
|
||
`and re-run this command.\n` +
|
||
`${
|
||
provider.toLowerCase() === "openai"
|
||
? `You can create a key here: ${chalk.bold(
|
||
chalk.underline("https://platform.openai.com/account/api-keys"),
|
||
)}\n`
|
||
: provider.toLowerCase() === "gemini"
|
||
? `You can create a ${chalk.bold(
|
||
`${provider.toUpperCase()}_API_KEY`,
|
||
)} ` + `in the ${chalk.bold(`Google AI Studio`)}.\n`
|
||
: `You can create a ${chalk.bold(
|
||
`${provider.toUpperCase()}_API_KEY`,
|
||
)} ` + `in the ${chalk.bold(`${provider}`)} dashboard.\n`
|
||
}`,
|
||
);
|
||
process.exit(1);
|
||
}
|
||
|
||
config = {
|
||
apiKey,
|
||
...config,
|
||
model: model ?? config.model,
|
||
notify: Boolean(cli.flags.notify),
|
||
flexMode: Boolean(cli.flags.flexMode),
|
||
provider,
|
||
disableResponseStorage:
|
||
cli.flags.disableResponseStorage !== undefined
|
||
? Boolean(cli.flags.disableResponseStorage)
|
||
: config.disableResponseStorage,
|
||
};
|
||
|
||
// Check for updates after loading config. This is important because we write state file in
|
||
// the config dir.
|
||
try {
|
||
await checkForUpdates();
|
||
} catch {
|
||
// ignore
|
||
}
|
||
|
||
// 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<string> = (
|
||
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(
|
||
<App
|
||
prompt={prompt}
|
||
config={config}
|
||
rollout={rollout}
|
||
imagePaths={imagePaths}
|
||
approvalPolicy={approvalPolicy}
|
||
additionalWritableRoots={additionalWritableRoots}
|
||
fullStdout={Boolean(cli.flags.fullStdout)}
|
||
/>,
|
||
{
|
||
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 "<Image>";
|
||
}
|
||
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<string> = [];
|
||
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<string>;
|
||
approvalPolicy: ApprovalPolicy;
|
||
additionalWritableRoots: ReadonlyArray<string>;
|
||
config: AppConfig;
|
||
}): Promise<void> {
|
||
const agent = new AgentLoop({
|
||
model: config.model,
|
||
config: config,
|
||
instructions: config.instructions,
|
||
provider: config.provider,
|
||
approvalPolicy,
|
||
additionalWritableRoots,
|
||
disableResponseStorage: config.disableResponseStorage,
|
||
onItem: (item: ResponseItem) => {
|
||
// eslint-disable-next-line no-console
|
||
console.log(formatResponseItemForQuietMode(item));
|
||
},
|
||
onLoading: () => {
|
||
/* intentionally ignored in quiet mode */
|
||
},
|
||
getCommandConfirmation: (
|
||
_command: Array<string>,
|
||
): Promise<CommandConfirmation> => {
|
||
// 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);
|