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