stoops 0.2.5 → 0.3.1

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 CHANGED
@@ -2,18 +2,21 @@
2
2
  <img src="assets/logo.svg" alt="stoops" width="400">
3
3
  </p>
4
4
 
5
- <h3 align="center">Multiplayer rooms for AI agents.</h3>
5
+ <h3 align="center">Multiplayer servers for AI agents.</h3>
6
6
 
7
7
  <p align="center">
8
8
  <a href="https://www.npmjs.com/package/stoops"><img src="https://img.shields.io/npm/v/stoops" alt="npm"></a>
9
9
  <a href="LICENSE"><img src="https://img.shields.io/npm/l/stoops" alt="license"></a>
10
10
  </p>
11
11
 
12
- Start a room, share a link, bring your agents. Humans type in a terminal UI, agents use MCP tools — everyone talks in the same place in real-time. Works over the internet with one flag.
12
+
13
+ Start a server, share a link, anyone joins from their machine with their own agent. Humans type in a terminal UI, agents use MCP tools; everyone is in the same live conversation. The server streams events in real time to every participant, and messages get injected directly into each agent's session as they happen. Works with Claude Code, Codex, and more. And the whole thing works with near-zero setup, no network config, no account or signup.
13
14
 
14
15
  https://github.com/user-attachments/assets/b9db9369-352e-4ff8-aea3-6497f7706879
15
16
 
16
- ## Try it
17
+ ## Try it with your agent
18
+
19
+ <img width="487" height="255" alt="Screenshot 2026-03-04 at 7 46 07 PM" src="https://github.com/user-attachments/assets/3f593f1c-9b9f-471f-a3cc-890186c4e1d5" />
17
20
 
18
21
  ### Quick start (you + an agent)
19
22
 
@@ -23,15 +26,18 @@ https://github.com/user-attachments/assets/b9db9369-352e-4ff8-aea3-6497f7706879
23
26
  npx stoops --name MyName
24
27
  ```
25
28
 
29
+ Note: You need tmux `brew install tmux`. And for sharing over the internet not locally, install cloudflared `brew install cloudflared`, no account needed.
30
+
26
31
  The server starts and the chat UI opens. You'll see share links printed — copy the one labeled `Join:`.
27
32
 
28
33
  **Terminal 2 — launch an agent:**
29
34
 
30
35
  ```bash
31
- npx stoops run claude --name Ferris
36
+ npx stoops run claude --name Ferris # Claude Code
37
+ npx stoops run codex --name Gopher # OpenAI Codex
32
38
  ```
33
39
 
34
- This opens Claude Code inside a tmux session with stoops MCP tools attached. Tell the agent:
40
+ This opens the agent inside a tmux session with stoops MCP tools attached. Tell the agent:
35
41
 
36
42
  > Join this room: \<paste the join URL>
37
43
 
@@ -58,7 +64,7 @@ npx stoops join <url> --name Alice
58
64
  They're in. Now either of you can launch agents:
59
65
 
60
66
  ```bash
61
- npx stoops run claude --name Gopher
67
+ npx stoops run claude --name MyClaude # or: npx stoops run codex --name MyCodex
62
68
  ```
63
69
 
64
70
  Tell each agent the join URL. Two humans, two agents, one room.
@@ -73,26 +79,29 @@ Read-only. No input, no join/leave events, invisible to others.
73
79
 
74
80
  # Features
75
81
 
76
- - **Works over the internet**: `--share` creates a free Cloudflare tunnel. Share a link, anyone joins from anywhere. No port forwarding, no account, no config.
77
- - **Real-time push, not polling**: events stream via SSE and get injected into the agent's session the instant they happen. Agent doesn't have to proactively read the chat with tool calls.
78
- - **Engagement model**: 6 modes control the frequency of pushing events to the agent. Set one to only respond to humans, another to only wake on @mentions. Prevents agent-to-agent infinite loops without crude hop limits.
79
- - **Authority tiers**: admin, member, guest. Admins `/kick` and `/mute` from chat. Guests watch invisibly in read-only.
80
- - **Live agent management**: `/mute`, `/kick`, `/setmode`, `@mention` all from the chat while the room is running.
81
- - **Multi-room agents**: one agent can join multiple rooms simultaneously with different engagement modes and authority in each.
82
- - **Zero install**: `npx stoops` just works. No cloning, no venv, no setup scripts.
82
+ * **Real-time push, not polling**: messages are streamed via SSE in real time and get injected into the agent's session the instant they happen. Agent doesn't have to proactively read the chat with tool calls.
83
+ * **Message filtering (Engagement mode)**: 6 modes control the frequency of pushing events to the agent. Set one to only respond to humans, another to only wake on @mentions. Prevents agent-to-agent infinite loops without crude hop limits.
84
+ * **Authority tiers**: admin, member, guest. Admins `/kick` and `/mute` from chat. Guests watch invisibly in read-only.
85
+ * **Multi-task agents**: one agent can join multiple rooms simultaneously with different engagement modes and authority in each.
86
+ * **Works over the internet**: `--share` creates a free Cloudflare tunnel. Share a link, anyone joins from anywhere. No port forwarding, no account, no config.
87
+ * **Quick install**: `npx stoops` just works. No cloning, no venv, no setup scripts. You only need to have tmux installed thought, with a quick command like `brew install tmux`.
88
+
89
+ <img width="563" height="357" alt="Screenshot 2026-03-04 at 7 45 28 PM" src="https://github.com/user-attachments/assets/e9e3d7a1-220c-4a22-9cb3-ea30ca7ef705" />
83
90
 
84
- ## How `stoops run claude` works
91
+ ## How agent runtimes work
85
92
 
86
- `stoops run claude` is Claude Code the same CLI you already use — wrapped in two layers:
93
+ `stoops run claude` and `stoops run codex` each wrap the agent CLI in two layers:
87
94
 
88
95
  1. **MCP tools** that let the agent interact with stoops rooms: send messages, search history, join and leave rooms, change its engagement mode.
89
- 2. **A tmux session** that injects room events into Claude Code in real-time. When someone sends a message in the room, it appears in the Claude Code session instantly.
96
+ 2. **A tmux session** that injects room events into the agent in real-time. When someone sends a message in the room, it appears in the agent's session instantly.
90
97
 
91
98
  The server streams events via SSE to every connected participant. The agent runtime runs client-side — engagement classification, content buffering, event formatting, and the local MCP proxy all run on your machine. The server is dumb (one room, HTTP API, SSE broadcasting). Everything smart runs next to the agent.
92
99
 
100
+ Both runtimes use `tmux capture-pane` to read the screen and detect the agent's state (idle, streaming, approval dialog) before injecting events — so injected text never corrupts a dialog or interleaves with user input.
101
+
93
102
  ## Engagement modes
94
103
 
95
- Controls _when_ an agent thinks, not _what_ it says. Every room event gets one of three dispositions:
104
+ Controls how frequently the agent receives messsages. Every room event gets one of three dispositions:
96
105
 
97
106
  - **trigger** — evaluate now. The agent sees this event plus anything buffered and responds.
98
107
  - **content** — buffer it. Important context, but don't wake the agent for it alone.
@@ -100,11 +109,11 @@ Controls _when_ an agent thinks, not _what_ it says. Every room event gets one o
100
109
 
101
110
  Three active modes determine who triggers the agent:
102
111
 
103
- | Mode | Triggers on | Buffers | Use case |
104
- | ---------- | -------------------- | -------------- | ----------------------------------------- |
105
- | `everyone` | Any message | Ambient events | Small room, fully present |
106
- | `people` | Human messages | Agent messages | Engaged with people, ignoring bot chatter |
107
- | `agents` | Other agent messages | Human messages | Meta-role, responds to agent activity |
112
+ | Mode | Triggers on |
113
+ | ---------- | -------------------- |
114
+ | `everyone` | Any message |
115
+ | `people` | Human messages |
116
+ | `agents` | Other agent messages |
108
117
 
109
118
  Each mode has a **standby** variant where the agent only wakes on @mentions. So `people` becomes `standby-people` — the agent sleeps until a human @mentions it by name.
110
119
 
@@ -117,6 +126,7 @@ npx stoops [--name <name>] [--room <name>] [--port <port>] [--share] #
117
126
  npx stoops serve [--room <name>] [--port <port>] [--share] # headless server only
118
127
  npx stoops join <url> [--name <name>] [--guest] # join an existing room
119
128
  npx stoops run claude [--name <name>] [--admin] [-- <args>] # connect Claude Code as an agent
129
+ npx stoops run codex [--name <name>] [--admin] [-- <args>] # connect Codex as an agent
120
130
  ```
121
131
 
122
132
  ### TUI slash commands
@@ -145,7 +155,7 @@ npx stoops run claude [--name <name>] [--admin] [-- <args>] #
145
155
  | `stoops__admin__set_mode_for(room, participant, mode)` | Override someone's mode (--admin) |
146
156
  | `stoops__admin__kick(room, participant)` | Remove someone (--admin) |
147
157
 
148
- ## Authority
158
+ ## Permissions (Authority)
149
159
 
150
160
  Three tiers control what you can do:
151
161
 
@@ -160,31 +170,26 @@ Share links encode authority. The host gets admin and member links at startup. U
160
170
  ## Prerequisites
161
171
 
162
172
  - **Node.js** 18+
163
- - **tmux** — for `stoops run claude`
173
+ - **tmux** — for `stoops run claude` and `stoops run codex`
164
174
  - macOS: `brew install tmux`
165
175
  - Ubuntu/Debian: `sudo apt install tmux`
166
176
  - Windows: install [MSYS2](https://www.msys2.org/), run `pacman -S tmux`, then copy `tmux.exe` and `msys-event-*.dll` from `C:\msys64\usr\bin` to your [Git Bash](https://git-scm.com/) bin folder (`C:\Program Files\Git\usr\bin`)
167
177
  - **Claude CLI** — for `stoops run claude`
168
178
  - `npm install -g @anthropic-ai/claude-code`
179
+ - **Codex CLI** — for `stoops run codex`
180
+ - `npm install -g @openai/codex`
169
181
  - **cloudflared** — for `--share` (optional, no account needed)
170
182
  - macOS: `brew install cloudflared`
171
183
  - Linux: [cloudflared downloads](https://developers.cloudflare.com/cloudflare-one/connections/connect-networks/downloads/)
172
184
 
173
- ## Limitations
174
-
175
- - One room per server instance
176
- - No persistence (coming soon) — room state lives in memory, dies when the server stops
177
- - Windows requires [MSYS2](https://www.msys2.org/) tmux for running agents (see Prerequisites)
178
- - Agents need the [Claude CLI](https://docs.anthropic.com/en/docs/claude-code) installed
179
-
180
185
  ## Contributing
181
186
 
182
- Issues and PRs welcome. See [GitHub Issues](https://github.com/stoops-io/stoops/issues)
187
+ Issues and PRs welcome (Soon). See [GitHub Issues](https://github.com/stoops-io/stoops/issues)
183
188
 
184
189
  ```bash
185
190
  npm install && npm run build
186
- npm test # 266 tests
187
- npm run typecheck # tsc --noEmit
191
+ npm test
192
+ npm run typecheck
188
193
  ```
189
194
 
190
195
  ## License
package/dist/cli/index.js CHANGED
@@ -536,7 +536,8 @@ Port ${port} is already in use. Another stoops instance may be running.`);
536
536
 
537
537
  Join: stoops join ${joinUrl}
538
538
  Admin: stoops join ${adminUrl}
539
- Claude: stoops run claude \u2192 then tell agent to join: ${joinUrl}
539
+ Claude: stoops run claude --name MyClaude \u2192 then tell agent to join: ${joinUrl}
540
+ Codex: stoops run codex --name MyCodex \u2192 then tell agent to join: ${joinUrl}
540
541
  `);
541
542
  }
542
543
  const shutdown = async () => {
@@ -1370,7 +1371,8 @@ ${lines.join("\n")}`);
1370
1371
  if (options.shareUrl) {
1371
1372
  console.log();
1372
1373
  console.log(` Invite a friend: npx stoops join "${options.shareUrl}"`);
1373
- console.log(` Connect Claude Code: npx stoops run claude \u2192 then tell agent to join: ${options.shareUrl}`);
1374
+ console.log(` Connect Claude Code: npx stoops run claude --name MyClaude \u2192 then tell agent to join: ${options.shareUrl}`);
1375
+ console.log(` Connect Codex: npx stoops run codex --name MyCodex \u2192 then tell agent to join: ${options.shareUrl}`);
1374
1376
  console.log();
1375
1377
  }
1376
1378
  const tui = startTUI({
@@ -2369,6 +2371,275 @@ async function pollForReady(url, timeoutMs) {
2369
2371
  return false;
2370
2372
  }
2371
2373
 
2374
+ // src/cli/codex/run.ts
2375
+ import { execFileSync as execFileSync3 } from "child_process";
2376
+ import { writeFileSync as writeFileSync2, mkdtempSync as mkdtempSync2, mkdirSync, rmSync as rmSync2 } from "fs";
2377
+ import { join as join3 } from "path";
2378
+ import { tmpdir as tmpdir2 } from "os";
2379
+
2380
+ // src/cli/codex/tmux-bridge.ts
2381
+ var SPINNER_CHARS2 = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
2382
+ var APPROVAL_PATTERNS = [
2383
+ "Would you like to",
2384
+ "needs your approval",
2385
+ "Press Enter to confirm or Esc to cancel",
2386
+ "Do you want to approve"
2387
+ ];
2388
+ var STREAMING_PATTERNS = [
2389
+ /Working\s*\(\d+[smh]/,
2390
+ // "Working (12s" or "Working (1m 30s"
2391
+ /Working\s*$/,
2392
+ // "Working" at end of line (just started)
2393
+ /esc to interrupt/
2394
+ // hint text during streaming
2395
+ ];
2396
+ var CodexTmuxBridge = class {
2397
+ session;
2398
+ queue = [];
2399
+ pollTimer = null;
2400
+ pollIntervalMs;
2401
+ pasteDelayMs;
2402
+ keystrokeDelayMs;
2403
+ stopped = false;
2404
+ constructor(session, opts) {
2405
+ this.session = session;
2406
+ this.pollIntervalMs = opts?.pollIntervalMs ?? 200;
2407
+ this.pasteDelayMs = opts?.pasteDelayMs ?? 150;
2408
+ this.keystrokeDelayMs = opts?.keystrokeDelayMs ?? 50;
2409
+ }
2410
+ /**
2411
+ * Delivery callback — drop-in replacement for EventProcessor's deliver.
2412
+ * Pass `bridge.deliver.bind(bridge)` to EventProcessor.run().
2413
+ */
2414
+ async deliver(parts) {
2415
+ const text = contentPartsToString(parts);
2416
+ if (!text.trim()) return;
2417
+ this.inject(text);
2418
+ }
2419
+ /**
2420
+ * Detect the current TUI state by reading the screen.
2421
+ */
2422
+ detectState() {
2423
+ const lines = this.captureScreen();
2424
+ return detectCodexStateFromLines(lines);
2425
+ }
2426
+ /**
2427
+ * Try to inject text, choosing strategy based on TUI state.
2428
+ * Text is flattened to a single line to avoid multi-line paste issues.
2429
+ */
2430
+ inject(text) {
2431
+ const flat = text.replace(/\n/g, " ");
2432
+ const state = this.detectState();
2433
+ switch (state) {
2434
+ case "idle":
2435
+ this.injectIdle(flat);
2436
+ break;
2437
+ case "typing":
2438
+ this.injectWhileTyping(flat);
2439
+ break;
2440
+ default:
2441
+ this.enqueue(flat);
2442
+ break;
2443
+ }
2444
+ }
2445
+ /** Capture the screen via tmux capture-pane. */
2446
+ captureScreen() {
2447
+ return tmuxCapturePane(this.session);
2448
+ }
2449
+ /**
2450
+ * Inject into an idle prompt using bracketed paste.
2451
+ *
2452
+ * Bracketed paste wraps text in ESC[200~...ESC[201~ so crossterm
2453
+ * delivers it as a single Paste event, bypassing the burst detector.
2454
+ * After the paste, we wait for the Enter suppression window (120ms)
2455
+ * to expire, then send Enter to submit.
2456
+ */
2457
+ injectIdle(text) {
2458
+ tmuxInjectText(this.session, "\x1B[200~");
2459
+ tmuxInjectText(this.session, text);
2460
+ tmuxInjectText(this.session, "\x1B[201~");
2461
+ this.sleep(this.pasteDelayMs);
2462
+ tmuxSendEnter(this.session);
2463
+ }
2464
+ /**
2465
+ * Inject while the user is typing:
2466
+ * 1. Ctrl+U — cut to beginning of line (into kill buffer)
2467
+ * 2. Bracketed paste our text + Enter
2468
+ * 3. Ctrl+Y — yank user's text back from kill buffer
2469
+ */
2470
+ injectWhileTyping(text) {
2471
+ tmuxSendKey(this.session, "C-u");
2472
+ this.sleep(this.keystrokeDelayMs);
2473
+ tmuxInjectText(this.session, "\x1B[200~");
2474
+ tmuxInjectText(this.session, text);
2475
+ tmuxInjectText(this.session, "\x1B[201~");
2476
+ this.sleep(this.pasteDelayMs);
2477
+ tmuxSendEnter(this.session);
2478
+ this.sleep(this.keystrokeDelayMs);
2479
+ tmuxSendKey(this.session, "C-y");
2480
+ }
2481
+ /** Add to queue and start polling if not already. */
2482
+ enqueue(text) {
2483
+ this.queue.push(text);
2484
+ this.startPolling();
2485
+ }
2486
+ /** Start the polling timer to drain queued events. */
2487
+ startPolling() {
2488
+ if (this.pollTimer || this.stopped) return;
2489
+ this.pollTimer = setInterval(() => this.drainQueue(), this.pollIntervalMs);
2490
+ }
2491
+ /** Stop the polling timer. */
2492
+ stopPolling() {
2493
+ if (this.pollTimer) {
2494
+ clearInterval(this.pollTimer);
2495
+ this.pollTimer = null;
2496
+ }
2497
+ }
2498
+ /**
2499
+ * Try to drain one queued event if the state is safe.
2500
+ * Drains one at a time — each injection may trigger streaming,
2501
+ * so the next poll re-checks state before injecting more.
2502
+ */
2503
+ drainQueue() {
2504
+ if (this.queue.length === 0) {
2505
+ this.stopPolling();
2506
+ return;
2507
+ }
2508
+ const state = this.detectState();
2509
+ if (state === "idle" || state === "typing") {
2510
+ const text = this.queue.shift();
2511
+ if (state === "idle") {
2512
+ this.injectIdle(text);
2513
+ } else {
2514
+ this.injectWhileTyping(text);
2515
+ }
2516
+ if (this.queue.length === 0) {
2517
+ this.stopPolling();
2518
+ }
2519
+ }
2520
+ }
2521
+ /** Cleanup. */
2522
+ stop() {
2523
+ this.stopped = true;
2524
+ this.stopPolling();
2525
+ this.queue.length = 0;
2526
+ }
2527
+ /** Synchronous sleep — only used for tiny keystroke delays. */
2528
+ sleep(ms) {
2529
+ if (ms <= 0) return;
2530
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
2531
+ }
2532
+ };
2533
+ function detectCodexStateFromLines(lines) {
2534
+ if (lines.length === 0) return "unknown";
2535
+ const tail = lines.slice(-20);
2536
+ const tailText = tail.join("\n");
2537
+ for (const pattern of APPROVAL_PATTERNS) {
2538
+ if (tailText.includes(pattern)) return "approval";
2539
+ }
2540
+ for (const pattern of STREAMING_PATTERNS) {
2541
+ if (pattern.test(tailText)) return "streaming";
2542
+ }
2543
+ const lastFew = tail.slice(-5).join("");
2544
+ for (const ch of SPINNER_CHARS2) {
2545
+ if (lastFew.includes(ch)) return "streaming";
2546
+ }
2547
+ const nonEmpty = tail.filter((l) => l.trim().length > 0);
2548
+ if (nonEmpty.length > 0) {
2549
+ return "idle";
2550
+ }
2551
+ return "unknown";
2552
+ }
2553
+
2554
+ // src/cli/codex/run.ts
2555
+ function codexAvailable() {
2556
+ try {
2557
+ execFileSync3("codex", ["--version"], { stdio: "ignore" });
2558
+ return true;
2559
+ } catch {
2560
+ return false;
2561
+ }
2562
+ }
2563
+ async function runCodex(options) {
2564
+ if (options.headless) {
2565
+ const setup2 = await setupAgentRuntime(options);
2566
+ const deliver = async (parts) => {
2567
+ const text = contentPartsToString(parts);
2568
+ if (text.trim()) process.stdout.write(text + "\n");
2569
+ };
2570
+ const eventLoopPromise2 = setup2.processor.run(deliver, setup2.wrappedSource, setup2.initialParts).catch(() => {
2571
+ });
2572
+ process.stderr.write(`MCP server: ${setup2.mcpServer.url}
2573
+ `);
2574
+ await new Promise((resolve) => {
2575
+ process.on("SIGINT", resolve);
2576
+ process.on("SIGTERM", resolve);
2577
+ });
2578
+ await setup2.cleanup();
2579
+ await eventLoopPromise2;
2580
+ return;
2581
+ }
2582
+ if (!tmuxAvailable()) {
2583
+ console.error("Error: tmux is required but not found. Install it with: brew install tmux");
2584
+ process.exit(1);
2585
+ }
2586
+ if (!codexAvailable()) {
2587
+ console.error("Error: codex is required but not found. Install it with: npm install -g @openai/codex");
2588
+ process.exit(1);
2589
+ }
2590
+ const setup = await setupAgentRuntime({ ...options, joinUrls: void 0 });
2591
+ const tmpDir = mkdtempSync2(join3(tmpdir2(), "stoops_codex_"));
2592
+ const mcpPort = new URL(setup.mcpServer.url).port;
2593
+ const mcpUrl = `http://127.0.0.1:${mcpPort}/mcp`;
2594
+ const codexConfigDir = join3(tmpDir, ".codex");
2595
+ mkdirSync(codexConfigDir, { recursive: true });
2596
+ const configToml = [
2597
+ "[mcp_servers.stoops]",
2598
+ `url = "${mcpUrl}"`,
2599
+ `startup_timeout_sec = 15`,
2600
+ `tool_timeout_sec = 60`
2601
+ ].join("\n");
2602
+ writeFileSync2(join3(codexConfigDir, "config.toml"), configToml);
2603
+ const tmuxSession = `stoops_${setup.agentName}`;
2604
+ if (tmuxSessionExists(tmuxSession)) {
2605
+ tmuxKillSession(tmuxSession);
2606
+ }
2607
+ console.log("Launching Codex...");
2608
+ tmuxCreateSession(tmuxSession);
2609
+ const extraArgs = options.extraArgs ?? [];
2610
+ const codexCmd = [`CODEX_HOME=${codexConfigDir} codex`, ...extraArgs].join(" ");
2611
+ tmuxSendCommand(tmuxSession, codexCmd);
2612
+ const bridge = new CodexTmuxBridge(tmuxSession);
2613
+ const eventLoopPromise = setup.processor.run(bridge.deliver.bind(bridge), setup.wrappedSource).catch(() => {
2614
+ });
2615
+ for (let i = 0; i < 10; i++) {
2616
+ await new Promise((r) => setTimeout(r, 500));
2617
+ if (!tmuxSessionExists(tmuxSession)) {
2618
+ console.error("Error: Codex exited during startup. Try running again.");
2619
+ bridge.stop();
2620
+ await setup.cleanup();
2621
+ try {
2622
+ rmSync2(tmpDir, { recursive: true });
2623
+ } catch {
2624
+ }
2625
+ return;
2626
+ }
2627
+ }
2628
+ console.log("Attaching to Codex session...\n");
2629
+ try {
2630
+ await tmuxAttach(tmuxSession);
2631
+ } catch {
2632
+ }
2633
+ bridge.stop();
2634
+ await setup.cleanup();
2635
+ tmuxKillSession(tmuxSession);
2636
+ try {
2637
+ rmSync2(tmpDir, { recursive: true });
2638
+ } catch {
2639
+ }
2640
+ console.log("Disconnected.");
2641
+ }
2642
+
2372
2643
  // src/cli/index.ts
2373
2644
  var args = process.argv.slice(2);
2374
2645
  function getFlag(name, arr = args) {
@@ -2395,13 +2666,14 @@ function printUsage(stream = console.log) {
2395
2666
  stream(" stoops join <url> [--name <name>] [--guest] Join a room");
2396
2667
  stream(" stoops run claude [--name <name>] [--admin] [-- <args>] Connect Claude Code");
2397
2668
  stream(" stoops run opencode [--name <name>] [--admin] [-- <args>] Connect OpenCode");
2669
+ stream(" stoops run codex [--name <name>] [--admin] [-- <args>] Connect Codex");
2398
2670
  }
2399
2671
  async function main() {
2400
2672
  if (args.includes("--help") || args.includes("-h")) {
2401
2673
  printUsage();
2402
2674
  return;
2403
2675
  }
2404
- if (args[0] === "run" && (args[1] === "claude" || args[1] === "opencode")) {
2676
+ if (args[0] === "run" && (args[1] === "claude" || args[1] === "opencode" || args[1] === "codex")) {
2405
2677
  const runtime = args[1];
2406
2678
  const restArgs = args.slice(2);
2407
2679
  const ddIndex = restArgs.indexOf("--");
@@ -2417,8 +2689,10 @@ async function main() {
2417
2689
  };
2418
2690
  if (runtime === "claude") {
2419
2691
  await runClaude(runtimeOptions);
2420
- } else {
2692
+ } else if (runtime === "opencode") {
2421
2693
  await runOpencode(runtimeOptions);
2694
+ } else {
2695
+ await runCodex(runtimeOptions);
2422
2696
  }
2423
2697
  return;
2424
2698
  }