2025-04-16 12:56:08 -04:00
|
|
|
|
// NOTE: We intentionally point the TypeScript import at the source file
|
|
|
|
|
|
// (`./auto-approval-mode.ts`) instead of the emitted `.js` bundle. This makes
|
|
|
|
|
|
// the module resolvable when the project is executed via `ts-node`, which
|
|
|
|
|
|
// resolves *source* paths rather than built artefacts. During a production
|
|
|
|
|
|
// build the TypeScript compiler will automatically rewrite the path to
|
|
|
|
|
|
// `./auto-approval-mode.js`, so the change is completely transparent for the
|
|
|
|
|
|
// compiled `dist/` output used by the published CLI.
|
|
|
|
|
|
|
|
|
|
|
|
import type { FullAutoErrorMode } from "./auto-approval-mode.js";
|
2025-04-29 20:00:49 +05:30
|
|
|
|
import type { ReasoningEffort } from "openai/resources.mjs";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
import { AutoApprovalMode } from "./auto-approval-mode.js";
|
2025-04-21 09:51:34 -04:00
|
|
|
|
import { log } from "./logger/log.js";
|
2025-04-20 23:59:34 -04:00
|
|
|
|
import { providers } from "./providers.js";
|
2025-04-26 19:13:30 +02:00
|
|
|
|
import { config as loadDotenv } from "dotenv";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
|
|
|
|
import { load as loadYaml, dump as dumpYaml } from "js-yaml";
|
|
|
|
|
|
import { homedir } from "os";
|
|
|
|
|
|
import { dirname, join, extname, resolve as resolvePath } from "path";
|
|
|
|
|
|
|
2025-04-26 19:13:30 +02:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
// User‑wide environment config (~/.codex.env)
|
|
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
// Load a user‑level dotenv file **after** process.env and any project‑local
|
|
|
|
|
|
// .env file (loaded via "dotenv/config" in cli.tsx) are in place. We rely on
|
|
|
|
|
|
// dotenv's default behaviour of *not* overriding existing variables so that
|
|
|
|
|
|
// the precedence order becomes:
|
|
|
|
|
|
// 1. Explicit environment variables
|
|
|
|
|
|
// 2. Project‑local .env (handled in cli.tsx)
|
|
|
|
|
|
// 3. User‑wide ~/.codex.env (loaded here)
|
|
|
|
|
|
// This guarantees that users can still override the global key on a per‑project
|
|
|
|
|
|
// basis while enjoying the convenience of a persistent default.
|
|
|
|
|
|
|
|
|
|
|
|
// Skip when running inside Vitest to avoid interfering with the FS mocks used
|
|
|
|
|
|
// by tests that stub out `fs` *after* importing this module.
|
|
|
|
|
|
const USER_WIDE_CONFIG_PATH = join(homedir(), ".codex.env");
|
|
|
|
|
|
|
|
|
|
|
|
const isVitest =
|
|
|
|
|
|
typeof (globalThis as { vitest?: unknown }).vitest !== "undefined";
|
|
|
|
|
|
|
|
|
|
|
|
if (!isVitest) {
|
|
|
|
|
|
loadDotenv({ path: USER_WIDE_CONFIG_PATH });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-16 08:04:00 -07:00
|
|
|
|
export const DEFAULT_AGENTIC_MODEL = "codex-mini-latest";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
export const DEFAULT_FULL_CONTEXT_MODEL = "gpt-4.1";
|
|
|
|
|
|
export const DEFAULT_APPROVAL_MODE = AutoApprovalMode.SUGGEST;
|
|
|
|
|
|
export const DEFAULT_INSTRUCTIONS = "";
|
|
|
|
|
|
|
2025-05-05 22:56:55 +05:30
|
|
|
|
// Default shell output limits
|
|
|
|
|
|
export const DEFAULT_SHELL_MAX_BYTES = 1024 * 10; // 10 KB
|
|
|
|
|
|
export const DEFAULT_SHELL_MAX_LINES = 256;
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
export const CONFIG_DIR = join(homedir(), ".codex");
|
|
|
|
|
|
export const CONFIG_JSON_FILEPATH = join(CONFIG_DIR, "config.json");
|
|
|
|
|
|
export const CONFIG_YAML_FILEPATH = join(CONFIG_DIR, "config.yaml");
|
|
|
|
|
|
export const CONFIG_YML_FILEPATH = join(CONFIG_DIR, "config.yml");
|
|
|
|
|
|
|
|
|
|
|
|
// Keep the original constant name for backward compatibility, but point it at
|
|
|
|
|
|
// the default JSON path. Code that relies on this constant will continue to
|
|
|
|
|
|
// work unchanged.
|
|
|
|
|
|
export const CONFIG_FILEPATH = CONFIG_JSON_FILEPATH;
|
|
|
|
|
|
export const INSTRUCTIONS_FILEPATH = join(CONFIG_DIR, "instructions.md");
|
|
|
|
|
|
|
|
|
|
|
|
export const OPENAI_TIMEOUT_MS =
|
|
|
|
|
|
parseInt(process.env["OPENAI_TIMEOUT_MS"] || "0", 10) || undefined;
|
|
|
|
|
|
export const OPENAI_BASE_URL = process.env["OPENAI_BASE_URL"] || "";
|
|
|
|
|
|
export let OPENAI_API_KEY = process.env["OPENAI_API_KEY"] || "";
|
2025-04-29 20:00:49 +05:30
|
|
|
|
|
Adds Azure OpenAI support (#769)
## Summary
This PR introduces support for Azure OpenAI as a provider within the
Codex CLI. Users can now configure the tool to leverage their Azure
OpenAI deployments by specifying `"azure"` as the provider in
`config.json` and setting the corresponding `AZURE_OPENAI_API_KEY` and
`AZURE_OPENAI_API_VERSION` environment variables. This functionality is
added alongside the existing provider options (OpenAI, OpenRouter,
etc.).
Related to #92
**Note:** This PR is currently in **Draft** status because tests on the
`main` branch are failing. It will be marked as ready for review once
the `main` branch is stable and tests are passing.
---
## What’s Changed
- **Configuration (`config.ts`, `providers.ts`, `README.md`):**
- Added `"azure"` to the supported `providers` list in `providers.ts`,
specifying its name, default base URL structure, and environment
variable key (`AZURE_OPENAI_API_KEY`).
- Defined the `AZURE_OPENAI_API_VERSION` environment variable in
`config.ts` with a default value (`2025-03-01-preview`).
- Updated `README.md` to:
- Include "azure" in the list of providers.
- Add a configuration section for Azure OpenAI, detailing the required
environment variables (`AZURE_OPENAI_API_KEY`,
`AZURE_OPENAI_API_VERSION`) with examples.
- **Client Instantiation (`terminal-chat.tsx`, `singlepass-cli-app.tsx`,
`agent-loop.ts`, `compact-summary.ts`, `model-utils.ts`):**
- Modified various components and utility functions where the OpenAI
client is initialized.
- Added conditional logic to check if the configured `provider` is
`"azure"`.
- If the provider is Azure, the `AzureOpenAI` client from the `openai`
package is instantiated, using the configured `baseURL`, `apiKey` (from
`AZURE_OPENAI_API_KEY`), and `apiVersion` (from
`AZURE_OPENAI_API_VERSION`).
- Otherwise, the standard `OpenAI` client is instantiated as before.
- **Dependencies:**
- Relies on the `openai` package's built-in support for `AzureOpenAI`.
No *new* external dependencies were added specifically for this Azure
implementation beyond the `openai` package itself.
---
## How to Test
*This has been tested locally and confirmed working with Azure OpenAI.*
1. **Configure `config.json`:**
Ensure your `~/.codex/config.json` (or project-specific config) includes
Azure and sets it as the active provider:
```json
{
"providers": {
// ... other providers
"azure": {
"name": "AzureOpenAI",
"baseURL": "https://YOUR_RESOURCE_NAME.openai.azure.com", // Replace
with your Azure endpoint
"envKey": "AZURE_OPENAI_API_KEY"
}
},
"provider": "azure", // Set Azure as the active provider
"model": "o4-mini" // Use your Azure deployment name here
// ... other config settings
}
```
2. **Set up Environment Variables:**
```bash
# Set the API Key for your Azure OpenAI resource
export AZURE_OPENAI_API_KEY="your-azure-api-key-here"
# Set the API Version (Optional - defaults to `2025-03-01-preview` if
not set)
# Ensure this version is supported by your Azure deployment and endpoint
export AZURE_OPENAI_API_VERSION="2025-03-01-preview"
```
3. **Get the Codex CLI by building from this PR branch:**
Clone your fork, checkout this branch (`feat/azure-openai`), navigate to
`codex-cli`, and build:
```bash
# cd /path/to/your/fork/codex
git checkout feat/azure-openai # Or your branch name
cd codex-cli
corepack enable
pnpm install
pnpm build
```
4. **Invoke Codex:**
Run the locally built CLI using `node` from the `codex-cli` directory:
```bash
node ./dist/cli.js "Explain the purpose of this PR"
```
*(Alternatively, if you ran `pnpm link` after building, you can use
`codex "Explain the purpose of this PR"` from anywhere)*.
5. **Verify:** Confirm that the command executes successfully and
interacts with your configured Azure OpenAI deployment.
---
## Tests
- [x] Tested locally against an Azure OpenAI deployment using API Key
authentication. Basic commands and interactions confirmed working.
---
## Checklist
- [x] Added Azure provider details to configuration files
(`providers.ts`, `config.ts`).
- [x] Implemented conditional `AzureOpenAI` client initialization based
on provider setting.
- [x] Ensured `apiVersion` is passed correctly to the Azure client.
- [x] Updated `README.md` with Azure OpenAI setup instructions.
- [x] Manually tested core functionality against a live Azure OpenAI
endpoint.
- [x] Add/update automated tests for the Azure code path (pending `main`
stability).
cc @theabhinavdas @nikodem-wrona @fouad-openai @tibo-openai (adjust as
needed)
---
I have read the CLA Document and I hereby sign the CLA
2025-05-09 18:11:32 -07:00
|
|
|
|
export const AZURE_OPENAI_API_VERSION =
|
|
|
|
|
|
process.env["AZURE_OPENAI_API_VERSION"] || "2025-03-01-preview";
|
|
|
|
|
|
|
2025-04-29 20:00:49 +05:30
|
|
|
|
export const DEFAULT_REASONING_EFFORT = "high";
|
2025-04-25 22:22:42 +05:30
|
|
|
|
export const OPENAI_ORGANIZATION = process.env["OPENAI_ORGANIZATION"] || "";
|
|
|
|
|
|
export const OPENAI_PROJECT = process.env["OPENAI_PROJECT"] || "";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-28 07:48:38 -07:00
|
|
|
|
// Can be set `true` when Codex is running in an environment that is marked as already
|
2025-05-14 12:39:49 -04:00
|
|
|
|
// considered sufficiently locked-down so that we allow running without an explicit sandbox.
|
2025-04-27 17:04:47 -07:00
|
|
|
|
export const CODEX_UNSAFE_ALLOW_NO_SANDBOX = Boolean(
|
|
|
|
|
|
process.env["CODEX_UNSAFE_ALLOW_NO_SANDBOX"] || "",
|
|
|
|
|
|
);
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
export function setApiKey(apiKey: string): void {
|
|
|
|
|
|
OPENAI_API_KEY = apiKey;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-22 10:51:26 -04:00
|
|
|
|
export function getBaseUrl(provider: string = "openai"): string | undefined {
|
2025-04-23 13:45:56 +08:00
|
|
|
|
// Check for a PROVIDER-specific override: e.g. OPENAI_BASE_URL or OLLAMA_BASE_URL.
|
2025-04-22 22:05:48 +03:00
|
|
|
|
const envKey = `${provider.toUpperCase()}_BASE_URL`;
|
|
|
|
|
|
if (process.env[envKey]) {
|
|
|
|
|
|
return process.env[envKey];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-23 13:45:56 +08:00
|
|
|
|
// Get providers config from config file.
|
|
|
|
|
|
const config = loadConfig();
|
|
|
|
|
|
const providersConfig = config.providers ?? providers;
|
|
|
|
|
|
const providerInfo = providersConfig[provider.toLowerCase()];
|
2025-04-20 23:59:34 -04:00
|
|
|
|
if (providerInfo) {
|
|
|
|
|
|
return providerInfo.baseURL;
|
|
|
|
|
|
}
|
2025-04-22 10:51:26 -04:00
|
|
|
|
|
2025-04-23 13:45:56 +08:00
|
|
|
|
// If the provider not found in the providers list and `OPENAI_BASE_URL` is set, use it.
|
2025-04-22 10:51:26 -04:00
|
|
|
|
if (OPENAI_BASE_URL !== "") {
|
|
|
|
|
|
return OPENAI_BASE_URL;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-23 13:45:56 +08:00
|
|
|
|
// We tried.
|
2025-04-20 23:59:34 -04:00
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-22 10:51:26 -04:00
|
|
|
|
export function getApiKey(provider: string = "openai"): string | undefined {
|
2025-04-23 13:45:56 +08:00
|
|
|
|
const config = loadConfig();
|
|
|
|
|
|
const providersConfig = config.providers ?? providers;
|
|
|
|
|
|
const providerInfo = providersConfig[provider.toLowerCase()];
|
2025-04-20 23:59:34 -04:00
|
|
|
|
if (providerInfo) {
|
|
|
|
|
|
if (providerInfo.name === "Ollama") {
|
|
|
|
|
|
return process.env[providerInfo.envKey] ?? "dummy";
|
|
|
|
|
|
}
|
|
|
|
|
|
return process.env[providerInfo.envKey];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-16 12:28:54 -07:00
|
|
|
|
// Checking `PROVIDER_API_KEY` feels more intuitive with a custom provider.
|
2025-04-25 02:08:19 +08:00
|
|
|
|
const customApiKey = process.env[`${provider.toUpperCase()}_API_KEY`];
|
|
|
|
|
|
if (customApiKey) {
|
|
|
|
|
|
return customApiKey;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-22 10:51:26 -04:00
|
|
|
|
// If the provider not found in the providers list and `OPENAI_API_KEY` is set, use it
|
|
|
|
|
|
if (OPENAI_API_KEY !== "") {
|
|
|
|
|
|
return OPENAI_API_KEY;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-23 13:45:56 +08:00
|
|
|
|
// We tried.
|
2025-04-20 23:59:34 -04:00
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-13 09:45:46 -07:00
|
|
|
|
export type FileOpenerScheme = "vscode" | "cursor" | "windsurf";
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// Represents config as persisted in config.json.
|
|
|
|
|
|
export type StoredConfig = {
|
|
|
|
|
|
model?: string;
|
2025-04-20 23:59:34 -04:00
|
|
|
|
provider?: string;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
approvalMode?: AutoApprovalMode;
|
|
|
|
|
|
fullAutoErrorMode?: FullAutoErrorMode;
|
|
|
|
|
|
memory?: MemoryConfig;
|
2025-04-17 16:19:26 -07:00
|
|
|
|
/** Whether to enable desktop notifications for responses */
|
|
|
|
|
|
notify?: boolean;
|
2025-04-22 01:30:16 -07:00
|
|
|
|
/** Disable server-side response storage (send full transcript each request) */
|
|
|
|
|
|
disableResponseStorage?: boolean;
|
2025-05-10 16:18:20 -07:00
|
|
|
|
flexMode?: boolean;
|
2025-04-23 13:45:56 +08:00
|
|
|
|
providers?: Record<string, { name: string; baseURL: string; envKey: string }>;
|
2025-04-17 21:41:54 +02:00
|
|
|
|
history?: {
|
|
|
|
|
|
maxSize?: number;
|
|
|
|
|
|
saveHistory?: boolean;
|
|
|
|
|
|
sensitivePatterns?: Array<string>;
|
|
|
|
|
|
};
|
2025-05-05 22:56:55 +05:30
|
|
|
|
tools?: {
|
|
|
|
|
|
shell?: {
|
|
|
|
|
|
maxBytes?: number;
|
|
|
|
|
|
maxLines?: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
2025-04-29 20:00:49 +05:30
|
|
|
|
/** User-defined safe commands */
|
|
|
|
|
|
safeCommands?: Array<string>;
|
|
|
|
|
|
reasoningEffort?: ReasoningEffort;
|
2025-05-13 09:45:46 -07:00
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
|
* URI-based file opener. This is used when linking code references in
|
|
|
|
|
|
* terminal output.
|
|
|
|
|
|
*/
|
|
|
|
|
|
fileOpener?: FileOpenerScheme;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Minimal config written on first run. An *empty* model string ensures that
|
|
|
|
|
|
// we always fall back to DEFAULT_MODEL on load, so updates to the default keep
|
|
|
|
|
|
// propagating to existing users until they explicitly set a model.
|
|
|
|
|
|
export const EMPTY_STORED_CONFIG: StoredConfig = { model: "" };
|
|
|
|
|
|
|
2025-04-26 19:13:30 +02:00
|
|
|
|
// Pre‑stringified JSON variant so we don't stringify repeatedly.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
const EMPTY_CONFIG_JSON = JSON.stringify(EMPTY_STORED_CONFIG, null, 2) + "\n";
|
|
|
|
|
|
|
|
|
|
|
|
export type MemoryConfig = {
|
|
|
|
|
|
enabled: boolean;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-16 10:32:45 -07:00
|
|
|
|
// Represents full runtime config, including loaded instructions.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
export type AppConfig = {
|
|
|
|
|
|
apiKey?: string;
|
|
|
|
|
|
model: string;
|
2025-04-20 23:59:34 -04:00
|
|
|
|
provider?: string;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
instructions: string;
|
2025-04-19 10:25:25 -04:00
|
|
|
|
approvalMode?: AutoApprovalMode;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
fullAutoErrorMode?: FullAutoErrorMode;
|
|
|
|
|
|
memory?: MemoryConfig;
|
2025-04-29 20:00:49 +05:30
|
|
|
|
reasoningEffort?: ReasoningEffort;
|
2025-04-17 16:19:26 -07:00
|
|
|
|
/** Whether to enable desktop notifications for responses */
|
2025-04-25 02:08:52 +08:00
|
|
|
|
notify?: boolean;
|
2025-04-18 22:15:01 -07:00
|
|
|
|
|
2025-04-22 01:30:16 -07:00
|
|
|
|
/** Disable server-side response storage (send full transcript each request) */
|
|
|
|
|
|
disableResponseStorage?: boolean;
|
|
|
|
|
|
|
2025-04-18 22:15:01 -07:00
|
|
|
|
/** Enable the "flex-mode" processing mode for supported models (o3, o4-mini) */
|
|
|
|
|
|
flexMode?: boolean;
|
2025-04-23 13:45:56 +08:00
|
|
|
|
providers?: Record<string, { name: string; baseURL: string; envKey: string }>;
|
2025-04-17 21:41:54 +02:00
|
|
|
|
history?: {
|
|
|
|
|
|
maxSize: number;
|
|
|
|
|
|
saveHistory: boolean;
|
|
|
|
|
|
sensitivePatterns: Array<string>;
|
|
|
|
|
|
};
|
2025-05-05 22:56:55 +05:30
|
|
|
|
tools?: {
|
|
|
|
|
|
shell?: {
|
|
|
|
|
|
maxBytes: number;
|
|
|
|
|
|
maxLines: number;
|
|
|
|
|
|
};
|
|
|
|
|
|
};
|
2025-05-13 09:45:46 -07:00
|
|
|
|
fileOpener?: FileOpenerScheme;
|
2025-04-16 12:56:08 -04:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-04-23 13:45:56 +08:00
|
|
|
|
// Formatting (quiet mode-only).
|
|
|
|
|
|
export const PRETTY_PRINT = Boolean(process.env["PRETTY_PRINT"] || "");
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// ---------------------------------------------------------------------------
|
2025-05-10 15:57:49 -07:00
|
|
|
|
// Project doc support (AGENTS.md / codex.md)
|
2025-04-16 12:56:08 -04:00
|
|
|
|
// ---------------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
export const PROJECT_DOC_MAX_BYTES = 32 * 1024; // 32 kB
|
|
|
|
|
|
|
2025-05-10 15:57:49 -07:00
|
|
|
|
// We support multiple filenames for project-level agent instructions. As of
|
|
|
|
|
|
// 2025 the recommended convention is to use `AGENTS.md`, however we keep
|
|
|
|
|
|
// the legacy `codex.md` variants for backwards-compatibility so that existing
|
|
|
|
|
|
// repositories continue to work without changes. The list is ordered so that
|
|
|
|
|
|
// the first match wins – newer conventions first, older fallbacks later.
|
|
|
|
|
|
const PROJECT_DOC_FILENAMES = [
|
|
|
|
|
|
"AGENTS.md", // preferred
|
|
|
|
|
|
"codex.md", // legacy
|
|
|
|
|
|
".codex.md",
|
|
|
|
|
|
"CODEX.md",
|
|
|
|
|
|
];
|
2025-04-25 08:32:33 +08:00
|
|
|
|
const PROJECT_DOC_SEPARATOR = "\n\n--- project-doc ---\n\n";
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
export function discoverProjectDocPath(startDir: string): string | null {
|
|
|
|
|
|
const cwd = resolvePath(startDir);
|
|
|
|
|
|
|
|
|
|
|
|
// 1) Look in the explicit CWD first:
|
|
|
|
|
|
for (const name of PROJECT_DOC_FILENAMES) {
|
|
|
|
|
|
const direct = join(cwd, name);
|
|
|
|
|
|
if (existsSync(direct)) {
|
|
|
|
|
|
return direct;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 10:32:45 -07:00
|
|
|
|
// 2) Fallback: walk up to the Git root and look there.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
let dir = cwd;
|
|
|
|
|
|
// eslint-disable-next-line no-constant-condition
|
|
|
|
|
|
while (true) {
|
|
|
|
|
|
const gitPath = join(dir, ".git");
|
|
|
|
|
|
if (existsSync(gitPath)) {
|
|
|
|
|
|
// Once we hit the Git root, search its top‑level for the doc
|
|
|
|
|
|
for (const name of PROJECT_DOC_FILENAMES) {
|
|
|
|
|
|
const candidate = join(dir, name);
|
|
|
|
|
|
if (existsSync(candidate)) {
|
|
|
|
|
|
return candidate;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2025-04-16 10:32:45 -07:00
|
|
|
|
// If Git root but no doc, stop looking.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const parent = dirname(dir);
|
|
|
|
|
|
if (parent === dir) {
|
2025-04-16 10:32:45 -07:00
|
|
|
|
// Reached filesystem root without finding Git.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
return null;
|
|
|
|
|
|
}
|
|
|
|
|
|
dir = parent;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/**
|
2025-05-10 15:57:49 -07:00
|
|
|
|
* Load the project documentation markdown (`AGENTS.md` – or the legacy
|
|
|
|
|
|
* `codex.md`) if present. If the file
|
2025-04-16 12:56:08 -04:00
|
|
|
|
* exceeds {@link PROJECT_DOC_MAX_BYTES} it will be truncated and a warning is
|
|
|
|
|
|
* logged.
|
|
|
|
|
|
*
|
|
|
|
|
|
* @param cwd The current working directory of the caller
|
|
|
|
|
|
* @param explicitPath If provided, skips discovery and loads the given path
|
|
|
|
|
|
*/
|
|
|
|
|
|
export function loadProjectDoc(cwd: string, explicitPath?: string): string {
|
|
|
|
|
|
let filepath: string | null = null;
|
|
|
|
|
|
|
|
|
|
|
|
if (explicitPath) {
|
|
|
|
|
|
filepath = resolvePath(cwd, explicitPath);
|
|
|
|
|
|
if (!existsSync(filepath)) {
|
|
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
|
|
console.warn(`codex: project doc not found at ${filepath}`);
|
|
|
|
|
|
filepath = null;
|
|
|
|
|
|
}
|
|
|
|
|
|
} else {
|
|
|
|
|
|
filepath = discoverProjectDocPath(cwd);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (!filepath) {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
const buf = readFileSync(filepath);
|
|
|
|
|
|
if (buf.byteLength > PROJECT_DOC_MAX_BYTES) {
|
|
|
|
|
|
// eslint-disable-next-line no-console
|
|
|
|
|
|
console.warn(
|
|
|
|
|
|
`codex: project doc '${filepath}' exceeds ${PROJECT_DOC_MAX_BYTES} bytes – truncating.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
return buf.slice(0, PROJECT_DOC_MAX_BYTES).toString("utf-8");
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
return "";
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
export type LoadConfigOptions = {
|
|
|
|
|
|
/** Working directory used for project doc discovery */
|
|
|
|
|
|
cwd?: string;
|
|
|
|
|
|
/** Disable inclusion of the project doc */
|
|
|
|
|
|
disableProjectDoc?: boolean;
|
|
|
|
|
|
/** Explicit path to project doc (overrides discovery) */
|
|
|
|
|
|
projectDocPath?: string;
|
|
|
|
|
|
/** Whether we are in fullcontext mode. */
|
|
|
|
|
|
isFullContext?: boolean;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const loadConfig = (
|
|
|
|
|
|
configPath: string | undefined = CONFIG_FILEPATH,
|
|
|
|
|
|
instructionsPath: string | undefined = INSTRUCTIONS_FILEPATH,
|
|
|
|
|
|
options: LoadConfigOptions = {},
|
|
|
|
|
|
): AppConfig => {
|
|
|
|
|
|
// Determine the actual path to load. If the provided path doesn't exist and
|
|
|
|
|
|
// the caller passed the default JSON path, automatically fall back to YAML
|
|
|
|
|
|
// variants.
|
|
|
|
|
|
let actualConfigPath = configPath;
|
|
|
|
|
|
if (!existsSync(actualConfigPath)) {
|
|
|
|
|
|
if (configPath === CONFIG_FILEPATH) {
|
|
|
|
|
|
if (existsSync(CONFIG_YAML_FILEPATH)) {
|
|
|
|
|
|
actualConfigPath = CONFIG_YAML_FILEPATH;
|
|
|
|
|
|
} else if (existsSync(CONFIG_YML_FILEPATH)) {
|
|
|
|
|
|
actualConfigPath = CONFIG_YML_FILEPATH;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
let storedConfig: StoredConfig = {};
|
|
|
|
|
|
if (existsSync(actualConfigPath)) {
|
|
|
|
|
|
const raw = readFileSync(actualConfigPath, "utf-8");
|
|
|
|
|
|
const ext = extname(actualConfigPath).toLowerCase();
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (ext === ".yaml" || ext === ".yml") {
|
|
|
|
|
|
storedConfig = loadYaml(raw) as unknown as StoredConfig;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
storedConfig = JSON.parse(raw);
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// If parsing fails, fall back to empty config to avoid crashing.
|
|
|
|
|
|
storedConfig = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-29 13:10:16 -04:00
|
|
|
|
if (
|
|
|
|
|
|
storedConfig.disableResponseStorage !== undefined &&
|
|
|
|
|
|
typeof storedConfig.disableResponseStorage !== "boolean"
|
|
|
|
|
|
) {
|
|
|
|
|
|
if (storedConfig.disableResponseStorage === "true") {
|
|
|
|
|
|
storedConfig.disableResponseStorage = true;
|
|
|
|
|
|
} else if (storedConfig.disableResponseStorage === "false") {
|
|
|
|
|
|
storedConfig.disableResponseStorage = false;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
log(
|
|
|
|
|
|
`[codex] Warning: 'disableResponseStorage' in config is not a boolean (got '${storedConfig.disableResponseStorage}'). Ignoring this value.`,
|
|
|
|
|
|
);
|
|
|
|
|
|
delete storedConfig.disableResponseStorage;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
const instructionsFilePathResolved =
|
|
|
|
|
|
instructionsPath ?? INSTRUCTIONS_FILEPATH;
|
|
|
|
|
|
const userInstructions = existsSync(instructionsFilePathResolved)
|
|
|
|
|
|
? readFileSync(instructionsFilePathResolved, "utf-8")
|
|
|
|
|
|
: DEFAULT_INSTRUCTIONS;
|
|
|
|
|
|
|
2025-04-16 10:32:45 -07:00
|
|
|
|
// Project doc support.
|
2025-04-16 12:56:08 -04:00
|
|
|
|
const shouldLoadProjectDoc =
|
|
|
|
|
|
!options.disableProjectDoc &&
|
|
|
|
|
|
process.env["CODEX_DISABLE_PROJECT_DOC"] !== "1";
|
|
|
|
|
|
|
|
|
|
|
|
let projectDoc = "";
|
|
|
|
|
|
let projectDocPath: string | null = null;
|
|
|
|
|
|
if (shouldLoadProjectDoc) {
|
|
|
|
|
|
const cwd = options.cwd ?? process.cwd();
|
|
|
|
|
|
projectDoc = loadProjectDoc(cwd, options.projectDocPath);
|
|
|
|
|
|
projectDocPath = options.projectDocPath
|
|
|
|
|
|
? resolvePath(cwd, options.projectDocPath)
|
|
|
|
|
|
: discoverProjectDocPath(cwd);
|
|
|
|
|
|
if (projectDocPath) {
|
2025-04-20 09:58:06 -07:00
|
|
|
|
log(
|
|
|
|
|
|
`[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`,
|
|
|
|
|
|
);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
} else {
|
2025-04-20 09:58:06 -07:00
|
|
|
|
log(`[codex] No project doc found in ${cwd}`);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const combinedInstructions = [userInstructions, projectDoc]
|
|
|
|
|
|
.filter((s) => s && s.trim() !== "")
|
2025-04-25 08:32:33 +08:00
|
|
|
|
.join(PROJECT_DOC_SEPARATOR);
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
|
|
|
|
|
// Treat empty string ("" or whitespace) as absence so we can fall back to
|
|
|
|
|
|
// the latest DEFAULT_MODEL.
|
|
|
|
|
|
const storedModel =
|
|
|
|
|
|
storedConfig.model && storedConfig.model.trim() !== ""
|
|
|
|
|
|
? storedConfig.model.trim()
|
|
|
|
|
|
: undefined;
|
|
|
|
|
|
|
|
|
|
|
|
const config: AppConfig = {
|
|
|
|
|
|
model:
|
|
|
|
|
|
storedModel ??
|
|
|
|
|
|
(options.isFullContext
|
|
|
|
|
|
? DEFAULT_FULL_CONTEXT_MODEL
|
|
|
|
|
|
: DEFAULT_AGENTIC_MODEL),
|
2025-04-20 23:59:34 -04:00
|
|
|
|
provider: storedConfig.provider,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
instructions: combinedInstructions,
|
2025-04-17 16:19:26 -07:00
|
|
|
|
notify: storedConfig.notify === true,
|
2025-04-19 10:25:25 -04:00
|
|
|
|
approvalMode: storedConfig.approvalMode,
|
2025-05-05 22:56:55 +05:30
|
|
|
|
tools: {
|
|
|
|
|
|
shell: {
|
|
|
|
|
|
maxBytes:
|
|
|
|
|
|
storedConfig.tools?.shell?.maxBytes ?? DEFAULT_SHELL_MAX_BYTES,
|
|
|
|
|
|
maxLines:
|
|
|
|
|
|
storedConfig.tools?.shell?.maxLines ?? DEFAULT_SHELL_MAX_LINES,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
2025-04-29 13:10:16 -04:00
|
|
|
|
disableResponseStorage: storedConfig.disableResponseStorage === true,
|
2025-04-29 20:00:49 +05:30
|
|
|
|
reasoningEffort: storedConfig.reasoningEffort,
|
2025-05-13 09:45:46 -07:00
|
|
|
|
fileOpener: storedConfig.fileOpener,
|
2025-04-16 12:56:08 -04:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
// First‑run bootstrap: if the configuration file (and/or its containing
|
|
|
|
|
|
// directory) didn't exist we create them now so that users end up with a
|
|
|
|
|
|
// materialised ~/.codex/config.json file on first execution. This mirrors
|
|
|
|
|
|
// what `saveConfig()` would do but without requiring callers to remember to
|
|
|
|
|
|
// invoke it separately.
|
|
|
|
|
|
//
|
|
|
|
|
|
// We intentionally perform this *after* we have computed the final
|
|
|
|
|
|
// `config` object so that we can just persist the resolved defaults. The
|
|
|
|
|
|
// write operations are guarded by `existsSync` checks so that subsequent
|
|
|
|
|
|
// runs that already have a config will remain read‑only here.
|
|
|
|
|
|
// -----------------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
if (!existsSync(actualConfigPath)) {
|
|
|
|
|
|
// Ensure the directory exists first.
|
|
|
|
|
|
const dir = dirname(actualConfigPath);
|
|
|
|
|
|
if (!existsSync(dir)) {
|
|
|
|
|
|
mkdirSync(dir, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Persist a minimal config – we include the `model` key but leave it as
|
|
|
|
|
|
// an empty string so that `loadConfig()` treats it as "unset" and falls
|
|
|
|
|
|
// back to whatever DEFAULT_MODEL is current at runtime. This prevents
|
|
|
|
|
|
// pinning users to an old default after upgrading Codex.
|
|
|
|
|
|
const ext = extname(actualConfigPath).toLowerCase();
|
|
|
|
|
|
if (ext === ".yaml" || ext === ".yml") {
|
|
|
|
|
|
writeFileSync(actualConfigPath, dumpYaml(EMPTY_STORED_CONFIG), "utf-8");
|
|
|
|
|
|
} else {
|
|
|
|
|
|
writeFileSync(actualConfigPath, EMPTY_CONFIG_JSON, "utf-8");
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Always ensure the instructions file exists so users can edit it.
|
|
|
|
|
|
if (!existsSync(instructionsFilePathResolved)) {
|
|
|
|
|
|
const instrDir = dirname(instructionsFilePathResolved);
|
|
|
|
|
|
if (!existsSync(instrDir)) {
|
|
|
|
|
|
mkdirSync(instrDir, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
writeFileSync(instructionsFilePathResolved, userInstructions, "utf-8");
|
|
|
|
|
|
}
|
|
|
|
|
|
} catch {
|
|
|
|
|
|
// Silently ignore any errors – failure to persist the defaults shouldn't
|
|
|
|
|
|
// block the CLI from starting. A future explicit `codex config` command
|
|
|
|
|
|
// or `saveConfig()` call can handle (re‑)writing later.
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Only include the "memory" key if it was explicitly set by the user. This
|
|
|
|
|
|
// preserves backward‑compatibility with older config files (and our test
|
|
|
|
|
|
// fixtures) that don't include a "memory" section.
|
|
|
|
|
|
if (storedConfig.memory !== undefined) {
|
|
|
|
|
|
config.memory = storedConfig.memory;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (storedConfig.fullAutoErrorMode) {
|
|
|
|
|
|
config.fullAutoErrorMode = storedConfig.fullAutoErrorMode;
|
|
|
|
|
|
}
|
2025-04-17 16:19:26 -07:00
|
|
|
|
// Notification setting: enable desktop notifications when set in config
|
|
|
|
|
|
config.notify = storedConfig.notify === true;
|
2025-05-10 16:18:20 -07:00
|
|
|
|
// Flex-mode setting: enable the flex-mode service tier when set in config
|
|
|
|
|
|
if (storedConfig.flexMode !== undefined) {
|
|
|
|
|
|
config.flexMode = storedConfig.flexMode;
|
|
|
|
|
|
}
|
2025-04-16 12:56:08 -04:00
|
|
|
|
|
2025-04-17 21:41:54 +02:00
|
|
|
|
// Add default history config if not provided
|
|
|
|
|
|
if (storedConfig.history !== undefined) {
|
|
|
|
|
|
config.history = {
|
|
|
|
|
|
maxSize: storedConfig.history.maxSize ?? 1000,
|
|
|
|
|
|
saveHistory: storedConfig.history.saveHistory ?? true,
|
|
|
|
|
|
sensitivePatterns: storedConfig.history.sensitivePatterns ?? [],
|
|
|
|
|
|
};
|
|
|
|
|
|
} else {
|
|
|
|
|
|
config.history = {
|
|
|
|
|
|
maxSize: 1000,
|
|
|
|
|
|
saveHistory: true,
|
|
|
|
|
|
sensitivePatterns: [],
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-23 13:45:56 +08:00
|
|
|
|
// Merge default providers with user configured providers in the config.
|
|
|
|
|
|
config.providers = { ...providers, ...storedConfig.providers };
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
return config;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
export const saveConfig = (
|
|
|
|
|
|
config: AppConfig,
|
|
|
|
|
|
configPath = CONFIG_FILEPATH,
|
|
|
|
|
|
instructionsPath = INSTRUCTIONS_FILEPATH,
|
|
|
|
|
|
): void => {
|
|
|
|
|
|
// If the caller passed the default JSON path *and* a YAML config already
|
|
|
|
|
|
// exists on disk, save back to that YAML file instead to preserve the
|
|
|
|
|
|
// user's chosen format.
|
|
|
|
|
|
let targetPath = configPath;
|
|
|
|
|
|
if (
|
|
|
|
|
|
configPath === CONFIG_FILEPATH &&
|
|
|
|
|
|
!existsSync(configPath) &&
|
|
|
|
|
|
(existsSync(CONFIG_YAML_FILEPATH) || existsSync(CONFIG_YML_FILEPATH))
|
|
|
|
|
|
) {
|
|
|
|
|
|
targetPath = existsSync(CONFIG_YAML_FILEPATH)
|
|
|
|
|
|
? CONFIG_YAML_FILEPATH
|
|
|
|
|
|
: CONFIG_YML_FILEPATH;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const dir = dirname(targetPath);
|
|
|
|
|
|
if (!existsSync(dir)) {
|
|
|
|
|
|
mkdirSync(dir, { recursive: true });
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const ext = extname(targetPath).toLowerCase();
|
2025-04-17 21:41:54 +02:00
|
|
|
|
// Create the config object to save
|
|
|
|
|
|
const configToSave: StoredConfig = {
|
|
|
|
|
|
model: config.model,
|
2025-04-20 23:59:34 -04:00
|
|
|
|
provider: config.provider,
|
2025-04-23 13:45:56 +08:00
|
|
|
|
providers: config.providers,
|
2025-04-19 10:25:25 -04:00
|
|
|
|
approvalMode: config.approvalMode,
|
2025-04-29 13:10:16 -04:00
|
|
|
|
disableResponseStorage: config.disableResponseStorage,
|
2025-05-10 16:18:20 -07:00
|
|
|
|
flexMode: config.flexMode,
|
2025-04-29 20:00:49 +05:30
|
|
|
|
reasoningEffort: config.reasoningEffort,
|
2025-04-17 21:41:54 +02:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Add history settings if they exist
|
|
|
|
|
|
if (config.history) {
|
|
|
|
|
|
configToSave.history = {
|
|
|
|
|
|
maxSize: config.history.maxSize,
|
|
|
|
|
|
saveHistory: config.history.saveHistory,
|
|
|
|
|
|
sensitivePatterns: config.history.sensitivePatterns,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-05-05 22:56:55 +05:30
|
|
|
|
// Add tools settings if they exist
|
|
|
|
|
|
if (config.tools) {
|
|
|
|
|
|
configToSave.tools = {
|
|
|
|
|
|
shell: config.tools.shell
|
|
|
|
|
|
? {
|
|
|
|
|
|
maxBytes: config.tools.shell.maxBytes,
|
|
|
|
|
|
maxLines: config.tools.shell.maxLines,
|
|
|
|
|
|
}
|
|
|
|
|
|
: undefined,
|
|
|
|
|
|
};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-16 12:56:08 -04:00
|
|
|
|
if (ext === ".yaml" || ext === ".yml") {
|
2025-04-17 21:41:54 +02:00
|
|
|
|
writeFileSync(targetPath, dumpYaml(configToSave), "utf-8");
|
2025-04-16 12:56:08 -04:00
|
|
|
|
} else {
|
2025-04-17 21:41:54 +02:00
|
|
|
|
writeFileSync(targetPath, JSON.stringify(configToSave, null, 2), "utf-8");
|
2025-04-16 12:56:08 -04:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-04-25 08:32:33 +08:00
|
|
|
|
// Take everything before the first PROJECT_DOC_SEPARATOR (or the whole string if none).
|
|
|
|
|
|
const [userInstructions = ""] = config.instructions.split(
|
|
|
|
|
|
PROJECT_DOC_SEPARATOR,
|
|
|
|
|
|
);
|
|
|
|
|
|
writeFileSync(instructionsPath, userInstructions, "utf-8");
|
2025-04-16 12:56:08 -04:00
|
|
|
|
};
|