@pulsemcp/air-adapter-codex 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md ADDED
@@ -0,0 +1,83 @@
1
+ # @pulsemcp/air-adapter-codex
2
+
3
+ AIR adapter extension for the [OpenAI Codex CLI](https://github.com/openai/codex). Translates AIR artifacts into Codex's native formats and prepares working directories for agent sessions.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ npm install @pulsemcp/air-adapter-codex
9
+ ```
10
+
11
+ ## Usage
12
+
13
+ ### With the AIR CLI
14
+
15
+ ```bash
16
+ # Install the adapter globally alongside the CLI
17
+ npm install -g @pulsemcp/air-cli @pulsemcp/air-adapter-codex
18
+
19
+ # Start a Codex session
20
+ air start codex --root web-app
21
+ ```
22
+
23
+ ### Programmatic
24
+
25
+ ```typescript
26
+ import { resolveArtifacts } from "@pulsemcp/air-core";
27
+ import { CodexAdapter } from "@pulsemcp/air-adapter-codex";
28
+
29
+ const artifacts = await resolveArtifacts("./air.json");
30
+ const adapter = new CodexAdapter();
31
+
32
+ // Prepare a working directory for a Codex session
33
+ const session = await adapter.prepareSession(artifacts, "./my-project", {
34
+ root: artifacts.roots["web-app"],
35
+ });
36
+
37
+ // session.configFiles — [] (Codex config is TOML, see "Secrets" below)
38
+ // session.skillPaths — skill dirs created in .agents/skills/
39
+ // session.hookPaths — hook dirs created in .codex/hooks/
40
+ // session.startCommand — { command: "codex", args: [], cwd: "..." }
41
+ ```
42
+
43
+ ## What `prepareSession()` does
44
+
45
+ 1. **Writes `.codex/config.toml`** — translates AIR MCP server configs into `[mcp_servers.*]` tables and registers path-based hooks under `[[hooks.<Event>]]`. User-authored servers, hooks, and top-level keys are preserved; only AIR-owned keys are replaced.
46
+ 2. **Injects skills** — copies `SKILL.md` files and associated content into `.agents/skills/{name}/`, where Codex discovers them.
47
+ 3. **Injects hooks** — copies hook directories into `.codex/hooks/{name}/` and registers their command in `config.toml`, anchored to the repo root.
48
+ 4. **Copies references** — attaches referenced documents into `{artifact}/references/`.
49
+ 5. **Respects local priority** — if a skill or hook directory already exists in the target, it is not overwritten.
50
+
51
+ ## Translation Details
52
+
53
+ | AIR Format | Codex Format |
54
+ |------------|--------------|
55
+ | `mcp.json` (flat map with `type`, `title`, `description`) | `[mcp_servers.<name>]` tables in `.codex/config.toml` (metadata stripped) |
56
+ | `stdio` servers | `{ command, args, env, env_vars }` |
57
+ | `sse` / `streamable-http` servers | `{ url, http_headers, env_http_headers }` (Codex auto-detects transport from the URL) |
58
+ | MCP `env` value `${VAR}` where key == `VAR` | `env_vars = ["VAR"]` (host env forwarding) |
59
+ | MCP `env` value `${OTHER}` (renamed) or literal | left in the `env` table |
60
+ | Header value `${VAR}` | `env_http_headers = { Header = "VAR" }` |
61
+ | Skills (`SKILL.md` + content) | `.agents/skills/{name}/` |
62
+ | Hooks (`HOOK.json` + scripts) | `.codex/hooks/{name}/` + `[[hooks.<Event>]]` registration |
63
+ | Hook events `session_start`, `pre_tool_call`, `post_tool_call`, `user_prompt_submit`, `stop` | Codex `SessionStart`, `PreToolUse`, `PostToolUse`, `UserPromptSubmit`, `Stop` |
64
+ | References | `{artifact}/references/` |
65
+
66
+ ## Secrets
67
+
68
+ Codex's config is TOML, which is outside AIR's JSON-based transform/validation pipeline. Instead of writing `${VAR}` placeholders, the adapter maps secret references to Codex's **native host-env forwarding** at translation time:
69
+
70
+ - `env: { GITHUB_TOKEN: "${GITHUB_TOKEN}" }` → `env_vars = ["GITHUB_TOKEN"]` — Codex injects the host's `GITHUB_TOKEN` at launch.
71
+ - `headers: { Authorization: "${API_TOKEN}" }` → `env_http_headers = { Authorization = "API_TOKEN" }`.
72
+
73
+ As a result, `prepareSession()` returns an **empty `configFiles` array** — there is no JSON config for secret transforms to post-process.
74
+
75
+ **Limitation — only whole-value, same-named refs forward.** Codex's `env_vars` forwards a host var to an env key of the *same name*, and `env_http_headers` forwards a host var as a *whole* header value. A renamed ref (`KEY = "${OTHER}"`) or a partial value (`"Bearer ${TOKEN}"`) can't be expressed either way, so it falls through to the literal `env` / `http_headers` table. Because the TOML never passes through AIR's `${VAR}` transform pipeline, Codex would inject the literal `${…}` string at runtime — so the adapter emits a `console.warn` for each such value instead of silently shipping a broken secret. Rewrite these as whole-value, same-named refs (or set the value directly).
76
+
77
+ ## Known gaps
78
+
79
+ These AIR features have no static Codex equivalent and are handled out of band:
80
+
81
+ - **OAuth MCP servers** — AIR's detailed OAuth config (`clientId`/`scopes`/`redirectUri`/…) has no static `config.toml` form. Codex performs interactive OAuth via `codex mcp login <name>`.
82
+ - **Plugins** — Codex's marketplace plugins are remote-installed (`codex plugin add`). AIR treats plugins as composition sugar: a plugin's declared MCP servers / skills / hooks are expanded into the activation set and materialized as their underlying Codex-native artifacts.
83
+ - **Subagent context** — Codex has no `--append-system-prompt` flag, so subagent-root context is returned to the caller via `PreparedSession.subagentContext` rather than passed to the CLI.
@@ -0,0 +1,226 @@
1
+ import type { AgentAdapter, AgentSessionConfig, StartCommand, ResolvedArtifacts, RootEntry, McpServerEntry, PluginEntry, PrepareSessionOptions, PreparedSession, CleanSessionOptions, CleanSessionResult, LocalArtifacts } from "@pulsemcp/air-core";
2
+ export declare class CodexAdapter implements AgentAdapter {
3
+ name: string;
4
+ displayName: string;
5
+ /**
6
+ * Map AIR lifecycle event names to Codex `config.toml` hook event names.
7
+ *
8
+ * Accepts both snake_case AIR names and PascalCase Codex lifecycle names as
9
+ * identity mappings, so hook authors targeting the Codex runtime can write
10
+ * Codex-native event names directly without translating to snake_case.
11
+ *
12
+ * AIR events without a Codex equivalent (session_end, subagent_stop,
13
+ * pre_compact, notification) are intentionally absent — `reconcileConfigHooks`
14
+ * warns and skips registration when it encounters an unrecognized event.
15
+ * Codex's PermissionRequest event has no AIR analog and is therefore not
16
+ * generated by AIR (but user-authored PermissionRequest hooks are preserved).
17
+ */
18
+ private static readonly AIR_TO_CODEX_EVENT;
19
+ isAvailable(): Promise<boolean>;
20
+ generateConfig(artifacts: ResolvedArtifacts, root?: RootEntry, _workDir?: string): AgentSessionConfig;
21
+ buildStartCommand(config: AgentSessionConfig): StartCommand;
22
+ /**
23
+ * Prepare a working directory for an OpenAI Codex CLI session.
24
+ *
25
+ * Writes `.codex/config.toml` (MCP servers under `[mcp_servers.*]` and hook
26
+ * registrations under `[hooks.*]`), injects skills + references into
27
+ * `.agents/skills/<name>/`, copies path-based hooks into `.codex/hooks/<name>/`,
28
+ * and returns the start command.
29
+ *
30
+ * Inputs to activation lists (root defaults, overrides, plugin-declared
31
+ * primitives) are accepted as either qualified (`@scope/id`) or short form;
32
+ * ambiguous short forms are rejected. Filesystem materialization uses
33
+ * shortnames — Codex's `config.toml`, `.agents/skills/`, `.codex/hooks/`,
34
+ * and the manifest are scope-naive. Two activated qualified IDs that share
35
+ * a shortname hard-fail with a clear "add one to exclude" message.
36
+ *
37
+ * NOTE: `configFiles` is intentionally returned empty. Codex's config is
38
+ * TOML, which is outside AIR's JSON-based transform/validation pipeline.
39
+ * Whole-value, same-named secret references (`${VAR}`) in MCP env/headers are
40
+ * mapped to Codex-native host-env forwarding (`env_vars`, `env_http_headers`)
41
+ * at translation time. Renamed or partial refs that can't be forwarded fall
42
+ * through to the literal table and emit a warning (see `warnUnforwardableSecret`),
43
+ * since the TOML never passes through the `${VAR}` transform pipeline.
44
+ */
45
+ prepareSession(artifacts: ResolvedArtifacts, targetDir: string, options?: PrepareSessionOptions): Promise<PreparedSession>;
46
+ /**
47
+ * Enumerate skills checked into `<targetDir>/.agents/skills/`. Codex loads
48
+ * these directly from the filesystem regardless of AIR's involvement, so
49
+ * they're always active and must not be overwritten or removed. The TUI uses
50
+ * this list to surface them as read-only entries.
51
+ */
52
+ listLocalArtifacts(targetDir: string): Promise<LocalArtifacts>;
53
+ /**
54
+ * Remove every artifact AIR has previously written into `targetDir`.
55
+ *
56
+ * Reads the per-target manifest, deletes each tracked skill / hook
57
+ * directory, and removes tracked MCP server keys + AIR-managed hook entries
58
+ * from `.codex/config.toml`. When every category is cleaned, the manifest
59
+ * itself is deleted; partial cleans (any `keep*` flag set) update the
60
+ * manifest with the kept entries so future runs continue to track them.
61
+ *
62
+ * Items in the manifest that no longer exist on disk are silently skipped
63
+ * — the manifest can drift if a user removed files manually between runs.
64
+ */
65
+ cleanSession(targetDir: string, options?: CleanSessionOptions): Promise<CleanSessionResult>;
66
+ /**
67
+ * Read `.codex/config.toml` and return the subset of `ids` whose key is
68
+ * actually present under `[mcp_servers]`. Returns an empty list if the file
69
+ * can't be parsed — we'd rather under-report than claim to have removed
70
+ * entries we never touched.
71
+ */
72
+ private mcpServerIdsPresent;
73
+ /**
74
+ * Resolve subagent roots from the root's default_subagent_roots.
75
+ * IDs are already qualified after composition-time canonicalization.
76
+ */
77
+ private resolveSubagentRoots;
78
+ /**
79
+ * Merge subagent roots' default_mcp_servers and default_skills into the
80
+ * parent's activated sets (union, preserving order with parent first).
81
+ */
82
+ private mergeSubagentArtifacts;
83
+ /**
84
+ * Build a system prompt section describing the subagent root dependencies.
85
+ */
86
+ private buildSubagentContext;
87
+ /**
88
+ * Translate a shortname-keyed MCP server map into Codex's `[mcp_servers.*]`
89
+ * shape (as a plain object ready for TOML serialization). Callers convert
90
+ * qualified IDs to shortnames before invoking this — Codex's config is
91
+ * scope-naive.
92
+ *
93
+ * Secret handling is Codex-native: an env value that is exactly `${VAR}`
94
+ * and whose key matches `VAR` becomes an `env_vars` forward (Codex injects
95
+ * the host's `VAR` at launch); any other value is written literally into the
96
+ * `[mcp_servers.<name>.env]` table. For remote servers, a header value of
97
+ * `${VAR}` becomes an `env_http_headers` entry; other header values are
98
+ * written into `http_headers`.
99
+ *
100
+ * Codex's native forwarding only expresses *whole-value* refs: `env_vars`
101
+ * forwards a host var to an env key of the same name, and `env_http_headers`
102
+ * forwards a host var as a whole header value. A *renamed* whole-value ref
103
+ * (`KEY = "${OTHER}"`) or a *partial* value (`"Bearer ${TOKEN}"`) can't be
104
+ * expressed either way, so it falls through to the literal table — and since
105
+ * the TOML never passes through AIR's `${VAR}` transform pipeline, Codex
106
+ * would inject the literal `${…}` string at runtime. We warn loudly in that
107
+ * case rather than silently shipping a broken secret.
108
+ */
109
+ translateMcpServersByShort(servers: Record<string, McpServerEntry>): Record<string, Record<string, unknown>>;
110
+ private translateMcpServer;
111
+ /**
112
+ * Warn that a secret reference can't be expressed via Codex's native host-env
113
+ * forwarding and will be written to `.codex/config.toml` as a literal `${…}`
114
+ * string. Codex would then inject that literal text at runtime — a silently
115
+ * broken secret. Only whole-value, same-named refs forward cleanly; renamed
116
+ * (`KEY = "${OTHER}"`) and partial (`"Bearer ${TOKEN}"`) refs land here.
117
+ */
118
+ private warnUnforwardableSecret;
119
+ /**
120
+ * Translate an AIR plugin to a Codex-facing descriptor.
121
+ *
122
+ * Codex's marketplace plugins are remote-installed (`codex plugin add`) and
123
+ * have no local package file an adapter can materialize. AIR therefore treats
124
+ * plugins as composition sugar: a plugin's declared MCP servers / skills /
125
+ * hooks are expanded into the activation set and materialized as their
126
+ * underlying Codex-native artifacts. This descriptor is informational only.
127
+ */
128
+ translatePlugin(shortId: string, plugin: PluginEntry): Record<string, unknown>;
129
+ /**
130
+ * Resolve a list of activation IDs (each qualified or short) into qualified
131
+ * IDs paired with shortnames suitable for filesystem materialization.
132
+ *
133
+ * Throws on:
134
+ * - unknown IDs (after attempting both qualified and short-form lookup)
135
+ * - ambiguous short references (multiple scopes contribute the shortname)
136
+ * - shortname collisions in the activation set itself (two qualified IDs
137
+ * with the same shortname can't share a single materialization dir)
138
+ */
139
+ private resolveActivations;
140
+ /**
141
+ * Build the warning emitted when a registered artifact's `path` does not
142
+ * exist on disk at materialization time. The qualified ID encodes the
143
+ * declaring catalog's scope, so a reviewer can trace the offending entry
144
+ * back to its index file. Materialization is skipped for this artifact and
145
+ * the rest of the session proceeds.
146
+ */
147
+ private missingSourceDirMessage;
148
+ private formatPoolKeys;
149
+ /**
150
+ * Copy referenced documents into a references/ subdirectory of the target.
151
+ * `refIds` are qualified IDs (post-canonicalization).
152
+ */
153
+ private copyReferences;
154
+ /** Parse an existing `config.toml`, returning `{}` when absent/unparseable. */
155
+ private readToml;
156
+ /**
157
+ * Merge AIR-managed MCP servers and hook registrations into
158
+ * `.codex/config.toml`, preserving any user-authored config.
159
+ *
160
+ * - MCP: keys in `staleMcpIds` are removed; keys in `translatedServers`
161
+ * are set/replaced; other servers and top-level keys pass through.
162
+ * - Hooks: AIR-owned entries (tagged with `_air_hook_id`) whose ID is in
163
+ * `managedHookIds` are pruned, then the current selection is registered.
164
+ *
165
+ * Returns the path written.
166
+ */
167
+ private writeCodexConfig;
168
+ /**
169
+ * Reconcile the `[hooks.*]` tables on the in-memory config object.
170
+ *
171
+ * Codex represents hooks as `hooks.<Event> = [ { matcher, hooks: [ {type,
172
+ * command, timeout?, statusMessage?} ] } ]`. AIR-owned matcher groups are
173
+ * identified by an `_air_hook_id` marker on the inner hook entry (Codex
174
+ * ignores unrecognized keys unless `--strict-config` is set). Groups whose ID
175
+ * is in `managedHookIds` are removed, then the current `newHookPaths` are
176
+ * registered for their mapped events.
177
+ */
178
+ private reconcileConfigHooks;
179
+ private isManagedHookEntry;
180
+ /**
181
+ * Remove `mcpIds` from `[mcp_servers]` and AIR-managed hook entries (matched
182
+ * by `_air_hook_id` ∈ `managedHookIds`) from `[hooks.*]` in
183
+ * `.codex/config.toml`, preserving user-authored entries and other top-level
184
+ * fields. Returns the path of the file that was rewritten, or null if the
185
+ * file became empty and was deleted.
186
+ */
187
+ private pruneCodexConfig;
188
+ /**
189
+ * Build a shell command string from HOOK.json's command and args fields.
190
+ *
191
+ * Hook authors write paths relative to their own hook directory. Codex
192
+ * invokes hooks with a working directory that is not guaranteed to be the
193
+ * project root, so hook-relative paths are anchored to the repository root
194
+ * via `"$(git rev-parse --show-toplevel)/.codex/hooks/<id>/<path>"` — the
195
+ * idiom used in Codex's own hook documentation. The double quotes keep the
196
+ * path safe for project directories with spaces.
197
+ *
198
+ * - `command` is anchored if it starts with `./` (explicit hook-relative).
199
+ * - Each `args` entry is anchored if it looks like a path AND the file
200
+ * exists under the hook's installed directory.
201
+ *
202
+ * Args that are not rewritten and contain shell metacharacters are
203
+ * single-quoted for safety.
204
+ */
205
+ private buildHookCommand;
206
+ /**
207
+ * Wrap a project-root-relative path in `"$(git rev-parse --show-toplevel)/..."`
208
+ * so the resolved path is independent of the cwd at hook invocation. Double
209
+ * quotes are required so the command substitution runs; characters that are
210
+ * special inside double quotes (`$`, `` ` ``, `"`, `\`) are escaped in the
211
+ * path component so an unusual hook ID or filename can't break out of the
212
+ * quoting.
213
+ */
214
+ private anchorHookPath;
215
+ /**
216
+ * If `arg` is a hook-relative path that points at a real file under the
217
+ * hook's installed directory, return its project-root-relative form
218
+ * (`.codex/hooks/<id>/<path>`) flagged as anchored so the caller can wrap it.
219
+ * Otherwise return `arg` unchanged with `anchored: false`.
220
+ */
221
+ private rewriteHookArgPath;
222
+ /** Human-readable list of the supported AIR hook event names (snake_case only). */
223
+ private supportedEventList;
224
+ private copyDirRecursive;
225
+ }
226
+ //# sourceMappingURL=codex-adapter.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"codex-adapter.d.ts","sourceRoot":"","sources":["../src/codex-adapter.ts"],"names":[],"mappings":"AAaA,OAAO,KAAK,EACV,YAAY,EACZ,kBAAkB,EAClB,YAAY,EACZ,iBAAiB,EACjB,SAAS,EACT,cAAc,EACd,WAAW,EACX,qBAAqB,EACrB,eAAe,EACf,mBAAmB,EACnB,kBAAkB,EAClB,cAAc,EAEf,MAAM,oBAAoB,CAAC;AA4B5B,qBAAa,YAAa,YAAW,YAAY;IAC/C,IAAI,SAAW;IACf,WAAW,SAAkB;IAE7B;;;;;;;;;;;;OAYG;IACH,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAexC;IAEI,WAAW,IAAI,OAAO,CAAC,OAAO,CAAC;IASrC,cAAc,CACZ,SAAS,EAAE,iBAAiB,EAC5B,IAAI,CAAC,EAAE,SAAS,EAChB,QAAQ,CAAC,EAAE,MAAM,GAChB,kBAAkB;IAmDrB,iBAAiB,CAAC,MAAM,EAAE,kBAAkB,GAAG,YAAY;IAa3D;;;;;;;;;;;;;;;;;;;;;;OAsBG;IACG,cAAc,CAClB,SAAS,EAAE,iBAAiB,EAC5B,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,qBAAqB,GAC9B,OAAO,CAAC,eAAe,CAAC;IAqM3B;;;;;OAKG;IACG,kBAAkB,CAAC,SAAS,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAIpE;;;;;;;;;;;OAWG;IACG,YAAY,CAChB,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE,mBAAmB,GAC5B,OAAO,CAAC,kBAAkB,CAAC;IA6G9B;;;;;OAKG;IACH,OAAO,CAAC,mBAAmB;IAM3B;;;OAGG;IACH,OAAO,CAAC,oBAAoB;IAkB5B;;;OAGG;IACH,OAAO,CAAC,sBAAsB;IAwB9B;;OAEG;IACH,OAAO,CAAC,oBAAoB;IA4B5B;;;;;;;;;;;;;;;;;;;;;OAqBG;IACH,0BAA0B,CACxB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,GACtC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAQ1C,OAAO,CAAC,kBAAkB;IAgD1B;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAY/B;;;;;;;;OAQG;IACH,eAAe,CAAC,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,WAAW,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC;IAQ9E;;;;;;;;;OASG;IACH,OAAO,CAAC,kBAAkB;IAgD1B;;;;;;OAMG;IACH,OAAO,CAAC,uBAAuB;IAY/B,OAAO,CAAC,cAAc;IAStB;;;OAGG;IACH,OAAO,CAAC,cAAc;IAyBtB,+EAA+E;IAC/E,OAAO,CAAC,QAAQ;IAShB;;;;;;;;;;OAUG;IACH,OAAO,CAAC,gBAAgB;IAmCxB;;;;;;;;;OASG;IACH,OAAO,CAAC,oBAAoB;IAgG5B,OAAO,CAAC,kBAAkB;IAO1B;;;;;;OAMG;IACH,OAAO,CAAC,gBAAgB;IA4BxB;;;;;;;;;;;;;;;;OAgBG;IACH,OAAO,CAAC,gBAAgB;IA4BxB;;;;;;;OAOG;IACH,OAAO,CAAC,cAAc;IAKtB;;;;;OAKG;IACH,OAAO,CAAC,kBAAkB;IAoB1B,mFAAmF;IACnF,OAAO,CAAC,kBAAkB;IAY1B,OAAO,CAAC,gBAAgB;CAYzB"}