2025-04-16 13:21:22 -07:00
|
|
|
|
#!/usr/bin/env node
|
2025-04-16 18:33:12 -04:00
|
|
|
|
import "dotenv/config";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-16 18:25:32 -04:00
|
|
|
|
// Hack to suppress deprecation warnings (punycode)
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
|
|
|
(process as any).noDeprecation = true;
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import type { AppRollout } from "./app";
|
2025-04-16 14:16:53 -07:00
|
|
|
|
import type { ApprovalPolicy } from "./approvals";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import type { CommandConfirmation } from "./utils/agent/agent-loop";
|
|
|
|
|
|
import type { AppConfig } from "./utils/config";
|
|
|
|
|
|
import type { ResponseItem } from "openai/resources/responses/responses";
|
2025-04-29 20:00:49 +05:30
|
|
|
|
import type { ReasoningEffort } from "openai/resources.mjs";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
import App from "./app";
|
2025-04-18 08:55:44 +05:30
|
|
|
|
import { runSinglePass } from "./cli-singlepass";
|
2025-05-16 12:28:22 -07:00
|
|
|
|
import SessionsOverlay from "./components/sessions-overlay.js";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import { AgentLoop } from "./utils/agent/agent-loop";
|
|
|
|
|
|
import { ReviewDecision } from "./utils/agent/review";
|
|
|
|
|
|
import { AutoApprovalMode } from "./utils/auto-approval-mode";
|
2025-04-19 08:00:45 +08:00
|
|
|
|
import { checkForUpdates } from "./utils/check-updates";
|
2025-04-16 21:01:33 -04:00
|
|
|
|
import {
|
|
|
|
|
|
loadConfig,
|
|
|
|
|
|
PRETTY_PRINT,
|
|
|
|
|
|
INSTRUCTIONS_FILEPATH,
|
|
|
|
|
|
} from "./utils/config";
|
2025-05-17 16:13:12 -07:00
|
|
|
|
import {
|
|
|
|
|
|
getApiKey as fetchApiKey,
|
|
|
|
|
|
maybeRedeemCredits,
|
|
|
|
|
|
} from "./utils/get-api-key";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import { createInputItem } from "./utils/input-utils";
|
2025-04-21 09:51:34 -04:00
|
|
|
|
import { initLogger } from "./utils/logger/log";
|
2025-04-20 23:59:34 -04:00
|
|
|
|
import { isModelSupportedForResponses } from "./utils/model-utils.js";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import { parseToolCall } from "./utils/parsers";
|
|
|
|
|
|
import { onExit, setInkRenderer } from "./utils/terminal";
|
|
|
|
|
|
import chalk from "chalk";
|
2025-04-16 21:01:33 -04:00
|
|
|
|
import { spawnSync } from "child_process";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import fs from "fs";
|
|
|
|
|
|
import { render } from "ink";
|
|
|
|
|
|
import meow from "meow";
|
2025-05-16 12:28:54 -07:00
|
|
|
|
import os from "os";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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>
|
2025-04-17 01:34:44 +02:00
|
|
|
|
$ codex completion <bash|zsh|fish>
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
Options
|
2025-04-22 09:28:21 -07:00
|
|
|
|
--version Print version and exit
|
|
|
|
|
|
|
2025-04-17 15:39:26 -07:00
|
|
|
|
-h, --help Show usage and exit
|
2025-05-16 08:04:00 -07:00
|
|
|
|
-m, --model <model> Model to use for completions (default: codex-mini-latest)
|
2025-04-21 17:53:09 -04:00
|
|
|
|
-p, --provider <provider> Provider to use for completions (default: openai)
|
2025-04-17 15:39:26 -07:00
|
|
|
|
-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
|
2025-05-16 12:28:22 -07:00
|
|
|
|
--history Browse previous sessions
|
2025-05-17 16:13:12 -07:00
|
|
|
|
--login Start a new sign in flow
|
|
|
|
|
|
--free Retry redeeming free credits
|
2025-04-17 15:39:26 -07:00
|
|
|
|
-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'
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
--auto-edit Automatically approve file edits; still prompt for commands
|
|
|
|
|
|
--full-auto Automatically approve edits and commands when executed in the sandbox
|
|
|
|
|
|
|
2025-05-10 15:57:49 -07:00
|
|
|
|
--no-project-doc Do not automatically include the repository's 'AGENTS.md'
|
2025-04-16 12:56:08 -04:00
|
|
|
|
--project-doc <file> Include an additional markdown file at <file> as context
|
|
|
|
|
|
--full-stdout Do not truncate stdout/stderr from command outputs
|
2025-04-17 16:19:26 -07:00
|
|
|
|
--notify Enable desktop notifications for responses
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-22 01:30:16 -07:00
|
|
|
|
--disable-response-storage Disable server‑side response storage (sends the
|
|
|
|
|
|
full conversation context with every request)
|
|
|
|
|
|
|
2025-04-18 22:15:01 -07:00
|
|
|
|
--flex-mode Use "flex-mode" processing mode for the request (only supported
|
|
|
|
|
|
with models o3 and o4-mini)
|
|
|
|
|
|
|
2025-05-10 15:58:59 -07:00
|
|
|
|
--reasoning <effort> Set the reasoning effort level (low, medium, high) (default: high)
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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"
|
2025-04-17 01:34:44 +02:00
|
|
|
|
$ codex completion bash
|
2025-04-16 12:56:08 -04:00
|
|
|
|
`,
|
|
|
|
|
|
{
|
|
|
|
|
|
importMeta: import.meta,
|
|
|
|
|
|
autoHelp: true,
|
|
|
|
|
|
flags: {
|
|
|
|
|
|
// misc
|
|
|
|
|
|
help: { type: "boolean", aliases: ["h"] },
|
2025-04-22 09:28:21 -07:00
|
|
|
|
version: { type: "boolean", description: "Print version and exit" },
|
2025-04-16 12:56:08 -04:00
|
|
|
|
view: { type: "string" },
|
2025-05-16 12:28:22 -07:00
|
|
|
|
history: { type: "boolean", description: "Browse previous sessions" },
|
2025-05-17 16:13:12 -07:00
|
|
|
|
login: { type: "boolean", description: "Force a new sign in flow" },
|
|
|
|
|
|
free: { type: "boolean", description: "Retry redeeming free credits" },
|
2025-04-16 12:56:08 -04:00
|
|
|
|
model: { type: "string", aliases: ["m"] },
|
2025-04-20 23:59:34 -04:00
|
|
|
|
provider: { type: "string", aliases: ["p"] },
|
2025-04-16 12:56:08 -04:00
|
|
|
|
image: { type: "string", isMultiple: true, aliases: ["i"] },
|
|
|
|
|
|
quiet: {
|
|
|
|
|
|
type: "boolean",
|
|
|
|
|
|
aliases: ["q"],
|
|
|
|
|
|
description: "Non-interactive quiet mode",
|
|
|
|
|
|
},
|
2025-04-16 21:01:33 -04:00
|
|
|
|
config: {
|
|
|
|
|
|
type: "boolean",
|
|
|
|
|
|
aliases: ["c"],
|
|
|
|
|
|
description: "Open the instructions file in your editor",
|
|
|
|
|
|
},
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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",
|
|
|
|
|
|
},
|
2025-04-17 15:39:26 -07:00
|
|
|
|
writableRoot: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
isMultiple: true,
|
|
|
|
|
|
aliases: ["w"],
|
|
|
|
|
|
description:
|
|
|
|
|
|
"Writable folder for sandbox in full-auto mode (can be specified multiple times)",
|
|
|
|
|
|
},
|
2025-04-16 12:56:08 -04:00
|
|
|
|
noProjectDoc: {
|
|
|
|
|
|
type: "boolean",
|
2025-05-10 15:57:49 -07:00
|
|
|
|
description: "Disable automatic inclusion of project-level AGENTS.md",
|
2025-04-16 12:56:08 -04:00
|
|
|
|
},
|
|
|
|
|
|
projectDoc: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
description: "Path to a markdown file to include as project doc",
|
|
|
|
|
|
},
|
2025-04-18 22:15:01 -07:00
|
|
|
|
flexMode: {
|
|
|
|
|
|
type: "boolean",
|
|
|
|
|
|
description:
|
|
|
|
|
|
"Enable the flex-mode service tier (only supported by models o3 and o4-mini)",
|
|
|
|
|
|
},
|
2025-04-16 12:56:08 -04:00
|
|
|
|
fullStdout: {
|
|
|
|
|
|
type: "boolean",
|
|
|
|
|
|
description:
|
|
|
|
|
|
"Disable truncation of command stdout/stderr messages (show everything)",
|
|
|
|
|
|
aliases: ["no-truncate"],
|
|
|
|
|
|
},
|
2025-04-29 20:00:49 +05:30
|
|
|
|
reasoning: {
|
|
|
|
|
|
type: "string",
|
|
|
|
|
|
description: "Set the reasoning effort level (low, medium, high)",
|
|
|
|
|
|
choices: ["low", "medium", "high"],
|
|
|
|
|
|
default: "high",
|
|
|
|
|
|
},
|
2025-04-17 16:19:26 -07:00
|
|
|
|
// Notification
|
|
|
|
|
|
notify: {
|
|
|
|
|
|
type: "boolean",
|
|
|
|
|
|
description: "Enable desktop notifications for responses",
|
|
|
|
|
|
},
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-22 01:30:16 -07:00
|
|
|
|
disableResponseStorage: {
|
|
|
|
|
|
type: "boolean",
|
|
|
|
|
|
description:
|
|
|
|
|
|
"Disable server-side response storage (sends full conversation context with every request)",
|
|
|
|
|
|
},
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// 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"],
|
2025-04-16 14:16:53 -07:00
|
|
|
|
description: `Run in full-context editing approach. The model is given the whole code
|
2025-04-16 12:56:08 -04:00
|
|
|
|
directory as context and performs changes in one go without acting.`,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-04-28 07:48:38 -07:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// Global flag handling
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2025-04-17 01:34:44 +02:00
|
|
|
|
// Handle 'completion' subcommand before any prompting or API calls
|
2025-04-16 21:01:33 -04:00
|
|
|
|
if (cli.input[0] === "completion") {
|
|
|
|
|
|
const shell = cli.input[1] || "bash";
|
|
|
|
|
|
const scripts: Record<string, string> = {
|
2025-04-17 01:34:44 +02:00
|
|
|
|
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
|
2025-04-22 02:45:49 +08:00
|
|
|
|
complete -c codex -a '(__fish_complete_path)' -d 'file path'`,
|
2025-04-17 01:34:44 +02:00
|
|
|
|
};
|
|
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-04-21 12:33:57 -04:00
|
|
|
|
|
|
|
|
|
|
// For --help, show help and exit.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
if (cli.flags.help) {
|
|
|
|
|
|
cli.showHelp();
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-21 12:33:57 -04:00
|
|
|
|
// For --config, open custom instructions file in editor and exit.
|
2025-04-16 21:01:33 -04:00
|
|
|
|
if (cli.flags.config) {
|
|
|
|
|
|
try {
|
2025-04-21 12:33:57 -04:00
|
|
|
|
loadConfig(); // Ensures the file is created if it doesn't already exit.
|
2025-04-16 21:01:33 -04:00
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore errors
|
|
|
|
|
|
}
|
2025-04-21 12:33:57 -04:00
|
|
|
|
|
2025-04-16 21:01:33 -04:00
|
|
|
|
const filePath = INSTRUCTIONS_FILEPATH;
|
|
|
|
|
|
const editor =
|
|
|
|
|
|
process.env["EDITOR"] || (process.platform === "win32" ? "notepad" : "vi");
|
|
|
|
|
|
spawnSync(editor, [filePath], { stdio: "inherit" });
|
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// API key handling
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
2025-04-20 23:59:34 -04:00
|
|
|
|
const fullContextMode = Boolean(cli.flags.fullContext);
|
|
|
|
|
|
let config = loadConfig(undefined, undefined, {
|
|
|
|
|
|
cwd: process.cwd(),
|
|
|
|
|
|
disableProjectDoc: Boolean(cli.flags.noProjectDoc),
|
2025-04-21 00:00:56 -07:00
|
|
|
|
projectDocPath: cli.flags.projectDoc,
|
2025-04-20 23:59:34 -04:00
|
|
|
|
isFullContext: fullContextMode,
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2025-05-16 12:28:22 -07:00
|
|
|
|
// `prompt` can be updated later when the user resumes a previous session
|
|
|
|
|
|
// via the `--history` flag. Therefore it must be declared with `let` rather
|
|
|
|
|
|
// than `const`.
|
|
|
|
|
|
let prompt = cli.input[0];
|
2025-04-20 23:59:34 -04:00
|
|
|
|
const model = cli.flags.model ?? config.model;
|
2025-04-21 00:00:56 -07:00
|
|
|
|
const imagePaths = cli.flags.image;
|
2025-04-21 12:33:57 -04:00
|
|
|
|
const provider = cli.flags.provider ?? config.provider ?? "openai";
|
2025-05-16 12:28:54 -07:00
|
|
|
|
|
|
|
|
|
|
const client = {
|
|
|
|
|
|
issuer: "https://auth.openai.com",
|
|
|
|
|
|
client_id: "app_EMoamEEZ73f0CkXaXp7hrann",
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
let apiKey = "";
|
2025-05-17 16:13:12 -07:00
|
|
|
|
let savedTokens:
|
|
|
|
|
|
| {
|
|
|
|
|
|
id_token?: string;
|
|
|
|
|
|
access_token?: string;
|
|
|
|
|
|
refresh_token: string;
|
|
|
|
|
|
}
|
|
|
|
|
|
| undefined;
|
2025-05-16 12:28:54 -07:00
|
|
|
|
|
|
|
|
|
|
// Try to load existing auth file if present
|
|
|
|
|
|
try {
|
|
|
|
|
|
const home = os.homedir();
|
|
|
|
|
|
const authDir = path.join(home, ".codex");
|
|
|
|
|
|
const authFile = path.join(authDir, "auth.json");
|
|
|
|
|
|
if (fs.existsSync(authFile)) {
|
|
|
|
|
|
const data = JSON.parse(fs.readFileSync(authFile, "utf-8"));
|
2025-05-17 16:13:12 -07:00
|
|
|
|
savedTokens = data.tokens;
|
2025-05-16 12:28:54 -07:00
|
|
|
|
const lastRefreshTime = data.last_refresh
|
|
|
|
|
|
? new Date(data.last_refresh).getTime()
|
|
|
|
|
|
: 0;
|
|
|
|
|
|
const expired = Date.now() - lastRefreshTime > 28 * 24 * 60 * 60 * 1000;
|
|
|
|
|
|
if (data.OPENAI_API_KEY && !expired) {
|
|
|
|
|
|
apiKey = data.OPENAI_API_KEY;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore errors
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-17 16:13:12 -07:00
|
|
|
|
if (cli.flags.login) {
|
|
|
|
|
|
apiKey = await fetchApiKey(client.issuer, client.client_id);
|
|
|
|
|
|
try {
|
|
|
|
|
|
const home = os.homedir();
|
|
|
|
|
|
const authDir = path.join(home, ".codex");
|
|
|
|
|
|
const authFile = path.join(authDir, "auth.json");
|
|
|
|
|
|
if (fs.existsSync(authFile)) {
|
|
|
|
|
|
const data = JSON.parse(fs.readFileSync(authFile, "utf-8"));
|
|
|
|
|
|
savedTokens = data.tokens;
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
/* ignore */
|
|
|
|
|
|
}
|
|
|
|
|
|
} else if (!apiKey) {
|
2025-05-16 12:28:54 -07:00
|
|
|
|
apiKey = await fetchApiKey(client.issuer, client.client_id);
|
|
|
|
|
|
}
|
|
|
|
|
|
// Ensure the API key is available as an environment variable for legacy code
|
|
|
|
|
|
process.env["OPENAI_API_KEY"] = apiKey;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-05-17 16:13:12 -07:00
|
|
|
|
if (cli.flags.free && savedTokens?.refresh_token) {
|
|
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
|
|
console.log(`${chalk.bold("codex --free")} attempting to redeem credits...`);
|
|
|
|
|
|
await maybeRedeemCredits(
|
|
|
|
|
|
client.issuer,
|
|
|
|
|
|
client.client_id,
|
|
|
|
|
|
savedTokens.refresh_token,
|
|
|
|
|
|
savedTokens.id_token,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-22 13:59:31 -04:00
|
|
|
|
// 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())) {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
|
|
console.error(
|
2025-04-21 12:33:57 -04:00
|
|
|
|
`\n${chalk.red(`Missing ${provider} API key.`)}\n\n` +
|
2025-04-22 22:25:08 +05:30
|
|
|
|
`Set the environment variable ${chalk.bold(
|
|
|
|
|
|
`${provider.toUpperCase()}_API_KEY`,
|
|
|
|
|
|
)} ` +
|
2025-04-16 12:56:08 -04:00
|
|
|
|
`and re-run this command.\n` +
|
2025-04-22 22:25:08 +05:30
|
|
|
|
`${
|
|
|
|
|
|
provider.toLowerCase() === "openai"
|
|
|
|
|
|
? `You can create a key here: ${chalk.bold(
|
|
|
|
|
|
chalk.underline("https://platform.openai.com/account/api-keys"),
|
|
|
|
|
|
)}\n`
|
2025-04-24 17:03:34 +05:30
|
|
|
|
: provider.toLowerCase() === "gemini"
|
2025-04-25 22:21:50 +08:00
|
|
|
|
? `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`
|
2025-04-22 22:25:08 +05:30
|
|
|
|
}`,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-29 13:10:16 -04:00
|
|
|
|
const flagPresent = Object.hasOwn(cli.flags, "disableResponseStorage");
|
|
|
|
|
|
|
|
|
|
|
|
const disableResponseStorage = flagPresent
|
|
|
|
|
|
? Boolean(cli.flags.disableResponseStorage) // value user actually passed
|
|
|
|
|
|
: (config.disableResponseStorage ?? false); // fall back to YAML, default to false
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
config = {
|
|
|
|
|
|
apiKey,
|
|
|
|
|
|
...config,
|
|
|
|
|
|
model: model ?? config.model,
|
2025-04-17 16:19:26 -07:00
|
|
|
|
notify: Boolean(cli.flags.notify),
|
2025-04-29 20:00:49 +05:30
|
|
|
|
reasoningEffort:
|
2025-05-14 08:38:41 -07:00
|
|
|
|
(cli.flags.reasoning as ReasoningEffort | undefined) ?? "medium",
|
2025-05-10 16:18:20 -07:00
|
|
|
|
flexMode: cli.flags.flexMode || (config.flexMode ?? false),
|
2025-04-20 23:59:34 -04:00
|
|
|
|
provider,
|
2025-04-29 13:10:16 -04:00
|
|
|
|
disableResponseStorage,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-21 12:33:57 -04:00
|
|
|
|
// Check for updates after loading config. This is important because we write state file in
|
|
|
|
|
|
// the config dir.
|
2025-04-23 15:21:00 -07:00
|
|
|
|
try {
|
|
|
|
|
|
await checkForUpdates();
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// ignore
|
|
|
|
|
|
}
|
2025-04-18 22:15:01 -07:00
|
|
|
|
|
2025-04-21 12:33:57 -04:00
|
|
|
|
// For --flex-mode, validate and exit if incorrect.
|
2025-05-10 16:18:20 -07:00
|
|
|
|
if (config.flexMode) {
|
2025-04-18 22:15:01 -07:00
|
|
|
|
const allowedFlexModels = new Set(["o3", "o4-mini"]);
|
|
|
|
|
|
if (!allowedFlexModels.has(config.model)) {
|
2025-05-10 16:18:20 -07:00
|
|
|
|
if (cli.flags.flexMode) {
|
|
|
|
|
|
// 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);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
config.flexMode = false;
|
|
|
|
|
|
}
|
2025-04-18 22:15:01 -07:00
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-19 08:00:45 +08:00
|
|
|
|
|
2025-04-20 23:59:34 -04:00
|
|
|
|
if (
|
2025-04-21 12:33:57 -04:00
|
|
|
|
!(await isModelSupportedForResponses(provider, config.model)) &&
|
2025-04-20 23:59:34 -04:00
|
|
|
|
(!provider || provider.toLowerCase() === "openai")
|
|
|
|
|
|
) {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
|
|
console.error(
|
|
|
|
|
|
`The model "${config.model}" does not appear in the list of models ` +
|
2025-04-21 12:33:57 -04:00
|
|
|
|
`available to your account. Double-check the spelling (use\n` +
|
2025-04-16 12:56:08 -04:00
|
|
|
|
` openai models list\n` +
|
|
|
|
|
|
`to see the full list) or choose another model with the --model flag.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let rollout: AppRollout | undefined;
|
|
|
|
|
|
|
2025-05-16 12:28:22 -07:00
|
|
|
|
// For --history, show session selector and optionally update prompt or rollout.
|
|
|
|
|
|
if (cli.flags.history) {
|
|
|
|
|
|
const result: { path: string; mode: "view" | "resume" } | null =
|
|
|
|
|
|
await new Promise((resolve) => {
|
|
|
|
|
|
const instance = render(
|
|
|
|
|
|
React.createElement(SessionsOverlay, {
|
|
|
|
|
|
onView: (p: string) => {
|
|
|
|
|
|
instance.unmount();
|
|
|
|
|
|
resolve({ path: p, mode: "view" });
|
|
|
|
|
|
},
|
|
|
|
|
|
onResume: (p: string) => {
|
|
|
|
|
|
instance.unmount();
|
|
|
|
|
|
resolve({ path: p, mode: "resume" });
|
|
|
|
|
|
},
|
|
|
|
|
|
onExit: () => {
|
|
|
|
|
|
instance.unmount();
|
|
|
|
|
|
resolve(null);
|
|
|
|
|
|
},
|
|
|
|
|
|
}),
|
|
|
|
|
|
);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (!result) {
|
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (result.mode === "view") {
|
|
|
|
|
|
try {
|
|
|
|
|
|
const content = fs.readFileSync(result.path, "utf-8");
|
|
|
|
|
|
rollout = JSON.parse(content) as AppRollout;
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
|
|
console.error("Error reading session file:", error);
|
|
|
|
|
|
process.exit(1);
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
prompt = `Resume this session: ${result.path}`;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-21 12:33:57 -04:00
|
|
|
|
// For --view, optionally load an existing rollout from disk, display it and exit.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-21 12:33:57 -04:00
|
|
|
|
// For --fullcontext, run the separate cli entrypoint and exit.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
if (fullContextMode) {
|
|
|
|
|
|
await runSinglePass({
|
|
|
|
|
|
originalPrompt: prompt,
|
|
|
|
|
|
config,
|
|
|
|
|
|
rootPath: process.cwd(),
|
|
|
|
|
|
});
|
|
|
|
|
|
onExit();
|
|
|
|
|
|
process.exit(0);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-17 15:39:26 -07:00
|
|
|
|
// Ensure that all values in additionalWritableRoots are absolute paths.
|
|
|
|
|
|
const additionalWritableRoots: ReadonlyArray<string> = (
|
|
|
|
|
|
cli.flags.writableRoot ?? []
|
|
|
|
|
|
).map((p) => path.resolve(p));
|
|
|
|
|
|
|
2025-04-21 12:33:57 -04:00
|
|
|
|
// For --quiet, run the cli without user interactions and exit.
|
|
|
|
|
|
if (cli.flags.quiet) {
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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);
|
|
|
|
|
|
}
|
2025-04-20 02:00:33 +02:00
|
|
|
|
|
|
|
|
|
|
// 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"
|
2025-04-25 22:21:50 +08:00
|
|
|
|
? AutoApprovalMode.AUTO_EDIT
|
|
|
|
|
|
: config.approvalMode || AutoApprovalMode.SUGGEST;
|
2025-04-20 02:00:33 +02:00
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
await runQuietMode({
|
2025-04-21 00:00:56 -07:00
|
|
|
|
prompt,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
imagePaths: imagePaths || [],
|
2025-04-20 02:00:33 +02:00
|
|
|
|
approvalPolicy: quietApprovalPolicy,
|
2025-04-17 15:39:26 -07:00
|
|
|
|
additionalWritableRoots,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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.
|
2025-04-19 10:25:25 -04:00
|
|
|
|
// 4. config.approvalMode - use the approvalMode setting from ~/.codex/config.json.
|
|
|
|
|
|
// 5. Default – suggest mode (prompt for everything).
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
const approvalPolicy: ApprovalPolicy =
|
|
|
|
|
|
cli.flags.fullAuto || cli.flags.approvalMode === "full-auto"
|
|
|
|
|
|
? AutoApprovalMode.FULL_AUTO
|
2025-04-16 22:23:18 +02:00
|
|
|
|
: cli.flags.autoEdit || cli.flags.approvalMode === "auto-edit"
|
2025-04-25 22:21:50 +08:00
|
|
|
|
? AutoApprovalMode.AUTO_EDIT
|
|
|
|
|
|
: config.approvalMode || AutoApprovalMode.SUGGEST;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
const instance = render(
|
|
|
|
|
|
<App
|
|
|
|
|
|
prompt={prompt}
|
|
|
|
|
|
config={config}
|
|
|
|
|
|
rollout={rollout}
|
|
|
|
|
|
imagePaths={imagePaths}
|
|
|
|
|
|
approvalPolicy={approvalPolicy}
|
2025-04-17 15:39:26 -07:00
|
|
|
|
additionalWritableRoots={additionalWritableRoots}
|
2025-04-21 12:33:57 -04:00
|
|
|
|
fullStdout={Boolean(cli.flags.fullStdout)}
|
2025-04-16 12:56:08 -04:00
|
|
|
|
/>,
|
|
|
|
|
|
{
|
|
|
|
|
|
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,
|
2025-04-17 15:39:26 -07:00
|
|
|
|
additionalWritableRoots,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
config,
|
|
|
|
|
|
}: {
|
|
|
|
|
|
prompt: string;
|
|
|
|
|
|
imagePaths: Array<string>;
|
|
|
|
|
|
approvalPolicy: ApprovalPolicy;
|
2025-04-17 15:39:26 -07:00
|
|
|
|
additionalWritableRoots: ReadonlyArray<string>;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
config: AppConfig;
|
|
|
|
|
|
}): Promise<void> {
|
|
|
|
|
|
const agent = new AgentLoop({
|
|
|
|
|
|
model: config.model,
|
|
|
|
|
|
config: config,
|
|
|
|
|
|
instructions: config.instructions,
|
2025-04-23 00:12:18 -05:00
|
|
|
|
provider: config.provider,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
approvalPolicy,
|
2025-04-17 15:39:26 -07:00
|
|
|
|
additionalWritableRoots,
|
2025-04-22 01:30:16 -07:00
|
|
|
|
disableResponseStorage: config.disableResponseStorage,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
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> => {
|
2025-04-20 02:00:33 +02:00
|
|
|
|
// 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 });
|
2025-04-16 12:56:08 -04:00
|
|
|
|
},
|
|
|
|
|
|
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);
|
|
|
|
|
|
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
2025-04-21 12:33:57 -04:00
|
|
|
|
// Fallback for Ctrl-C when stdin is in raw-mode
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
if (process.stdin.isTTY) {
|
|
|
|
|
|
// Ensure we do not leave the terminal in raw mode if the user presses
|
2025-04-21 12:33:57 -04:00
|
|
|
|
// 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
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-21 12:33:57 -04:00
|
|
|
|
// Ensure terminal clean-up always runs, even when other code calls
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// `process.exit()` directly.
|
|
|
|
|
|
process.once("exit", onExit);
|