@cerulin/chell 0.2.5

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.
Files changed (53) hide show
  1. package/README.md +75 -0
  2. package/bin/chell-mcp.mjs +33 -0
  3. package/bin/chell.mjs +37 -0
  4. package/dist/codex/chellMcpStdioBridge.cjs +80 -0
  5. package/dist/codex/chellMcpStdioBridge.d.cts +2 -0
  6. package/dist/codex/chellMcpStdioBridge.d.mts +2 -0
  7. package/dist/codex/chellMcpStdioBridge.mjs +78 -0
  8. package/dist/index-B443j7JQ.mjs +6714 -0
  9. package/dist/index-qS668VWY.cjs +6730 -0
  10. package/dist/index.cjs +42 -0
  11. package/dist/index.d.cts +1 -0
  12. package/dist/index.d.mts +1 -0
  13. package/dist/index.mjs +39 -0
  14. package/dist/lib.cjs +32 -0
  15. package/dist/lib.d.cts +891 -0
  16. package/dist/lib.d.mts +891 -0
  17. package/dist/lib.mjs +22 -0
  18. package/dist/runCodex-DHtm7TWT.cjs +2020 -0
  19. package/dist/runCodex-DLbjgnc4.mjs +2017 -0
  20. package/dist/runGemini-C03RUmvr.mjs +788 -0
  21. package/dist/runGemini-fdb5jxAA.cjs +791 -0
  22. package/dist/types-DBjv5m4J.cjs +2499 -0
  23. package/dist/types-fM_iFuNp.mjs +2452 -0
  24. package/package.json +131 -0
  25. package/scripts/claude_local_launcher.cjs +98 -0
  26. package/scripts/claude_remote_launcher.cjs +13 -0
  27. package/scripts/codex_local_launcher.cjs +155 -0
  28. package/scripts/codex_preload.cjs +56 -0
  29. package/scripts/codex_remote_launcher.cjs +129 -0
  30. package/scripts/obfuscate-dist.mjs +73 -0
  31. package/scripts/pack-chell.cjs +32 -0
  32. package/scripts/publish-scoped.ps1 +58 -0
  33. package/scripts/ripgrep_launcher.cjs +33 -0
  34. package/scripts/unpack-tools.cjs +163 -0
  35. package/tools/archives/difftastic-LICENSE +21 -0
  36. package/tools/archives/difftastic-arm64-darwin.tar.gz +0 -0
  37. package/tools/archives/difftastic-arm64-linux.tar.gz +0 -0
  38. package/tools/archives/difftastic-x64-darwin.tar.gz +0 -0
  39. package/tools/archives/difftastic-x64-linux.tar.gz +0 -0
  40. package/tools/archives/difftastic-x64-win32.tar.gz +0 -0
  41. package/tools/archives/ripgrep-LICENSE +3 -0
  42. package/tools/archives/ripgrep-arm64-darwin.tar.gz +0 -0
  43. package/tools/archives/ripgrep-arm64-linux.tar.gz +0 -0
  44. package/tools/archives/ripgrep-x64-darwin.tar.gz +0 -0
  45. package/tools/archives/ripgrep-x64-linux.tar.gz +0 -0
  46. package/tools/archives/ripgrep-x64-win32.tar.gz +0 -0
  47. package/tools/licenses/difftastic-LICENSE +21 -0
  48. package/tools/licenses/ripgrep-LICENSE +3 -0
  49. package/tools/unpacked/difft +0 -0
  50. package/tools/unpacked/difft.exe +0 -0
  51. package/tools/unpacked/rg +0 -0
  52. package/tools/unpacked/rg.exe +0 -0
  53. package/tools/unpacked/ripgrep.node +0 -0
@@ -0,0 +1,2017 @@
1
+ import os, { homedir } from 'node:os';
2
+ import { randomUUID } from 'node:crypto';
3
+ import { l as logger, p as projectPath, c as configuration, w as writeHeadlessJson, A as ApiClient, r as readSettings, b as packageJson } from './types-fM_iFuNp.mjs';
4
+ import { spawn as spawn$1 } from 'node:child_process';
5
+ import { spawn } from 'node-pty';
6
+ import { join, resolve, dirname } from 'node:path';
7
+ import { createInterface } from 'node:readline';
8
+ import { mkdirSync, watch, existsSync, statSync, writeFileSync, appendFileSync, readFileSync } from 'node:fs';
9
+ import { createRequire } from 'node:module';
10
+ import 'node:https';
11
+ import 'node:http';
12
+ import { I as InvalidateSync, b as startFileWatcher, F as Future, p as parseSpecialCommand, R as RemoteModeDisplay, a as MessageBuffer, i as initialMachineMetadata, n as notifyDaemonSessionStarted, s as startChellServer, c as startCaffeinate, M as MessageQueue2, h as hashObject, r as registerKillSessionHandler, d as stopCaffeinate } from './index-B443j7JQ.mjs';
13
+ import { readdir, readFile } from 'node:fs/promises';
14
+ import { Client } from '@modelcontextprotocol/sdk/client/index.js';
15
+ import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
16
+ import { z } from 'zod';
17
+ import { ElicitRequestSchema } from '@modelcontextprotocol/sdk/types.js';
18
+ import { execSync } from 'child_process';
19
+ import { render } from 'ink';
20
+ import React from 'react';
21
+ import 'axios';
22
+ import 'chalk';
23
+ import 'fs';
24
+ import 'tweetnacl';
25
+ import 'node:events';
26
+ import 'socket.io-client';
27
+ import 'util';
28
+ import 'fs/promises';
29
+ import 'crypto';
30
+ import 'path';
31
+ import 'url';
32
+ import 'os';
33
+ import 'expo-server-sdk';
34
+ import 'node:url';
35
+ import 'ps-list';
36
+ import 'cross-spawn';
37
+ import 'qrcode-terminal';
38
+ import 'open';
39
+ import 'fastify';
40
+ import 'fastify-type-provider-zod';
41
+ import '@modelcontextprotocol/sdk/server/mcp.js';
42
+ import '@modelcontextprotocol/sdk/server/streamableHttp.js';
43
+ import 'http';
44
+ import 'readline';
45
+
46
+ class CodexSession {
47
+ path;
48
+ logPath;
49
+ api;
50
+ client;
51
+ queue;
52
+ mcpServers;
53
+ _onModeChange;
54
+ sessionId;
55
+ conversationId = null;
56
+ mode = "local";
57
+ thinking = false;
58
+ constructor(opts) {
59
+ this.path = opts.path;
60
+ this.api = opts.api;
61
+ this.client = opts.client;
62
+ this.logPath = opts.logPath;
63
+ this.sessionId = opts.sessionId;
64
+ this.queue = opts.messageQueue;
65
+ this.mcpServers = opts.mcpServers;
66
+ this._onModeChange = opts.onModeChange;
67
+ this.client.keepAlive(this.thinking, this.mode);
68
+ setInterval(() => {
69
+ this.client.keepAlive(this.thinking, this.mode);
70
+ }, 2e3);
71
+ }
72
+ onThinkingChange = (thinking) => {
73
+ this.thinking = thinking;
74
+ this.client.keepAlive(thinking, this.mode);
75
+ };
76
+ onModeChange = (mode) => {
77
+ this.mode = mode;
78
+ this.client.keepAlive(this.thinking, mode);
79
+ this._onModeChange(mode);
80
+ };
81
+ onSessionFound = (sessionId) => {
82
+ this.sessionId = sessionId;
83
+ logger.debug(`[CodexSession] Codex session ID ${sessionId} set for resume`);
84
+ this.client.updateMetadata((metadata) => ({
85
+ ...metadata,
86
+ codexSessionId: sessionId
87
+ }));
88
+ };
89
+ onConversationFound = (conversationId) => {
90
+ if (!conversationId || this.conversationId === conversationId) return;
91
+ this.conversationId = conversationId;
92
+ logger.debug(`[CodexSession] Codex conversation ID ${conversationId} recorded`);
93
+ this.client.updateMetadata((metadata) => ({
94
+ ...metadata,
95
+ codexConversationId: conversationId
96
+ }));
97
+ };
98
+ }
99
+
100
+ function resolveBundledCodexCli() {
101
+ try {
102
+ const require = createRequire(import.meta.url);
103
+ const pkgJsonPath = require.resolve("@openai/codex/package.json");
104
+ const pkgDir = dirname(pkgJsonPath);
105
+ const cliPath = resolve(join(pkgDir, "bin", "codex.js"));
106
+ return cliPath;
107
+ } catch {
108
+ logger.debug("[CodexLocal] Failed to resolve @openai/codex locally, using PATH fallback");
109
+ return "codex";
110
+ }
111
+ }
112
+ function quoteForShellDisplay(s) {
113
+ return `'${s.replace(/'/g, `"'"'`)}'`;
114
+ }
115
+ function visitUrlWithCurl(url) {
116
+ return new Promise((resolve, reject) => {
117
+ const args = [
118
+ "-sS",
119
+ // silent but show errors
120
+ "-L",
121
+ // follow redirects
122
+ "--max-time",
123
+ "10",
124
+ "--retry",
125
+ "2",
126
+ "--retry-delay",
127
+ "0",
128
+ "--retry-max-time",
129
+ "15",
130
+ "--fail-with-body",
131
+ "-o",
132
+ "/dev/null",
133
+ "--url",
134
+ url
135
+ ];
136
+ const cmdDisplay = `curl -sS -L --max-time 10 --retry 2 --retry-delay 0 --retry-max-time 15 --fail-with-body -o /dev/null --url ${quoteForShellDisplay(url)}`;
137
+ logger.debug(`[CodexLocal] Executing: ${cmdDisplay}`);
138
+ console.log(`[AUTH] curl: ${cmdDisplay}`);
139
+ const child = spawn$1("curl", args, { stdio: ["ignore", "ignore", "pipe"] });
140
+ let stderr = "";
141
+ if (child.stderr) {
142
+ child.stderr.on("data", (d) => {
143
+ try {
144
+ stderr += d.toString();
145
+ } catch {
146
+ }
147
+ });
148
+ }
149
+ child.on("error", (err) => {
150
+ logger.debug(`[CodexLocal] curl spawn error: ${err}`);
151
+ reject(err);
152
+ });
153
+ child.on("exit", (code) => {
154
+ logger.debug(`[CodexLocal] curl exit code: ${code}${stderr ? `, stderr: ${stderr.trim().slice(-500)}` : ""}`);
155
+ if (code === 0) return resolve();
156
+ reject(new Error(`curl exited with code ${code}${stderr ? `: ${stderr.trim().slice(-200)}` : ""}`));
157
+ });
158
+ });
159
+ }
160
+ async function codexLocal(opts) {
161
+ let thinking = false;
162
+ const updateThinking = (newThinking) => {
163
+ if (thinking !== newThinking) {
164
+ thinking = newThinking;
165
+ logger.debug(`[CodexLocal] Thinking state: ${thinking}`);
166
+ if (opts.onThinkingChange) {
167
+ opts.onThinkingChange(thinking);
168
+ }
169
+ }
170
+ };
171
+ const isHeadless = process.env.CHELL_HEADLESS === "1";
172
+ const showUi = process.env.CHELL_HEADLESS_SHOW_UI === "1";
173
+ let sessionDetected = false;
174
+ let fileWatcher = null;
175
+ const codexHome = process.env.CODEX_HOME || join(homedir(), ".codex");
176
+ const sessionsDir = join(codexHome, "sessions");
177
+ try {
178
+ mkdirSync(sessionsDir, { recursive: true });
179
+ fileWatcher = watch(sessionsDir, { recursive: true }, (eventType, filename) => {
180
+ if (sessionDetected || !filename) return;
181
+ const filenameStr = String(filename);
182
+ const match = filenameStr.match(/rollout-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/i);
183
+ if (match) {
184
+ const sessionId = match[1];
185
+ logger.debug(`[CodexLocal] Detected new session file: ${filenameStr}`);
186
+ logger.debug(`[CodexLocal] Extracted session ID: ${sessionId}`);
187
+ sessionDetected = true;
188
+ opts.onSessionFound(sessionId);
189
+ }
190
+ });
191
+ logger.debug(`[CodexLocal] Watching for session files in: ${sessionsDir}`);
192
+ } catch (err) {
193
+ logger.debug(`[CodexLocal] Failed to setup file watcher: ${err}`);
194
+ }
195
+ try {
196
+ process.stdin.pause();
197
+ let restartRequested = false;
198
+ const runOnce = () => new Promise((resolve$1, reject) => {
199
+ const args = [];
200
+ const resumeId = opts.sessionId || opts.conversationId || null;
201
+ if (resumeId) {
202
+ args.push("resume", resumeId);
203
+ }
204
+ const preloadPath = resolve(join(projectPath(), "scripts", "codex_preload.cjs"));
205
+ const launcherPath = resolve(join(projectPath(), "scripts", "codex_local_launcher.cjs"));
206
+ const useLauncher = existsSync(launcherPath);
207
+ const codexEntrypoint = resolveBundledCodexCli();
208
+ logger.debug(`[CodexLocal] Spawning Codex locally (${useLauncher ? launcherPath : codexEntrypoint}): ${args.join(" ")}`);
209
+ logger.debug(`[CodexLocal] Working directory: ${opts.path}`);
210
+ logger.debug(`[CodexLocal] Headless mode: ${isHeadless}`);
211
+ logger.debug(`[CodexLocal] useLauncher: ${useLauncher}, codexEntrypoint: ${codexEntrypoint}`);
212
+ if (isHeadless) {
213
+ const cols = process.stdout.columns || 120;
214
+ const rows = process.stdout.rows || 24;
215
+ const ptyEnv = {
216
+ ...process.env,
217
+ COLUMNS: cols.toString(),
218
+ LINES: rows.toString(),
219
+ // Encourage non-interactive fallbacks and reduce fancy terminal behavior
220
+ CI: process.env.CI || "1",
221
+ NO_COLOR: process.env.NO_COLOR || "1"
222
+ };
223
+ const ptyArgv = codexEntrypoint === "codex" ? ["codex", ...args] : [process.execPath, "-r", preloadPath, codexEntrypoint, ...args];
224
+ logger.debug(`[CodexLocal] PTY spawn command: ${ptyArgv[0]}`);
225
+ logger.debug(`[CodexLocal] PTY spawn args: ${JSON.stringify(ptyArgv.slice(1))}`);
226
+ console.log(`[CodexLocal] Spawning: ${ptyArgv.join(" ")}
227
+ `);
228
+ const pty = spawn(ptyArgv[0], ptyArgv.slice(1), {
229
+ name: "xterm-color",
230
+ // Use same terminal type as Claude
231
+ cols,
232
+ rows,
233
+ cwd: opts.path,
234
+ env: {
235
+ ...ptyEnv,
236
+ TERM: "xterm-256color",
237
+ DISABLE_AUTOUPDATER: "1",
238
+ ...codexEntrypoint === "codex" ? { NODE_OPTIONS: `${process.env.NODE_OPTIONS ?? ""} -r ${preloadPath}`.trim() } : {}
239
+ }
240
+ });
241
+ if (showUi) {
242
+ try {
243
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
244
+ process.stdin.resume();
245
+ process.stdin.on("data", (d) => {
246
+ try {
247
+ pty.write(d.toString());
248
+ } catch {
249
+ }
250
+ });
251
+ } catch {
252
+ }
253
+ }
254
+ let buffer = "";
255
+ let authStep = 0;
256
+ const capturedUrls = /* @__PURE__ */ new Set();
257
+ const teePath = join(configuration.happyHomeDir, "link", "codex-headless-output");
258
+ let teeWriteCount = 0;
259
+ const TEE_MAX_BYTES = Number(process.env.CHELL_HEADLESS_TEE_MAX_BYTES || 512 * 1024);
260
+ let lastSnippet = "";
261
+ let lastSnippetAt = 0;
262
+ const stripAnsi = (s) => s.replace(/\x1B\[[0-9;?]*[ -\/]*[@-~]/g, "");
263
+ const isMostlySpinner = (s) => {
264
+ const clean = s.replace(/\n/g, "");
265
+ const crCount = (clean.match(/\r/g) || []).length;
266
+ const hasAlphaNum = /[A-Za-z0-9]/.test(stripAnsi(clean));
267
+ return crCount >= 1 && !hasAlphaNum && clean.length <= 200;
268
+ };
269
+ const isInteresting = (s) => {
270
+ const t = stripAnsi(s);
271
+ return t.includes("auth.openai.com") || t.toLowerCase().includes("oauth") || t.toLowerCase().includes("authorization code") || t.toLowerCase().includes("sign in") || t.toLowerCase().includes("provide your own api key") || t.toLowerCase().includes("callback") || t.toLowerCase().includes("[auth]");
272
+ };
273
+ const writeTee = (text) => {
274
+ try {
275
+ if (isMostlySpinner(text)) return;
276
+ const now = Date.now();
277
+ const normalized = stripAnsi(text).replace(/\r/g, "");
278
+ if (normalized && normalized === lastSnippet && now - lastSnippetAt < 1e3) {
279
+ return;
280
+ }
281
+ mkdirSync(configuration.happyHomeDir + "/link", { recursive: true });
282
+ if (existsSync(teePath)) {
283
+ try {
284
+ const size = statSync(teePath).size;
285
+ if (size > TEE_MAX_BYTES) {
286
+ writeFileSync(teePath, `[truncated at ${(/* @__PURE__ */ new Date()).toISOString()}]
287
+ `);
288
+ }
289
+ } catch {
290
+ }
291
+ }
292
+ appendFileSync(teePath, text);
293
+ lastSnippet = normalized;
294
+ lastSnippetAt = now;
295
+ teeWriteCount++;
296
+ } catch {
297
+ }
298
+ };
299
+ const findAndSaveOAuthUrls = (cleanBuf) => {
300
+ try {
301
+ const hay = cleanBuf;
302
+ const needle = "https://auth.openai.com/";
303
+ let idx = 0;
304
+ const maxLen = 2048;
305
+ const isAllowed = (ch) => /[A-Za-z0-9\-._~:/?#\[\]@!$&'()*+,;=%]/.test(ch);
306
+ while ((idx = hay.indexOf(needle, idx)) !== -1) {
307
+ let i = idx;
308
+ let out = "";
309
+ while (i < hay.length && out.length < maxLen) {
310
+ const ch = hay[i];
311
+ if (isAllowed(ch)) {
312
+ out += ch;
313
+ i++;
314
+ continue;
315
+ }
316
+ if (ch === " " || ch === '"' || ch === "'" || ch === "<" || ch === ">" || ch === ")" || ch === "]" || ch === "\\") {
317
+ break;
318
+ }
319
+ if (ch === "\\r" || ch === "\\n" || ch === "\\t") {
320
+ i++;
321
+ continue;
322
+ }
323
+ break;
324
+ }
325
+ idx = i;
326
+ if (out.includes("?") && !capturedUrls.has(out)) {
327
+ capturedUrls.add(out);
328
+ try {
329
+ writeHeadlessJson({ codex_auth_url: out });
330
+ const jsonPath = join(configuration.happyHomeDir, "link", "headless.json");
331
+ console.log(`
332
+ [AUTH] OAuth URL saved to: ${jsonPath}
333
+ `);
334
+ logger.debug(`[CodexLocal] OAuth URL captured: ${out}`);
335
+ } catch (err) {
336
+ logger.debug(`[CodexLocal] Failed to write OAuth URL to headless.json: ${err}`);
337
+ }
338
+ }
339
+ }
340
+ } catch {
341
+ }
342
+ };
343
+ const sendToStdin = (text) => {
344
+ try {
345
+ pty.write(text);
346
+ } catch (err) {
347
+ logger.debug(`[CodexLocal] Failed to write to PTY: ${err}`);
348
+ }
349
+ };
350
+ const sendEnter = () => sendToStdin("\r");
351
+ const sendCtrlC = () => {
352
+ try {
353
+ pty.write("");
354
+ } catch (err) {
355
+ logger.debug(`[CodexLocal] Failed to send Ctrl+C: ${err}`);
356
+ }
357
+ };
358
+ const headlessJsonPath = join(configuration.happyHomeDir, "link", "headless.json");
359
+ let lastSeenCodexReturnAuth = null;
360
+ let callbackVisited = false;
361
+ let lastRestartAt = 0;
362
+ let pendingAuthCode = null;
363
+ const pollReturnAuth = setInterval(async () => {
364
+ if (callbackVisited) return;
365
+ try {
366
+ if (!existsSync(headlessJsonPath)) return;
367
+ const raw = readFileSync(headlessJsonPath, "utf8");
368
+ if (!raw) return;
369
+ const data = JSON.parse(raw);
370
+ const val = typeof data.codex_return_auth === "string" ? data.codex_return_auth.trim() : "";
371
+ if (val && val !== lastSeenCodexReturnAuth) {
372
+ lastSeenCodexReturnAuth = val;
373
+ callbackVisited = true;
374
+ logger.debug(`[CodexLocal] Handling return auth via curl`);
375
+ console.log(`[AUTH] Visiting callback URL (curl)...
376
+ `);
377
+ try {
378
+ await visitUrlWithCurl(val);
379
+ console.log(`[AUTH] Callback visited
380
+ `);
381
+ } catch (err) {
382
+ logger.debug(`[CodexLocal] curl visit failed: ${err}`);
383
+ console.log(`[AUTH] Callback visit failed: ${err}
384
+ `);
385
+ } finally {
386
+ try {
387
+ writeHeadlessJson({ codex_return_auth: "" });
388
+ } catch {
389
+ }
390
+ const now = Date.now();
391
+ if (!restartRequested && now - lastRestartAt > 4e3) {
392
+ restartRequested = true;
393
+ lastRestartAt = now + 1e3;
394
+ setTimeout(() => {
395
+ try {
396
+ pty.kill();
397
+ } catch {
398
+ }
399
+ }, 1e3);
400
+ }
401
+ }
402
+ }
403
+ } catch (err) {
404
+ }
405
+ }, 500);
406
+ pty.onData((data) => {
407
+ if (data.includes("\x1B[6n")) {
408
+ try {
409
+ const reply = `\x1B[${rows};1R`;
410
+ pty.write(reply);
411
+ logger.debug("[CodexLocal] Responded to DSR (cursor position) with", reply);
412
+ } catch {
413
+ }
414
+ }
415
+ if (showUi) {
416
+ try {
417
+ process.stdout.write(data);
418
+ } catch {
419
+ }
420
+ }
421
+ writeTee(data);
422
+ buffer += data;
423
+ const cleanBuffer = stripAnsi(buffer);
424
+ findAndSaveOAuthUrls(cleanBuffer);
425
+ if (authStep === 0 && (cleanBuffer.includes("Sign in with ChatGPT") || cleanBuffer.includes("Provide your own API key") || cleanBuffer.includes("Press Enter to continue"))) {
426
+ if (cleanBuffer.includes("1. Sign in with ChatGPT") || cleanBuffer.includes("1) Sign in with ChatGPT")) {
427
+ logger.debug("[CodexLocal] Detected login method selection prompt");
428
+ console.log(`
429
+ [AUTH] Auto-selecting "Sign in with ChatGPT"...
430
+ `);
431
+ authStep = 1;
432
+ setTimeout(() => {
433
+ sendToStdin("1");
434
+ logger.debug('[CodexLocal] Sent "1" to select ChatGPT login');
435
+ }, 500);
436
+ }
437
+ }
438
+ if (authStep === 1 && capturedUrls.size > 0) {
439
+ logger.debug("[CodexLocal] OAuth URL detected, waiting for callback...");
440
+ console.log(`[AUTH] Waiting for callback URL in headless.json...
441
+ `);
442
+ authStep = 2;
443
+ }
444
+ if (pendingAuthCode) {
445
+ const lower = cleanBuffer.toLowerCase();
446
+ if (lower.includes("enter authorization code") || lower.includes("paste code") || lower.includes("enter code")) {
447
+ try {
448
+ const code = pendingAuthCode;
449
+ pendingAuthCode = null;
450
+ pty.write(code + "\r");
451
+ logger.debug("[CodexLocal] Submitted authorization code to CLI");
452
+ } catch {
453
+ }
454
+ }
455
+ }
456
+ if (buffer.length > 2e4) buffer = buffer.slice(-1e4);
457
+ });
458
+ process.stdout.on("resize", () => {
459
+ try {
460
+ pty.resize(process.stdout.columns || 80, process.stdout.rows || 24);
461
+ } catch {
462
+ }
463
+ });
464
+ opts.abort.addEventListener("abort", () => {
465
+ try {
466
+ pty.kill();
467
+ } catch {
468
+ }
469
+ });
470
+ pty.onExit((exitCode) => {
471
+ clearInterval(pollReturnAuth);
472
+ logger.debug(`[CodexLocal] PTY exited with code: ${exitCode.exitCode}, signal: ${exitCode.signal}`);
473
+ console.log(`
474
+ [CodexLocal] Codex process exited (code: ${exitCode.exitCode})
475
+ `);
476
+ resolve$1();
477
+ });
478
+ return;
479
+ }
480
+ const interactiveEnv = {
481
+ ...process.env,
482
+ COLUMNS: (process.stdout.columns || 120).toString(),
483
+ LINES: (process.stdout.rows || 24).toString()
484
+ };
485
+ let command;
486
+ let childArgs;
487
+ let childEnv = {
488
+ ...interactiveEnv,
489
+ DISABLE_AUTOUPDATER: "1"
490
+ };
491
+ if (useLauncher) {
492
+ command = process.execPath;
493
+ childArgs = [launcherPath, ...args];
494
+ } else if (codexEntrypoint === "codex") {
495
+ command = "codex";
496
+ childArgs = [...args];
497
+ childEnv.NODE_OPTIONS = `${process.env.NODE_OPTIONS ?? ""} -r ${preloadPath}`.trim();
498
+ } else {
499
+ command = process.execPath;
500
+ childArgs = ["-r", preloadPath, codexEntrypoint, ...args];
501
+ }
502
+ const child = spawn$1(command, childArgs, {
503
+ stdio: ["inherit", "inherit", "inherit", "pipe"],
504
+ signal: opts.abort,
505
+ cwd: opts.path,
506
+ env: childEnv
507
+ });
508
+ if (child.stdio[3]) {
509
+ const rl = createInterface({
510
+ input: child.stdio[3],
511
+ crlfDelay: Infinity
512
+ });
513
+ rl.on("line", (line) => {
514
+ try {
515
+ const message = JSON.parse(line);
516
+ logger.debug(`[CodexLocal] fd3 message: ${JSON.stringify(message)}`);
517
+ switch (message.type) {
518
+ case "launcher_start":
519
+ logger.debug(`[CodexLocal] Launcher started`);
520
+ break;
521
+ case "import_attempt":
522
+ logger.debug(`[CodexLocal] Attempting to import: ${message.path}`);
523
+ break;
524
+ case "import_success":
525
+ logger.debug(`[CodexLocal] Successfully imported: ${message.path}`);
526
+ break;
527
+ case "import_failed":
528
+ logger.debug(`[CodexLocal] Failed to import Codex: ${message.error}`);
529
+ break;
530
+ case "uuid":
531
+ logger.debug(`[CodexLocal] Session UUID detected: ${message.value}`);
532
+ opts.onSessionFound(message.value);
533
+ break;
534
+ case "fetch-start":
535
+ logger.debug(`[CodexLocal] Fetch start detected (thinking)`);
536
+ updateThinking(true);
537
+ break;
538
+ case "fetch-end":
539
+ logger.debug(`[CodexLocal] Fetch end detected (done thinking)`);
540
+ updateThinking(false);
541
+ break;
542
+ default:
543
+ logger.debug(`[CodexLocal] Unknown fd3 message type: ${message.type}`);
544
+ }
545
+ } catch (e) {
546
+ logger.debug(`[CodexLocal] Non-JSON line from fd3: ${line}`);
547
+ }
548
+ });
549
+ rl.on("error", (err) => {
550
+ logger.debug("[CodexLocal] Error reading from fd3:", err);
551
+ });
552
+ }
553
+ child.on("error", (error) => {
554
+ if (error?.name === "AbortError" || error?.code === "ABORT_ERR") {
555
+ logger.debug("[CodexLocal] Spawn aborted during mode switch");
556
+ return resolve$1();
557
+ }
558
+ logger.debug("[CodexLocal] Spawn error:", error);
559
+ console.error("\n\u274C Failed to spawn codex CLI:", error?.message || String(error));
560
+ console.error('Make sure "codex" is installed and available in your PATH.');
561
+ console.error("Try installing it from: https://github.com/openai/codex\n");
562
+ reject(error);
563
+ });
564
+ child.on("exit", (code, signal) => {
565
+ if (signal === "SIGTERM" && opts.abort.aborted) {
566
+ resolve$1();
567
+ } else if (signal) {
568
+ reject(new Error(`Process terminated with signal: ${signal}`));
569
+ } else {
570
+ resolve$1();
571
+ }
572
+ });
573
+ });
574
+ do {
575
+ restartRequested = false;
576
+ await runOnce();
577
+ } while (restartRequested);
578
+ } finally {
579
+ if (fileWatcher) {
580
+ fileWatcher.close();
581
+ }
582
+ process.stdin.resume();
583
+ updateThinking(false);
584
+ }
585
+ return null;
586
+ }
587
+
588
+ function getCodexHomeDir() {
589
+ return process.env.CODEX_HOME || resolve(homedir(), ".codex");
590
+ }
591
+ function getCodexSessionsDir() {
592
+ return join(getCodexHomeDir(), "sessions");
593
+ }
594
+ function getCodexSessionDirForDate(date = /* @__PURE__ */ new Date()) {
595
+ const year = date.getFullYear();
596
+ const month = String(date.getMonth() + 1).padStart(2, "0");
597
+ const day = String(date.getDate()).padStart(2, "0");
598
+ return join(getCodexSessionsDir(), String(year), month, day);
599
+ }
600
+
601
+ async function createCodexSessionScanner(opts) {
602
+ let finishedSessions = /* @__PURE__ */ new Set();
603
+ let pendingSessions = /* @__PURE__ */ new Set();
604
+ let currentSessionId = null;
605
+ let watchers = /* @__PURE__ */ new Map();
606
+ let processedMessageKeys = /* @__PURE__ */ new Set();
607
+ let sessionFilePaths = /* @__PURE__ */ new Map();
608
+ if (opts.sessionId) {
609
+ const filePath = await findSessionFile$1(opts.sessionId);
610
+ if (filePath) {
611
+ sessionFilePaths.set(opts.sessionId, filePath);
612
+ const messages = await readSessionLog(filePath);
613
+ for (const m of messages) {
614
+ processedMessageKeys.add(messageKey(m));
615
+ }
616
+ currentSessionId = opts.sessionId;
617
+ logger.debug(`[CODEX_SESSION_SCANNER] Initialized with resumed session: ${opts.sessionId}`);
618
+ }
619
+ }
620
+ const sync = new InvalidateSync(async () => {
621
+ const sessions = [];
622
+ for (const p of pendingSessions) {
623
+ sessions.push(p);
624
+ }
625
+ if (currentSessionId) {
626
+ sessions.push(currentSessionId);
627
+ }
628
+ for (const sessionId of sessions) {
629
+ if (!sessionFilePaths.has(sessionId)) {
630
+ const filePath2 = await findSessionFile$1(sessionId);
631
+ if (filePath2) {
632
+ sessionFilePaths.set(sessionId, filePath2);
633
+ }
634
+ }
635
+ const filePath = sessionFilePaths.get(sessionId);
636
+ if (!filePath) {
637
+ logger.debug(`[CODEX_SESSION_SCANNER] No file path found for session: ${sessionId}`);
638
+ continue;
639
+ }
640
+ const messages = await readSessionLog(filePath);
641
+ for (const message of messages) {
642
+ const key = messageKey(message);
643
+ if (processedMessageKeys.has(key)) {
644
+ continue;
645
+ }
646
+ processedMessageKeys.add(key);
647
+ if (shouldFilterMessage(message)) {
648
+ logger.debug(`[CODEX_SESSION_SCANNER] Filtering out message: ${message.type || "unknown"}`);
649
+ continue;
650
+ }
651
+ opts.onMessage(message);
652
+ }
653
+ }
654
+ for (const p of sessions) {
655
+ if (pendingSessions.has(p)) {
656
+ pendingSessions.delete(p);
657
+ finishedSessions.add(p);
658
+ }
659
+ }
660
+ for (const sessionId of sessions) {
661
+ const filePath = sessionFilePaths.get(sessionId);
662
+ if (filePath && !watchers.has(sessionId)) {
663
+ logger.debug(`[CODEX_SESSION_SCANNER] Setting up watcher for: ${filePath}`);
664
+ watchers.set(sessionId, startFileWatcher(filePath, () => {
665
+ logger.debug(`[CODEX_SESSION_SCANNER] File change detected for session: ${sessionId}`);
666
+ sync.invalidate();
667
+ }));
668
+ }
669
+ }
670
+ });
671
+ await sync.invalidateAndAwait();
672
+ const intervalId = setInterval(() => {
673
+ sync.invalidate();
674
+ }, 3e3);
675
+ return {
676
+ cleanup: async () => {
677
+ clearInterval(intervalId);
678
+ for (const w of watchers.values()) {
679
+ w();
680
+ }
681
+ watchers.clear();
682
+ await sync.invalidateAndAwait();
683
+ sync.stop();
684
+ },
685
+ onNewSession: (sessionId) => {
686
+ if (currentSessionId === sessionId) {
687
+ logger.debug(`[CODEX_SESSION_SCANNER] New session: ${sessionId} is the same as current, skipping`);
688
+ return;
689
+ }
690
+ if (pendingSessions.has(sessionId)) {
691
+ logger.debug(`[CODEX_SESSION_SCANNER] New session: ${sessionId} is already pending, skipping`);
692
+ return;
693
+ }
694
+ if (finishedSessions.has(sessionId)) {
695
+ logger.debug(`[CODEX_SESSION_SCANNER] Reactivating finished session: ${sessionId}`);
696
+ finishedSessions.delete(sessionId);
697
+ }
698
+ if (currentSessionId) {
699
+ pendingSessions.add(currentSessionId);
700
+ }
701
+ logger.debug(`[CODEX_SESSION_SCANNER] New session: ${sessionId}`);
702
+ currentSessionId = sessionId;
703
+ sync.invalidate();
704
+ }
705
+ };
706
+ }
707
+ function messageKey(message) {
708
+ if (message.id) {
709
+ return `id:${String(message.id)}`;
710
+ }
711
+ if (message.uuid) {
712
+ return `uuid:${String(message.uuid)}`;
713
+ }
714
+ const parts = [];
715
+ if (message.type) {
716
+ parts.push(`type:${message.type}`);
717
+ }
718
+ if (message.timestamp || message.time || message.created_at) {
719
+ const ts = message.timestamp || message.time || message.created_at;
720
+ parts.push(`ts:${ts}`);
721
+ }
722
+ const contentStr = JSON.stringify(message);
723
+ const len = contentStr.length;
724
+ const sample = contentStr.substring(0, 100) + contentStr.substring(Math.max(0, len - 100));
725
+ parts.push(`len:${len}`);
726
+ parts.push(`hash:${Buffer.from(sample).toString("base64").substring(0, 32)}`);
727
+ return parts.join("|");
728
+ }
729
+ function shouldFilterMessage(message) {
730
+ if (message.type === "session_meta") {
731
+ return true;
732
+ }
733
+ if (message.type === "event_msg" && message.payload?.type === "user_message") {
734
+ return true;
735
+ }
736
+ if (message.type === "event_msg") {
737
+ const pt = message.payload?.type;
738
+ if (pt === "agent_message_delta" || pt === "agent_reasoning_delta" || pt === "agent_reasoning_section_break") {
739
+ return true;
740
+ }
741
+ if (pt === "agent_message") {
742
+ return true;
743
+ }
744
+ }
745
+ if (message.type === "response_item" && message.payload) {
746
+ const payload = message.payload;
747
+ if (payload.role === "user" && Array.isArray(payload.content)) {
748
+ for (const contentItem of payload.content) {
749
+ if (contentItem.text && typeof contentItem.text === "string") {
750
+ const text = contentItem.text.trim();
751
+ if (text.startsWith("<environment_context>") && text.endsWith("</environment_context>")) {
752
+ return true;
753
+ }
754
+ }
755
+ }
756
+ }
757
+ }
758
+ return false;
759
+ }
760
+ async function findSessionFile$1(sessionId) {
761
+ const today = /* @__PURE__ */ new Date();
762
+ let sessionDir = getCodexSessionDirForDate(today);
763
+ if (!existsSync(sessionDir)) {
764
+ logger.debug(`[CODEX_SESSION_SCANNER] Session directory does not exist: ${sessionDir}`);
765
+ const yesterday = new Date(today);
766
+ yesterday.setDate(yesterday.getDate() - 1);
767
+ sessionDir = getCodexSessionDirForDate(yesterday);
768
+ if (!existsSync(sessionDir)) {
769
+ logger.debug(`[CODEX_SESSION_SCANNER] Yesterday's directory also doesn't exist: ${sessionDir}`);
770
+ return null;
771
+ }
772
+ }
773
+ try {
774
+ const files = await readdir(sessionDir);
775
+ const matchingFile = files.find(
776
+ (file) => file.startsWith("rollout-") && file.endsWith(`-${sessionId}.jsonl`)
777
+ );
778
+ if (matchingFile) {
779
+ const fullPath = join(sessionDir, matchingFile);
780
+ logger.debug(`[CODEX_SESSION_SCANNER] Found session file: ${fullPath}`);
781
+ return fullPath;
782
+ }
783
+ logger.debug(`[CODEX_SESSION_SCANNER] No matching file found for session: ${sessionId}`);
784
+ return null;
785
+ } catch (error) {
786
+ logger.debug(`[CODEX_SESSION_SCANNER] Error reading session directory: ${error}`);
787
+ return null;
788
+ }
789
+ }
790
+ async function readSessionLog(filePath) {
791
+ try {
792
+ const content = await readFile(filePath, "utf-8");
793
+ const lines = content.split("\n");
794
+ const messages = [];
795
+ for (const line of lines) {
796
+ const trimmed = line.trim();
797
+ if (trimmed === "") {
798
+ continue;
799
+ }
800
+ try {
801
+ const message = JSON.parse(trimmed);
802
+ messages.push(message);
803
+ } catch (error) {
804
+ logger.debug(`[CODEX_SESSION_SCANNER] Failed to parse line: ${trimmed.substring(0, 100)}`);
805
+ continue;
806
+ }
807
+ }
808
+ return messages;
809
+ } catch (error) {
810
+ logger.debug(`[CODEX_SESSION_SCANNER] Error reading session file: ${error}`);
811
+ return [];
812
+ }
813
+ }
814
+
815
+ async function codexLocalLauncher(session) {
816
+ const scanner = await createCodexSessionScanner({
817
+ sessionId: session.sessionId,
818
+ onMessage: (message) => {
819
+ let shouldForwardRaw = true;
820
+ try {
821
+ const t = message?.type;
822
+ if (t === "event_msg") {
823
+ const pt = message?.payload?.type;
824
+ if (pt === "agent_message_delta" || pt === "agent_reasoning_delta" || pt === "agent_reasoning_section_break" || pt === "agent_message") {
825
+ shouldForwardRaw = false;
826
+ }
827
+ }
828
+ if (t === "response_item" && message?.payload?.type === "message" && message?.payload?.role === "assistant" && Array.isArray(message?.payload?.content)) {
829
+ shouldForwardRaw = false;
830
+ }
831
+ } catch {
832
+ }
833
+ if (shouldForwardRaw) {
834
+ logger.debug(`[CodexLocal] Forwarding message to mobile: ${message.type || "unknown"}`);
835
+ session.client.sendCodexMessage(message);
836
+ }
837
+ try {
838
+ if (message && typeof message === "object" && message.type === "response_item" && message.payload && message.payload.type === "message" && message.payload.role === "assistant" && Array.isArray(message.payload.content)) {
839
+ for (const block of message.payload.content) {
840
+ if (block && typeof block === "object" && block.type === "output_text" && typeof block.text === "string") {
841
+ const text = block.text;
842
+ session.client.sendCodexMessage({
843
+ type: "message",
844
+ message: text,
845
+ id: randomUUID()
846
+ });
847
+ try {
848
+ session.client.sendClaudeSessionMessage({
849
+ type: "assistant",
850
+ uuid: randomUUID(),
851
+ message: { content: [{ type: "text", text }] }
852
+ });
853
+ } catch {
854
+ }
855
+ }
856
+ }
857
+ }
858
+ } catch {
859
+ }
860
+ }
861
+ });
862
+ let exitReason = null;
863
+ const processAbortController = new AbortController();
864
+ let exitFuture = new Future();
865
+ try {
866
+ async function abort() {
867
+ if (!processAbortController.signal.aborted) {
868
+ processAbortController.abort();
869
+ }
870
+ await exitFuture.promise;
871
+ }
872
+ async function doAbort() {
873
+ logger.debug("[CodexLocal]: doAbort");
874
+ if (!exitReason) {
875
+ exitReason = "switch";
876
+ }
877
+ session.queue.reset();
878
+ await abort();
879
+ }
880
+ async function doSwitch() {
881
+ logger.debug("[CodexLocal]: doSwitch");
882
+ if (!exitReason) {
883
+ exitReason = "switch";
884
+ }
885
+ await abort();
886
+ }
887
+ session.client.rpcHandlerManager.registerHandler("abort", doAbort);
888
+ session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
889
+ session.queue.setOnMessage((message, mode) => {
890
+ doSwitch();
891
+ });
892
+ if (session.queue.size() > 0) {
893
+ return "switch";
894
+ }
895
+ const handleSessionStart = (sessionId) => {
896
+ session.onSessionFound(sessionId);
897
+ scanner.onNewSession(sessionId);
898
+ (async () => {
899
+ try {
900
+ const filePath = await findSessionFile(sessionId);
901
+ if (!filePath) return;
902
+ const content = await readFile(filePath, "utf-8");
903
+ const lines = content.split("\n");
904
+ for (const line of lines) {
905
+ const trimmed = line.trim();
906
+ if (!trimmed) continue;
907
+ try {
908
+ const obj = JSON.parse(trimmed);
909
+ const conv = obj.conversation_id || obj.conversationId || obj?.payload?.conversation_id || obj?.payload?.conversationId;
910
+ if (typeof conv === "string" && conv) {
911
+ session.onConversationFound(conv);
912
+ break;
913
+ }
914
+ } catch {
915
+ }
916
+ }
917
+ } catch {
918
+ }
919
+ })();
920
+ };
921
+ logger.debug(`[CodexLocal]: Starting Codex CLI (headless=${process.env.CHELL_HEADLESS === "1"})`);
922
+ const result = codexLocal({
923
+ abort: processAbortController.signal,
924
+ sessionId: session.sessionId,
925
+ conversationId: session.conversationId ?? null,
926
+ mcpServers: session.mcpServers,
927
+ path: session.path,
928
+ onSessionFound: handleSessionStart,
929
+ onThinkingChange: session.onThinkingChange
930
+ });
931
+ await result;
932
+ exitFuture.resolve();
933
+ logger.debug("[CodexLocal]: Process exited");
934
+ } catch (error) {
935
+ exitFuture.resolve();
936
+ if (error?.message?.includes("SIGTERM") || processAbortController.signal.aborted) {
937
+ logger.debug("[CodexLocal]: Process aborted (switching modes)");
938
+ } else {
939
+ logger.debug("[CodexLocal]: Process error:", error);
940
+ throw error;
941
+ }
942
+ } finally {
943
+ session.client.rpcHandlerManager.registerHandler("abort", async () => {
944
+ });
945
+ session.client.rpcHandlerManager.registerHandler("switch", async () => {
946
+ });
947
+ session.queue.setOnMessage(null);
948
+ await scanner.cleanup();
949
+ }
950
+ return exitReason || "exit";
951
+ }
952
+ async function findSessionFile(sessionId) {
953
+ const today = /* @__PURE__ */ new Date();
954
+ let dir = getCodexSessionDirForDate(today);
955
+ if (!existsSync(dir)) {
956
+ const yesterday = new Date(today);
957
+ yesterday.setDate(yesterday.getDate() - 1);
958
+ dir = getCodexSessionDirForDate(yesterday);
959
+ if (!existsSync(dir)) return null;
960
+ }
961
+ try {
962
+ const files = await readdir(dir);
963
+ const match = files.find((f) => f.startsWith("rollout-") && f.endsWith(`-${sessionId}.jsonl`));
964
+ return match ? join(dir, match) : null;
965
+ } catch {
966
+ return null;
967
+ }
968
+ }
969
+
970
+ const DEFAULT_TIMEOUT = 14 * 24 * 60 * 60 * 1e3;
971
+ function resolveCodexCliPath() {
972
+ try {
973
+ const require = createRequire(import.meta.url);
974
+ const pkgJsonPath = require.resolve("@openai/codex/package.json");
975
+ const pkgDir = dirname(pkgJsonPath);
976
+ const cliPath = resolve(join(pkgDir, "bin", "codex.js"));
977
+ return cliPath;
978
+ } catch (e) {
979
+ logger.debug("[CodexMCP] Failed to resolve @openai/codex locally, using PATH fallback");
980
+ return "codex";
981
+ }
982
+ }
983
+ function detectMcpSubcommand(codexEntrypoint) {
984
+ try {
985
+ const cmd = codexEntrypoint === "codex" ? "codex --version" : `"${process.execPath}" "${codexEntrypoint}" --version`;
986
+ const version = execSync(cmd, { encoding: "utf8" }).trim();
987
+ const match = version.match(/codex-cli\s+(\d+\.\d+\.\d+(?:-alpha\.\d+)?)/);
988
+ if (!match) return "mcp-server";
989
+ const versionStr = match[1];
990
+ const [major, minor, patch] = versionStr.split(/[-.]/).map(Number);
991
+ if (major > 0 || minor > 43) return "mcp-server";
992
+ if (minor === 43 && patch === 0) {
993
+ if (versionStr.includes("-alpha.")) {
994
+ const alphaNum = parseInt(versionStr.split("-alpha.")[1]);
995
+ return alphaNum >= 5 ? "mcp-server" : "mcp";
996
+ }
997
+ return "mcp-server";
998
+ }
999
+ return "mcp";
1000
+ } catch (error) {
1001
+ logger.debug("[CodexMCP] Error detecting codex version, defaulting to mcp-server:", error);
1002
+ return "mcp-server";
1003
+ }
1004
+ }
1005
+ class CodexMcpClient {
1006
+ client;
1007
+ transport = null;
1008
+ connected = false;
1009
+ sessionId = null;
1010
+ conversationId = null;
1011
+ handler = null;
1012
+ permissionHandler = null;
1013
+ launchConfig = null;
1014
+ constructor() {
1015
+ this.client = new Client(
1016
+ { name: "happy-codex-client", version: "1.0.0" },
1017
+ { capabilities: { tools: {}, elicitation: {} } }
1018
+ );
1019
+ this.client.setNotificationHandler(z.object({
1020
+ method: z.literal("codex/event"),
1021
+ params: z.object({
1022
+ msg: z.any()
1023
+ })
1024
+ }).passthrough(), (data) => {
1025
+ const msg = data.params.msg;
1026
+ this.updateIdentifiersFromEvent(msg);
1027
+ this.handler?.(msg);
1028
+ });
1029
+ }
1030
+ setHandler(handler) {
1031
+ this.handler = handler;
1032
+ }
1033
+ /**
1034
+ * Provide initial Codex config key/values to pass via `-c key=value` when spawning mcp-server.
1035
+ */
1036
+ setLaunchConfig(config) {
1037
+ this.launchConfig = config && Object.keys(config).length > 0 ? config : null;
1038
+ }
1039
+ /**
1040
+ * Set the permission handler for tool approval
1041
+ */
1042
+ setPermissionHandler(handler) {
1043
+ this.permissionHandler = handler;
1044
+ }
1045
+ async connect() {
1046
+ if (this.connected) return;
1047
+ const codexEntrypoint = resolveCodexCliPath();
1048
+ const mcpCommand = detectMcpSubcommand(codexEntrypoint);
1049
+ logger.debug(`[CodexMCP] Connecting to Codex MCP server using command: ${codexEntrypoint === "codex" ? "codex" : codexEntrypoint} ${mcpCommand}`);
1050
+ const baseArgs = [mcpCommand];
1051
+ if (this.launchConfig) {
1052
+ for (const [k, v] of Object.entries(this.launchConfig)) {
1053
+ baseArgs.push("-c");
1054
+ baseArgs.push(`${k}=${v}`);
1055
+ }
1056
+ }
1057
+ try {
1058
+ const fullCmd = codexEntrypoint === "codex" ? ["codex", ...baseArgs] : [process.execPath, codexEntrypoint, ...baseArgs];
1059
+ logger.debug("[CodexMCP] Spawn argv:", fullCmd);
1060
+ if (this.launchConfig) {
1061
+ logger.debug("[CodexMCP] Launch config (-c flags):", this.launchConfig);
1062
+ }
1063
+ } catch {
1064
+ }
1065
+ this.transport = new StdioClientTransport({
1066
+ command: codexEntrypoint === "codex" ? "codex" : process.execPath,
1067
+ args: codexEntrypoint === "codex" ? baseArgs : [codexEntrypoint, ...baseArgs],
1068
+ env: Object.keys(process.env).reduce((acc, key) => {
1069
+ const value = process.env[key];
1070
+ if (typeof value === "string") acc[key] = value;
1071
+ return acc;
1072
+ }, {})
1073
+ });
1074
+ this.registerPermissionHandlers();
1075
+ await this.client.connect(this.transport);
1076
+ this.connected = true;
1077
+ logger.debug("[CodexMCP] Connected to Codex");
1078
+ }
1079
+ registerPermissionHandlers() {
1080
+ this.client.setRequestHandler(
1081
+ ElicitRequestSchema,
1082
+ async (request) => {
1083
+ console.log("[CodexMCP] Received elicitation request:", request.params);
1084
+ const params = request.params;
1085
+ const toolName = "CodexBash";
1086
+ if (!this.permissionHandler) {
1087
+ logger.debug("[CodexMCP] No permission handler set, denying by default");
1088
+ return {
1089
+ decision: "denied"
1090
+ };
1091
+ }
1092
+ try {
1093
+ const result = await this.permissionHandler.handleToolCall(
1094
+ params.codex_call_id,
1095
+ toolName,
1096
+ {
1097
+ command: params.codex_command,
1098
+ cwd: params.codex_cwd
1099
+ }
1100
+ );
1101
+ logger.debug("[CodexMCP] Permission result:", result);
1102
+ return {
1103
+ decision: result.decision
1104
+ };
1105
+ } catch (error) {
1106
+ logger.debug("[CodexMCP] Error handling permission request:", error);
1107
+ return {
1108
+ decision: "denied",
1109
+ reason: error instanceof Error ? error.message : "Permission request failed"
1110
+ };
1111
+ }
1112
+ }
1113
+ );
1114
+ logger.debug("[CodexMCP] Permission handlers registered");
1115
+ }
1116
+ async startSession(config, options) {
1117
+ if (!this.connected) await this.connect();
1118
+ logger.debug("[CodexMCP] Starting Codex session:", config);
1119
+ const response = await this.client.callTool({
1120
+ name: "codex",
1121
+ arguments: config
1122
+ }, void 0, {
1123
+ signal: options?.signal,
1124
+ timeout: DEFAULT_TIMEOUT
1125
+ // maxTotalTimeout: 10000000000
1126
+ });
1127
+ logger.debug("[CodexMCP] startSession response:", response);
1128
+ this.extractIdentifiers(response);
1129
+ return response;
1130
+ }
1131
+ async continueSession(prompt, options) {
1132
+ if (!this.connected) await this.connect();
1133
+ if (!this.sessionId) {
1134
+ throw new Error("No active session. Call startSession first.");
1135
+ }
1136
+ if (!this.conversationId) {
1137
+ this.conversationId = this.sessionId;
1138
+ logger.debug("[CodexMCP] conversationId missing, defaulting to sessionId:", this.conversationId);
1139
+ }
1140
+ const args = { sessionId: this.sessionId, conversationId: this.conversationId, prompt };
1141
+ logger.debug("[CodexMCP] Continuing Codex session:", args);
1142
+ const response = await this.client.callTool({
1143
+ name: "codex-reply",
1144
+ arguments: args
1145
+ }, void 0, {
1146
+ signal: options?.signal,
1147
+ timeout: DEFAULT_TIMEOUT
1148
+ });
1149
+ logger.debug("[CodexMCP] continueSession response:", response);
1150
+ this.extractIdentifiers(response);
1151
+ return response;
1152
+ }
1153
+ updateIdentifiersFromEvent(event) {
1154
+ if (!event || typeof event !== "object") {
1155
+ return;
1156
+ }
1157
+ const candidates = [event];
1158
+ if (event.data && typeof event.data === "object") {
1159
+ candidates.push(event.data);
1160
+ }
1161
+ for (const candidate of candidates) {
1162
+ const sessionId = candidate.session_id ?? candidate.sessionId;
1163
+ if (sessionId) {
1164
+ this.sessionId = sessionId;
1165
+ logger.debug("[CodexMCP] Session ID extracted from event:", this.sessionId);
1166
+ }
1167
+ const conversationId = candidate.conversation_id ?? candidate.conversationId;
1168
+ if (conversationId) {
1169
+ this.conversationId = conversationId;
1170
+ logger.debug("[CodexMCP] Conversation ID extracted from event:", this.conversationId);
1171
+ }
1172
+ }
1173
+ }
1174
+ extractIdentifiers(response) {
1175
+ const meta = response?.meta || {};
1176
+ if (meta.sessionId) {
1177
+ this.sessionId = meta.sessionId;
1178
+ logger.debug("[CodexMCP] Session ID extracted:", this.sessionId);
1179
+ } else if (response?.sessionId) {
1180
+ this.sessionId = response.sessionId;
1181
+ logger.debug("[CodexMCP] Session ID extracted:", this.sessionId);
1182
+ }
1183
+ if (meta.conversationId) {
1184
+ this.conversationId = meta.conversationId;
1185
+ logger.debug("[CodexMCP] Conversation ID extracted:", this.conversationId);
1186
+ } else if (response?.conversationId) {
1187
+ this.conversationId = response.conversationId;
1188
+ logger.debug("[CodexMCP] Conversation ID extracted:", this.conversationId);
1189
+ }
1190
+ const content = response?.content;
1191
+ if (Array.isArray(content)) {
1192
+ for (const item of content) {
1193
+ if (!this.sessionId && item?.sessionId) {
1194
+ this.sessionId = item.sessionId;
1195
+ logger.debug("[CodexMCP] Session ID extracted from content:", this.sessionId);
1196
+ }
1197
+ if (!this.conversationId && item && typeof item === "object" && "conversationId" in item && item.conversationId) {
1198
+ this.conversationId = item.conversationId;
1199
+ logger.debug("[CodexMCP] Conversation ID extracted from content:", this.conversationId);
1200
+ }
1201
+ }
1202
+ }
1203
+ }
1204
+ getSessionId() {
1205
+ return this.sessionId;
1206
+ }
1207
+ hasActiveSession() {
1208
+ return this.sessionId !== null;
1209
+ }
1210
+ clearSession() {
1211
+ const previousSessionId = this.sessionId;
1212
+ this.sessionId = null;
1213
+ this.conversationId = null;
1214
+ logger.debug("[CodexMCP] Session cleared, previous sessionId:", previousSessionId);
1215
+ }
1216
+ /**
1217
+ * Store the current session ID without clearing it, useful for abort handling
1218
+ */
1219
+ storeSessionForResume() {
1220
+ logger.debug("[CodexMCP] Storing session for potential resume:", this.sessionId);
1221
+ return this.sessionId;
1222
+ }
1223
+ async disconnect() {
1224
+ if (!this.connected) return;
1225
+ const pid = this.transport?.pid ?? null;
1226
+ logger.debug(`[CodexMCP] Disconnecting; child pid=${pid ?? "none"}`);
1227
+ try {
1228
+ logger.debug("[CodexMCP] client.close begin");
1229
+ await this.client.close();
1230
+ logger.debug("[CodexMCP] client.close done");
1231
+ } catch (e) {
1232
+ logger.debug("[CodexMCP] Error closing client, attempting transport close directly", e);
1233
+ try {
1234
+ logger.debug("[CodexMCP] transport.close begin");
1235
+ await this.transport?.close?.();
1236
+ logger.debug("[CodexMCP] transport.close done");
1237
+ } catch {
1238
+ }
1239
+ }
1240
+ if (pid) {
1241
+ try {
1242
+ process.kill(pid, 0);
1243
+ logger.debug("[CodexMCP] Child still alive, sending SIGKILL");
1244
+ try {
1245
+ process.kill(pid, "SIGKILL");
1246
+ } catch {
1247
+ }
1248
+ } catch {
1249
+ }
1250
+ }
1251
+ this.transport = null;
1252
+ this.connected = false;
1253
+ this.sessionId = null;
1254
+ this.conversationId = null;
1255
+ logger.debug("[CodexMCP] Disconnected");
1256
+ }
1257
+ }
1258
+
1259
+ async function codexRemote(opts) {
1260
+ const mcp = new CodexMcpClient();
1261
+ mcp.setPermissionHandler(opts.permissionHandler);
1262
+ let thinking = false;
1263
+ const updateThinking = (newThinking) => {
1264
+ if (thinking !== newThinking) {
1265
+ thinking = newThinking;
1266
+ logger.debug(`[codexRemote] Thinking state changed to: ${thinking}`);
1267
+ if (opts.onThinkingChange) {
1268
+ opts.onThinkingChange(thinking);
1269
+ }
1270
+ }
1271
+ };
1272
+ mcp.setHandler((event) => {
1273
+ try {
1274
+ const t = event && typeof event === "object" ? event.type : void 0;
1275
+ const isDelta = t === "agent_message_delta" || t === "agent_reasoning_delta" || t === "agent_reasoning_section_break";
1276
+ if (!isDelta) {
1277
+ logger.debugLargeJson("[codexRemote] MCP Event:", event);
1278
+ }
1279
+ } catch {
1280
+ logger.debugLargeJson("[codexRemote] MCP Event:", event);
1281
+ }
1282
+ opts.onMessage(event);
1283
+ if (event.type === "task_started" || event.type === "task_progress") {
1284
+ updateThinking(true);
1285
+ } else if (event.type === "task_complete" || event.type === "turn_aborted") {
1286
+ updateThinking(false);
1287
+ }
1288
+ });
1289
+ try {
1290
+ try {
1291
+ const launchConfig = {};
1292
+ const resumeHint = (opts.conversationId || opts.sessionId) ?? null;
1293
+ if (resumeHint) {
1294
+ launchConfig["resume"] = resumeHint;
1295
+ launchConfig["experimental_resume"] = resumeHint;
1296
+ launchConfig["session_id"] = resumeHint;
1297
+ launchConfig["conversationId"] = resumeHint;
1298
+ launchConfig["conversation_id"] = resumeHint;
1299
+ launchConfig["thread_id"] = resumeHint;
1300
+ launchConfig["thread"] = resumeHint;
1301
+ launchConfig["resume_session"] = resumeHint;
1302
+ launchConfig["resume_session_id"] = resumeHint;
1303
+ launchConfig["resume_conversation"] = resumeHint;
1304
+ launchConfig["resume_thread"] = resumeHint;
1305
+ launchConfig["resume_thread_id"] = resumeHint;
1306
+ }
1307
+ if (Object.keys(launchConfig).length > 0) {
1308
+ logger.debug("[codexRemote] Launch config for MCP (-c flags):", launchConfig);
1309
+ mcp.setLaunchConfig(launchConfig);
1310
+ }
1311
+ } catch {
1312
+ }
1313
+ await mcp.connect();
1314
+ const initial = await opts.nextMessage();
1315
+ if (!initial) {
1316
+ logger.debug("[codexRemote] No initial message, exiting");
1317
+ return;
1318
+ }
1319
+ const specialCommand = parseSpecialCommand(initial.message);
1320
+ if (specialCommand.type === "clear") {
1321
+ logger.debug("[codexRemote] /clear command detected");
1322
+ mcp.clearSession();
1323
+ if (opts.onCompletionEvent) {
1324
+ opts.onCompletionEvent("Context was reset");
1325
+ }
1326
+ if (opts.onSessionReset) {
1327
+ opts.onSessionReset();
1328
+ }
1329
+ return;
1330
+ }
1331
+ let isCompactCommand = false;
1332
+ if (specialCommand.type === "compact") {
1333
+ logger.debug("[codexRemote] /compact command detected");
1334
+ isCompactCommand = true;
1335
+ if (opts.onCompletionEvent) {
1336
+ opts.onCompletionEvent("Compaction started");
1337
+ }
1338
+ }
1339
+ const config = {
1340
+ prompt: initial.message,
1341
+ cwd: opts.path
1342
+ };
1343
+ if (initial.mode.permissionMode === "read-only") {
1344
+ config.sandbox = "read-only";
1345
+ } else if (initial.mode.permissionMode === "safe-yolo") {
1346
+ config.sandbox = "workspace-write";
1347
+ } else if (initial.mode.permissionMode === "yolo") {
1348
+ config.sandbox = "danger-full-access";
1349
+ } else {
1350
+ config.sandbox = "workspace-write";
1351
+ }
1352
+ if (initial.mode.model) {
1353
+ config.model = initial.mode.model;
1354
+ }
1355
+ if (opts.sessionId || opts.conversationId || opts.mcpServers) {
1356
+ config.config = {};
1357
+ const resumeHint = (opts.conversationId || opts.sessionId) ?? null;
1358
+ if (resumeHint) {
1359
+ config.resume = resumeHint;
1360
+ config.experimental_resume = resumeHint;
1361
+ config.session_id = resumeHint;
1362
+ config.conversationId = resumeHint;
1363
+ config.conversation_id = resumeHint;
1364
+ config.config.resume = resumeHint;
1365
+ config.config.experimental_resume = resumeHint;
1366
+ config.config.conversationId = resumeHint;
1367
+ config.config.conversation_id = resumeHint;
1368
+ config.thread_id = resumeHint;
1369
+ config.threadId = resumeHint;
1370
+ config.config.thread_id = resumeHint;
1371
+ config.config.threadId = resumeHint;
1372
+ logger.debug("[codexRemote] Attempting resume with hint:", resumeHint);
1373
+ }
1374
+ if (opts.mcpServers) {
1375
+ config.config.mcp_servers = opts.mcpServers;
1376
+ }
1377
+ }
1378
+ updateThinking(true);
1379
+ logger.debug("[codexRemote] Starting MCP session");
1380
+ await mcp.startSession(config, { signal: opts.signal });
1381
+ const sessionId = mcp.getSessionId();
1382
+ if (sessionId) {
1383
+ logger.debug("[codexRemote] Session ID extracted:", sessionId);
1384
+ opts.onSessionFound(sessionId);
1385
+ }
1386
+ if (isCompactCommand) {
1387
+ logger.debug("[codexRemote] Compaction completed");
1388
+ if (opts.onCompletionEvent) {
1389
+ opts.onCompletionEvent("Compaction completed");
1390
+ }
1391
+ }
1392
+ updateThinking(false);
1393
+ opts.onReady();
1394
+ while (true) {
1395
+ const next = await opts.nextMessage();
1396
+ if (!next) {
1397
+ logger.debug("[codexRemote] No next message, exiting");
1398
+ break;
1399
+ }
1400
+ const nextSpecialCommand = parseSpecialCommand(next.message);
1401
+ if (nextSpecialCommand.type === "clear") {
1402
+ logger.debug("[codexRemote] /clear command in continuation");
1403
+ mcp.clearSession();
1404
+ if (opts.onCompletionEvent) {
1405
+ opts.onCompletionEvent("Context was reset");
1406
+ }
1407
+ if (opts.onSessionReset) {
1408
+ opts.onSessionReset();
1409
+ }
1410
+ break;
1411
+ }
1412
+ updateThinking(true);
1413
+ logger.debug("[codexRemote] Continuing MCP session");
1414
+ await mcp.continueSession(next.message, { signal: opts.signal });
1415
+ updateThinking(false);
1416
+ opts.onReady();
1417
+ }
1418
+ } catch (error) {
1419
+ if (error?.name === "AbortError" || opts.signal?.aborted) {
1420
+ logger.debug("[codexRemote] Aborted");
1421
+ } else {
1422
+ logger.debug("[codexRemote] Error in codexRemote:", error);
1423
+ throw error;
1424
+ }
1425
+ } finally {
1426
+ updateThinking(false);
1427
+ try {
1428
+ await mcp.disconnect();
1429
+ } catch (error) {
1430
+ logger.debug("[codexRemote] Error disconnecting from MCP:", error);
1431
+ }
1432
+ }
1433
+ }
1434
+
1435
+ class CodexPermissionHandler {
1436
+ pendingRequests = /* @__PURE__ */ new Map();
1437
+ session;
1438
+ constructor(session) {
1439
+ this.session = session;
1440
+ this.setupRpcHandler();
1441
+ }
1442
+ /**
1443
+ * Handle a tool permission request
1444
+ * @param toolCallId - The unique ID of the tool call
1445
+ * @param toolName - The name of the tool being called
1446
+ * @param input - The input parameters for the tool
1447
+ * @returns Promise resolving to permission result
1448
+ */
1449
+ async handleToolCall(toolCallId, toolName, input) {
1450
+ return new Promise((resolve, reject) => {
1451
+ this.pendingRequests.set(toolCallId, {
1452
+ resolve,
1453
+ reject,
1454
+ toolName,
1455
+ input
1456
+ });
1457
+ this.session.updateAgentState((currentState) => ({
1458
+ ...currentState,
1459
+ requests: {
1460
+ ...currentState.requests,
1461
+ [toolCallId]: {
1462
+ tool: toolName,
1463
+ arguments: input,
1464
+ createdAt: Date.now()
1465
+ }
1466
+ }
1467
+ }));
1468
+ logger.debug(`[Codex] Permission request sent for tool: ${toolName} (${toolCallId})`);
1469
+ });
1470
+ }
1471
+ /**
1472
+ * Setup RPC handler for permission responses
1473
+ */
1474
+ setupRpcHandler() {
1475
+ this.session.rpcHandlerManager.registerHandler(
1476
+ "permission",
1477
+ async (response) => {
1478
+ const pending = this.pendingRequests.get(response.id);
1479
+ if (!pending) {
1480
+ logger.debug("[Codex] Permission request not found or already resolved");
1481
+ return;
1482
+ }
1483
+ this.pendingRequests.delete(response.id);
1484
+ const result = response.approved ? { decision: response.decision === "approved_for_session" ? "approved_for_session" : "approved" } : { decision: response.decision === "denied" ? "denied" : "abort" };
1485
+ pending.resolve(result);
1486
+ this.session.updateAgentState((currentState) => {
1487
+ const request = currentState.requests?.[response.id];
1488
+ if (!request) return currentState;
1489
+ const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
1490
+ let res = {
1491
+ ...currentState,
1492
+ requests: remainingRequests,
1493
+ completedRequests: {
1494
+ ...currentState.completedRequests,
1495
+ [response.id]: {
1496
+ ...request,
1497
+ completedAt: Date.now(),
1498
+ status: response.approved ? "approved" : "denied",
1499
+ decision: result.decision
1500
+ }
1501
+ }
1502
+ };
1503
+ return res;
1504
+ });
1505
+ logger.debug(`[Codex] Permission ${response.approved ? "approved" : "denied"} for ${pending.toolName}`);
1506
+ }
1507
+ );
1508
+ }
1509
+ /**
1510
+ * Reset state for new sessions
1511
+ */
1512
+ reset() {
1513
+ for (const [id, pending] of this.pendingRequests.entries()) {
1514
+ pending.reject(new Error("Session reset"));
1515
+ }
1516
+ this.pendingRequests.clear();
1517
+ this.session.updateAgentState((currentState) => {
1518
+ const pendingRequests = currentState.requests || {};
1519
+ const completedRequests = { ...currentState.completedRequests };
1520
+ for (const [id, request] of Object.entries(pendingRequests)) {
1521
+ completedRequests[id] = {
1522
+ ...request,
1523
+ completedAt: Date.now(),
1524
+ status: "canceled",
1525
+ reason: "Session reset"
1526
+ };
1527
+ }
1528
+ return {
1529
+ ...currentState,
1530
+ requests: {},
1531
+ completedRequests
1532
+ };
1533
+ });
1534
+ logger.debug("[Codex] Permission handler reset");
1535
+ }
1536
+ }
1537
+
1538
+ function formatCodexEventForInk(event, messageBuffer) {
1539
+ try {
1540
+ if (!event || typeof event !== "object") return;
1541
+ switch (event.type) {
1542
+ case "session_configured": {
1543
+ messageBuffer.addMessage("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "status");
1544
+ if (event.session_id) messageBuffer.addMessage(`Session: ${event.session_id}`, "system");
1545
+ if (event.model) messageBuffer.addMessage(`Model: ${event.model}`, "status");
1546
+ if (event.rollout_path) messageBuffer.addMessage(`Session log: ${event.rollout_path}`, "status");
1547
+ messageBuffer.addMessage("\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500", "status");
1548
+ break;
1549
+ }
1550
+ case "task_started": {
1551
+ messageBuffer.addMessage("Thinking\u2026", "status");
1552
+ break;
1553
+ }
1554
+ case "task_complete": {
1555
+ messageBuffer.addMessage("Done", "status");
1556
+ break;
1557
+ }
1558
+ case "message": {
1559
+ const role = event.role || "system";
1560
+ const blocks = Array.isArray(event.content) ? event.content : [];
1561
+ for (const b of blocks) {
1562
+ if (b && typeof b === "object") {
1563
+ if (b.type === "input_text" || b.type === "text") {
1564
+ const text = b.text ?? "";
1565
+ if (role === "user") {
1566
+ messageBuffer.addMessage(`User: ${text}`, "user");
1567
+ } else if (role === "assistant") {
1568
+ messageBuffer.addMessage(text, "assistant");
1569
+ }
1570
+ } else if (b.type === "output_text") {
1571
+ const text = b.text ?? "";
1572
+ const last = messageBuffer.peekLast();
1573
+ if (!(last && last.type === "assistant" && last.content === text)) {
1574
+ messageBuffer.addMessage(text, "assistant");
1575
+ }
1576
+ }
1577
+ }
1578
+ }
1579
+ break;
1580
+ }
1581
+ case "agent_message_delta": {
1582
+ const delta = event.delta ?? "";
1583
+ if (typeof delta === "string" && delta) {
1584
+ messageBuffer.appendToLast(delta, "assistant");
1585
+ }
1586
+ break;
1587
+ }
1588
+ case "agent_message": {
1589
+ break;
1590
+ }
1591
+ case "agent_reasoning_delta": {
1592
+ if (event.delta) {
1593
+ const text = String(event.delta);
1594
+ const last = messageBuffer.peekLast();
1595
+ if (last && last.type === "status" && /^Tokens\s+—/.test(last.content)) {
1596
+ messageBuffer.addMessage(text, "status");
1597
+ } else {
1598
+ messageBuffer.appendToLast(text, "status");
1599
+ }
1600
+ }
1601
+ break;
1602
+ }
1603
+ case "agent_reasoning": {
1604
+ if (event.text) messageBuffer.addMessage(String(event.text), "status");
1605
+ break;
1606
+ }
1607
+ case "token_count": {
1608
+ try {
1609
+ const input = event.info?.last_token_usage?.input_tokens ?? event.info?.total_token_usage?.input_tokens;
1610
+ const output = event.info?.last_token_usage?.output_tokens ?? event.info?.total_token_usage?.output_tokens;
1611
+ if (typeof input === "number" || typeof output === "number") {
1612
+ messageBuffer.addMessage(`Tokens \u2014 in: ${input ?? 0}, out: ${output ?? 0}`, "status");
1613
+ }
1614
+ } catch {
1615
+ }
1616
+ break;
1617
+ }
1618
+ default: {
1619
+ if (process.env.DEBUG) {
1620
+ messageBuffer.addMessage(`[${event.type}]`, "status");
1621
+ }
1622
+ }
1623
+ }
1624
+ } catch (e) {
1625
+ logger.debug("[CODEX INK] Failed to format event:", e);
1626
+ }
1627
+ }
1628
+
1629
+ async function codexRemoteLauncher(session) {
1630
+ let exitReason = null;
1631
+ const abortController = new AbortController();
1632
+ let uiStreaming = true;
1633
+ const permissionHandler = new CodexPermissionHandler(session.client);
1634
+ const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
1635
+ let inkInstance = null;
1636
+ let messageBuffer = null;
1637
+ if (hasTTY) {
1638
+ try {
1639
+ console.clear();
1640
+ } catch {
1641
+ }
1642
+ messageBuffer = new MessageBuffer();
1643
+ inkInstance = render(React.createElement(RemoteModeDisplay, {
1644
+ messageBuffer,
1645
+ title: "Codex Messages",
1646
+ logPath: process.env.DEBUG ? session.logPath : void 0,
1647
+ onExit: async () => {
1648
+ logger.debug("[CodexRemote]: Exit requested via UI");
1649
+ if (!exitReason) exitReason = "exit";
1650
+ try {
1651
+ messageBuffer?.addMessage("Exiting\u2026", "status");
1652
+ } catch {
1653
+ }
1654
+ uiStreaming = false;
1655
+ abortController.abort();
1656
+ },
1657
+ onSwitchToLocal: async () => {
1658
+ logger.debug("[CodexRemote]: Switch to local requested via UI");
1659
+ if (!exitReason) exitReason = "switch";
1660
+ try {
1661
+ messageBuffer?.addMessage("Switching to local mode\u2026", "status");
1662
+ } catch {
1663
+ }
1664
+ uiStreaming = false;
1665
+ abortController.abort();
1666
+ }
1667
+ }), { exitOnCtrlC: false, patchConsole: false });
1668
+ process.stdin.resume();
1669
+ if (process.stdin.isTTY) process.stdin.setRawMode(true);
1670
+ process.stdin.setEncoding("utf8");
1671
+ }
1672
+ try {
1673
+ async function doSwitch() {
1674
+ logger.debug("[CodexRemote]: doSwitch - switching to local");
1675
+ if (!exitReason) {
1676
+ exitReason = "switch";
1677
+ }
1678
+ abortController.abort();
1679
+ }
1680
+ async function doExit() {
1681
+ logger.debug("[CodexRemote]: doExit");
1682
+ if (!exitReason) {
1683
+ exitReason = "exit";
1684
+ }
1685
+ abortController.abort();
1686
+ }
1687
+ session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
1688
+ logger.debug("[CodexRemote]: Starting remote Codex control via MCP");
1689
+ const remoteOptions = {
1690
+ path: session.path,
1691
+ sessionId: session.sessionId,
1692
+ conversationId: session.conversationId ?? null,
1693
+ mcpServers: session.mcpServers,
1694
+ signal: abortController.signal,
1695
+ permissionHandler,
1696
+ nextMessage: async () => {
1697
+ const batch = await session.queue.waitForMessagesAndGetAsString(abortController.signal);
1698
+ if (!batch) return null;
1699
+ return {
1700
+ message: batch.message,
1701
+ mode: batch.mode
1702
+ };
1703
+ },
1704
+ onSessionFound: (sessionId) => {
1705
+ session.onSessionFound(sessionId);
1706
+ },
1707
+ onReady: () => {
1708
+ session.client.sendSessionEvent({ type: "ready" });
1709
+ },
1710
+ onThinkingChange: (isThinking) => {
1711
+ session.onThinkingChange(isThinking);
1712
+ },
1713
+ onMessage: (message) => {
1714
+ if (messageBuffer && uiStreaming) {
1715
+ formatCodexEventForInk(message, messageBuffer);
1716
+ }
1717
+ let shouldForwardRaw = true;
1718
+ try {
1719
+ const t = message?.type;
1720
+ if (t === "agent_message_delta" || t === "agent_reasoning_delta" || t === "agent_reasoning_section_break") {
1721
+ shouldForwardRaw = false;
1722
+ }
1723
+ if (t === "message" && message?.role === "assistant" && Array.isArray(message?.content) && message.content.some((b) => b && typeof b === "object" && b.type === "output_text" && typeof b.text === "string")) {
1724
+ shouldForwardRaw = false;
1725
+ }
1726
+ } catch {
1727
+ }
1728
+ if (shouldForwardRaw) {
1729
+ session.client.sendCodexMessage(message);
1730
+ }
1731
+ try {
1732
+ const candidates = [];
1733
+ if (message && typeof message === "object") candidates.push(message);
1734
+ if (message?.data && typeof message.data === "object") candidates.push(message.data);
1735
+ for (const c of candidates) {
1736
+ const conv = c.conversation_id || c.conversationId;
1737
+ if (typeof conv === "string" && conv) {
1738
+ session.onConversationFound(conv);
1739
+ break;
1740
+ }
1741
+ }
1742
+ } catch {
1743
+ }
1744
+ try {
1745
+ if (message && typeof message === "object" && message.type === "message" && message.role === "assistant" && Array.isArray(message.content)) {
1746
+ for (const block of message.content) {
1747
+ if (block && typeof block === "object" && block.type === "output_text" && typeof block.text === "string") {
1748
+ const text = block.text;
1749
+ session.client.sendCodexMessage({
1750
+ type: "message",
1751
+ message: text,
1752
+ id: randomUUID()
1753
+ });
1754
+ try {
1755
+ session.client.sendClaudeSessionMessage({
1756
+ type: "assistant",
1757
+ uuid: randomUUID(),
1758
+ message: { content: [{ type: "text", text }] }
1759
+ });
1760
+ } catch {
1761
+ }
1762
+ }
1763
+ }
1764
+ }
1765
+ } catch {
1766
+ }
1767
+ },
1768
+ onCompletionEvent: (message) => {
1769
+ logger.debug(`[CodexRemote]: ${message}`);
1770
+ },
1771
+ onSessionReset: () => {
1772
+ permissionHandler.reset();
1773
+ }
1774
+ };
1775
+ await codexRemote(remoteOptions);
1776
+ } catch (error) {
1777
+ if (error?.name === "AbortError" || abortController.signal.aborted) {
1778
+ logger.debug("[CodexRemote]: Aborted");
1779
+ } else {
1780
+ logger.debug("[CodexRemote]: Error:", error);
1781
+ throw error;
1782
+ }
1783
+ } finally {
1784
+ session.client.rpcHandlerManager.registerHandler("switch", async () => {
1785
+ });
1786
+ permissionHandler.reset();
1787
+ try {
1788
+ inkInstance?.unmount?.();
1789
+ } catch {
1790
+ }
1791
+ try {
1792
+ inkInstance?.clear?.();
1793
+ } catch {
1794
+ }
1795
+ try {
1796
+ if (process.stdin.isTTY) process.stdin.setRawMode(false);
1797
+ } catch {
1798
+ }
1799
+ }
1800
+ return exitReason || "exit";
1801
+ }
1802
+
1803
+ async function codexLoop(opts) {
1804
+ const logPath = logger.logFilePath;
1805
+ let session = new CodexSession({
1806
+ api: opts.api,
1807
+ client: opts.session,
1808
+ path: opts.path,
1809
+ sessionId: null,
1810
+ mcpServers: opts.mcpServers,
1811
+ logPath,
1812
+ messageQueue: opts.messageQueue,
1813
+ onModeChange: opts.onModeChange
1814
+ });
1815
+ if (opts.onSessionReady) {
1816
+ opts.onSessionReady(session);
1817
+ }
1818
+ let mode = opts.startingMode ?? "local";
1819
+ while (true) {
1820
+ logger.debug(`[CodexLoop] Iteration with mode: ${mode}`);
1821
+ if (mode === "local") {
1822
+ let reason = await codexLocalLauncher(session);
1823
+ if (reason === "exit") {
1824
+ return;
1825
+ }
1826
+ mode = "remote";
1827
+ if (opts.onModeChange) {
1828
+ opts.onModeChange(mode);
1829
+ }
1830
+ continue;
1831
+ }
1832
+ if (mode === "remote") {
1833
+ let reason = await codexRemoteLauncher(session);
1834
+ if (reason === "exit") {
1835
+ return;
1836
+ }
1837
+ mode = "local";
1838
+ if (opts.onModeChange) {
1839
+ opts.onModeChange(mode);
1840
+ }
1841
+ continue;
1842
+ }
1843
+ }
1844
+ }
1845
+
1846
+ async function runCodex(credentials, options = {}) {
1847
+ const workingDirectory = process.cwd();
1848
+ const sessionTag = randomUUID();
1849
+ logger.debug("[START] Starting Codex process");
1850
+ logger.debug(`[START] Options: startedBy=${options.startedBy}, startingMode=${options.startingMode}`);
1851
+ if (options.startedBy === "daemon" && options.startingMode === "local") {
1852
+ logger.debug("Daemon spawn requested with local mode - forcing remote mode");
1853
+ options.startingMode = "remote";
1854
+ }
1855
+ const api = new ApiClient(credentials.token, credentials.secret);
1856
+ let state = {};
1857
+ const settings = await readSettings();
1858
+ let machineId = settings?.machineId;
1859
+ if (!machineId) {
1860
+ console.error(`[START] No machine ID found in settings. Please report this issue.`);
1861
+ process.exit(1);
1862
+ }
1863
+ logger.debug(`Using machineId: ${machineId}`);
1864
+ const machine = await api.getOrCreateMachine({
1865
+ machineId,
1866
+ metadata: initialMachineMetadata
1867
+ });
1868
+ const foregroundMachineClient = api.machineSyncClient(machine);
1869
+ foregroundMachineClient.connect();
1870
+ let metadata = {
1871
+ path: workingDirectory,
1872
+ host: os.hostname(),
1873
+ version: packageJson.version,
1874
+ os: os.platform(),
1875
+ machineId,
1876
+ homeDir: os.homedir(),
1877
+ happyHomeDir: configuration.happyHomeDir,
1878
+ happyLibDir: projectPath(),
1879
+ happyToolsDir: resolve(projectPath(), "tools", "unpacked"),
1880
+ startedFromDaemon: options.startedBy === "daemon",
1881
+ hostPid: process.pid,
1882
+ startedBy: options.startedBy || "terminal",
1883
+ lifecycleState: "running",
1884
+ lifecycleStateSince: Date.now(),
1885
+ flavor: "codex"
1886
+ };
1887
+ const response = await api.getOrCreateSession({ tag: sessionTag, metadata, state });
1888
+ logger.debug(`Session created: ${response.id}`);
1889
+ try {
1890
+ logger.debug(`[START] Reporting session ${response.id} to daemon`);
1891
+ const result = await notifyDaemonSessionStarted(response.id, metadata);
1892
+ if (result.error) {
1893
+ logger.debug(`[START] Failed to report to daemon:`, result.error);
1894
+ } else {
1895
+ logger.debug(`[START] Reported session ${response.id} to daemon`);
1896
+ }
1897
+ } catch (error) {
1898
+ logger.debug("[START] Failed to report to daemon:", error);
1899
+ }
1900
+ const session = api.sessionSyncClient(response);
1901
+ const chellServer = await startChellServer(session);
1902
+ logger.debug(`[START] Chell MCP server started at ${chellServer.url}`);
1903
+ const logPath = logger.logFilePath;
1904
+ logger.infoDeveloper(`Session: ${response.id}`);
1905
+ logger.infoDeveloper(`Logs: ${logPath}`);
1906
+ session.updateAgentState((currentState) => ({
1907
+ ...currentState,
1908
+ controlledByUser: options.startingMode !== "remote"
1909
+ }));
1910
+ const caffeinateStarted = startCaffeinate();
1911
+ if (caffeinateStarted) {
1912
+ logger.infoDeveloper("Sleep prevention enabled (macOS)");
1913
+ }
1914
+ const messageQueue = new MessageQueue2((mode) => hashObject({
1915
+ permissionMode: mode.permissionMode,
1916
+ model: mode.model
1917
+ }));
1918
+ let currentPermissionMode = void 0;
1919
+ let currentModel = void 0;
1920
+ session.onUserMessage((message) => {
1921
+ let messagePermissionMode = currentPermissionMode;
1922
+ if (message.meta?.permissionMode) {
1923
+ const validModes = ["default", "read-only", "safe-yolo", "yolo"];
1924
+ if (validModes.includes(message.meta.permissionMode)) {
1925
+ messagePermissionMode = message.meta.permissionMode;
1926
+ currentPermissionMode = messagePermissionMode;
1927
+ logger.debug(`[Codex] Permission mode updated: ${currentPermissionMode}`);
1928
+ }
1929
+ }
1930
+ let messageModel = currentModel;
1931
+ if (message.meta?.hasOwnProperty("model")) {
1932
+ messageModel = message.meta.model || void 0;
1933
+ currentModel = messageModel;
1934
+ logger.debug(`[Codex] Model updated: ${messageModel || "default"}`);
1935
+ }
1936
+ const enhancedMode = {
1937
+ permissionMode: messagePermissionMode || "default",
1938
+ model: messageModel
1939
+ };
1940
+ messageQueue.push(message.content.text, enhancedMode);
1941
+ logger.debugLargeJson("User message pushed to queue:", message);
1942
+ });
1943
+ const cleanup = async () => {
1944
+ logger.debug("[START] Received termination signal, cleaning up...");
1945
+ try {
1946
+ if (session) {
1947
+ session.updateMetadata((currentMetadata) => ({
1948
+ ...currentMetadata,
1949
+ lifecycleState: "archived",
1950
+ lifecycleStateSince: Date.now(),
1951
+ archivedBy: "cli",
1952
+ archiveReason: "User terminated"
1953
+ }));
1954
+ session.sendSessionDeath();
1955
+ await session.flush();
1956
+ await session.close();
1957
+ }
1958
+ stopCaffeinate();
1959
+ chellServer.stop();
1960
+ try {
1961
+ foregroundMachineClient.shutdown();
1962
+ } catch {
1963
+ }
1964
+ logger.debug("[START] Cleanup complete, exiting");
1965
+ process.exit(0);
1966
+ } catch (error) {
1967
+ logger.debug("[START] Error during cleanup:", error);
1968
+ process.exit(1);
1969
+ }
1970
+ };
1971
+ process.on("SIGTERM", cleanup);
1972
+ process.on("SIGINT", cleanup);
1973
+ process.on("uncaughtException", (error) => {
1974
+ logger.debug("[START] Uncaught exception:", error);
1975
+ cleanup();
1976
+ });
1977
+ process.on("unhandledRejection", (reason) => {
1978
+ logger.debug("[START] Unhandled rejection:", reason);
1979
+ cleanup();
1980
+ });
1981
+ registerKillSessionHandler(session.rpcHandlerManager, cleanup);
1982
+ const mcpServers = {
1983
+ chell: {
1984
+ command: "node",
1985
+ args: [resolve(projectPath(), "bin", "chell-mcp.mjs"), "--url", chellServer.url]
1986
+ }
1987
+ };
1988
+ await codexLoop({
1989
+ path: workingDirectory,
1990
+ startingMode: options.startingMode,
1991
+ messageQueue,
1992
+ api,
1993
+ onModeChange: (newMode) => {
1994
+ session.sendSessionEvent({ type: "switch", mode: newMode });
1995
+ session.updateAgentState((currentState) => ({
1996
+ ...currentState,
1997
+ controlledByUser: newMode === "local"
1998
+ }));
1999
+ },
2000
+ onSessionReady: (_sessionInstance) => {
2001
+ },
2002
+ mcpServers,
2003
+ session
2004
+ });
2005
+ session.sendSessionDeath();
2006
+ logger.debug("Waiting for socket to flush...");
2007
+ await session.flush();
2008
+ logger.debug("Closing session...");
2009
+ await session.close();
2010
+ stopCaffeinate();
2011
+ logger.debug("Stopped sleep prevention");
2012
+ chellServer.stop();
2013
+ logger.debug("Stopped Chell MCP server");
2014
+ process.exit(0);
2015
+ }
2016
+
2017
+ export { runCodex };