@xallindrome-labs/codex-bridge 0.0.0-stage → 0.2.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.
@@ -0,0 +1,12 @@
1
+ {
2
+ "name": "codex-bridge",
3
+ "owner": { "name": "Xallindrome", "url": "https://xallindrome.co" },
4
+ "metadata": { "description": "Claude Code add-ons for codex-bridge." },
5
+ "plugins": [
6
+ {
7
+ "name": "codex-jobs",
8
+ "source": "./plugins/codex-jobs",
9
+ "description": "A pane beside the chat that lists your codex-bridge jobs live: running, done, interrupted."
10
+ }
11
+ ]
12
+ }
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Wilson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,186 @@
1
- # Temporary Holding Version
1
+ # codex-bridge
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ **Codex jobs for Claude Code that survive crashes, closed windows and reboots, with Codex image generation built in.** A dependency-free MCP server built Windows-first: every call is a job saved on disk, so you can close Claude, lose power, or kill a process, and pick the work up again with `codex_resume`.
4
+
5
+ Roles: **Claude Code implements. Codex plans and builds images.** Optionally (opt-in, one folder), Codex can also write code while Claude reviews: see [Let Codex write code](#let-codex-write-code-opt-in). And an optional pane beside the chat lists your jobs live and shows your Codex usage and when it resets: see [Codex jobs pane](#codex-jobs-pane).
6
+
7
+ ![Recorded session: Claude Code starts codex-bridge and asks Codex for a plan](docs/demo.svg)
8
+
9
+ <sub>The server calls in this recording are real (Codex at `effort: low`, idle time trimmed); the `claude mcp add` line is scripted. The raw recording is [docs/demo.cast](docs/demo.cast); play it with `asciinema play docs/demo.cast`.</sub>
10
+
11
+ ## Quickstart
12
+
13
+ You need [Node.js](https://nodejs.org) 20 or newer and the [Codex CLI](https://github.com/openai/codex) installed and logged in.
14
+
15
+ ```bash
16
+ claude mcp add codex-bridge -- npx -y @xallindrome-labs/codex-bridge
17
+ ```
18
+
19
+ Restart Claude Code, then ask it something like *"Get a plan from Codex for adding rate limiting, in the background, and keep going"*. Claude will call `codex_plan` with `background: true` and collect the answer later.
20
+
21
+ Other MCP clients: run `npx -y @xallindrome-labs/codex-bridge` as a stdio server.
22
+
23
+ > **Platform note:** developed and tested on Windows 11, where real Codex jobs have been run. CI checks on Linux, macOS and Windows (Node 20 and 22) that the server starts, speaks MCP and works when installed from the npm tarball, and on Linux and macOS that jobs run, finish and survive the server being killed, using a fake `codex`. CI has no real Codex CLI, so real Codex jobs on macOS and Linux are untested. Reports and fixes are welcome.
24
+
25
+ | Tool | What it does | Sandbox | Default model / effort |
26
+ |---|---|---|---|
27
+ | `codex_plan` | Codex writes implementation plans, design reviews and second opinions. It inspects the project in `cwd`. | `read-only` (always) | Your Codex config defaults |
28
+ | `codex_image` | Codex generates or edits images. The bridge copies each one into `output_dir` as soon as it's made. | `read-only` (always) | **gpt-6-luna / high** |
29
+ | `codex_code` | **Off by default.** Codex implements a change by editing files in one folder you choose. See [below](#let-codex-write-code-opt-in). | `workspace-write`, that folder only, no network | Your Codex config defaults |
30
+ | `codex_result` | Gets a job's result or progress (`wait_seconds` up to 240), or lists recent jobs. | | |
31
+ | `codex_resume` | Continues an interrupted, timed-out or failed job from where it stopped. | | |
32
+ | `codex_cancel` | Stops a running job. | | |
33
+
34
+ `codex_plan`, `codex_image` and `codex_code` accept:
35
+ - `effort`: `low`, `medium`, `high`, `xhigh`, `max` or `ultra` (Luna supports up to `max`).
36
+ - `model`: overrides the default model for that call.
37
+ - `background: true`: returns a `job_id` immediately so Claude can keep working.
38
+ - `session_id`: continues an earlier Codex thread, for example "make the icon darker".
39
+
40
+ `codex_image` also takes `count`, the number of images wanted. Set it for batches: it enables `k/count` progress and resumes that ask for exactly the missing images.
41
+
42
+ ## Let Codex write code (opt-in)
43
+
44
+ By default Codex can only read. If you want it to implement things while Claude reviews, name the one folder it may edit:
45
+
46
+ ```bash
47
+ claude mcp add codex-bridge -e CODEX_BRIDGE_WRITE_ROOT=/path/to/your/project -- npx -y @xallindrome-labs/codex-bridge
48
+ ```
49
+
50
+ Restart Claude Code and a fourth tool, `codex_code`, appears. Ask for it in plain words: *"Have Codex implement the plan in the background, then review its diff."*
51
+
52
+ - **One folder.** `cwd` must be `CODEX_BRIDGE_WRITE_ROOT` or inside it. Anything else is refused, checked on real paths so `..` and symlinks don't escape. A drive root or your home folder is refused as too broad.
53
+ - **No network, no commits.** Codex runs with network access off and is told to leave changes uncommitted so you can see them in `git diff`. The no-commit rule is a prompt, not a sandbox rule.
54
+ - **Claude reviews.** Every `codex_code` result ends with a reminder to read the diff before accepting it. A failed or cancelled run says Codex may have left partial edits.
55
+ - **Same job machinery.** It survives restarts, `background: true` and `codex_resume` work the same way, and a resume re-checks the folder rule.
56
+ - **Temp files.** In our test on Windows 11, writes outside the folder and network access were blocked, but the system temp folder stayed writable. Treat temp as writable. `CODEX_BRIDGE_WRITE_ALLOW_TMP=1` stops asking Codex to block it.
57
+ - **Don't edit the same files yourself** while a `codex_code` job runs.
58
+
59
+ Unset the variable and restart, and the tool is gone; `codex_plan` and `codex_image` are always read-only.
60
+
61
+ ## Codex jobs pane
62
+
63
+ An optional add-on that opens a pane beside the Claude Code chat listing your Codex jobs live: running, done, interrupted, with age, request and `images k/n` for batches. It also shows your Codex usage: the 5-hour and weekly windows, percent used, when each resets, and how many images were generated in each (Codex has one shared meter, so images use the same limits). It only reads the job files on disk, so it shows jobs from any session, including ones that kept running after you closed Claude. It also puts `codex: N running` in the status line and shows a toast when a job you watched finishes.
64
+
65
+ At the prompt of a terminal Claude Code session:
66
+
67
+ ```
68
+ /plugin install codex-jobs --marketplace xallindrome-labs/codex-bridge
69
+ ```
70
+
71
+ In the desktop app, where `/plugin` does not run: click **+** next to the prompt box, then **Plugins**, **Add marketplace** with `xallindrome-labs/codex-bridge`, then **Add plugin**. Without a marketplace, for one session: `claude --plugin-dir "$(npx -y @xallindrome-labs/codex-bridge --pane-path)"`.
72
+
73
+ Then type `/codex-jobs`. It also opens by itself the first time a job is running in a session. Details and how it works are in [plugins/codex-jobs](plugins/codex-jobs/README.md). It needs a Claude Code build that supports mods; it is tested against the engine's own test kit, not every surface's paint.
74
+
75
+ ## How it compares
76
+
77
+ OpenAI's [codex-plugin-cc](https://github.com/openai/codex-plugin-cc) is the official way to use Codex from Claude Code, and it's good at what it does: slash commands for review and rescue. codex-bridge is an MCP server instead, with a different focus: **jobs that don't get lost**, and **images**.
78
+
79
+ Several open issues in the plugin's tracker are about jobs getting lost, and codex-bridge is designed to avoid each of them. These come from reading the designs, not from a head-to-head test:
80
+
81
+ | Reported in codex-plugin-cc | How codex-bridge handles it |
82
+ |---|---|
83
+ | Background job shows "running" forever after its worker dies ([#704](https://github.com/openai/codex-plugin-cc/issues/704), [#699](https://github.com/openai/codex-plugin-cc/issues/699)) | Status is worked out from files on disk plus a heartbeat and a process check. A dead runner shows as `interrupted`, never "running", and `codex_resume` continues it. |
84
+ | Windows `cancel` silently fails ([#708](https://github.com/openai/codex-plugin-cc/issues/708)) | Cancel writes a marker file; the runner that owns the Codex process sees it within about 2 seconds and stops the process tree by calling `taskkill.exe` directly, with no shell in between. |
85
+ | Windows broker processes leak ([#718](https://github.com/openai/codex-plugin-cc/issues/718), [#782](https://github.com/openai/codex-plugin-cc/issues/782)) | There is no shared broker. Each attempt is one runner process that exits when Codex does. |
86
+ | One session ending kills other sessions' jobs ([#671](https://github.com/openai/codex-plugin-cc/issues/671)) | Runners are started outside Claude's process tree (via WMI on Windows), so closing a session can't take them down. |
87
+
88
+ What the plugin does that this doesn't: slash commands, and its review-gate and rescue workflows. And this one is newer, with far fewer users. If the plugin works for you, keep using it. If you've lost jobs, or want Codex to generate images that survive a crash, try this.
89
+
90
+ ## Jobs survive restarts
91
+
92
+ Every call is a job saved under `~/.codex-bridge/jobs/<job_id>/`, including calls that aren't run in the background.
93
+
94
+ - **Claude closes or crashes:** the job keeps running. `server.mjs` starts `runner.mjs` through WMI (`Win32_Process.Create`), so the job isn't part of Claude Code's process tree. Closing Claude can't kill it, and any later Claude session can collect it with `codex_result`.
95
+ - **The job itself dies** (reboot, power loss, Codex crash, or killed process): the job is marked `interrupted`. The next Claude session is told about it when it starts. `codex_resume` continues it:
96
+ 1. It copies out any images generated just before the interruption.
97
+ 2. It resumes the same Codex thread: "N are already done, generate only the remaining ones."
98
+ 3. If no Codex thread had started yet, it reruns the original request.
99
+
100
+ ## How images are collected
101
+
102
+ Codex never writes image files. Its image tool stores every generated image in `~/.codex/generated_images/<thread>/`. The runner checks that folder every 2 seconds and copies new images into `output_dir` as `<filename>.png`, `<filename>-2.png`, and so on, in the order they were made. `copied.json` in the job folder records what has been copied, so an image is never copied twice across attempts.
103
+
104
+ This gives three benefits:
105
+ - Image jobs run in a read-only sandbox.
106
+ - A crash loses at most the one image being generated.
107
+ - Resume counts exactly what's done.
108
+
109
+ It also avoids a real problem: when Luna was allowed to save files itself, it re-typed each image as base64 through dozens of shell commands. That was slow, and the saved file no longer matched the original.
110
+ - **Codex stalls:** the runner stops a run after 10 minutes with no output. The job is marked `timed_out`, and it can be resumed.
111
+
112
+ Each attempt keeps its own `prompt.txt`, `events.jsonl` (the Codex event stream), `stderr.txt`, heartbeat and `result.json`. Finished jobs are deleted after 14 days.
113
+
114
+ ## How it works
115
+
116
+ Each attempt runs `codex exec --json` (or `codex exec resume <thread>`) with the prompt on stdin. No shell is involved.
117
+
118
+ Every call runs read-only, except `codex_code` when you have opted in (it runs `workspace-write` in your one folder, with network off). New sessions pass `-s read-only` plus `-c sandbox_mode="read-only"`; resumed sessions pass `-c sandbox_mode="read-only"` (`codex exec resume` takes no `-s`). Your Codex config may default to `danger-full-access`, but that default never applies to bridge calls. The `plan` smoke test checks that a write attempted from a resumed session fails.
119
+
120
+ | File | Role |
121
+ |---|---|
122
+ | `server.mjs` | The MCP server. It creates jobs, launches runners, works out job status from the files on disk, and handles resume and cancel. |
123
+ | `runner.mjs` | Runs one attempt: starts Codex, records the heartbeat, watches for a stall or cancellation, collects images and writes `result.json`. |
124
+ | `lib.mjs` | Shared helpers: finding the Codex binary, building arguments, parsing events, handling images. |
125
+
126
+ ## Install options
127
+
128
+ ```bash
129
+ # for all your projects
130
+ claude mcp add -s user codex-bridge -- npx -y @xallindrome-labs/codex-bridge
131
+
132
+ # or from a local checkout
133
+ claude mcp add codex-bridge -- node /path/to/codex-bridge/server.mjs
134
+ ```
135
+
136
+ Remove it with `claude mcp remove codex-bridge` (add `-s user` if you installed it that way).
137
+
138
+ ## Configuration (env vars)
139
+
140
+ | Var | Default |
141
+ |---|---|
142
+ | `CODEX_BIN` | Auto-detected. It prefers a `codex.exe` next to `codex-code-mode-host.exe`, which is currently `~/.codex/plugins/.plugin-appserver/codex.exe`. |
143
+ | `CODEX_BRIDGE_HOME` | `~/.codex-bridge` |
144
+ | `CODEX_BRIDGE_IDLE_TIMEOUT_MS` | `600000` (stops a run after 10 minutes with no Codex output) |
145
+ | `CODEX_BRIDGE_PLAN_MODEL` / `CODEX_BRIDGE_PLAN_EFFORT` | Your Codex config defaults |
146
+ | `CODEX_BRIDGE_IMAGE_MODEL` / `CODEX_BRIDGE_IMAGE_EFFORT` | `gpt-6-luna` / `high` |
147
+ | `CODEX_BRIDGE_WRITE_ROOT` | Unset: `codex_code` is off. Set to one folder to let `codex_code` edit files there. |
148
+ | `CODEX_BRIDGE_WRITE_ALLOW_TMP` | Unset. `1` stops asking Codex to block temp-folder writes (not enforced on Windows anyway). |
149
+ | `CODEX_BRIDGE_CODE_MODEL` / `CODEX_BRIDGE_CODE_EFFORT` | Your Codex config defaults |
150
+
151
+ ## Test
152
+
153
+ ```bash
154
+ node smoke-test.mjs <scratch-dir> [plan|image|jobs|durable|all]
155
+ ```
156
+
157
+ The smoke test drives the server over stdio like an MCP client and needs a logged-in Codex CLI; it uses a little of your Codex quota. The `durable` part uses `taskkill`, so it is Windows-only for now.
158
+
159
+ - `plan`: runs a plan, then tries a write from a resumed session, which must fail.
160
+ - `image`: generates one image.
161
+ - `jobs`: tests background jobs, cancellation and the `effort` check.
162
+ - `durable`:
163
+ 1. Kills the server while a job is running, then checks that a new server collects the result.
164
+ 2. Hard-kills an image batch partway through, then checks that a new server reports it as interrupted and that `codex_resume` finishes it.
165
+
166
+ ## Speed
167
+
168
+ Each call has a fixed cost of about 4–5 seconds, and changing settings barely moves it. The rest of the time is Codex thinking and generating, which takes seconds to minutes. To spend less time waiting, use `background: true`, a lower `effort` for quick questions, and include relevant code in `context` so Codex reads less of the project.
169
+
170
+ ## Notes
171
+
172
+ - This uses Codex's own login and quota, with no separate API key. A job left running after Claude closes keeps using that quota until it finishes.
173
+ - Bridge calls run headlessly, so they don't show up as conversations in the Codex Desktop sidebar.
174
+ - Don't send secrets in prompts. Prompts go to OpenAI.
175
+
176
+ ## Issues and contributing
177
+
178
+ Bug reports and ideas are welcome on the [issue tracker](https://github.com/xallindrome-labs/codex-bridge/issues). Pull requests are welcome too, but reviewed slowly; see [CONTRIBUTING.md](CONTRIBUTING.md). Please report security problems privately, as described in [SECURITY.md](SECURITY.md).
179
+
180
+ ## License
181
+
182
+ [MIT](LICENSE)
183
+
184
+ ---
185
+
186
+ **Built by [Xallindrome](https://xallindrome.co)**: we build automation and AI workflows for small businesses. If you'd like something like this for your business, [book a diagnostic](https://xallindrome.co).
package/SECURITY.md ADDED
@@ -0,0 +1,23 @@
1
+ # Security policy
2
+
3
+ ## Reporting a vulnerability
4
+
5
+ Please report security problems privately through GitHub:
6
+ **Security → Report a vulnerability** on https://github.com/xallindrome-labs/codex-bridge/security/advisories/new
7
+
8
+ Please don't open a public issue for a vulnerability. This is a one-person project, so expect an acknowledgement within a week, not hours.
9
+
10
+ ## What codex-bridge does that matters for security
11
+
12
+ - It runs the Codex CLI on your machine, by default always with a **read-only sandbox**: `-c sandbox_mode="read-only"` on every call, plus `-s read-only` when starting a new session. Your Codex config's default sandbox mode never applies to bridge calls.
13
+ - **Opt-in exception: `codex_code`.** Only if you set `CODEX_BRIDGE_WRITE_ROOT` to one folder, an extra tool `codex_code` appears. It runs Codex in the `workspace-write` sandbox with network access off, and refuses any `cwd` that is not that folder or inside it (checked on the real path, so `..` and symlinks don't escape; a drive root or your home folder is refused as too broad). The check repeats on every resume. Everything else stays read-only. In our test on Windows 11, writes outside the folder and network access were blocked, but the system temp folder stayed writable even with the flags that ask Codex to block it (`CODEX_BRIDGE_WRITE_ALLOW_TMP` unset); treat temp as writable. Codex is told not to commit or push, but that is a prompt, not a sandbox rule: review the diff.
14
+ - It starts Codex without a shell: arguments are passed as an array and the prompt goes in over stdin.
15
+ - Prompts and any `context` you pass are sent to OpenAI through the Codex CLI, under your own Codex login. **Don't put secrets in prompts.**
16
+ - Job files (prompts, Codex event streams, results) are stored unencrypted under `~/.codex-bridge/jobs/` (override with `CODEX_BRIDGE_HOME`) and deleted after 14 days.
17
+ - It has no network listener and no dependencies. It talks to its MCP client over stdio only.
18
+
19
+ Reports about any of the above not holding true are in scope.
20
+
21
+ ## Supported versions
22
+
23
+ Only the latest release.
package/docs/demo.svg ADDED
@@ -0,0 +1,40 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="855.2" height="605" viewBox="0 0 855.2 605" role="img" aria-label="codex-bridge: Claude Code asks Codex for a plan (recorded terminal session)">
2
+ <title>codex-bridge: Claude Code asks Codex for a plan</title>
3
+ <style>:root{--bg:#0f1115;--fg:#d7dae0;--dim:#7d8590;--cy:#56b6c2;--gr:#7ee787}
4
+ text{font:14px ui-monospace,SFMono-Regular,Menlo,Consolas,monospace;fill:var(--fg);white-space:pre}
5
+ .dim{fill:var(--dim)}.cy{fill:var(--cy)}.gr{fill:var(--gr)}
6
+ .l{opacity:0}@keyframes k0{0%,0.032%{opacity:0}0.042%,96%{opacity:1}100%{opacity:0}}.l0{animation:k0 18.88s linear infinite}@keyframes k1{0%,14.165%{opacity:0}14.175%,96%{opacity:1}100%{opacity:0}}.l1{animation:k1 18.88s linear infinite}@keyframes k2{0%,17.380%{opacity:0}17.390%,96%{opacity:1}100%{opacity:0}}.l2{animation:k2 18.88s linear infinite}@keyframes k3{0%,32.949%{opacity:0}32.959%,96%{opacity:1}100%{opacity:0}}.l3{animation:k3 18.88s linear infinite}@keyframes k4{0%,33.367%{opacity:0}33.377%,96%{opacity:1}100%{opacity:0}}.l4{animation:k4 18.88s linear infinite}@keyframes k5{0%,37.672%{opacity:0}37.682%,96%{opacity:1}100%{opacity:0}}.l5{animation:k5 18.88s linear infinite}@keyframes k6{0%,48.030%{opacity:0}48.040%,96%{opacity:1}100%{opacity:0}}.l6{animation:k6 18.88s linear infinite}@keyframes k7{0%,54.385%{opacity:0}54.395%,96%{opacity:1}100%{opacity:0}}.l7{animation:k7 18.88s linear infinite}@keyframes k8{0%,60.739%{opacity:0}60.749%,96%{opacity:1}100%{opacity:0}}.l8{animation:k8 18.88s linear infinite}@keyframes k9{0%,60.739%{opacity:0}60.749%,96%{opacity:1}100%{opacity:0}}.l9{animation:k9 18.88s linear infinite}@keyframes k10{0%,60.745%{opacity:0}60.755%,96%{opacity:1}100%{opacity:0}}.l10{animation:k10 18.88s linear infinite}@keyframes k11{0%,67.099%{opacity:0}67.109%,96%{opacity:1}100%{opacity:0}}.l11{animation:k11 18.88s linear infinite}@keyframes k12{0%,67.099%{opacity:0}67.109%,96%{opacity:1}100%{opacity:0}}.l12{animation:k12 18.88s linear infinite}@keyframes k13{0%,67.099%{opacity:0}67.109%,96%{opacity:1}100%{opacity:0}}.l13{animation:k13 18.88s linear infinite}@keyframes k14{0%,73.454%{opacity:0}73.464%,96%{opacity:1}100%{opacity:0}}.l14{animation:k14 18.88s linear infinite}@keyframes k15{0%,73.454%{opacity:0}73.464%,96%{opacity:1}100%{opacity:0}}.l15{animation:k15 18.88s linear infinite}@keyframes k16{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l16{animation:k16 18.88s linear infinite}@keyframes k17{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l17{animation:k17 18.88s linear infinite}@keyframes k18{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l18{animation:k18 18.88s linear infinite}@keyframes k19{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l19{animation:k19 18.88s linear infinite}@keyframes k20{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l20{animation:k20 18.88s linear infinite}@keyframes k21{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l21{animation:k21 18.88s linear infinite}@keyframes k22{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l22{animation:k22 18.88s linear infinite}@keyframes k23{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l23{animation:k23 18.88s linear infinite}@keyframes k24{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l24{animation:k24 18.88s linear infinite}@keyframes k25{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l25{animation:k25 18.88s linear infinite}@keyframes k26{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l26{animation:k26 18.88s linear infinite}@keyframes k27{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l27{animation:k27 18.88s linear infinite}@keyframes k28{0%,78.818%{opacity:0}78.828%,96%{opacity:1}100%{opacity:0}}.l28{animation:k28 18.88s linear infinite}</style>
7
+ <rect width="855.2" height="605" rx="8" fill="var(--bg)"/>
8
+ <circle cx="20" cy="18" r="5.5" fill="#ff5f56"/><circle cx="40" cy="18" r="5.5" fill="#ffbd2e"/><circle cx="60" cy="18" r="5.5" fill="#27c93f"/>
9
+ <text x="427.6" y="22" text-anchor="middle" class="dim" style="font-size:12px">codex-bridge (real run, idle time trimmed)</text>
10
+ <text class="l l0" x="16" y="52" xml:space="preserve"><tspan class="">$ claude mcp add codex-bridge -- npx -y @xallindrome-labs/codex-bridge</tspan></text>
11
+ <text class="l l1" x="16" y="71" xml:space="preserve"><tspan class="">Added stdio MCP server codex-bridge</tspan></text>
12
+ <text class="l l2" x="16" y="90" xml:space="preserve"><tspan class=""># Claude Code starts the server and asks what it offers:</tspan></text>
13
+ <text class="l l3" x="16" y="109" xml:space="preserve"><tspan class="">server: codex-bridge 0.1.0</tspan></text>
14
+ <text class="l l4" x="16" y="128" xml:space="preserve"><tspan class="">tools: codex_plan, codex_image, codex_result, codex_resume, codex_cancel</tspan></text>
15
+ <text class="l l5" x="16" y="147" xml:space="preserve"><tspan class=""># Claude asks Codex for a plan (read-only sandbox, effort low):</tspan></text>
16
+ <text class="l l6" x="16" y="166" xml:space="preserve"><tspan class="cy">codex_plan</tspan><tspan class=""> request="app.js add() returns a - b. Plan a fix plus a test, under 8 lines."</tspan></text>
17
+ <text class="l l7" x="16" y="185" xml:space="preserve"><tspan class="dim"> ... codex: I’ll inspect `app.js` and the test setup.</tspan></text>
18
+ <text class="l l8" x="16" y="204" xml:space="preserve"><tspan class="dim"> ... codex: running "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command "rg </tspan></text>
19
+ <text class="l l9" x="16" y="223" xml:space="preserve"><tspan class="dim">--files -g '"'!node_modules'"' -g '"'!pack</tspan></text>
20
+ <text class="l l10" x="16" y="242" xml:space="preserve"><tspan class="dim"> ... codex: command_execution</tspan></text>
21
+ <text class="l l11" x="16" y="261" xml:space="preserve"><tspan class="dim"> ... codex: running "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe" -Command </tspan></text>
22
+ <text class="l l12" x="16" y="280" xml:space="preserve"><tspan class="dim">'Get-Content app.js; Get-ChildItem -Force -Nam</tspan></text>
23
+ <text class="l l13" x="16" y="299" xml:space="preserve"><tspan class="dim"> ... codex: command_execution</tspan></text>
24
+ <text class="l l14" x="16" y="318" xml:space="preserve"><tspan class="dim"> ... codex: Goal/acceptance: `app.js`’s exported `add(a, b)` returns the sum; a regression test </tspan></text>
25
+ <text class="l l15" x="16" y="337" xml:space="preserve"><tspan class="dim">passes.
26
+ 1. In `app.js`, change `add()` from `return a - b;` to `return a + b</tspan></text>
27
+ <text class="l l16" x="16" y="356" xml:space="preserve"><tspan class="gr">done in 19s</tspan><tspan class=""> isError=false</tspan></text>
28
+ <text class="l l17" x="16" y="375" xml:space="preserve"><tspan class="">Goal/acceptance: `app.js`’s exported `add(a, b)` returns the sum; a regression test passes.</tspan></text>
29
+ <text class="l l18" x="16" y="394" xml:space="preserve"><tspan class="">1. In `app.js`, change `add()` from `return a - b;` to `return a + b;`, preserving the CommonJS </tspan></text>
30
+ <text class="l l19" x="16" y="413" xml:space="preserve"><tspan class="">export.</tspan></text>
31
+ <text class="l l20" x="16" y="432" xml:space="preserve"><tspan class="">2. Create `app.test.js` using `node:test` and `node:assert/strict`; import `add` via </tspan></text>
32
+ <text class="l l21" x="16" y="451" xml:space="preserve"><tspan class="">`require('./app')` and assert `add(2, 3) === 5`.</tspan></text>
33
+ <text class="l l22" x="16" y="470" xml:space="preserve"><tspan class="">3. Verify with `node --test app.test.js`; confirm the test fails against the original subtraction.</tspan></text>
34
+ <text class="l l23" x="16" y="489" xml:space="preserve"><tspan class="">Risk/open question: No package manifest or test setup exists; confirm Node supports `node:test`.</tspan></text>
35
+ <text class="l l24" x="16" y="508" xml:space="preserve"><tspan class="">---</tspan></text>
36
+ <text class="l l25" x="16" y="527" xml:space="preserve"><tspan class="">job_id: job-261022182741-042a</tspan></text>
37
+ <text class="l l26" x="16" y="546" xml:space="preserve"><tspan class="">session_id: 01a164de-7990-76d3-bd87-ba22f1b9a31b (pass as session_id to continue this Codex </tspan></text>
38
+ <text class="l l27" x="16" y="565" xml:space="preserve"><tspan class="">thread)</tspan></text>
39
+ <text class="l l28" x="16" y="584" xml:space="preserve"><tspan class="">model: codex default, effort: low</tspan></text>
40
+ </svg>
package/lib.mjs ADDED
@@ -0,0 +1,208 @@
1
+ // Shared helpers for server.mjs (MCP side) and runner.mjs (detached job runner).
2
+
3
+ import { spawnSync } from 'node:child_process';
4
+ import fs from 'node:fs';
5
+ import os from 'node:os';
6
+ import path from 'node:path';
7
+
8
+ export const isWin = process.platform === 'win32';
9
+ export const CODEX_HOME = process.env.CODEX_HOME || path.join(os.homedir(), '.codex');
10
+ export const BRIDGE_HOME = process.env.CODEX_BRIDGE_HOME || path.join(os.homedir(), '.codex-bridge');
11
+ export const JOBS_DIR = path.join(BRIDGE_HOME, 'jobs');
12
+ export const IMAGE_EXT = /\.(png|jpe?g|webp|gif)$/i;
13
+
14
+ // ---------------------------------------------------------------- codex binary
15
+
16
+ function findOnPath(name) {
17
+ const r = spawnSync(isWin ? 'where.exe' : 'which', [name], { encoding: 'utf8', windowsHide: true });
18
+ if (r.status !== 0) return [];
19
+ return r.stdout.split(/\r?\n/).map(s => s.trim()).filter(Boolean);
20
+ }
21
+
22
+ // Prefer a codex binary that sits next to codex-code-mode-host, otherwise
23
+ // Codex logs a "Code Mode is unavailable" error on every run.
24
+ export function resolveCodexBin() {
25
+ if (process.env.CODEX_BIN) return process.env.CODEX_BIN;
26
+ const exe = isWin ? 'codex.exe' : 'codex';
27
+ const candidates = [
28
+ ...findOnPath(exe),
29
+ path.join(CODEX_HOME, 'plugins', '.plugin-appserver', exe),
30
+ path.join(CODEX_HOME, '.sandbox-bin', exe),
31
+ ].filter(p => fs.existsSync(p));
32
+ const host = isWin ? 'codex-code-mode-host.exe' : 'codex-code-mode-host';
33
+ return candidates.find(p => fs.existsSync(path.join(path.dirname(p), host))) ?? candidates[0] ?? null;
34
+ }
35
+
36
+ // `codex exec` (or `codex exec resume`) arguments; the prompt is always read from stdin.
37
+ // workspace-write is the opt-in codex_code sandbox: Codex may write in `cwd` only, with no network.
38
+ // Unless allowTmp, the system temp dirs are not writable either, so "one folder" really means one folder.
39
+ export function buildCodexArgs({ sandbox, cwd, sessionId, model, effort, images = [], allowTmp = false }) {
40
+ const args = ['exec'];
41
+ if (sessionId) args.push('resume');
42
+ args.push('--json', '--skip-git-repo-check', '-c', `sandbox_mode="${sandbox}"`);
43
+ if (sandbox === 'workspace-write') {
44
+ args.push('-c', 'sandbox_workspace_write.network_access=false');
45
+ if (!allowTmp) args.push('-c', 'sandbox_workspace_write.exclude_tmpdir_env_var=true', '-c', 'sandbox_workspace_write.exclude_slash_tmp=true');
46
+ }
47
+ if (!sessionId) args.push('-s', sandbox, '-C', cwd);
48
+ if (model) args.push('-m', model);
49
+ if (effort) args.push('-c', `model_reasoning_effort="${effort}"`);
50
+ for (const img of images) args.push('-i', img);
51
+ if (sessionId) args.push(sessionId);
52
+ args.push('-');
53
+ return args;
54
+ }
55
+
56
+ // ---------------------------------------------------------------- write root
57
+
58
+ // The one folder codex_code may write in (CODEX_BRIDGE_WRITE_ROOT), as a real path. Throws with a
59
+ // reason if it is unusable: missing, not a folder, a drive root or the home folder (too broad).
60
+ export function resolveWriteRoot(raw = process.env.CODEX_BRIDGE_WRITE_ROOT) {
61
+ if (!raw) return null;
62
+ let root;
63
+ try { root = fs.realpathSync(path.resolve(raw)); } catch { throw new Error(`CODEX_BRIDGE_WRITE_ROOT does not exist: ${raw}`); }
64
+ if (!fs.statSync(root).isDirectory()) throw new Error(`CODEX_BRIDGE_WRITE_ROOT is not a folder: ${raw}`);
65
+ let home = os.homedir();
66
+ try { home = fs.realpathSync(home); } catch {}
67
+ if (path.parse(root).root === root || root === home) {
68
+ throw new Error(`CODEX_BRIDGE_WRITE_ROOT is too broad (${root}); point it at one project folder`);
69
+ }
70
+ return root;
71
+ }
72
+
73
+ // Real path of `dir` if it is `root` or inside it, otherwise throws.
74
+ export function insideWriteRoot(root, dir) {
75
+ let real;
76
+ try { real = fs.realpathSync(path.resolve(dir)); } catch { throw new Error(`cwd does not exist: ${dir}`); }
77
+ const rel = path.relative(root, real);
78
+ if (rel === '..' || rel.startsWith(`..${path.sep}`) || path.isAbsolute(rel)) {
79
+ throw new Error(`codex_code may only write inside ${root}; ${real} is outside it`);
80
+ }
81
+ return real;
82
+ }
83
+
84
+ // ---------------------------------------------------------------- files
85
+
86
+ export function readJson(p, fallback = null) {
87
+ try { return JSON.parse(fs.readFileSync(p, 'utf8')); } catch { return fallback; }
88
+ }
89
+
90
+ // Atomic write so a reader never sees a half-written file.
91
+ export function writeJson(p, obj) {
92
+ const tmp = `${p}.${process.pid}.tmp`;
93
+ fs.writeFileSync(tmp, JSON.stringify(obj, null, 2));
94
+ fs.renameSync(tmp, p);
95
+ }
96
+
97
+ export const jobDir = id => path.join(JOBS_DIR, id);
98
+ export const attemptDir = (id, n) => path.join(JOBS_DIR, id, `attempt-${n}`);
99
+
100
+ export function pidAlive(pid) {
101
+ if (!pid) return false;
102
+ try { process.kill(pid, 0); return true; } catch (e) { return e.code === 'EPERM'; }
103
+ }
104
+
105
+ export function killTree(pid) {
106
+ if (!pid) return;
107
+ if (isWin) spawnSync('taskkill.exe', ['/PID', String(pid), '/T', '/F'], { windowsHide: true });
108
+ else try { process.kill(-pid, 'SIGTERM'); } catch { try { process.kill(pid, 'SIGTERM'); } catch {} }
109
+ }
110
+
111
+ // ---------------------------------------------------------------- codex events
112
+
113
+ export function newEventState() {
114
+ return { threadId: null, messages: [], errors: [], usage: null, failed: null, completed: false, activity: 'starting' };
115
+ }
116
+
117
+ export function summarizeEvent(ev) {
118
+ if (ev.type === 'item.started' && ev.item?.type === 'command_execution') return `running ${String(ev.item.command).slice(0, 120)}`;
119
+ if (ev.type === 'item.completed' && ev.item?.type === 'agent_message') return ev.item.text.slice(0, 160);
120
+ if (ev.type === 'item.completed' && ev.item?.type) return ev.item.type;
121
+ return null;
122
+ }
123
+
124
+ export function applyEvent(s, ev) {
125
+ if (ev.type === 'thread.started') s.threadId = ev.thread_id;
126
+ else if (ev.type === 'turn.completed') { s.completed = true; s.usage = ev.usage; }
127
+ else if (ev.type === 'turn.failed') s.failed = ev.error?.message || JSON.stringify(ev.error);
128
+ else if (ev.type === 'error') s.errors.push(ev.message);
129
+ else if (ev.type === 'item.completed' && ev.item) {
130
+ if (ev.item.type === 'agent_message') s.messages.push(ev.item.text);
131
+ else if (ev.item.type === 'error' && !/Code Mode is unavailable/.test(ev.item.message)) s.errors.push(ev.item.message);
132
+ }
133
+ s.activity = summarizeEvent(ev) ?? s.activity;
134
+ }
135
+
136
+ // Incrementally reads a growing JSONL events file.
137
+ export class EventTail {
138
+ constructor(file) { this.file = file; this.offset = 0; this.partial = ''; this.state = newEventState(); }
139
+ poll(onEvent) {
140
+ let fd;
141
+ try { fd = fs.openSync(this.file, 'r'); } catch { return this.state; }
142
+ try {
143
+ const size = fs.fstatSync(fd).size;
144
+ if (size > this.offset) {
145
+ const buf = Buffer.alloc(size - this.offset);
146
+ fs.readSync(fd, buf, 0, buf.length, this.offset);
147
+ this.offset = size;
148
+ const lines = (this.partial + buf.toString('utf8')).split(/\r?\n/);
149
+ this.partial = lines.pop();
150
+ for (const line of lines) {
151
+ let ev;
152
+ try { ev = JSON.parse(line); } catch { continue; }
153
+ applyEvent(this.state, ev);
154
+ onEvent?.(ev);
155
+ }
156
+ }
157
+ } finally { fs.closeSync(fd); }
158
+ return this.state;
159
+ }
160
+ }
161
+
162
+ // ---------------------------------------------------------------- images
163
+
164
+ export function listImages(dir, sinceMs = 0) {
165
+ if (!dir || !fs.existsSync(dir)) return [];
166
+ return fs.readdirSync(dir)
167
+ .filter(f => IMAGE_EXT.test(f))
168
+ .map(f => path.join(dir, f))
169
+ .filter(p => fs.statSync(p).mtimeMs >= sinceMs)
170
+ .sort((a, b) => fs.statSync(a).mtimeMs - fs.statSync(b).mtimeMs);
171
+ }
172
+
173
+ function uniquePath(dir, name) {
174
+ const { name: base, ext } = path.parse(name);
175
+ let p = path.join(dir, name);
176
+ for (let i = 2; fs.existsSync(p); i++) p = path.join(dir, `${base}-${i}${ext}`);
177
+ return p;
178
+ }
179
+
180
+ // Codex never writes images itself: its image tool drops every generated image
181
+ // into generated_images/<thread>/, and we copy them out from there. copied.json
182
+ // maps store filename -> output path, so nothing is copied twice across attempts.
183
+ const copiedFile = job => path.join(jobDir(job.id), 'copied.json');
184
+
185
+ export const jobImages = job => Object.values(readJson(copiedFile(job), {})).filter(p => fs.existsSync(p));
186
+
187
+ // Copies newly generated images of `threadId` into the job's output dir as
188
+ // <base>.png, <base>-2.png, ... in generation order. With `settled`, skips files
189
+ // modified in the last 1.5s (may still be being written). Returns the new paths.
190
+ export function collectImages(job, threadId, { settled = true } = {}) {
191
+ if (!threadId) return [];
192
+ const store = path.join(CODEX_HOME, 'generated_images', threadId);
193
+ const copied = readJson(copiedFile(job), {});
194
+ const base = path.parse(job.args.filename || 'image').name;
195
+ const added = [];
196
+ for (const src of listImages(store, job.created - 1000)) {
197
+ const key = path.basename(src);
198
+ if (copied[key]) continue;
199
+ if (settled && Date.now() - fs.statSync(src).mtimeMs < 1500) continue;
200
+ const n = Object.keys(copied).length + 1;
201
+ const dest = uniquePath(job.outDir, `${base}${n > 1 ? `-${n}` : ''}${path.extname(src)}`);
202
+ fs.copyFileSync(src, dest);
203
+ copied[key] = dest;
204
+ added.push(dest);
205
+ }
206
+ if (added.length) writeJson(copiedFile(job), copied);
207
+ return added;
208
+ }
package/package.json CHANGED
@@ -1,6 +1,40 @@
1
1
  {
2
2
  "name": "@xallindrome-labs/codex-bridge",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.2.0",
4
+ "description": "MCP server that lets Claude Code consult the Codex CLI for plans, second opinions and images, as durable background jobs. Optional write mode, and a live jobs pane.",
5
+ "keywords": ["mcp", "model-context-protocol", "claude-code", "codex", "openai-codex", "planning", "image-generation", "claude-code-plugin"],
6
+ "homepage": "https://github.com/xallindrome-labs/codex-bridge#readme",
7
+ "bugs": "https://github.com/xallindrome-labs/codex-bridge/issues",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/xallindrome-labs/codex-bridge.git"
11
+ },
12
+ "license": "MIT",
13
+ "publishConfig": {
14
+ "access": "public"
15
+ },
16
+ "author": "Andrew Wilson (https://xallindrome.co)",
17
+ "mcpName": "io.github.xallindrome-labs/codex-bridge",
18
+ "type": "module",
19
+ "bin": {
20
+ "codex-bridge": "server.mjs"
21
+ },
22
+ "files": [
23
+ "server.mjs",
24
+ "runner.mjs",
25
+ "lib.mjs",
26
+ "smoke-test.mjs",
27
+ "docs/demo.svg",
28
+ "SECURITY.md",
29
+ ".claude-plugin/marketplace.json",
30
+ "plugins/codex-jobs",
31
+ "!plugins/codex-jobs/**/*.test.tsx"
32
+ ],
33
+ "engines": {
34
+ "node": ">=20"
35
+ },
36
+ "scripts": {
37
+ "test": "node test/protocol.mjs && node test/jobs.mjs",
38
+ "smoke": "node smoke-test.mjs smoke-tmp plan"
39
+ }
40
+ }
@@ -0,0 +1,9 @@
1
+ {
2
+ "name": "codex-jobs",
3
+ "version": "0.1.0",
4
+ "description": "A pane beside the chat that lists your codex-bridge jobs live: running, done, interrupted.",
5
+ "author": { "name": "Xallindrome", "url": "https://xallindrome.co" },
6
+ "homepage": "https://github.com/xallindrome-labs/codex-bridge#codex-jobs-pane",
7
+ "license": "MIT",
8
+ "types": "./types/index.d.ts"
9
+ }
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Andrew Wilson
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,45 @@
1
+ # codex-jobs
2
+
3
+ A Claude Code add-on (a "mod") that opens a pane beside the chat listing your [codex-bridge](https://github.com/xallindrome-labs/codex-bridge) jobs, live. It is read-only: it looks at the job files codex-bridge keeps on disk, so it shows jobs started by any Claude session, including ones that kept running after you closed Claude.
4
+
5
+ For each job it shows the tool (`plan`, `code` or `image`), the status (running, done, failed, interrupted, cancelled, timed out), how long it has run or took, the request, and `images k/n` for image batches. Above the list it shows your **Codex usage**: the 5-hour and weekly windows with percent used, a bar, when each resets, and how many images the bridge generated in each window. Codex has one shared meter, so images use the same limits as text; there is no separate image meter. A status-line entry shows `codex: N running`, and a short toast appears when a job you watched start finishes.
6
+
7
+ ## Install
8
+
9
+ At the prompt of a terminal Claude Code session:
10
+
11
+ ```
12
+ /plugin install codex-jobs --marketplace xallindrome-labs/codex-bridge
13
+ ```
14
+
15
+ From a shell: `claude plugin install codex-jobs --marketplace xallindrome-labs/codex-bridge`.
16
+
17
+ In the desktop app, where `/plugin` does not run: click **+** next to the prompt box, then **Plugins**, **Add marketplace** with `xallindrome-labs/codex-bridge`, then **Add plugin**.
18
+
19
+ Or, without a marketplace, from the npm package (this session only):
20
+
21
+ ```
22
+ claude --plugin-dir "$(npx -y @xallindrome-labs/codex-bridge --pane-path)"
23
+ ```
24
+
25
+ ## Use
26
+
27
+ - Type `/codex-jobs` to open the pane. It also opens by itself the first time a job is running in a session.
28
+ - The list refreshes every 3 seconds and shows the 12 newest jobs.
29
+ - If a job shows **interrupted**, its Codex process is gone (reboot, crash or kill). Ask Claude to run `codex_resume` with that job id.
30
+
31
+ It finds the jobs in `$CODEX_BRIDGE_HOME/jobs`, or `~/.codex-bridge/jobs` if that is not set. Usage comes from the newest Codex session logs in `$CODEX_HOME/sessions` (default `~/.codex/sessions`), where Codex records its usage windows; it is re-read every 30 seconds and shows when it was last written, so it can lag until Codex runs again. A window whose reset time has passed shows as reset.
32
+
33
+ ## What it needs
34
+
35
+ - codex-bridge, which creates the jobs. This add-on only reads local files (codex-bridge job files and Codex session logs); it does not call Codex or the network.
36
+ - A Claude Code build that supports mods (hooks modules and `Pane`). Whether a pane is drawn depends on the surface; the add-on is tested against the terminal, desktop, VS Code and mobile element tables, not every paint.
37
+
38
+ ## Develop
39
+
40
+ ```
41
+ claude plugin validate plugins/codex-jobs
42
+ claude plugin test plugins/codex-jobs
43
+ ```
44
+
45
+ MIT licensed, like codex-bridge. Built by [Xallindrome](https://xallindrome.co).
@@ -0,0 +1 @@
1
+ { "modules": ["./register.tsx"] }