stoops 0.2.5 → 0.3.2
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 +50 -34
- package/dist/agent/index.d.ts +3 -3
- package/dist/{chunk-TN56PBF3.js → chunk-XEKY3KEU.js} +62 -7
- package/dist/chunk-XEKY3KEU.js.map +1 -0
- package/dist/claude/index.d.ts +2 -2
- package/dist/cli/index.js +310 -12
- package/dist/cli/index.js.map +1 -1
- package/dist/{index-DGncuUqB.d.ts → index-DwVKKxqK.d.ts} +13 -2
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/langgraph/index.d.ts +2 -2
- package/dist/{types-Co2KKpkh.d.ts → types-B9xf8w53.d.ts} +1 -1
- package/package.json +1 -1
- package/dist/chunk-TN56PBF3.js.map +0 -1
package/dist/cli/index.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import {
|
|
3
|
-
|
|
3
|
+
FileBackedStorage,
|
|
4
4
|
Room,
|
|
5
5
|
randomName,
|
|
6
6
|
randomRoomName
|
|
7
|
-
} from "../chunk-
|
|
7
|
+
} from "../chunk-XEKY3KEU.js";
|
|
8
8
|
import {
|
|
9
9
|
EventProcessor,
|
|
10
10
|
RemoteRoomDataSource,
|
|
@@ -25,6 +25,8 @@ import { createServer } from "http";
|
|
|
25
25
|
import { spawn, execFileSync } from "child_process";
|
|
26
26
|
import { randomUUID } from "crypto";
|
|
27
27
|
import { createRequire } from "module";
|
|
28
|
+
import { tmpdir } from "os";
|
|
29
|
+
import { join as pathJoin } from "path";
|
|
28
30
|
|
|
29
31
|
// src/cli/auth.ts
|
|
30
32
|
import { randomBytes } from "crypto";
|
|
@@ -123,7 +125,24 @@ async function serve(options) {
|
|
|
123
125
|
} : logServer;
|
|
124
126
|
let publicUrl = serverUrl;
|
|
125
127
|
let tunnelProcess = null;
|
|
126
|
-
const
|
|
128
|
+
const timestamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
129
|
+
const savePath = options.save ?? options.load ?? pathJoin(tmpdir(), `stoops-${roomName}-${timestamp}.json`);
|
|
130
|
+
let storage;
|
|
131
|
+
if (options.load) {
|
|
132
|
+
try {
|
|
133
|
+
storage = await FileBackedStorage.load(options.load);
|
|
134
|
+
log(`loaded room state from ${options.load}`);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (err.code === "ENOENT") {
|
|
137
|
+
storage = new FileBackedStorage(options.load);
|
|
138
|
+
log(`no existing file at ${options.load}, starting fresh`);
|
|
139
|
+
} else {
|
|
140
|
+
throw err;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
} else {
|
|
144
|
+
storage = new FileBackedStorage(savePath);
|
|
145
|
+
}
|
|
127
146
|
const room = new Room(roomName, storage);
|
|
128
147
|
const tokens = new TokenManager();
|
|
129
148
|
const participants = /* @__PURE__ */ new Map();
|
|
@@ -513,7 +532,7 @@ Port ${port} is already in use. Another stoops instance may be running.`);
|
|
|
513
532
|
const adminToken = tokens.generateShareToken("admin", "admin");
|
|
514
533
|
const memberToken = tokens.generateShareToken("admin", "member");
|
|
515
534
|
if (options.headless) {
|
|
516
|
-
process.stdout.write(JSON.stringify({ serverUrl, publicUrl, roomName, adminToken, memberToken }) + "\n");
|
|
535
|
+
process.stdout.write(JSON.stringify({ serverUrl, publicUrl, roomName, adminToken, memberToken, savePath }) + "\n");
|
|
517
536
|
} else if (!options.quiet) {
|
|
518
537
|
let version = process.env.npm_package_version ?? "";
|
|
519
538
|
if (!version) {
|
|
@@ -533,10 +552,12 @@ Port ${port} is already in use. Another stoops instance may be running.`);
|
|
|
533
552
|
Room: ${roomName}
|
|
534
553
|
Server: ${serverUrl}${publicUrl !== serverUrl ? `
|
|
535
554
|
Tunnel: ${publicUrl}` : ""}
|
|
555
|
+
Saving: ${savePath}
|
|
536
556
|
|
|
537
557
|
Join: stoops join ${joinUrl}
|
|
538
558
|
Admin: stoops join ${adminUrl}
|
|
539
|
-
Claude: stoops run claude \u2192 then tell agent to join: ${joinUrl}
|
|
559
|
+
Claude: stoops run claude --name MyClaude \u2192 then tell agent to join: ${joinUrl}
|
|
560
|
+
Codex: stoops run codex --name MyCodex \u2192 then tell agent to join: ${joinUrl}
|
|
540
561
|
`);
|
|
541
562
|
}
|
|
542
563
|
const shutdown = async () => {
|
|
@@ -1370,7 +1391,8 @@ ${lines.join("\n")}`);
|
|
|
1370
1391
|
if (options.shareUrl) {
|
|
1371
1392
|
console.log();
|
|
1372
1393
|
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}`);
|
|
1394
|
+
console.log(` Connect Claude Code: npx stoops run claude --name MyClaude \u2192 then tell agent to join: ${options.shareUrl}`);
|
|
1395
|
+
console.log(` Connect Codex: npx stoops run codex --name MyCodex \u2192 then tell agent to join: ${options.shareUrl}`);
|
|
1374
1396
|
console.log();
|
|
1375
1397
|
}
|
|
1376
1398
|
const tui = startTUI({
|
|
@@ -1572,7 +1594,7 @@ function toDisplayEvent(event, selfId, participantTypes) {
|
|
|
1572
1594
|
// src/cli/claude/run.ts
|
|
1573
1595
|
import { writeFileSync, mkdtempSync, rmSync, chmodSync } from "fs";
|
|
1574
1596
|
import { join as join2 } from "path";
|
|
1575
|
-
import { tmpdir } from "os";
|
|
1597
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
1576
1598
|
|
|
1577
1599
|
// src/cli/tmux.ts
|
|
1578
1600
|
import { execFileSync as execFileSync2, spawn as spawn2 } from "child_process";
|
|
@@ -2176,7 +2198,7 @@ async function runClaude(options) {
|
|
|
2176
2198
|
process.exit(1);
|
|
2177
2199
|
}
|
|
2178
2200
|
const setup = await setupAgentRuntime({ ...options, joinUrls: void 0 });
|
|
2179
|
-
const tmpDir = mkdtempSync(join2(
|
|
2201
|
+
const tmpDir = mkdtempSync(join2(tmpdir2(), "stoops_agent_"));
|
|
2180
2202
|
const bridgePath = join2(tmpDir, "mcp-bridge.cjs");
|
|
2181
2203
|
writeFileSync(bridgePath, MCP_STDIO_BRIDGE);
|
|
2182
2204
|
chmodSync(bridgePath, 493);
|
|
@@ -2369,6 +2391,275 @@ async function pollForReady(url, timeoutMs) {
|
|
|
2369
2391
|
return false;
|
|
2370
2392
|
}
|
|
2371
2393
|
|
|
2394
|
+
// src/cli/codex/run.ts
|
|
2395
|
+
import { execFileSync as execFileSync3 } from "child_process";
|
|
2396
|
+
import { writeFileSync as writeFileSync2, mkdtempSync as mkdtempSync2, mkdirSync, rmSync as rmSync2 } from "fs";
|
|
2397
|
+
import { join as join3 } from "path";
|
|
2398
|
+
import { tmpdir as tmpdir3 } from "os";
|
|
2399
|
+
|
|
2400
|
+
// src/cli/codex/tmux-bridge.ts
|
|
2401
|
+
var SPINNER_CHARS2 = "\u280B\u2819\u2839\u2838\u283C\u2834\u2826\u2827\u2807\u280F";
|
|
2402
|
+
var APPROVAL_PATTERNS = [
|
|
2403
|
+
"Would you like to",
|
|
2404
|
+
"needs your approval",
|
|
2405
|
+
"Press Enter to confirm or Esc to cancel",
|
|
2406
|
+
"Do you want to approve"
|
|
2407
|
+
];
|
|
2408
|
+
var STREAMING_PATTERNS = [
|
|
2409
|
+
/Working\s*\(\d+[smh]/,
|
|
2410
|
+
// "Working (12s" or "Working (1m 30s"
|
|
2411
|
+
/Working\s*$/,
|
|
2412
|
+
// "Working" at end of line (just started)
|
|
2413
|
+
/esc to interrupt/
|
|
2414
|
+
// hint text during streaming
|
|
2415
|
+
];
|
|
2416
|
+
var CodexTmuxBridge = class {
|
|
2417
|
+
session;
|
|
2418
|
+
queue = [];
|
|
2419
|
+
pollTimer = null;
|
|
2420
|
+
pollIntervalMs;
|
|
2421
|
+
pasteDelayMs;
|
|
2422
|
+
keystrokeDelayMs;
|
|
2423
|
+
stopped = false;
|
|
2424
|
+
constructor(session, opts) {
|
|
2425
|
+
this.session = session;
|
|
2426
|
+
this.pollIntervalMs = opts?.pollIntervalMs ?? 200;
|
|
2427
|
+
this.pasteDelayMs = opts?.pasteDelayMs ?? 150;
|
|
2428
|
+
this.keystrokeDelayMs = opts?.keystrokeDelayMs ?? 50;
|
|
2429
|
+
}
|
|
2430
|
+
/**
|
|
2431
|
+
* Delivery callback — drop-in replacement for EventProcessor's deliver.
|
|
2432
|
+
* Pass `bridge.deliver.bind(bridge)` to EventProcessor.run().
|
|
2433
|
+
*/
|
|
2434
|
+
async deliver(parts) {
|
|
2435
|
+
const text = contentPartsToString(parts);
|
|
2436
|
+
if (!text.trim()) return;
|
|
2437
|
+
this.inject(text);
|
|
2438
|
+
}
|
|
2439
|
+
/**
|
|
2440
|
+
* Detect the current TUI state by reading the screen.
|
|
2441
|
+
*/
|
|
2442
|
+
detectState() {
|
|
2443
|
+
const lines = this.captureScreen();
|
|
2444
|
+
return detectCodexStateFromLines(lines);
|
|
2445
|
+
}
|
|
2446
|
+
/**
|
|
2447
|
+
* Try to inject text, choosing strategy based on TUI state.
|
|
2448
|
+
* Text is flattened to a single line to avoid multi-line paste issues.
|
|
2449
|
+
*/
|
|
2450
|
+
inject(text) {
|
|
2451
|
+
const flat = text.replace(/\n/g, " ");
|
|
2452
|
+
const state = this.detectState();
|
|
2453
|
+
switch (state) {
|
|
2454
|
+
case "idle":
|
|
2455
|
+
this.injectIdle(flat);
|
|
2456
|
+
break;
|
|
2457
|
+
case "typing":
|
|
2458
|
+
this.injectWhileTyping(flat);
|
|
2459
|
+
break;
|
|
2460
|
+
default:
|
|
2461
|
+
this.enqueue(flat);
|
|
2462
|
+
break;
|
|
2463
|
+
}
|
|
2464
|
+
}
|
|
2465
|
+
/** Capture the screen via tmux capture-pane. */
|
|
2466
|
+
captureScreen() {
|
|
2467
|
+
return tmuxCapturePane(this.session);
|
|
2468
|
+
}
|
|
2469
|
+
/**
|
|
2470
|
+
* Inject into an idle prompt using bracketed paste.
|
|
2471
|
+
*
|
|
2472
|
+
* Bracketed paste wraps text in ESC[200~...ESC[201~ so crossterm
|
|
2473
|
+
* delivers it as a single Paste event, bypassing the burst detector.
|
|
2474
|
+
* After the paste, we wait for the Enter suppression window (120ms)
|
|
2475
|
+
* to expire, then send Enter to submit.
|
|
2476
|
+
*/
|
|
2477
|
+
injectIdle(text) {
|
|
2478
|
+
tmuxInjectText(this.session, "\x1B[200~");
|
|
2479
|
+
tmuxInjectText(this.session, text);
|
|
2480
|
+
tmuxInjectText(this.session, "\x1B[201~");
|
|
2481
|
+
this.sleep(this.pasteDelayMs);
|
|
2482
|
+
tmuxSendEnter(this.session);
|
|
2483
|
+
}
|
|
2484
|
+
/**
|
|
2485
|
+
* Inject while the user is typing:
|
|
2486
|
+
* 1. Ctrl+U — cut to beginning of line (into kill buffer)
|
|
2487
|
+
* 2. Bracketed paste our text + Enter
|
|
2488
|
+
* 3. Ctrl+Y — yank user's text back from kill buffer
|
|
2489
|
+
*/
|
|
2490
|
+
injectWhileTyping(text) {
|
|
2491
|
+
tmuxSendKey(this.session, "C-u");
|
|
2492
|
+
this.sleep(this.keystrokeDelayMs);
|
|
2493
|
+
tmuxInjectText(this.session, "\x1B[200~");
|
|
2494
|
+
tmuxInjectText(this.session, text);
|
|
2495
|
+
tmuxInjectText(this.session, "\x1B[201~");
|
|
2496
|
+
this.sleep(this.pasteDelayMs);
|
|
2497
|
+
tmuxSendEnter(this.session);
|
|
2498
|
+
this.sleep(this.keystrokeDelayMs);
|
|
2499
|
+
tmuxSendKey(this.session, "C-y");
|
|
2500
|
+
}
|
|
2501
|
+
/** Add to queue and start polling if not already. */
|
|
2502
|
+
enqueue(text) {
|
|
2503
|
+
this.queue.push(text);
|
|
2504
|
+
this.startPolling();
|
|
2505
|
+
}
|
|
2506
|
+
/** Start the polling timer to drain queued events. */
|
|
2507
|
+
startPolling() {
|
|
2508
|
+
if (this.pollTimer || this.stopped) return;
|
|
2509
|
+
this.pollTimer = setInterval(() => this.drainQueue(), this.pollIntervalMs);
|
|
2510
|
+
}
|
|
2511
|
+
/** Stop the polling timer. */
|
|
2512
|
+
stopPolling() {
|
|
2513
|
+
if (this.pollTimer) {
|
|
2514
|
+
clearInterval(this.pollTimer);
|
|
2515
|
+
this.pollTimer = null;
|
|
2516
|
+
}
|
|
2517
|
+
}
|
|
2518
|
+
/**
|
|
2519
|
+
* Try to drain one queued event if the state is safe.
|
|
2520
|
+
* Drains one at a time — each injection may trigger streaming,
|
|
2521
|
+
* so the next poll re-checks state before injecting more.
|
|
2522
|
+
*/
|
|
2523
|
+
drainQueue() {
|
|
2524
|
+
if (this.queue.length === 0) {
|
|
2525
|
+
this.stopPolling();
|
|
2526
|
+
return;
|
|
2527
|
+
}
|
|
2528
|
+
const state = this.detectState();
|
|
2529
|
+
if (state === "idle" || state === "typing") {
|
|
2530
|
+
const text = this.queue.shift();
|
|
2531
|
+
if (state === "idle") {
|
|
2532
|
+
this.injectIdle(text);
|
|
2533
|
+
} else {
|
|
2534
|
+
this.injectWhileTyping(text);
|
|
2535
|
+
}
|
|
2536
|
+
if (this.queue.length === 0) {
|
|
2537
|
+
this.stopPolling();
|
|
2538
|
+
}
|
|
2539
|
+
}
|
|
2540
|
+
}
|
|
2541
|
+
/** Cleanup. */
|
|
2542
|
+
stop() {
|
|
2543
|
+
this.stopped = true;
|
|
2544
|
+
this.stopPolling();
|
|
2545
|
+
this.queue.length = 0;
|
|
2546
|
+
}
|
|
2547
|
+
/** Synchronous sleep — only used for tiny keystroke delays. */
|
|
2548
|
+
sleep(ms) {
|
|
2549
|
+
if (ms <= 0) return;
|
|
2550
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
2551
|
+
}
|
|
2552
|
+
};
|
|
2553
|
+
function detectCodexStateFromLines(lines) {
|
|
2554
|
+
if (lines.length === 0) return "unknown";
|
|
2555
|
+
const tail = lines.slice(-20);
|
|
2556
|
+
const tailText = tail.join("\n");
|
|
2557
|
+
for (const pattern of APPROVAL_PATTERNS) {
|
|
2558
|
+
if (tailText.includes(pattern)) return "approval";
|
|
2559
|
+
}
|
|
2560
|
+
for (const pattern of STREAMING_PATTERNS) {
|
|
2561
|
+
if (pattern.test(tailText)) return "streaming";
|
|
2562
|
+
}
|
|
2563
|
+
const lastFew = tail.slice(-5).join("");
|
|
2564
|
+
for (const ch of SPINNER_CHARS2) {
|
|
2565
|
+
if (lastFew.includes(ch)) return "streaming";
|
|
2566
|
+
}
|
|
2567
|
+
const nonEmpty = tail.filter((l) => l.trim().length > 0);
|
|
2568
|
+
if (nonEmpty.length > 0) {
|
|
2569
|
+
return "idle";
|
|
2570
|
+
}
|
|
2571
|
+
return "unknown";
|
|
2572
|
+
}
|
|
2573
|
+
|
|
2574
|
+
// src/cli/codex/run.ts
|
|
2575
|
+
function codexAvailable() {
|
|
2576
|
+
try {
|
|
2577
|
+
execFileSync3("codex", ["--version"], { stdio: "ignore" });
|
|
2578
|
+
return true;
|
|
2579
|
+
} catch {
|
|
2580
|
+
return false;
|
|
2581
|
+
}
|
|
2582
|
+
}
|
|
2583
|
+
async function runCodex(options) {
|
|
2584
|
+
if (options.headless) {
|
|
2585
|
+
const setup2 = await setupAgentRuntime(options);
|
|
2586
|
+
const deliver = async (parts) => {
|
|
2587
|
+
const text = contentPartsToString(parts);
|
|
2588
|
+
if (text.trim()) process.stdout.write(text + "\n");
|
|
2589
|
+
};
|
|
2590
|
+
const eventLoopPromise2 = setup2.processor.run(deliver, setup2.wrappedSource, setup2.initialParts).catch(() => {
|
|
2591
|
+
});
|
|
2592
|
+
process.stderr.write(`MCP server: ${setup2.mcpServer.url}
|
|
2593
|
+
`);
|
|
2594
|
+
await new Promise((resolve) => {
|
|
2595
|
+
process.on("SIGINT", resolve);
|
|
2596
|
+
process.on("SIGTERM", resolve);
|
|
2597
|
+
});
|
|
2598
|
+
await setup2.cleanup();
|
|
2599
|
+
await eventLoopPromise2;
|
|
2600
|
+
return;
|
|
2601
|
+
}
|
|
2602
|
+
if (!tmuxAvailable()) {
|
|
2603
|
+
console.error("Error: tmux is required but not found. Install it with: brew install tmux");
|
|
2604
|
+
process.exit(1);
|
|
2605
|
+
}
|
|
2606
|
+
if (!codexAvailable()) {
|
|
2607
|
+
console.error("Error: codex is required but not found. Install it with: npm install -g @openai/codex");
|
|
2608
|
+
process.exit(1);
|
|
2609
|
+
}
|
|
2610
|
+
const setup = await setupAgentRuntime({ ...options, joinUrls: void 0 });
|
|
2611
|
+
const tmpDir = mkdtempSync2(join3(tmpdir3(), "stoops_codex_"));
|
|
2612
|
+
const mcpPort = new URL(setup.mcpServer.url).port;
|
|
2613
|
+
const mcpUrl = `http://127.0.0.1:${mcpPort}/mcp`;
|
|
2614
|
+
const codexConfigDir = join3(tmpDir, ".codex");
|
|
2615
|
+
mkdirSync(codexConfigDir, { recursive: true });
|
|
2616
|
+
const configToml = [
|
|
2617
|
+
"[mcp_servers.stoops]",
|
|
2618
|
+
`url = "${mcpUrl}"`,
|
|
2619
|
+
`startup_timeout_sec = 15`,
|
|
2620
|
+
`tool_timeout_sec = 60`
|
|
2621
|
+
].join("\n");
|
|
2622
|
+
writeFileSync2(join3(codexConfigDir, "config.toml"), configToml);
|
|
2623
|
+
const tmuxSession = `stoops_${setup.agentName}`;
|
|
2624
|
+
if (tmuxSessionExists(tmuxSession)) {
|
|
2625
|
+
tmuxKillSession(tmuxSession);
|
|
2626
|
+
}
|
|
2627
|
+
console.log("Launching Codex...");
|
|
2628
|
+
tmuxCreateSession(tmuxSession);
|
|
2629
|
+
const extraArgs = options.extraArgs ?? [];
|
|
2630
|
+
const codexCmd = [`CODEX_HOME=${codexConfigDir} codex`, ...extraArgs].join(" ");
|
|
2631
|
+
tmuxSendCommand(tmuxSession, codexCmd);
|
|
2632
|
+
const bridge = new CodexTmuxBridge(tmuxSession);
|
|
2633
|
+
const eventLoopPromise = setup.processor.run(bridge.deliver.bind(bridge), setup.wrappedSource).catch(() => {
|
|
2634
|
+
});
|
|
2635
|
+
for (let i = 0; i < 10; i++) {
|
|
2636
|
+
await new Promise((r) => setTimeout(r, 500));
|
|
2637
|
+
if (!tmuxSessionExists(tmuxSession)) {
|
|
2638
|
+
console.error("Error: Codex exited during startup. Try running again.");
|
|
2639
|
+
bridge.stop();
|
|
2640
|
+
await setup.cleanup();
|
|
2641
|
+
try {
|
|
2642
|
+
rmSync2(tmpDir, { recursive: true });
|
|
2643
|
+
} catch {
|
|
2644
|
+
}
|
|
2645
|
+
return;
|
|
2646
|
+
}
|
|
2647
|
+
}
|
|
2648
|
+
console.log("Attaching to Codex session...\n");
|
|
2649
|
+
try {
|
|
2650
|
+
await tmuxAttach(tmuxSession);
|
|
2651
|
+
} catch {
|
|
2652
|
+
}
|
|
2653
|
+
bridge.stop();
|
|
2654
|
+
await setup.cleanup();
|
|
2655
|
+
tmuxKillSession(tmuxSession);
|
|
2656
|
+
try {
|
|
2657
|
+
rmSync2(tmpDir, { recursive: true });
|
|
2658
|
+
} catch {
|
|
2659
|
+
}
|
|
2660
|
+
console.log("Disconnected.");
|
|
2661
|
+
}
|
|
2662
|
+
|
|
2372
2663
|
// src/cli/index.ts
|
|
2373
2664
|
var args = process.argv.slice(2);
|
|
2374
2665
|
function getFlag(name, arr = args) {
|
|
@@ -2395,13 +2686,14 @@ function printUsage(stream = console.log) {
|
|
|
2395
2686
|
stream(" stoops join <url> [--name <name>] [--guest] Join a room");
|
|
2396
2687
|
stream(" stoops run claude [--name <name>] [--admin] [-- <args>] Connect Claude Code");
|
|
2397
2688
|
stream(" stoops run opencode [--name <name>] [--admin] [-- <args>] Connect OpenCode");
|
|
2689
|
+
stream(" stoops run codex [--name <name>] [--admin] [-- <args>] Connect Codex");
|
|
2398
2690
|
}
|
|
2399
2691
|
async function main() {
|
|
2400
2692
|
if (args.includes("--help") || args.includes("-h")) {
|
|
2401
2693
|
printUsage();
|
|
2402
2694
|
return;
|
|
2403
2695
|
}
|
|
2404
|
-
if (args[0] === "run" && (args[1] === "claude" || args[1] === "opencode")) {
|
|
2696
|
+
if (args[0] === "run" && (args[1] === "claude" || args[1] === "opencode" || args[1] === "codex")) {
|
|
2405
2697
|
const runtime = args[1];
|
|
2406
2698
|
const restArgs = args.slice(2);
|
|
2407
2699
|
const ddIndex = restArgs.indexOf("--");
|
|
@@ -2417,8 +2709,10 @@ async function main() {
|
|
|
2417
2709
|
};
|
|
2418
2710
|
if (runtime === "claude") {
|
|
2419
2711
|
await runClaude(runtimeOptions);
|
|
2420
|
-
} else {
|
|
2712
|
+
} else if (runtime === "opencode") {
|
|
2421
2713
|
await runOpencode(runtimeOptions);
|
|
2714
|
+
} else {
|
|
2715
|
+
await runCodex(runtimeOptions);
|
|
2422
2716
|
}
|
|
2423
2717
|
return;
|
|
2424
2718
|
}
|
|
@@ -2447,7 +2741,9 @@ async function main() {
|
|
|
2447
2741
|
room: getFlag("room"),
|
|
2448
2742
|
port,
|
|
2449
2743
|
share: args.includes("--share"),
|
|
2450
|
-
headless: args.includes("--headless")
|
|
2744
|
+
headless: args.includes("--headless"),
|
|
2745
|
+
save: getFlag("save"),
|
|
2746
|
+
load: getFlag("load")
|
|
2451
2747
|
});
|
|
2452
2748
|
return;
|
|
2453
2749
|
}
|
|
@@ -2462,7 +2758,9 @@ async function main() {
|
|
|
2462
2758
|
room: getFlag("room"),
|
|
2463
2759
|
port,
|
|
2464
2760
|
share: args.includes("--share"),
|
|
2465
|
-
quiet: true
|
|
2761
|
+
quiet: true,
|
|
2762
|
+
save: getFlag("save"),
|
|
2763
|
+
load: getFlag("load")
|
|
2466
2764
|
});
|
|
2467
2765
|
const adminJoinUrl = buildShareUrl(result.serverUrl, result.adminToken);
|
|
2468
2766
|
const participantShareUrl = buildShareUrl(
|