Files
llmx/codex-cli/src/utils/config.ts

582 lines
20 KiB
TypeScript
Raw Normal View History

// 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";
import type { ReasoningEffort } from "openai/resources.mjs";
import { AutoApprovalMode } from "./auto-approval-mode.js";
import { log } from "./logger/log.js";
import { providers } from "./providers.js";
import { config as loadDotenv } from "dotenv";
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";
// ---------------------------------------------------------------------------
// Userwide environment config (~/.codex.env)
// ---------------------------------------------------------------------------
// Load a userlevel dotenv file **after** process.env and any projectlocal
// .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. Projectlocal .env (handled in cli.tsx)
// 3. Userwide ~/.codex.env (loaded here)
// This guarantees that users can still override the global key on a perproject
// 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 });
}
export const DEFAULT_AGENTIC_MODEL = "o4-mini";
export const DEFAULT_FULL_CONTEXT_MODEL = "gpt-4.1";
export const DEFAULT_APPROVAL_MODE = AutoApprovalMode.SUGGEST;
export const DEFAULT_INSTRUCTIONS = "";
// Default shell output limits
export const DEFAULT_SHELL_MAX_BYTES = 1024 * 10; // 10 KB
export const DEFAULT_SHELL_MAX_LINES = 256;
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"] || "";
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";
export const DEFAULT_REASONING_EFFORT = "high";
export const OPENAI_ORGANIZATION = process.env["OPENAI_ORGANIZATION"] || "";
export const OPENAI_PROJECT = process.env["OPENAI_PROJECT"] || "";
// Can be set `true` when Codex is running in an environment that is marked as already
// considered sufficiently locked-down so that we allow running wihtout an explicit sandbox.
export const CODEX_UNSAFE_ALLOW_NO_SANDBOX = Boolean(
process.env["CODEX_UNSAFE_ALLOW_NO_SANDBOX"] || "",
);
export function setApiKey(apiKey: string): void {
OPENAI_API_KEY = apiKey;
}
export function getBaseUrl(provider: string = "openai"): string | undefined {
// Check for a PROVIDER-specific override: e.g. OPENAI_BASE_URL or OLLAMA_BASE_URL.
const envKey = `${provider.toUpperCase()}_BASE_URL`;
if (process.env[envKey]) {
return process.env[envKey];
}
// Get providers config from config file.
const config = loadConfig();
const providersConfig = config.providers ?? providers;
const providerInfo = providersConfig[provider.toLowerCase()];
if (providerInfo) {
return providerInfo.baseURL;
}
// If the provider not found in the providers list and `OPENAI_BASE_URL` is set, use it.
if (OPENAI_BASE_URL !== "") {
return OPENAI_BASE_URL;
}
// We tried.
return undefined;
}
export function getApiKey(provider: string = "openai"): string | undefined {
const config = loadConfig();
const providersConfig = config.providers ?? providers;
const providerInfo = providersConfig[provider.toLowerCase()];
if (providerInfo) {
if (providerInfo.name === "Ollama") {
return process.env[providerInfo.envKey] ?? "dummy";
}
return process.env[providerInfo.envKey];
}
// Checking `PROVIDER_API_KEY feels more intuitive with a custom provider.
const customApiKey = process.env[`${provider.toUpperCase()}_API_KEY`];
if (customApiKey) {
return customApiKey;
}
// 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;
}
// We tried.
return undefined;
}
// Represents config as persisted in config.json.
export type StoredConfig = {
model?: string;
provider?: string;
approvalMode?: AutoApprovalMode;
fullAutoErrorMode?: FullAutoErrorMode;
memory?: MemoryConfig;
/** Whether to enable desktop notifications for responses */
notify?: boolean;
/** Disable server-side response storage (send full transcript each request) */
disableResponseStorage?: boolean;
providers?: Record<string, { name: string; baseURL: string; envKey: string }>;
history?: {
maxSize?: number;
saveHistory?: boolean;
sensitivePatterns?: Array<string>;
};
tools?: {
shell?: {
maxBytes?: number;
maxLines?: number;
};
};
/** User-defined safe commands */
safeCommands?: Array<string>;
reasoningEffort?: ReasoningEffort;
};
// 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: "" };
// Prestringified JSON variant so we don't stringify repeatedly.
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.
export type AppConfig = {
apiKey?: string;
model: string;
provider?: string;
instructions: string;
feat: read approvalMode from config file (#298) This PR implements support for reading the approvalMode setting from the user's config file (`~/.codex/config.json` or `~/.codex/config.yaml`), allowing users to set a persistent default approval mode without needing to specify command-line flags for each session. Changes: - Added approvalMode to the AppConfig type in config.ts - Updated loadConfig() to read the approval mode from the config file - Modified saveConfig() to persist the approval mode setting - Updated CLI logic to respect the config-defined approval mode (while maintaining CLI flag priority) - Added comprehensive tests for approval mode config functionality - Updated README to document the new config option in both YAML and JSON formats - additions to `.gitignore` for other CLI tools Motivation: As a user who regularly works with CLI-tools, I found it odd to have to alias this with the command flags I wanted when `approvalMode` simply wasn't being parsed even though it was an optional prop in `config.ts`. This change allows me (and other users) to set the preference once in the config file, streamlining daily usage while maintaining the ability to override via command-line flags when needed. Testing: I've added a new test case loads and saves approvalMode correctly that verifies: - Reading the approvalMode from the config file works correctly - Saving the approvalMode to the config file works as expected - The value persists through load/save operations All tests related to the implementation are passing.
2025-04-19 10:25:25 -04:00
approvalMode?: AutoApprovalMode;
fullAutoErrorMode?: FullAutoErrorMode;
memory?: MemoryConfig;
reasoningEffort?: ReasoningEffort;
/** Whether to enable desktop notifications for responses */
notify?: boolean;
/** Disable server-side response storage (send full transcript each request) */
disableResponseStorage?: boolean;
/** Enable the "flex-mode" processing mode for supported models (o3, o4-mini) */
flexMode?: boolean;
providers?: Record<string, { name: string; baseURL: string; envKey: string }>;
history?: {
maxSize: number;
saveHistory: boolean;
sensitivePatterns: Array<string>;
};
tools?: {
shell?: {
maxBytes: number;
maxLines: number;
};
};
};
// Formatting (quiet mode-only).
export const PRETTY_PRINT = Boolean(process.env["PRETTY_PRINT"] || "");
// ---------------------------------------------------------------------------
// Project doc support (AGENTS.md / codex.md)
// ---------------------------------------------------------------------------
export const PROJECT_DOC_MAX_BYTES = 32 * 1024; // 32 kB
// 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",
];
const PROJECT_DOC_SEPARATOR = "\n\n--- project-doc ---\n\n";
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.
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 toplevel 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.
return null;
}
const parent = dirname(dir);
if (parent === dir) {
2025-04-16 10:32:45 -07:00
// Reached filesystem root without finding Git.
return null;
}
dir = parent;
}
}
/**
* Load the project documentation markdown (`AGENTS.md` or the legacy
* `codex.md`) if present. If the file
* 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 = {};
}
}
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;
}
}
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.
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) {
log(
`[codex] Loaded project doc from ${projectDocPath} (${projectDoc.length} bytes)`,
);
} else {
log(`[codex] No project doc found in ${cwd}`);
}
}
const combinedInstructions = [userInstructions, projectDoc]
.filter((s) => s && s.trim() !== "")
.join(PROJECT_DOC_SEPARATOR);
// 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),
provider: storedConfig.provider,
instructions: combinedInstructions,
notify: storedConfig.notify === true,
feat: read approvalMode from config file (#298) This PR implements support for reading the approvalMode setting from the user's config file (`~/.codex/config.json` or `~/.codex/config.yaml`), allowing users to set a persistent default approval mode without needing to specify command-line flags for each session. Changes: - Added approvalMode to the AppConfig type in config.ts - Updated loadConfig() to read the approval mode from the config file - Modified saveConfig() to persist the approval mode setting - Updated CLI logic to respect the config-defined approval mode (while maintaining CLI flag priority) - Added comprehensive tests for approval mode config functionality - Updated README to document the new config option in both YAML and JSON formats - additions to `.gitignore` for other CLI tools Motivation: As a user who regularly works with CLI-tools, I found it odd to have to alias this with the command flags I wanted when `approvalMode` simply wasn't being parsed even though it was an optional prop in `config.ts`. This change allows me (and other users) to set the preference once in the config file, streamlining daily usage while maintaining the ability to override via command-line flags when needed. Testing: I've added a new test case loads and saves approvalMode correctly that verifies: - Reading the approvalMode from the config file works correctly - Saving the approvalMode to the config file works as expected - The value persists through load/save operations All tests related to the implementation are passing.
2025-04-19 10:25:25 -04:00
approvalMode: storedConfig.approvalMode,
tools: {
shell: {
maxBytes:
storedConfig.tools?.shell?.maxBytes ?? DEFAULT_SHELL_MAX_BYTES,
maxLines:
storedConfig.tools?.shell?.maxLines ?? DEFAULT_SHELL_MAX_LINES,
},
},
disableResponseStorage: storedConfig.disableResponseStorage === true,
reasoningEffort: storedConfig.reasoningEffort,
};
// -----------------------------------------------------------------------
// Firstrun 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 readonly 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 backwardcompatibility 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;
}
// Notification setting: enable desktop notifications when set in config
config.notify = storedConfig.notify === true;
// 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: [],
};
}
// Merge default providers with user configured providers in the config.
config.providers = { ...providers, ...storedConfig.providers };
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();
// Create the config object to save
const configToSave: StoredConfig = {
model: config.model,
provider: config.provider,
providers: config.providers,
feat: read approvalMode from config file (#298) This PR implements support for reading the approvalMode setting from the user's config file (`~/.codex/config.json` or `~/.codex/config.yaml`), allowing users to set a persistent default approval mode without needing to specify command-line flags for each session. Changes: - Added approvalMode to the AppConfig type in config.ts - Updated loadConfig() to read the approval mode from the config file - Modified saveConfig() to persist the approval mode setting - Updated CLI logic to respect the config-defined approval mode (while maintaining CLI flag priority) - Added comprehensive tests for approval mode config functionality - Updated README to document the new config option in both YAML and JSON formats - additions to `.gitignore` for other CLI tools Motivation: As a user who regularly works with CLI-tools, I found it odd to have to alias this with the command flags I wanted when `approvalMode` simply wasn't being parsed even though it was an optional prop in `config.ts`. This change allows me (and other users) to set the preference once in the config file, streamlining daily usage while maintaining the ability to override via command-line flags when needed. Testing: I've added a new test case loads and saves approvalMode correctly that verifies: - Reading the approvalMode from the config file works correctly - Saving the approvalMode to the config file works as expected - The value persists through load/save operations All tests related to the implementation are passing.
2025-04-19 10:25:25 -04:00
approvalMode: config.approvalMode,
disableResponseStorage: config.disableResponseStorage,
reasoningEffort: config.reasoningEffort,
};
// Add history settings if they exist
if (config.history) {
configToSave.history = {
maxSize: config.history.maxSize,
saveHistory: config.history.saveHistory,
sensitivePatterns: config.history.sensitivePatterns,
};
}
// 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,
};
}
if (ext === ".yaml" || ext === ".yml") {
writeFileSync(targetPath, dumpYaml(configToSave), "utf-8");
} else {
writeFileSync(targetPath, JSON.stringify(configToSave, null, 2), "utf-8");
}
// 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");
};