|
|
@@ -1,15 +1,29 @@
|
|
|
/**
|
|
|
* OpenAI Codex CLI target.
|
|
|
*
|
|
|
- * - MCP server entry to `~/.codex/config.toml` as the dotted-key
|
|
|
- * table `[mcp_servers.codegraph]`. TOML — not JSON — handled by
|
|
|
- * the narrow serializer in `./toml.ts`.
|
|
|
- * - Instructions to `~/.codex/AGENTS.md`.
|
|
|
+ * - MCP server entry to `config.toml` as the dotted-key table
|
|
|
+ * `[mcp_servers.codegraph]`. TOML — not JSON — handled by the
|
|
|
+ * narrow serializer in `./toml.ts`.
|
|
|
+ * - Instructions to `AGENTS.md`.
|
|
|
*
|
|
|
- * Codex CLI as of 2026-05 has no project-local config concept —
|
|
|
- * everything lives under `~/.codex/`. `supportsLocation('local')`
|
|
|
- * returns false; the orchestrator skips Codex when the user picks
|
|
|
- * the local install location.
|
|
|
+ * Both locations are supported (#1531):
|
|
|
+ * - global: `~/.codex/config.toml` + `~/.codex/AGENTS.md`
|
|
|
+ * - local: `<cwd>/.codex/config.toml` + `<cwd>/AGENTS.md`
|
|
|
+ *
|
|
|
+ * Codex has a first-class project config layer: `.codex/config.toml`
|
|
|
+ * is layer 4 of the loader's stack, above the user config (layer 6),
|
|
|
+ * merged recursively top-over-bottom
|
|
|
+ * (`codex-rs/config/src/loader/README.md` in openai/codex). It landed
|
|
|
+ * in openai/codex#8354 (2025-12-22), so the "Codex has no
|
|
|
+ * project-local config" note this file used to carry was never
|
|
|
+ * accurate. The project layer strips a denylist of settings that
|
|
|
+ * repo contents shouldn't get to choose (base URLs, model providers,
|
|
|
+ * `notify`, profiles, otel — `loader/mod.rs`), and `mcp_servers` is
|
|
|
+ * NOT on it, so a project-scoped `[mcp_servers.codegraph]` is honored.
|
|
|
+ *
|
|
|
+ * Caveat surfaced as an install note: project layers are "loaded but
|
|
|
+ * disabled when untrusted," so a local install only takes effect in a
|
|
|
+ * project the user has marked trusted.
|
|
|
*
|
|
|
* No permissions concept.
|
|
|
*/
|
|
|
@@ -38,14 +52,31 @@ import { buildTomlTable, removeTomlTable, upsertTomlTable } from './toml';
|
|
|
|
|
|
const TOML_HEADER = 'mcp_servers.codegraph';
|
|
|
|
|
|
-function configDir(): string {
|
|
|
- return path.join(os.homedir(), '.codex');
|
|
|
+function configDir(loc: Location): string {
|
|
|
+ return loc === 'global'
|
|
|
+ ? path.join(os.homedir(), '.codex')
|
|
|
+ : path.join(process.cwd(), '.codex');
|
|
|
+}
|
|
|
+function tomlConfigPath(loc: Location): string {
|
|
|
+ return path.join(configDir(loc), 'config.toml');
|
|
|
}
|
|
|
-function tomlConfigPath(): string {
|
|
|
- return path.join(configDir(), 'config.toml');
|
|
|
+function instructionsPath(loc: Location): string {
|
|
|
+ // Global AGENTS.md lives under ~/.codex/; project-local AGENTS.md
|
|
|
+ // lives at the project root (NOT under .codex/) — that's the file
|
|
|
+ // Codex reads for repo instructions, and it matches the local
|
|
|
+ // layout the opencode and gemini targets already use.
|
|
|
+ return loc === 'global'
|
|
|
+ ? path.join(configDir('global'), 'AGENTS.md')
|
|
|
+ : path.join(process.cwd(), 'AGENTS.md');
|
|
|
}
|
|
|
-function instructionsPath(): string {
|
|
|
- return path.join(configDir(), 'AGENTS.md');
|
|
|
+
|
|
|
+/**
|
|
|
+ * Project layers are "loaded but disabled when untrusted" (openai/codex
|
|
|
+ * `loader/mod.rs`), so a local install can be written correctly and
|
|
|
+ * still do nothing. Say so rather than reporting silent success.
|
|
|
+ */
|
|
|
+function trustNote(): string {
|
|
|
+ return `Codex applies ${tomlConfigPath('local')} only in a project marked trusted — otherwise the layer is loaded but disabled. Trust this project in Codex to activate it.`;
|
|
|
}
|
|
|
|
|
|
class CodexTarget implements AgentTarget {
|
|
|
@@ -53,15 +84,12 @@ class CodexTarget implements AgentTarget {
|
|
|
readonly displayName = 'Codex CLI';
|
|
|
readonly docsUrl = 'https://github.com/openai/codex';
|
|
|
|
|
|
- supportsLocation(loc: Location): boolean {
|
|
|
- return loc === 'global';
|
|
|
+ supportsLocation(_loc: Location): boolean {
|
|
|
+ return true;
|
|
|
}
|
|
|
|
|
|
detect(loc: Location): DetectionResult {
|
|
|
- if (loc !== 'global') {
|
|
|
- return { installed: false, alreadyConfigured: false };
|
|
|
- }
|
|
|
- const tomlPath = tomlConfigPath();
|
|
|
+ const tomlPath = tomlConfigPath(loc);
|
|
|
let alreadyConfigured = false;
|
|
|
if (fs.existsSync(tomlPath)) {
|
|
|
try {
|
|
|
@@ -69,34 +97,30 @@ class CodexTarget implements AgentTarget {
|
|
|
alreadyConfigured = content.includes(`[${TOML_HEADER}]`);
|
|
|
} catch { /* ignore */ }
|
|
|
}
|
|
|
- const installed = fs.existsSync(configDir());
|
|
|
+ // Global: ~/.codex/ existing means Codex has run here. Local: the
|
|
|
+ // project only counts as "Codex-enabled" once it actually has a
|
|
|
+ // .codex/ dir or config file of its own.
|
|
|
+ const installed = fs.existsSync(configDir(loc)) || fs.existsSync(tomlPath);
|
|
|
return { installed, alreadyConfigured, configPath: tomlPath };
|
|
|
}
|
|
|
|
|
|
install(loc: Location, _opts: InstallOptions): WriteResult {
|
|
|
- if (loc !== 'global') {
|
|
|
- return {
|
|
|
- files: [],
|
|
|
- notes: ['Codex CLI has no project-local config — re-run with --location=global to install.'],
|
|
|
- };
|
|
|
- }
|
|
|
const files: WriteResult['files'] = [];
|
|
|
|
|
|
- files.push(writeMcpEntry());
|
|
|
+ files.push(writeMcpEntry(loc));
|
|
|
|
|
|
// AGENTS.md gets the short marker-fenced CodeGraph block (#704):
|
|
|
// subagents and non-MCP harnesses read AGENTS.md but never the MCP
|
|
|
// initialize instructions. Upsert self-heals a stale pre-#529 block.
|
|
|
- files.push(upsertInstructionsEntry(instructionsPath()));
|
|
|
+ files.push(upsertInstructionsEntry(instructionsPath(loc)));
|
|
|
|
|
|
- return { files };
|
|
|
+ return loc === 'local' ? { files, notes: [trustNote()] } : { files };
|
|
|
}
|
|
|
|
|
|
uninstall(loc: Location): WriteResult {
|
|
|
- if (loc !== 'global') return { files: [] };
|
|
|
const files: WriteResult['files'] = [];
|
|
|
|
|
|
- const tomlPath = tomlConfigPath();
|
|
|
+ const tomlPath = tomlConfigPath(loc);
|
|
|
if (fs.existsSync(tomlPath)) {
|
|
|
const content = fs.readFileSync(tomlPath, 'utf-8');
|
|
|
const { content: nextContent, action } = removeTomlTable(content, TOML_HEADER);
|
|
|
@@ -114,22 +138,18 @@ class CodexTarget implements AgentTarget {
|
|
|
files.push({ path: tomlPath, action: 'not-found' });
|
|
|
}
|
|
|
|
|
|
- files.push(removeInstructionsEntry());
|
|
|
+ files.push(removeInstructionsEntry(loc));
|
|
|
|
|
|
return { files };
|
|
|
}
|
|
|
|
|
|
printConfig(loc: Location): string {
|
|
|
- if (loc !== 'global') {
|
|
|
- return '# Codex CLI has no project-local config — use --location=global.\n';
|
|
|
- }
|
|
|
const block = buildCodegraphBlock();
|
|
|
- return `# Add to ${tomlConfigPath()}\n\n${block}\n`;
|
|
|
+ return `# Add to ${tomlConfigPath(loc)}\n\n${block}\n`;
|
|
|
}
|
|
|
|
|
|
describePaths(loc: Location): string[] {
|
|
|
- if (loc !== 'global') return [];
|
|
|
- return [tomlConfigPath(), instructionsPath()];
|
|
|
+ return [tomlConfigPath(loc), instructionsPath(loc)];
|
|
|
}
|
|
|
}
|
|
|
|
|
|
@@ -141,8 +161,8 @@ function buildCodegraphBlock(): string {
|
|
|
});
|
|
|
}
|
|
|
|
|
|
-function writeMcpEntry(): WriteResult['files'][number] {
|
|
|
- const file = tomlConfigPath();
|
|
|
+function writeMcpEntry(loc: Location): WriteResult['files'][number] {
|
|
|
+ const file = tomlConfigPath(loc);
|
|
|
const dir = path.dirname(file);
|
|
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
|
|
|
|
|
|
@@ -162,12 +182,12 @@ function writeMcpEntry(): WriteResult['files'][number] {
|
|
|
}
|
|
|
|
|
|
/**
|
|
|
- * Strip the marker-delimited CodeGraph block from `~/.codex/AGENTS.md`
|
|
|
- * if a prior install wrote one. Used by both install (self-heal on
|
|
|
- * upgrade) and uninstall — see issue #529.
|
|
|
+ * Strip the marker-delimited CodeGraph block from this location's
|
|
|
+ * AGENTS.md if a prior install wrote one. Used by both install
|
|
|
+ * (self-heal on upgrade) and uninstall — see issue #529.
|
|
|
*/
|
|
|
-function removeInstructionsEntry(): WriteResult['files'][number] {
|
|
|
- const file = instructionsPath();
|
|
|
+function removeInstructionsEntry(loc: Location): WriteResult['files'][number] {
|
|
|
+ const file = instructionsPath(loc);
|
|
|
const action = removeMarkedSection(file, CODEGRAPH_SECTION_START, CODEGRAPH_SECTION_END);
|
|
|
return { path: file, action };
|
|
|
}
|