happy-imou-cloud 1.1.7 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{setupOfflineReconnection-ndObLZk0.mjs → BaseReasoningProcessor-BKLRCKTU.mjs} +133 -90
- package/dist/{setupOfflineReconnection-obypStdD.cjs → BaseReasoningProcessor-BRCQXCZY.cjs} +134 -90
- package/dist/{types-BXyraW9R.mjs → api-BGXYX0yH.mjs} +198 -170
- package/dist/{types-BSTmyv9d.cjs → api-D7OK-mML.cjs} +219 -192
- package/dist/command-CnLtKtP-.mjs +51 -0
- package/dist/command-G85giEAF.cjs +54 -0
- package/dist/future-Dq4Ha1Dn.cjs +24 -0
- package/dist/future-xRdLl3vf.mjs +22 -0
- package/dist/{index-DVI4b0mv.cjs → index-B_wlQBy2.cjs} +5493 -7142
- package/dist/{index-CUmYqKWt.mjs → index-C7Y0R-MI.mjs} +5482 -7143
- package/dist/index.cjs +19 -21
- package/dist/index.mjs +19 -21
- package/dist/lib.cjs +3 -2
- package/dist/lib.d.cts +17 -0
- package/dist/lib.d.mts +17 -0
- package/dist/lib.mjs +2 -1
- package/dist/{persistence-BGsuPqaO.mjs → persistence-BA_unuca.mjs} +8 -4
- package/dist/{persistence-BRH9F6RS.cjs → persistence-DHgf1CTG.cjs} +10 -6
- package/dist/registerKillSessionHandler-C2-yHm1V.mjs +428 -0
- package/dist/registerKillSessionHandler-CLREXN11.cjs +433 -0
- package/dist/runClaude-CwAitpX-.cjs +3274 -0
- package/dist/runClaude-uNC5Eym4.mjs +3271 -0
- package/dist/runCodex-B-05E-YZ.mjs +1846 -0
- package/dist/runCodex-Cm0VTqw_.cjs +1848 -0
- package/dist/{runGemini-C3dDtGOV.cjs → runGemini-CLWjwDYS.cjs} +25 -1366
- package/dist/{runGemini-B-EK_BJQ.mjs → runGemini-_biXvQAH.mjs} +12 -1353
- package/dist/types-CiliQpqS.mjs +52 -0
- package/dist/types-DVk3crez.cjs +54 -0
- package/package.json +13 -12
- package/scripts/devtools/README.md +9 -0
- package/scripts/devtools/generate-mock-credentials.ts +94 -0
- package/scripts/release-smoke.mjs +62 -0
- package/dist/config-BQNrtwRY.cjs +0 -183
- package/dist/config-Dn99YH37.mjs +0 -173
- package/dist/runCodex-Cez8cuIh.cjs +0 -1143
- package/dist/runCodex-X0BfjcZH.mjs +0 -1140
|
@@ -0,0 +1,1848 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var node_crypto = require('node:crypto');
|
|
4
|
+
var api = require('./api-D7OK-mML.cjs');
|
|
5
|
+
var persistence = require('./persistence-DHgf1CTG.cjs');
|
|
6
|
+
var index = require('./index-B_wlQBy2.cjs');
|
|
7
|
+
var BaseReasoningProcessor = require('./BaseReasoningProcessor-BRCQXCZY.cjs');
|
|
8
|
+
var registerKillSessionHandler = require('./registerKillSessionHandler-CLREXN11.cjs');
|
|
9
|
+
var future = require('./future-Dq4Ha1Dn.cjs');
|
|
10
|
+
var node_child_process = require('node:child_process');
|
|
11
|
+
var fs = require('node:fs');
|
|
12
|
+
var os = require('node:os');
|
|
13
|
+
var path = require('node:path');
|
|
14
|
+
var React = require('react');
|
|
15
|
+
var ink = require('ink');
|
|
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
|
+
require('axios');
|
|
22
|
+
require('chalk');
|
|
23
|
+
require('fs');
|
|
24
|
+
require('node:events');
|
|
25
|
+
require('socket.io-client');
|
|
26
|
+
require('tweetnacl');
|
|
27
|
+
require('util');
|
|
28
|
+
require('fs/promises');
|
|
29
|
+
require('crypto');
|
|
30
|
+
require('path');
|
|
31
|
+
require('expo-server-sdk');
|
|
32
|
+
require('node:fs/promises');
|
|
33
|
+
require('os');
|
|
34
|
+
require('tmp');
|
|
35
|
+
require('qrcode-terminal');
|
|
36
|
+
require('node:module');
|
|
37
|
+
require('open');
|
|
38
|
+
require('url');
|
|
39
|
+
require('ps-list');
|
|
40
|
+
require('cross-spawn');
|
|
41
|
+
require('fastify');
|
|
42
|
+
require('fastify-type-provider-zod');
|
|
43
|
+
require('node:readline');
|
|
44
|
+
require('http');
|
|
45
|
+
require('@agentclientprotocol/sdk');
|
|
46
|
+
require('./types-DVk3crez.cjs');
|
|
47
|
+
|
|
48
|
+
const DEFAULT_CAPABILITIES = {
|
|
49
|
+
committedMessageAcks: false,
|
|
50
|
+
pendingQueueV2: false,
|
|
51
|
+
userScopedSocket: false,
|
|
52
|
+
snapshotChangeCatchup: false,
|
|
53
|
+
structuredSpawnErrors: false,
|
|
54
|
+
metadataWatch: false
|
|
55
|
+
};
|
|
56
|
+
function normalizeHappyUserMessage(message) {
|
|
57
|
+
return {
|
|
58
|
+
text: message.content.text,
|
|
59
|
+
permissionMode: message.meta?.permissionMode,
|
|
60
|
+
model: message.meta?.model ?? void 0,
|
|
61
|
+
localId: message.localKey ?? null,
|
|
62
|
+
raw: message
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function createLegacyHappyCodexSessionAdapter(options) {
|
|
66
|
+
let metadataSnapshot = options.initialMetadata ?? options.session.getMetadataSnapshot?.() ?? null;
|
|
67
|
+
let agentStateSnapshot = options.initialAgentState ?? options.session.getAgentStateSnapshot?.() ?? null;
|
|
68
|
+
return {
|
|
69
|
+
sessionId: options.session.sessionId,
|
|
70
|
+
capabilities: DEFAULT_CAPABILITIES,
|
|
71
|
+
onUserMessage(callback) {
|
|
72
|
+
options.session.onUserMessage((message) => {
|
|
73
|
+
callback(normalizeHappyUserMessage(message));
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
sendCodexMessage(body) {
|
|
77
|
+
options.session.sendCodexMessage(body);
|
|
78
|
+
},
|
|
79
|
+
sendAgentMessage(body) {
|
|
80
|
+
options.session.sendAgentMessage("codex", body);
|
|
81
|
+
},
|
|
82
|
+
sendSessionEvent(event, id) {
|
|
83
|
+
options.session.sendSessionEvent(event, id);
|
|
84
|
+
},
|
|
85
|
+
keepAlive(thinking, mode) {
|
|
86
|
+
options.session.keepAlive(thinking, mode);
|
|
87
|
+
},
|
|
88
|
+
sendSessionDeath() {
|
|
89
|
+
options.session.sendSessionDeath();
|
|
90
|
+
},
|
|
91
|
+
updateMetadata(handler) {
|
|
92
|
+
options.session.updateMetadata((current) => {
|
|
93
|
+
const next = handler(current);
|
|
94
|
+
metadataSnapshot = next;
|
|
95
|
+
return next;
|
|
96
|
+
});
|
|
97
|
+
},
|
|
98
|
+
updateAgentState(handler) {
|
|
99
|
+
options.session.updateAgentState((current) => {
|
|
100
|
+
const next = handler(current);
|
|
101
|
+
agentStateSnapshot = next;
|
|
102
|
+
return next;
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
getMetadataSnapshot() {
|
|
106
|
+
return metadataSnapshot;
|
|
107
|
+
},
|
|
108
|
+
getAgentStateSnapshot() {
|
|
109
|
+
return agentStateSnapshot;
|
|
110
|
+
},
|
|
111
|
+
async waitForMetadataUpdate(signal) {
|
|
112
|
+
const metadata = await (options.session.waitForMetadataUpdate?.(signal) ?? Promise.resolve(metadataSnapshot));
|
|
113
|
+
metadataSnapshot = metadata;
|
|
114
|
+
return metadata;
|
|
115
|
+
},
|
|
116
|
+
async flush() {
|
|
117
|
+
await options.session.flush();
|
|
118
|
+
},
|
|
119
|
+
async close() {
|
|
120
|
+
await options.session.close();
|
|
121
|
+
}
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
class CodexSession {
|
|
126
|
+
path;
|
|
127
|
+
logPath;
|
|
128
|
+
api;
|
|
129
|
+
client;
|
|
130
|
+
runtimeSession;
|
|
131
|
+
queue;
|
|
132
|
+
codexArgs;
|
|
133
|
+
sessionId;
|
|
134
|
+
mode;
|
|
135
|
+
thinking = false;
|
|
136
|
+
keepAliveInterval;
|
|
137
|
+
onModeChangeCallback;
|
|
138
|
+
clientSwapCallbacks = [];
|
|
139
|
+
supersedePendingInteractionsCallback = null;
|
|
140
|
+
constructor(opts) {
|
|
141
|
+
this.path = opts.path;
|
|
142
|
+
this.api = opts.api;
|
|
143
|
+
this.client = opts.client;
|
|
144
|
+
this.runtimeSession = createLegacyHappyCodexSessionAdapter({
|
|
145
|
+
session: opts.client
|
|
146
|
+
});
|
|
147
|
+
this.logPath = opts.logPath;
|
|
148
|
+
this.sessionId = opts.sessionId;
|
|
149
|
+
this.mode = opts.mode;
|
|
150
|
+
this.queue = opts.messageQueue;
|
|
151
|
+
this.codexArgs = opts.codexArgs;
|
|
152
|
+
this.onModeChangeCallback = opts.onModeChange;
|
|
153
|
+
this.client.keepAlive(this.thinking, this.mode);
|
|
154
|
+
this.keepAliveInterval = setInterval(() => {
|
|
155
|
+
this.client.keepAlive(this.thinking, this.mode);
|
|
156
|
+
}, 2e3);
|
|
157
|
+
}
|
|
158
|
+
cleanup() {
|
|
159
|
+
clearInterval(this.keepAliveInterval);
|
|
160
|
+
api.logger.debug("[CodexSession] Cleaned up resources");
|
|
161
|
+
}
|
|
162
|
+
updateClient = (client) => {
|
|
163
|
+
this.client = client;
|
|
164
|
+
this.runtimeSession = createLegacyHappyCodexSessionAdapter({
|
|
165
|
+
session: client
|
|
166
|
+
});
|
|
167
|
+
for (const callback of this.clientSwapCallbacks) {
|
|
168
|
+
callback(client);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
addClientSwapCallback = (callback) => {
|
|
172
|
+
this.clientSwapCallbacks.push(callback);
|
|
173
|
+
};
|
|
174
|
+
removeClientSwapCallback = (callback) => {
|
|
175
|
+
const index = this.clientSwapCallbacks.indexOf(callback);
|
|
176
|
+
if (index !== -1) {
|
|
177
|
+
this.clientSwapCallbacks.splice(index, 1);
|
|
178
|
+
}
|
|
179
|
+
};
|
|
180
|
+
setPendingInteractionSuperseder = (callback) => {
|
|
181
|
+
this.supersedePendingInteractionsCallback = callback;
|
|
182
|
+
};
|
|
183
|
+
supersedePendingInteractions = (reason) => {
|
|
184
|
+
return this.supersedePendingInteractionsCallback?.(reason) ?? false;
|
|
185
|
+
};
|
|
186
|
+
onThinkingChange = (thinking) => {
|
|
187
|
+
this.thinking = thinking;
|
|
188
|
+
this.client.keepAlive(thinking, this.mode);
|
|
189
|
+
};
|
|
190
|
+
onModeChange = (mode) => {
|
|
191
|
+
this.mode = mode;
|
|
192
|
+
this.client.keepAlive(this.thinking, mode);
|
|
193
|
+
this.onModeChangeCallback(mode);
|
|
194
|
+
};
|
|
195
|
+
onSessionFound = (sessionId) => {
|
|
196
|
+
if (this.sessionId === sessionId) {
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
199
|
+
this.sessionId = sessionId;
|
|
200
|
+
this.client.updateMetadata((metadata) => ({
|
|
201
|
+
...metadata,
|
|
202
|
+
codexSessionId: sessionId
|
|
203
|
+
}));
|
|
204
|
+
api.logger.debug(`[CodexSession] Session ID ${sessionId} added to metadata`);
|
|
205
|
+
};
|
|
206
|
+
clearSessionId = () => {
|
|
207
|
+
this.sessionId = null;
|
|
208
|
+
api.logger.debug("[CodexSession] Session ID cleared");
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
function getCodexSessionsRoot() {
|
|
213
|
+
const codexHomeDir = process.env.CODEX_HOME || path.join(os.homedir(), ".codex");
|
|
214
|
+
return path.join(codexHomeDir, "sessions");
|
|
215
|
+
}
|
|
216
|
+
function collectFilesRecursive(dir, acc = []) {
|
|
217
|
+
let entries;
|
|
218
|
+
try {
|
|
219
|
+
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
220
|
+
} catch {
|
|
221
|
+
return acc;
|
|
222
|
+
}
|
|
223
|
+
for (const entry of entries) {
|
|
224
|
+
const full = path.join(dir, entry.name);
|
|
225
|
+
if (entry.isDirectory()) {
|
|
226
|
+
collectFilesRecursive(full, acc);
|
|
227
|
+
} else if (entry.isFile()) {
|
|
228
|
+
acc.push(full);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return acc;
|
|
232
|
+
}
|
|
233
|
+
function extractCodexSessionIdFromPath(filePath) {
|
|
234
|
+
const match = filePath.match(/-([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})\.jsonl$/);
|
|
235
|
+
return match?.[1] ?? null;
|
|
236
|
+
}
|
|
237
|
+
function findCodexResumeFile(sessionId) {
|
|
238
|
+
if (!sessionId) return null;
|
|
239
|
+
try {
|
|
240
|
+
const candidates = collectFilesRecursive(getCodexSessionsRoot()).filter((full) => full.endsWith(`-${sessionId}.jsonl`)).filter((full) => {
|
|
241
|
+
try {
|
|
242
|
+
return fs.statSync(full).isFile();
|
|
243
|
+
} catch {
|
|
244
|
+
return false;
|
|
245
|
+
}
|
|
246
|
+
}).sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
247
|
+
return candidates[0] || null;
|
|
248
|
+
} catch {
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function findLatestCodexSessionIdSince(startTimeMs) {
|
|
253
|
+
try {
|
|
254
|
+
const candidates = collectFilesRecursive(getCodexSessionsRoot()).filter((full) => full.endsWith(".jsonl")).filter((full) => {
|
|
255
|
+
try {
|
|
256
|
+
return fs.statSync(full).mtimeMs >= startTimeMs;
|
|
257
|
+
} catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
}).sort((a, b) => fs.statSync(b).mtimeMs - fs.statSync(a).mtimeMs);
|
|
261
|
+
for (const candidate of candidates) {
|
|
262
|
+
const sessionId = extractCodexSessionIdFromPath(candidate);
|
|
263
|
+
if (sessionId) {
|
|
264
|
+
return sessionId;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
} catch {
|
|
268
|
+
return null;
|
|
269
|
+
}
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
class CodexExitCodeError extends Error {
|
|
274
|
+
exitCode;
|
|
275
|
+
constructor(exitCode) {
|
|
276
|
+
super(`Codex process exited with code: ${exitCode}`);
|
|
277
|
+
this.name = "CodexExitCodeError";
|
|
278
|
+
this.exitCode = exitCode;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function terminateCodexLocalChild(pid) {
|
|
282
|
+
if (!pid) {
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
try {
|
|
286
|
+
if (process.platform === "win32") {
|
|
287
|
+
node_child_process.execFileSync("taskkill", ["/F", "/T", "/PID", pid.toString()], { stdio: "ignore" });
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
290
|
+
process.kill(pid, "SIGTERM");
|
|
291
|
+
} catch (error) {
|
|
292
|
+
api.logger.debug("[CodexLocal] Failed to terminate child process", error);
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
async function codexLocal(opts) {
|
|
296
|
+
const baseArgs = [...opts.codexArgs ?? []];
|
|
297
|
+
const args = opts.sessionId ? ["resume", opts.sessionId, ...baseArgs] : baseArgs;
|
|
298
|
+
const startTime = Date.now();
|
|
299
|
+
let detectedSessionId = opts.sessionId;
|
|
300
|
+
const codexExecutable = index.resolveCodexExecutable();
|
|
301
|
+
api.logger.debug(`[CodexLocal] Spawning ${codexExecutable} with args: ${JSON.stringify(args)}`);
|
|
302
|
+
process.stdin.pause();
|
|
303
|
+
try {
|
|
304
|
+
await new Promise((resolve, reject) => {
|
|
305
|
+
const child = node_child_process.spawn(codexExecutable, args, {
|
|
306
|
+
cwd: opts.path,
|
|
307
|
+
env: process.env,
|
|
308
|
+
stdio: "inherit",
|
|
309
|
+
signal: opts.abort,
|
|
310
|
+
shell: index.shouldUseShellForCodex(codexExecutable)
|
|
311
|
+
});
|
|
312
|
+
const abortChild = () => {
|
|
313
|
+
if (child.exitCode === null) {
|
|
314
|
+
api.logger.debug(`[CodexLocal] Abort received, terminating child tree pid=${child.pid ?? "none"}`);
|
|
315
|
+
terminateCodexLocalChild(child.pid);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
opts.abort.addEventListener("abort", abortChild, { once: true });
|
|
319
|
+
const sessionPoll = setInterval(() => {
|
|
320
|
+
if (detectedSessionId) {
|
|
321
|
+
return;
|
|
322
|
+
}
|
|
323
|
+
const nextSessionId = findLatestCodexSessionIdSince(startTime);
|
|
324
|
+
if (nextSessionId) {
|
|
325
|
+
detectedSessionId = nextSessionId;
|
|
326
|
+
api.logger.debug(`[CodexLocal] Detected session ID: ${nextSessionId}`);
|
|
327
|
+
opts.onSessionFound(nextSessionId);
|
|
328
|
+
}
|
|
329
|
+
}, 1e3);
|
|
330
|
+
child.on("error", (error) => {
|
|
331
|
+
clearInterval(sessionPoll);
|
|
332
|
+
opts.abort.removeEventListener("abort", abortChild);
|
|
333
|
+
reject(error);
|
|
334
|
+
});
|
|
335
|
+
child.on("exit", (code, signal) => {
|
|
336
|
+
clearInterval(sessionPoll);
|
|
337
|
+
opts.abort.removeEventListener("abort", abortChild);
|
|
338
|
+
opts.onThinkingChange?.(false);
|
|
339
|
+
if (signal === "SIGTERM" && opts.abort.aborted) {
|
|
340
|
+
resolve();
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
if (signal) {
|
|
344
|
+
reject(new Error(`Codex terminated with signal: ${signal}`));
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
if (code !== 0 && code !== null) {
|
|
348
|
+
reject(new CodexExitCodeError(code));
|
|
349
|
+
return;
|
|
350
|
+
}
|
|
351
|
+
resolve();
|
|
352
|
+
});
|
|
353
|
+
});
|
|
354
|
+
} finally {
|
|
355
|
+
process.stdin.resume();
|
|
356
|
+
opts.onThinkingChange?.(false);
|
|
357
|
+
}
|
|
358
|
+
return detectedSessionId;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
async function codexLocalLauncher(session) {
|
|
362
|
+
let exitReason = null;
|
|
363
|
+
const processAbortController = new AbortController();
|
|
364
|
+
const exitFuture = new future.Future();
|
|
365
|
+
try {
|
|
366
|
+
async function abort() {
|
|
367
|
+
if (!processAbortController.signal.aborted) {
|
|
368
|
+
processAbortController.abort();
|
|
369
|
+
}
|
|
370
|
+
await exitFuture.promise;
|
|
371
|
+
}
|
|
372
|
+
async function doSwitch() {
|
|
373
|
+
api.logger.debug("[codex-local]: switching to remote mode");
|
|
374
|
+
if (!exitReason) {
|
|
375
|
+
exitReason = { type: "switch" };
|
|
376
|
+
}
|
|
377
|
+
await abort();
|
|
378
|
+
}
|
|
379
|
+
async function doAbort() {
|
|
380
|
+
api.logger.debug("[codex-local]: abort requested");
|
|
381
|
+
if (!exitReason) {
|
|
382
|
+
exitReason = { type: "switch" };
|
|
383
|
+
}
|
|
384
|
+
session.queue.reset();
|
|
385
|
+
await abort();
|
|
386
|
+
}
|
|
387
|
+
session.client.rpcHandlerManager.registerHandler("abort", doAbort);
|
|
388
|
+
session.client.rpcHandlerManager.registerHandler("switch", doSwitch);
|
|
389
|
+
session.queue.setOnMessage(() => {
|
|
390
|
+
void doSwitch();
|
|
391
|
+
});
|
|
392
|
+
if (session.queue.size() > 0) {
|
|
393
|
+
return { type: "switch" };
|
|
394
|
+
}
|
|
395
|
+
while (true) {
|
|
396
|
+
if (exitReason) {
|
|
397
|
+
return exitReason;
|
|
398
|
+
}
|
|
399
|
+
try {
|
|
400
|
+
const sessionId = await codexLocal({
|
|
401
|
+
path: session.path,
|
|
402
|
+
sessionId: session.sessionId,
|
|
403
|
+
abort: processAbortController.signal,
|
|
404
|
+
codexArgs: session.codexArgs,
|
|
405
|
+
onSessionFound: session.onSessionFound,
|
|
406
|
+
onThinkingChange: session.onThinkingChange
|
|
407
|
+
});
|
|
408
|
+
if (sessionId) {
|
|
409
|
+
session.onSessionFound(sessionId);
|
|
410
|
+
}
|
|
411
|
+
if (!exitReason) {
|
|
412
|
+
exitReason = { type: "exit", code: 0 };
|
|
413
|
+
break;
|
|
414
|
+
}
|
|
415
|
+
} catch (error) {
|
|
416
|
+
api.logger.debug("[codex-local]: launch error", error);
|
|
417
|
+
if (error instanceof CodexExitCodeError && !exitReason) {
|
|
418
|
+
exitReason = { type: "exit", code: error.exitCode };
|
|
419
|
+
break;
|
|
420
|
+
}
|
|
421
|
+
if (error instanceof Error && "code" in error && (error.code === "ENOENT" || error.code === "EINVAL") && !exitReason) {
|
|
422
|
+
exitReason = { type: "exit", code: 1 };
|
|
423
|
+
session.runtimeSession.sendSessionEvent({ type: "message", message: "Codex CLI not found. Check your local Codex installation." });
|
|
424
|
+
break;
|
|
425
|
+
}
|
|
426
|
+
if (!exitReason) {
|
|
427
|
+
session.runtimeSession.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
428
|
+
continue;
|
|
429
|
+
}
|
|
430
|
+
break;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
} finally {
|
|
434
|
+
exitFuture.resolve(void 0);
|
|
435
|
+
session.client.rpcHandlerManager.registerHandler("abort", async () => {
|
|
436
|
+
});
|
|
437
|
+
session.client.rpcHandlerManager.registerHandler("switch", async () => {
|
|
438
|
+
});
|
|
439
|
+
session.queue.setOnMessage(null);
|
|
440
|
+
}
|
|
441
|
+
return exitReason || { type: "exit", code: 0 };
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
const CodexDisplay = ({ messageBuffer, logPath, onExit, onSwitchToLocal, title }) => {
|
|
445
|
+
const [messages, setMessages] = React.useState([]);
|
|
446
|
+
const [confirmationMode, setConfirmationMode] = React.useState(null);
|
|
447
|
+
const [actionInProgress, setActionInProgress] = React.useState(null);
|
|
448
|
+
const confirmationTimeoutRef = React.useRef(null);
|
|
449
|
+
const { stdout } = ink.useStdout();
|
|
450
|
+
const terminalWidth = stdout.columns || 80;
|
|
451
|
+
const terminalHeight = stdout.rows || 24;
|
|
452
|
+
React.useEffect(() => {
|
|
453
|
+
setMessages(messageBuffer.getMessages());
|
|
454
|
+
const unsubscribe = messageBuffer.onUpdate((newMessages) => {
|
|
455
|
+
setMessages(newMessages);
|
|
456
|
+
});
|
|
457
|
+
return () => {
|
|
458
|
+
unsubscribe();
|
|
459
|
+
if (confirmationTimeoutRef.current) {
|
|
460
|
+
clearTimeout(confirmationTimeoutRef.current);
|
|
461
|
+
}
|
|
462
|
+
};
|
|
463
|
+
}, [messageBuffer]);
|
|
464
|
+
const resetConfirmation = React.useCallback(() => {
|
|
465
|
+
setConfirmationMode(null);
|
|
466
|
+
if (confirmationTimeoutRef.current) {
|
|
467
|
+
clearTimeout(confirmationTimeoutRef.current);
|
|
468
|
+
confirmationTimeoutRef.current = null;
|
|
469
|
+
}
|
|
470
|
+
}, []);
|
|
471
|
+
const setConfirmationWithTimeout = React.useCallback((mode) => {
|
|
472
|
+
setConfirmationMode(mode);
|
|
473
|
+
if (confirmationTimeoutRef.current) {
|
|
474
|
+
clearTimeout(confirmationTimeoutRef.current);
|
|
475
|
+
}
|
|
476
|
+
confirmationTimeoutRef.current = setTimeout(() => {
|
|
477
|
+
resetConfirmation();
|
|
478
|
+
}, 15e3);
|
|
479
|
+
}, [resetConfirmation]);
|
|
480
|
+
ink.useInput(React.useCallback(async (input, key) => {
|
|
481
|
+
if (actionInProgress) return;
|
|
482
|
+
if (key.ctrl && input === "c") {
|
|
483
|
+
if (confirmationMode === "exit") {
|
|
484
|
+
resetConfirmation();
|
|
485
|
+
setActionInProgress("exiting");
|
|
486
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
487
|
+
onExit?.();
|
|
488
|
+
} else {
|
|
489
|
+
setConfirmationWithTimeout("exit");
|
|
490
|
+
}
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
if (input === " ") {
|
|
494
|
+
if (confirmationMode === "switch") {
|
|
495
|
+
resetConfirmation();
|
|
496
|
+
setActionInProgress("switching");
|
|
497
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
498
|
+
onSwitchToLocal?.();
|
|
499
|
+
} else {
|
|
500
|
+
setConfirmationWithTimeout("switch");
|
|
501
|
+
}
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
if (confirmationMode) {
|
|
505
|
+
resetConfirmation();
|
|
506
|
+
}
|
|
507
|
+
}, [confirmationMode, actionInProgress, onExit, onSwitchToLocal, setConfirmationWithTimeout, resetConfirmation]));
|
|
508
|
+
const getMessageColor = (type) => {
|
|
509
|
+
switch (type) {
|
|
510
|
+
case "user":
|
|
511
|
+
return "magenta";
|
|
512
|
+
case "assistant":
|
|
513
|
+
return "cyan";
|
|
514
|
+
case "system":
|
|
515
|
+
return "blue";
|
|
516
|
+
case "tool":
|
|
517
|
+
return "yellow";
|
|
518
|
+
case "result":
|
|
519
|
+
return "green";
|
|
520
|
+
case "status":
|
|
521
|
+
return "gray";
|
|
522
|
+
default:
|
|
523
|
+
return "white";
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
const formatMessage = (msg) => {
|
|
527
|
+
const lines = msg.content.split("\n");
|
|
528
|
+
const maxLineLength = terminalWidth - 10;
|
|
529
|
+
return lines.map((line) => {
|
|
530
|
+
if (line.length <= maxLineLength) return line;
|
|
531
|
+
const chunks = [];
|
|
532
|
+
for (let i = 0; i < line.length; i += maxLineLength) {
|
|
533
|
+
chunks.push(line.slice(i, i + maxLineLength));
|
|
534
|
+
}
|
|
535
|
+
return chunks.join("\n");
|
|
536
|
+
}).join("\n");
|
|
537
|
+
};
|
|
538
|
+
return /* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", width: terminalWidth, height: terminalHeight }, /* @__PURE__ */ React.createElement(
|
|
539
|
+
ink.Box,
|
|
540
|
+
{
|
|
541
|
+
flexDirection: "column",
|
|
542
|
+
width: terminalWidth,
|
|
543
|
+
height: terminalHeight - 4,
|
|
544
|
+
borderStyle: "round",
|
|
545
|
+
borderColor: "gray",
|
|
546
|
+
paddingX: 1,
|
|
547
|
+
overflow: "hidden"
|
|
548
|
+
},
|
|
549
|
+
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, title || "\u{1F916} Codex Agent Messages"), /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "\u2500".repeat(Math.min(terminalWidth - 4, 60)))),
|
|
550
|
+
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", height: terminalHeight - 10, overflow: "hidden" }, messages.length === 0 ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Waiting for messages...") : (
|
|
551
|
+
// Show only the last messages that fit in the available space
|
|
552
|
+
messages.slice(-Math.max(1, terminalHeight - 10)).map((msg) => /* @__PURE__ */ React.createElement(ink.Box, { key: msg.id, flexDirection: "column", marginBottom: 1 }, /* @__PURE__ */ React.createElement(ink.Text, { color: getMessageColor(msg.type), dimColor: true }, formatMessage(msg))))
|
|
553
|
+
))
|
|
554
|
+
), /* @__PURE__ */ React.createElement(
|
|
555
|
+
ink.Box,
|
|
556
|
+
{
|
|
557
|
+
width: terminalWidth,
|
|
558
|
+
borderStyle: "round",
|
|
559
|
+
borderColor: actionInProgress ? "gray" : confirmationMode === "exit" ? "red" : confirmationMode === "switch" ? "yellow" : "green",
|
|
560
|
+
paddingX: 2,
|
|
561
|
+
justifyContent: "center",
|
|
562
|
+
alignItems: "center",
|
|
563
|
+
flexDirection: "column"
|
|
564
|
+
},
|
|
565
|
+
/* @__PURE__ */ React.createElement(ink.Box, { flexDirection: "column", alignItems: "center" }, actionInProgress === "exiting" ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Exiting agent...") : actionInProgress === "switching" ? /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", bold: true }, "Switching to local mode...") : confirmationMode === "exit" ? /* @__PURE__ */ React.createElement(ink.Text, { color: "red", bold: true }, "\u26A0\uFE0F Press Ctrl-C again to exit the agent") : confirmationMode === "switch" ? /* @__PURE__ */ React.createElement(ink.Text, { color: "yellow", bold: true }, "\u23F8\uFE0F Press space again to switch to local mode") : /* @__PURE__ */ React.createElement(React.Fragment, null, /* @__PURE__ */ React.createElement(ink.Text, { color: "green", bold: true }, "\u{1F916} Codex Agent Running \u2022 Press space for local \u2022 Ctrl-C to exit")), process.env.DEBUG && logPath && /* @__PURE__ */ React.createElement(ink.Text, { color: "gray", dimColor: true }, "Debug logs: ", logPath))
|
|
566
|
+
));
|
|
567
|
+
};
|
|
568
|
+
|
|
569
|
+
const DEFAULT_TIMEOUT = 14 * 24 * 60 * 60 * 1e3;
|
|
570
|
+
function getCodexMcpCommand() {
|
|
571
|
+
const codexExecutable = index.resolveCodexExecutable();
|
|
572
|
+
try {
|
|
573
|
+
const version = child_process.execSync(`"${codexExecutable}" --version`, { encoding: "utf8" }).trim();
|
|
574
|
+
const match = version.match(/codex-cli\s+(\d+\.\d+\.\d+(?:-alpha\.\d+)?)/);
|
|
575
|
+
if (!match) {
|
|
576
|
+
api.logger.debug("[CodexMCP] Could not parse codex version:", version);
|
|
577
|
+
return null;
|
|
578
|
+
}
|
|
579
|
+
const versionStr = match[1];
|
|
580
|
+
const [major, minor, patch] = versionStr.split(/[-.]/).map(Number);
|
|
581
|
+
if (major > 0 || minor > 43) return "mcp-server";
|
|
582
|
+
if (minor === 43 && patch === 0) {
|
|
583
|
+
if (versionStr.includes("-alpha.")) {
|
|
584
|
+
const alphaNum = parseInt(versionStr.split("-alpha.")[1]);
|
|
585
|
+
return alphaNum >= 5 ? "mcp-server" : "mcp";
|
|
586
|
+
}
|
|
587
|
+
return "mcp-server";
|
|
588
|
+
}
|
|
589
|
+
return "mcp";
|
|
590
|
+
} catch (error) {
|
|
591
|
+
api.logger.debug("[CodexMCP] Codex CLI not found or not executable:", error);
|
|
592
|
+
return null;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
function resolveCodexMcpTransport(executable, mcpCommand) {
|
|
596
|
+
return {
|
|
597
|
+
// Preserve the resolved executable path so daemon/service environments do not
|
|
598
|
+
// depend on APPDATA npm shims being present on PATH.
|
|
599
|
+
command: executable,
|
|
600
|
+
args: [mcpCommand]
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
function extractCodexIdentifiers(response) {
|
|
604
|
+
const meta = response?.meta || {};
|
|
605
|
+
const structured = response?.structuredContent || response?.structured_content || response?.structured_output || {};
|
|
606
|
+
const sessionId = meta.sessionId || meta.session_id || response?.sessionId || response?.session_id || meta.threadId || meta.thread_id || response?.threadId || response?.thread_id || structured.threadId || structured.thread_id || null;
|
|
607
|
+
const conversationId = meta.conversationId || meta.conversation_id || response?.conversationId || response?.conversation_id || meta.threadId || meta.thread_id || response?.threadId || response?.thread_id || structured.threadId || structured.thread_id || structured.conversationId || structured.conversation_id || null;
|
|
608
|
+
return { sessionId, conversationId };
|
|
609
|
+
}
|
|
610
|
+
function normalizeCodexPermissionRequest(params) {
|
|
611
|
+
return {
|
|
612
|
+
requestId: params.codex_call_id || params.call_id || params.codex_mcp_tool_call_id || params.codex_event_id || `codex-permission-${Date.now()}`,
|
|
613
|
+
toolName: "CodexBash",
|
|
614
|
+
input: {
|
|
615
|
+
command: params.codex_command || params.command,
|
|
616
|
+
cwd: params.codex_cwd || params.cwd,
|
|
617
|
+
proposed_execpolicy_amendment: params.proposed_execpolicy_amendment,
|
|
618
|
+
parsed_cmd: params.parsed_cmd
|
|
619
|
+
}
|
|
620
|
+
};
|
|
621
|
+
}
|
|
622
|
+
function resolveCodexPermissionRequest(params, fallback) {
|
|
623
|
+
const normalized = normalizeCodexPermissionRequest(params);
|
|
624
|
+
const hasInlinePayload = Boolean(
|
|
625
|
+
normalized.input.command || normalized.input.cwd || normalized.input.proposed_execpolicy_amendment || normalized.input.parsed_cmd
|
|
626
|
+
);
|
|
627
|
+
const hasInlineId = Boolean(
|
|
628
|
+
params.codex_call_id || params.call_id || params.codex_mcp_tool_call_id || params.codex_event_id
|
|
629
|
+
);
|
|
630
|
+
if (!fallback || hasInlinePayload && hasInlineId) {
|
|
631
|
+
return normalized;
|
|
632
|
+
}
|
|
633
|
+
return {
|
|
634
|
+
requestId: hasInlineId ? normalized.requestId : fallback.call_id || normalized.requestId,
|
|
635
|
+
toolName: normalized.toolName,
|
|
636
|
+
input: {
|
|
637
|
+
command: normalized.input.command || fallback.command,
|
|
638
|
+
cwd: normalized.input.cwd || fallback.cwd,
|
|
639
|
+
proposed_execpolicy_amendment: normalized.input.proposed_execpolicy_amendment || fallback.proposed_execpolicy_amendment,
|
|
640
|
+
parsed_cmd: normalized.input.parsed_cmd || fallback.parsed_cmd
|
|
641
|
+
}
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
class CodexMcpClient {
|
|
645
|
+
client;
|
|
646
|
+
transport = null;
|
|
647
|
+
connected = false;
|
|
648
|
+
sessionId = null;
|
|
649
|
+
conversationId = null;
|
|
650
|
+
handler = null;
|
|
651
|
+
permissionHandler = null;
|
|
652
|
+
selectionHandler = null;
|
|
653
|
+
pendingExecApprovalContexts = [];
|
|
654
|
+
constructor() {
|
|
655
|
+
this.client = this.createClient();
|
|
656
|
+
}
|
|
657
|
+
createClient() {
|
|
658
|
+
const client = new index_js.Client(
|
|
659
|
+
{ name: "happy-codex-client", version: "1.0.0" },
|
|
660
|
+
{ capabilities: { elicitation: {} } }
|
|
661
|
+
);
|
|
662
|
+
client.setNotificationHandler(z.z.object({
|
|
663
|
+
method: z.z.literal("codex/event"),
|
|
664
|
+
params: z.z.object({
|
|
665
|
+
msg: z.z.any()
|
|
666
|
+
})
|
|
667
|
+
}).passthrough(), (data) => {
|
|
668
|
+
const msg = data.params.msg;
|
|
669
|
+
if (msg?.type === "exec_approval_request") {
|
|
670
|
+
this.pendingExecApprovalContexts.push({
|
|
671
|
+
call_id: msg.call_id,
|
|
672
|
+
command: msg.command,
|
|
673
|
+
cwd: msg.cwd,
|
|
674
|
+
proposed_execpolicy_amendment: msg.proposed_execpolicy_amendment,
|
|
675
|
+
parsed_cmd: msg.parsed_cmd
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
this.updateIdentifiersFromEvent(msg);
|
|
679
|
+
this.handler?.(msg);
|
|
680
|
+
});
|
|
681
|
+
return client;
|
|
682
|
+
}
|
|
683
|
+
setHandler(handler) {
|
|
684
|
+
this.handler = handler;
|
|
685
|
+
}
|
|
686
|
+
/**
|
|
687
|
+
* Set the permission handler for tool approval
|
|
688
|
+
*/
|
|
689
|
+
setPermissionHandler(handler) {
|
|
690
|
+
this.permissionHandler = handler;
|
|
691
|
+
}
|
|
692
|
+
setSelectionHandler(handler) {
|
|
693
|
+
this.selectionHandler = handler;
|
|
694
|
+
}
|
|
695
|
+
async connect() {
|
|
696
|
+
if (this.connected) return;
|
|
697
|
+
const mcpCommand = getCodexMcpCommand();
|
|
698
|
+
if (mcpCommand === null) {
|
|
699
|
+
throw new Error(
|
|
700
|
+
"Codex CLI not found or not executable.\n\nTo install codex:\n npm install -g @openai/codex\n\nAlternatively, use Claude:\n happy claude"
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
api.logger.debug(`[CodexMCP] Connecting to Codex MCP server using command: codex ${mcpCommand}`);
|
|
704
|
+
const codexExecutable = index.resolveCodexExecutable();
|
|
705
|
+
const transport = resolveCodexMcpTransport(codexExecutable, mcpCommand);
|
|
706
|
+
this.transport = new stdio_js.StdioClientTransport({
|
|
707
|
+
command: transport.command,
|
|
708
|
+
args: transport.args,
|
|
709
|
+
env: Object.keys(process.env).reduce((acc, key) => {
|
|
710
|
+
const value = process.env[key];
|
|
711
|
+
if (typeof value === "string") acc[key] = value;
|
|
712
|
+
return acc;
|
|
713
|
+
}, {})
|
|
714
|
+
});
|
|
715
|
+
this.registerPermissionHandlers();
|
|
716
|
+
await this.client.connect(this.transport);
|
|
717
|
+
this.connected = true;
|
|
718
|
+
api.logger.debug("[CodexMCP] Connected to Codex");
|
|
719
|
+
}
|
|
720
|
+
isClosedTransportError(error) {
|
|
721
|
+
const record = error;
|
|
722
|
+
const code = typeof record?.code === "number" ? record.code : null;
|
|
723
|
+
if (code === -32e3) {
|
|
724
|
+
return true;
|
|
725
|
+
}
|
|
726
|
+
const message = typeof record?.message === "string" ? record.message.toLowerCase() : "";
|
|
727
|
+
if (code === -32001 && message.includes("aborterror")) {
|
|
728
|
+
return true;
|
|
729
|
+
}
|
|
730
|
+
return message.includes("connection closed") || message.includes("not connected");
|
|
731
|
+
}
|
|
732
|
+
async callToolWithReconnectRetry(call) {
|
|
733
|
+
try {
|
|
734
|
+
return await call();
|
|
735
|
+
} catch (error) {
|
|
736
|
+
if (!this.isClosedTransportError(error)) {
|
|
737
|
+
throw error;
|
|
738
|
+
}
|
|
739
|
+
api.logger.debug("[CodexMCP] Transport closed during tool call; reconnecting and retrying once");
|
|
740
|
+
try {
|
|
741
|
+
await this.disconnect();
|
|
742
|
+
} catch {
|
|
743
|
+
}
|
|
744
|
+
await this.connect();
|
|
745
|
+
return await call();
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
registerPermissionHandlers() {
|
|
749
|
+
this.client.setRequestHandler(
|
|
750
|
+
types_js.ElicitRequestSchema,
|
|
751
|
+
async (request) => {
|
|
752
|
+
const params = request.params;
|
|
753
|
+
api.logger.debug("[CodexMCP] Received elicitation request:", params);
|
|
754
|
+
try {
|
|
755
|
+
if (this.isSelectionRequest(params)) {
|
|
756
|
+
return await this.handleSelectionRequest(params);
|
|
757
|
+
}
|
|
758
|
+
if (!this.permissionHandler) {
|
|
759
|
+
api.logger.debug("[CodexMCP] No permission handler set, denying by default");
|
|
760
|
+
return {
|
|
761
|
+
action: "decline",
|
|
762
|
+
decision: "denied",
|
|
763
|
+
content: {
|
|
764
|
+
decision: "denied"
|
|
765
|
+
}
|
|
766
|
+
};
|
|
767
|
+
}
|
|
768
|
+
const normalized = resolveCodexPermissionRequest(
|
|
769
|
+
params,
|
|
770
|
+
this.takeExecApprovalContextForPermission(params)
|
|
771
|
+
);
|
|
772
|
+
const result = await this.permissionHandler.handleToolCall(
|
|
773
|
+
normalized.requestId,
|
|
774
|
+
normalized.toolName,
|
|
775
|
+
normalized.input
|
|
776
|
+
);
|
|
777
|
+
api.logger.debug("[CodexMCP] Permission result:", result);
|
|
778
|
+
return {
|
|
779
|
+
action: result.decision === "abort" ? "cancel" : result.decision === "denied" ? "decline" : "accept",
|
|
780
|
+
decision: result.decision,
|
|
781
|
+
content: {
|
|
782
|
+
decision: result.decision
|
|
783
|
+
}
|
|
784
|
+
};
|
|
785
|
+
} catch (error) {
|
|
786
|
+
if (error instanceof Error && error.message === "Session reset") {
|
|
787
|
+
api.logger.debug("[CodexMCP] Permission request cancelled during session reset");
|
|
788
|
+
return {
|
|
789
|
+
action: "cancel"
|
|
790
|
+
};
|
|
791
|
+
}
|
|
792
|
+
if (error instanceof Error && error.message === BaseReasoningProcessor.INTERACTION_SUPERSEDED_ERROR) {
|
|
793
|
+
api.logger.debug("[CodexMCP] Selection request cancelled because a new user message arrived");
|
|
794
|
+
return {
|
|
795
|
+
action: "cancel"
|
|
796
|
+
};
|
|
797
|
+
}
|
|
798
|
+
api.logger.debug("[CodexMCP] Error handling permission request:", error);
|
|
799
|
+
return {
|
|
800
|
+
action: "decline",
|
|
801
|
+
decision: "denied",
|
|
802
|
+
reason: error instanceof Error ? error.message : "Permission request failed"
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
}
|
|
806
|
+
);
|
|
807
|
+
api.logger.debug("[CodexMCP] Permission handlers registered");
|
|
808
|
+
}
|
|
809
|
+
isSelectionRequest(params) {
|
|
810
|
+
if (Array.isArray(params.codex_command) && params.codex_command.length > 0) {
|
|
811
|
+
return false;
|
|
812
|
+
}
|
|
813
|
+
if (Array.isArray(params.options) && params.options.length > 0) {
|
|
814
|
+
return true;
|
|
815
|
+
}
|
|
816
|
+
return Boolean(params.requestedSchema?.properties?.optionId);
|
|
817
|
+
}
|
|
818
|
+
async handleSelectionRequest(params) {
|
|
819
|
+
const optionMap = (params.options || []).filter((option) => Boolean(option.optionId));
|
|
820
|
+
const options = optionMap.map((option) => ({
|
|
821
|
+
optionId: option.optionId,
|
|
822
|
+
label: option.label || option.name || option.optionId,
|
|
823
|
+
description: option.description || option.name || option.optionId
|
|
824
|
+
}));
|
|
825
|
+
const defaultOptionId = params.requestedSchema?.properties?.optionId?.default;
|
|
826
|
+
if (options.length === 0) {
|
|
827
|
+
api.logger.debug("[CodexMCP] Selection request has no options, cancelling");
|
|
828
|
+
return {
|
|
829
|
+
action: "cancel"
|
|
830
|
+
};
|
|
831
|
+
}
|
|
832
|
+
if (!this.selectionHandler) {
|
|
833
|
+
api.logger.debug("[CodexMCP] No selection handler set, cancelling selection request");
|
|
834
|
+
return {
|
|
835
|
+
action: "cancel"
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
const requestId = params.codex_event_id || params.codex_elicitation || `selection-${Date.now()}`;
|
|
839
|
+
const response = await this.selectionHandler.requestSelection({
|
|
840
|
+
id: requestId,
|
|
841
|
+
message: params.message,
|
|
842
|
+
options,
|
|
843
|
+
defaultOptionId
|
|
844
|
+
});
|
|
845
|
+
return {
|
|
846
|
+
action: "accept",
|
|
847
|
+
optionId: response.optionId,
|
|
848
|
+
content: {
|
|
849
|
+
optionId: response.optionId
|
|
850
|
+
}
|
|
851
|
+
};
|
|
852
|
+
}
|
|
853
|
+
takeExecApprovalContextForPermission(params) {
|
|
854
|
+
const hasInlinePayload = Boolean(
|
|
855
|
+
params.codex_command || params.command || params.codex_cwd || params.cwd || params.proposed_execpolicy_amendment || params.parsed_cmd
|
|
856
|
+
);
|
|
857
|
+
const explicitId = params.codex_call_id || params.call_id || params.codex_mcp_tool_call_id || params.codex_event_id;
|
|
858
|
+
if (hasInlinePayload && explicitId) {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
if (!this.pendingExecApprovalContexts.length) {
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
if (explicitId) {
|
|
865
|
+
const index = this.pendingExecApprovalContexts.findIndex((context) => context.call_id === explicitId);
|
|
866
|
+
if (index >= 0) {
|
|
867
|
+
return this.pendingExecApprovalContexts.splice(index, 1)[0] || null;
|
|
868
|
+
}
|
|
869
|
+
}
|
|
870
|
+
return this.pendingExecApprovalContexts.shift() || null;
|
|
871
|
+
}
|
|
872
|
+
async startSession(config, options) {
|
|
873
|
+
if (!this.connected) await this.connect();
|
|
874
|
+
api.logger.debug("[CodexMCP] Starting Codex session:", config);
|
|
875
|
+
const response = await this.callToolWithReconnectRetry(
|
|
876
|
+
() => this.client.callTool({
|
|
877
|
+
name: "codex",
|
|
878
|
+
arguments: config
|
|
879
|
+
}, void 0, {
|
|
880
|
+
signal: options?.signal,
|
|
881
|
+
timeout: DEFAULT_TIMEOUT
|
|
882
|
+
})
|
|
883
|
+
);
|
|
884
|
+
api.logger.debug("[CodexMCP] startSession response:", response);
|
|
885
|
+
this.extractIdentifiers(response);
|
|
886
|
+
return response;
|
|
887
|
+
}
|
|
888
|
+
async continueSession(prompt, options) {
|
|
889
|
+
if (!this.connected) await this.connect();
|
|
890
|
+
if (!this.sessionId) {
|
|
891
|
+
throw new Error("No active session. Call startSession first.");
|
|
892
|
+
}
|
|
893
|
+
if (!this.conversationId) {
|
|
894
|
+
this.conversationId = this.sessionId;
|
|
895
|
+
api.logger.debug("[CodexMCP] conversationId missing, defaulting to sessionId:", this.conversationId);
|
|
896
|
+
}
|
|
897
|
+
const args = { sessionId: this.sessionId, conversationId: this.conversationId, prompt };
|
|
898
|
+
api.logger.debug("[CodexMCP] Continuing Codex session:", args);
|
|
899
|
+
const response = await this.callToolWithReconnectRetry(
|
|
900
|
+
() => this.client.callTool({
|
|
901
|
+
name: "codex-reply",
|
|
902
|
+
arguments: args
|
|
903
|
+
}, void 0, {
|
|
904
|
+
signal: options?.signal,
|
|
905
|
+
timeout: DEFAULT_TIMEOUT
|
|
906
|
+
})
|
|
907
|
+
);
|
|
908
|
+
api.logger.debug("[CodexMCP] continueSession response:", response);
|
|
909
|
+
this.extractIdentifiers(response);
|
|
910
|
+
return response;
|
|
911
|
+
}
|
|
912
|
+
updateIdentifiersFromEvent(event) {
|
|
913
|
+
if (!event || typeof event !== "object") {
|
|
914
|
+
return;
|
|
915
|
+
}
|
|
916
|
+
const candidates = [event];
|
|
917
|
+
if (event.data && typeof event.data === "object") {
|
|
918
|
+
candidates.push(event.data);
|
|
919
|
+
}
|
|
920
|
+
for (const candidate of candidates) {
|
|
921
|
+
const sessionId = candidate.session_id ?? candidate.sessionId ?? candidate.thread_id ?? candidate.threadId;
|
|
922
|
+
if (sessionId) {
|
|
923
|
+
this.sessionId = sessionId;
|
|
924
|
+
api.logger.debug("[CodexMCP] Session ID extracted from event:", this.sessionId);
|
|
925
|
+
}
|
|
926
|
+
const conversationId = candidate.conversation_id ?? candidate.conversationId;
|
|
927
|
+
if (conversationId) {
|
|
928
|
+
this.conversationId = conversationId;
|
|
929
|
+
api.logger.debug("[CodexMCP] Conversation ID extracted from event:", this.conversationId);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
extractIdentifiers(response) {
|
|
934
|
+
const identifiers = extractCodexIdentifiers(response);
|
|
935
|
+
if (identifiers.sessionId) {
|
|
936
|
+
this.sessionId = identifiers.sessionId;
|
|
937
|
+
api.logger.debug("[CodexMCP] Session ID extracted:", this.sessionId);
|
|
938
|
+
}
|
|
939
|
+
if (identifiers.conversationId) {
|
|
940
|
+
this.conversationId = identifiers.conversationId;
|
|
941
|
+
api.logger.debug("[CodexMCP] Conversation ID extracted:", this.conversationId);
|
|
942
|
+
}
|
|
943
|
+
const content = response?.content;
|
|
944
|
+
if (Array.isArray(content)) {
|
|
945
|
+
for (const item of content) {
|
|
946
|
+
if (!this.sessionId && item?.sessionId) {
|
|
947
|
+
this.sessionId = item.sessionId;
|
|
948
|
+
api.logger.debug("[CodexMCP] Session ID extracted from content:", this.sessionId);
|
|
949
|
+
}
|
|
950
|
+
if (!this.conversationId && item && typeof item === "object" && "conversationId" in item && item.conversationId) {
|
|
951
|
+
this.conversationId = item.conversationId;
|
|
952
|
+
api.logger.debug("[CodexMCP] Conversation ID extracted from content:", this.conversationId);
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
getSessionId() {
|
|
958
|
+
return this.sessionId;
|
|
959
|
+
}
|
|
960
|
+
hasActiveSession() {
|
|
961
|
+
return this.sessionId !== null;
|
|
962
|
+
}
|
|
963
|
+
clearSession() {
|
|
964
|
+
const previousSessionId = this.sessionId;
|
|
965
|
+
this.sessionId = null;
|
|
966
|
+
this.conversationId = null;
|
|
967
|
+
api.logger.debug("[CodexMCP] Session cleared, previous sessionId:", previousSessionId);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Store the current session ID without clearing it, useful for abort handling
|
|
971
|
+
*/
|
|
972
|
+
storeSessionForResume() {
|
|
973
|
+
api.logger.debug("[CodexMCP] Storing session for potential resume:", this.sessionId);
|
|
974
|
+
return this.sessionId;
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* Force close the Codex MCP transport and clear all session identifiers.
|
|
978
|
+
* Use this for permanent shutdown (e.g. kill/exit). Prefer `disconnect()` for
|
|
979
|
+
* transient connection resets where you may want to keep the session id.
|
|
980
|
+
*/
|
|
981
|
+
async forceCloseSession() {
|
|
982
|
+
api.logger.debug("[CodexMCP] Force closing session");
|
|
983
|
+
try {
|
|
984
|
+
await this.disconnect();
|
|
985
|
+
} finally {
|
|
986
|
+
this.clearSession();
|
|
987
|
+
}
|
|
988
|
+
api.logger.debug("[CodexMCP] Session force-closed");
|
|
989
|
+
}
|
|
990
|
+
async disconnect() {
|
|
991
|
+
if (!this.connected) return;
|
|
992
|
+
const pid = this.transport?.pid ?? null;
|
|
993
|
+
api.logger.debug(`[CodexMCP] Disconnecting; child pid=${pid ?? "none"}`);
|
|
994
|
+
try {
|
|
995
|
+
api.logger.debug("[CodexMCP] client.close begin");
|
|
996
|
+
await this.client.close();
|
|
997
|
+
api.logger.debug("[CodexMCP] client.close done");
|
|
998
|
+
} catch (e) {
|
|
999
|
+
api.logger.debug("[CodexMCP] Error closing client, attempting transport close directly", e);
|
|
1000
|
+
try {
|
|
1001
|
+
api.logger.debug("[CodexMCP] transport.close begin");
|
|
1002
|
+
await this.transport?.close?.();
|
|
1003
|
+
api.logger.debug("[CodexMCP] transport.close done");
|
|
1004
|
+
} catch {
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
if (pid) {
|
|
1008
|
+
try {
|
|
1009
|
+
process.kill(pid, 0);
|
|
1010
|
+
api.logger.debug("[CodexMCP] Child still alive, sending SIGKILL");
|
|
1011
|
+
try {
|
|
1012
|
+
process.kill(pid, "SIGKILL");
|
|
1013
|
+
} catch {
|
|
1014
|
+
}
|
|
1015
|
+
} catch {
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
this.transport = null;
|
|
1019
|
+
this.connected = false;
|
|
1020
|
+
this.client = this.createClient();
|
|
1021
|
+
api.logger.debug(`[CodexMCP] Disconnected; session ${this.sessionId ?? "none"} preserved`);
|
|
1022
|
+
}
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
class CodexPermissionHandler extends BaseReasoningProcessor.BasePermissionHandler {
|
|
1026
|
+
constructor(session) {
|
|
1027
|
+
super(session);
|
|
1028
|
+
}
|
|
1029
|
+
getLogPrefix() {
|
|
1030
|
+
return "[Codex]";
|
|
1031
|
+
}
|
|
1032
|
+
/**
|
|
1033
|
+
* Handle a tool permission request
|
|
1034
|
+
* @param toolCallId - The unique ID of the tool call
|
|
1035
|
+
* @param toolName - The name of the tool being called
|
|
1036
|
+
* @param input - The input parameters for the tool
|
|
1037
|
+
* @returns Promise resolving to permission result
|
|
1038
|
+
*/
|
|
1039
|
+
async handleToolCall(toolCallId, toolName, input) {
|
|
1040
|
+
return new Promise((resolve, reject) => {
|
|
1041
|
+
this.pendingRequests.set(toolCallId, {
|
|
1042
|
+
resolve,
|
|
1043
|
+
reject,
|
|
1044
|
+
toolName,
|
|
1045
|
+
input
|
|
1046
|
+
});
|
|
1047
|
+
this.addPendingRequestToState(toolCallId, toolName, input);
|
|
1048
|
+
api.logger.debug(`${this.getLogPrefix()} Permission request sent for tool: ${toolName} (${toolCallId})`);
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
class CodexSelectionHandler {
|
|
1054
|
+
pendingRequests = /* @__PURE__ */ new Map();
|
|
1055
|
+
session;
|
|
1056
|
+
constructor(session) {
|
|
1057
|
+
this.session = session;
|
|
1058
|
+
this.setupRpcHandler();
|
|
1059
|
+
}
|
|
1060
|
+
updateSession(newSession) {
|
|
1061
|
+
this.session = newSession;
|
|
1062
|
+
this.setupRpcHandler();
|
|
1063
|
+
}
|
|
1064
|
+
async requestSelection(request) {
|
|
1065
|
+
return new Promise((resolve, reject) => {
|
|
1066
|
+
this.pendingRequests.set(request.id, {
|
|
1067
|
+
resolve,
|
|
1068
|
+
reject,
|
|
1069
|
+
request
|
|
1070
|
+
});
|
|
1071
|
+
this.session.updateAgentState((currentState) => ({
|
|
1072
|
+
...currentState,
|
|
1073
|
+
requests: {
|
|
1074
|
+
...currentState.requests,
|
|
1075
|
+
[request.id]: {
|
|
1076
|
+
tool: "AskUserQuestion",
|
|
1077
|
+
arguments: {
|
|
1078
|
+
requestKind: "selection",
|
|
1079
|
+
questions: [
|
|
1080
|
+
{
|
|
1081
|
+
header: "Codex",
|
|
1082
|
+
question: request.message,
|
|
1083
|
+
multiSelect: false,
|
|
1084
|
+
options: request.options.map((option) => ({
|
|
1085
|
+
label: option.label,
|
|
1086
|
+
description: option.description || option.optionId,
|
|
1087
|
+
optionId: option.optionId
|
|
1088
|
+
}))
|
|
1089
|
+
}
|
|
1090
|
+
]
|
|
1091
|
+
},
|
|
1092
|
+
createdAt: Date.now(),
|
|
1093
|
+
requestKind: "selection",
|
|
1094
|
+
options: request.options,
|
|
1095
|
+
defaultOptionId: request.defaultOptionId
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}));
|
|
1099
|
+
api.logger.debug(`[Codex] Selection request sent (${request.id}) with ${request.options.length} options`);
|
|
1100
|
+
});
|
|
1101
|
+
}
|
|
1102
|
+
hasPendingRequests() {
|
|
1103
|
+
return this.pendingRequests.size > 0;
|
|
1104
|
+
}
|
|
1105
|
+
supersedePendingRequests(reason = BaseReasoningProcessor.INTERACTION_SUPERSEDED_ERROR) {
|
|
1106
|
+
const pendingSnapshot = Array.from(this.pendingRequests.entries());
|
|
1107
|
+
if (pendingSnapshot.length === 0) {
|
|
1108
|
+
return 0;
|
|
1109
|
+
}
|
|
1110
|
+
this.pendingRequests.clear();
|
|
1111
|
+
const completedAt = Date.now();
|
|
1112
|
+
for (const [, pending] of pendingSnapshot) {
|
|
1113
|
+
pending.reject(new Error(reason));
|
|
1114
|
+
}
|
|
1115
|
+
this.session.updateAgentState((currentState) => {
|
|
1116
|
+
const requests = { ...currentState.requests || {} };
|
|
1117
|
+
const completedRequests = { ...currentState.completedRequests || {} };
|
|
1118
|
+
for (const [id, request] of Object.entries(requests)) {
|
|
1119
|
+
if (request.requestKind !== "selection") {
|
|
1120
|
+
continue;
|
|
1121
|
+
}
|
|
1122
|
+
completedRequests[id] = {
|
|
1123
|
+
...request,
|
|
1124
|
+
completedAt,
|
|
1125
|
+
status: "canceled",
|
|
1126
|
+
reason,
|
|
1127
|
+
requestKind: "selection"
|
|
1128
|
+
};
|
|
1129
|
+
delete requests[id];
|
|
1130
|
+
}
|
|
1131
|
+
return {
|
|
1132
|
+
...currentState,
|
|
1133
|
+
requests,
|
|
1134
|
+
completedRequests
|
|
1135
|
+
};
|
|
1136
|
+
});
|
|
1137
|
+
api.logger.debug(`[Codex] Superseded ${pendingSnapshot.length} pending selection request(s)`);
|
|
1138
|
+
return pendingSnapshot.length;
|
|
1139
|
+
}
|
|
1140
|
+
reset(reason = "Session reset") {
|
|
1141
|
+
const pendingSnapshot = Array.from(this.pendingRequests.entries());
|
|
1142
|
+
this.pendingRequests.clear();
|
|
1143
|
+
for (const [, pending] of pendingSnapshot) {
|
|
1144
|
+
pending.reject(new Error(reason));
|
|
1145
|
+
}
|
|
1146
|
+
this.session.updateAgentState((currentState) => {
|
|
1147
|
+
const requests = { ...currentState.requests || {} };
|
|
1148
|
+
const completedRequests = { ...currentState.completedRequests || {} };
|
|
1149
|
+
for (const [id, request] of Object.entries(requests)) {
|
|
1150
|
+
if (request.requestKind !== "selection") {
|
|
1151
|
+
continue;
|
|
1152
|
+
}
|
|
1153
|
+
completedRequests[id] = {
|
|
1154
|
+
...request,
|
|
1155
|
+
completedAt: Date.now(),
|
|
1156
|
+
status: "canceled",
|
|
1157
|
+
reason,
|
|
1158
|
+
requestKind: "selection"
|
|
1159
|
+
};
|
|
1160
|
+
delete requests[id];
|
|
1161
|
+
}
|
|
1162
|
+
return {
|
|
1163
|
+
...currentState,
|
|
1164
|
+
requests,
|
|
1165
|
+
completedRequests
|
|
1166
|
+
};
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
setupRpcHandler() {
|
|
1170
|
+
this.session.rpcHandlerManager.registerHandler("selection", async (response) => {
|
|
1171
|
+
const pending = this.pendingRequests.get(response.id);
|
|
1172
|
+
if (!pending) {
|
|
1173
|
+
api.logger.debug("[Codex] Selection request not found or already resolved");
|
|
1174
|
+
return;
|
|
1175
|
+
}
|
|
1176
|
+
this.pendingRequests.delete(response.id);
|
|
1177
|
+
pending.resolve(response);
|
|
1178
|
+
this.session.updateAgentState((currentState) => {
|
|
1179
|
+
const request = currentState.requests?.[response.id];
|
|
1180
|
+
if (!request) {
|
|
1181
|
+
return currentState;
|
|
1182
|
+
}
|
|
1183
|
+
const { [response.id]: _, ...remainingRequests } = currentState.requests || {};
|
|
1184
|
+
return {
|
|
1185
|
+
...currentState,
|
|
1186
|
+
requests: remainingRequests,
|
|
1187
|
+
completedRequests: {
|
|
1188
|
+
...currentState.completedRequests,
|
|
1189
|
+
[response.id]: {
|
|
1190
|
+
...request,
|
|
1191
|
+
completedAt: Date.now(),
|
|
1192
|
+
status: "approved",
|
|
1193
|
+
requestKind: "selection",
|
|
1194
|
+
selectedOptionId: response.optionId
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
};
|
|
1198
|
+
});
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
class ReasoningProcessor extends BaseReasoningProcessor.BaseReasoningProcessor {
|
|
1204
|
+
getToolName() {
|
|
1205
|
+
return "CodexReasoning";
|
|
1206
|
+
}
|
|
1207
|
+
getLogPrefix() {
|
|
1208
|
+
return "[ReasoningProcessor]";
|
|
1209
|
+
}
|
|
1210
|
+
/**
|
|
1211
|
+
* Process a reasoning delta and accumulate content.
|
|
1212
|
+
*/
|
|
1213
|
+
processDelta(delta) {
|
|
1214
|
+
this.processInput(delta);
|
|
1215
|
+
}
|
|
1216
|
+
/**
|
|
1217
|
+
* Complete the reasoning section with final text.
|
|
1218
|
+
*/
|
|
1219
|
+
complete(fullText) {
|
|
1220
|
+
this.completeReasoning(fullText);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
class DiffProcessor {
|
|
1225
|
+
previousDiff = null;
|
|
1226
|
+
onMessage = null;
|
|
1227
|
+
constructor(onMessage) {
|
|
1228
|
+
this.onMessage = onMessage || null;
|
|
1229
|
+
}
|
|
1230
|
+
/**
|
|
1231
|
+
* Process a turn_diff message and check if the unified_diff has changed
|
|
1232
|
+
*/
|
|
1233
|
+
processDiff(unifiedDiff) {
|
|
1234
|
+
if (this.previousDiff !== unifiedDiff) {
|
|
1235
|
+
api.logger.debug("[DiffProcessor] Unified diff changed, sending CodexDiff tool call");
|
|
1236
|
+
const callId = node_crypto.randomUUID();
|
|
1237
|
+
const toolCall = {
|
|
1238
|
+
type: "tool-call",
|
|
1239
|
+
name: "CodexDiff",
|
|
1240
|
+
callId,
|
|
1241
|
+
input: {
|
|
1242
|
+
unified_diff: unifiedDiff
|
|
1243
|
+
},
|
|
1244
|
+
id: node_crypto.randomUUID()
|
|
1245
|
+
};
|
|
1246
|
+
this.onMessage?.(toolCall);
|
|
1247
|
+
const toolResult = {
|
|
1248
|
+
type: "tool-call-result",
|
|
1249
|
+
callId,
|
|
1250
|
+
output: {
|
|
1251
|
+
status: "completed"
|
|
1252
|
+
},
|
|
1253
|
+
id: node_crypto.randomUUID()
|
|
1254
|
+
};
|
|
1255
|
+
this.onMessage?.(toolResult);
|
|
1256
|
+
}
|
|
1257
|
+
this.previousDiff = unifiedDiff;
|
|
1258
|
+
api.logger.debug("[DiffProcessor] Updated stored diff");
|
|
1259
|
+
}
|
|
1260
|
+
/**
|
|
1261
|
+
* Reset the processor state (called on task_complete or turn_aborted)
|
|
1262
|
+
*/
|
|
1263
|
+
reset() {
|
|
1264
|
+
api.logger.debug("[DiffProcessor] Resetting diff state");
|
|
1265
|
+
this.previousDiff = null;
|
|
1266
|
+
}
|
|
1267
|
+
/**
|
|
1268
|
+
* Set the message callback for sending messages directly
|
|
1269
|
+
*/
|
|
1270
|
+
setMessageCallback(callback) {
|
|
1271
|
+
this.onMessage = callback;
|
|
1272
|
+
}
|
|
1273
|
+
/**
|
|
1274
|
+
* Get the current diff value
|
|
1275
|
+
*/
|
|
1276
|
+
getCurrentDiff() {
|
|
1277
|
+
return this.previousDiff;
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
function shouldEmitCodexReadyOnTurnEnd(opts) {
|
|
1282
|
+
return !opts.shouldExit && opts.exitReason !== "switch";
|
|
1283
|
+
}
|
|
1284
|
+
function shouldEmitCodexReadyAfterWait(opts) {
|
|
1285
|
+
if (opts.readyAlreadySent) {
|
|
1286
|
+
return false;
|
|
1287
|
+
}
|
|
1288
|
+
return !opts.shouldExit && !opts.pending && opts.queueSize === 0;
|
|
1289
|
+
}
|
|
1290
|
+
function registerCodexRemoteRpcHandlers(clientSession, handlers) {
|
|
1291
|
+
clientSession.rpcHandlerManager.registerHandler("abort", handlers.handleAbort);
|
|
1292
|
+
clientSession.rpcHandlerManager.registerHandler("switch", handlers.handleSwitchToLocal);
|
|
1293
|
+
registerKillSessionHandler.registerKillSessionHandler(clientSession.rpcHandlerManager, handlers.handleKillSession);
|
|
1294
|
+
}
|
|
1295
|
+
async function codexRemoteLauncher(session) {
|
|
1296
|
+
const messageBuffer = new registerKillSessionHandler.MessageBuffer();
|
|
1297
|
+
const hasTTY = process.stdout.isTTY && process.stdin.isTTY;
|
|
1298
|
+
let inkInstance = null;
|
|
1299
|
+
let exitReason = null;
|
|
1300
|
+
let shouldExit = false;
|
|
1301
|
+
let abortController = new AbortController();
|
|
1302
|
+
let storedSessionIdForResume = null;
|
|
1303
|
+
const client = new CodexMcpClient();
|
|
1304
|
+
let pendingModeResumeFile = null;
|
|
1305
|
+
const permissionHandler = new CodexPermissionHandler(session.client);
|
|
1306
|
+
const selectionHandler = new CodexSelectionHandler(session.client);
|
|
1307
|
+
const reasoningProcessor = new ReasoningProcessor((message) => {
|
|
1308
|
+
session.runtimeSession.sendCodexMessage(message);
|
|
1309
|
+
});
|
|
1310
|
+
const diffProcessor = new DiffProcessor((message) => {
|
|
1311
|
+
session.runtimeSession.sendCodexMessage(message);
|
|
1312
|
+
});
|
|
1313
|
+
const handleSwitchToLocal = async () => {
|
|
1314
|
+
api.logger.debug("[Codex] Switching to local from RPC");
|
|
1315
|
+
exitReason = "switch";
|
|
1316
|
+
shouldExit = true;
|
|
1317
|
+
await handleAbort();
|
|
1318
|
+
};
|
|
1319
|
+
client.setPermissionHandler(permissionHandler);
|
|
1320
|
+
client.setSelectionHandler(selectionHandler);
|
|
1321
|
+
session.setPendingInteractionSuperseder((reason = BaseReasoningProcessor.INTERACTION_SUPERSEDED_ERROR) => {
|
|
1322
|
+
const supersededPermissions = permissionHandler.supersedePendingRequests(reason);
|
|
1323
|
+
const supersededSelections = selectionHandler.supersedePendingRequests(reason);
|
|
1324
|
+
const total = supersededPermissions + supersededSelections;
|
|
1325
|
+
if (total > 0) {
|
|
1326
|
+
const message = "Previous pending interaction was canceled because a new user message arrived. Processing the latest message instead.";
|
|
1327
|
+
messageBuffer.addMessage(message, "status");
|
|
1328
|
+
session.runtimeSession.sendSessionEvent({ type: "message", message });
|
|
1329
|
+
}
|
|
1330
|
+
return total > 0;
|
|
1331
|
+
});
|
|
1332
|
+
const handleClientSwap = (clientSession) => {
|
|
1333
|
+
permissionHandler.updateSession(clientSession);
|
|
1334
|
+
selectionHandler.updateSession(clientSession);
|
|
1335
|
+
registerCodexRemoteRpcHandlers(clientSession, {
|
|
1336
|
+
handleAbort,
|
|
1337
|
+
handleSwitchToLocal,
|
|
1338
|
+
handleKillSession
|
|
1339
|
+
});
|
|
1340
|
+
};
|
|
1341
|
+
session.addClientSwapCallback(handleClientSwap);
|
|
1342
|
+
const sendReady = () => {
|
|
1343
|
+
session.runtimeSession.sendSessionEvent({ type: "ready" });
|
|
1344
|
+
try {
|
|
1345
|
+
session.api.push().sendToAllDevices(
|
|
1346
|
+
"It's ready!",
|
|
1347
|
+
"Codex is waiting for your command",
|
|
1348
|
+
{ sessionId: session.runtimeSession.sessionId }
|
|
1349
|
+
);
|
|
1350
|
+
} catch (pushError) {
|
|
1351
|
+
api.logger.debug("[Codex] Failed to send ready push", pushError);
|
|
1352
|
+
}
|
|
1353
|
+
};
|
|
1354
|
+
async function handleAbort() {
|
|
1355
|
+
api.logger.debug("[Codex] Abort requested - stopping current task");
|
|
1356
|
+
try {
|
|
1357
|
+
if (client.hasActiveSession()) {
|
|
1358
|
+
storedSessionIdForResume = client.storeSessionForResume();
|
|
1359
|
+
}
|
|
1360
|
+
abortController.abort();
|
|
1361
|
+
reasoningProcessor.abort();
|
|
1362
|
+
} catch (error) {
|
|
1363
|
+
api.logger.debug("[Codex] Error during abort:", error);
|
|
1364
|
+
} finally {
|
|
1365
|
+
abortController = new AbortController();
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
async function handleKillSession() {
|
|
1369
|
+
api.logger.debug("[Codex] Kill session requested - terminating process");
|
|
1370
|
+
await handleAbort();
|
|
1371
|
+
try {
|
|
1372
|
+
session.runtimeSession.updateMetadata((currentMetadata) => ({
|
|
1373
|
+
...currentMetadata,
|
|
1374
|
+
lifecycleState: "archived",
|
|
1375
|
+
lifecycleStateSince: Date.now(),
|
|
1376
|
+
archivedBy: "cli",
|
|
1377
|
+
archiveReason: "User terminated"
|
|
1378
|
+
}));
|
|
1379
|
+
session.runtimeSession.sendSessionDeath();
|
|
1380
|
+
await session.runtimeSession.flush();
|
|
1381
|
+
await session.runtimeSession.close();
|
|
1382
|
+
await client.forceCloseSession();
|
|
1383
|
+
index.stopCaffeinate();
|
|
1384
|
+
process.exit(0);
|
|
1385
|
+
} catch (error) {
|
|
1386
|
+
api.logger.debug("[Codex] Error during session termination:", error);
|
|
1387
|
+
process.exit(1);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
if (hasTTY) {
|
|
1391
|
+
console.clear();
|
|
1392
|
+
inkInstance = ink.render(React.createElement(CodexDisplay, {
|
|
1393
|
+
messageBuffer,
|
|
1394
|
+
logPath: process.env.DEBUG ? session.logPath : void 0,
|
|
1395
|
+
title: "Remote Mode - Codex Messages",
|
|
1396
|
+
onExit: async () => {
|
|
1397
|
+
api.logger.debug("[Codex] Switching to exit from local keyboard");
|
|
1398
|
+
exitReason = "exit";
|
|
1399
|
+
shouldExit = true;
|
|
1400
|
+
await handleAbort();
|
|
1401
|
+
},
|
|
1402
|
+
onSwitchToLocal: async () => {
|
|
1403
|
+
api.logger.debug("[Codex] Switching to local from local keyboard");
|
|
1404
|
+
exitReason = "switch";
|
|
1405
|
+
shouldExit = true;
|
|
1406
|
+
await handleAbort();
|
|
1407
|
+
}
|
|
1408
|
+
}), {
|
|
1409
|
+
exitOnCtrlC: false,
|
|
1410
|
+
patchConsole: false
|
|
1411
|
+
});
|
|
1412
|
+
}
|
|
1413
|
+
if (hasTTY) {
|
|
1414
|
+
process.stdin.resume();
|
|
1415
|
+
if (process.stdin.isTTY) {
|
|
1416
|
+
process.stdin.setRawMode(true);
|
|
1417
|
+
}
|
|
1418
|
+
process.stdin.setEncoding("utf8");
|
|
1419
|
+
}
|
|
1420
|
+
registerCodexRemoteRpcHandlers(session.client, {
|
|
1421
|
+
handleAbort,
|
|
1422
|
+
handleSwitchToLocal,
|
|
1423
|
+
handleKillSession
|
|
1424
|
+
});
|
|
1425
|
+
client.setHandler((msg) => {
|
|
1426
|
+
api.logger.debug(`[Codex] Message: ${JSON.stringify(msg)}`);
|
|
1427
|
+
if (msg.type === "agent_message") {
|
|
1428
|
+
messageBuffer.addMessage(msg.message, "assistant");
|
|
1429
|
+
} else if (msg.type === "agent_reasoning") {
|
|
1430
|
+
messageBuffer.addMessage(`[Thinking] ${msg.text.substring(0, 100)}...`, "system");
|
|
1431
|
+
} else if (msg.type === "exec_command_begin") {
|
|
1432
|
+
messageBuffer.addMessage(`Executing: ${msg.command}`, "tool");
|
|
1433
|
+
} else if (msg.type === "exec_command_end") {
|
|
1434
|
+
const output = msg.output || msg.error || "Command completed";
|
|
1435
|
+
const truncatedOutput = output.substring(0, 200);
|
|
1436
|
+
messageBuffer.addMessage(`Result: ${truncatedOutput}${output.length > 200 ? "..." : ""}`, "result");
|
|
1437
|
+
} else if (msg.type === "task_started") {
|
|
1438
|
+
messageBuffer.addMessage("Starting task...", "status");
|
|
1439
|
+
session.onThinkingChange(true);
|
|
1440
|
+
} else if (msg.type === "task_complete") {
|
|
1441
|
+
messageBuffer.addMessage("Task completed", "status");
|
|
1442
|
+
session.onThinkingChange(false);
|
|
1443
|
+
diffProcessor.reset();
|
|
1444
|
+
if (shouldEmitCodexReadyOnTurnEnd({ shouldExit, exitReason })) {
|
|
1445
|
+
sendReady();
|
|
1446
|
+
readyAlreadySent = true;
|
|
1447
|
+
}
|
|
1448
|
+
} else if (msg.type === "turn_aborted") {
|
|
1449
|
+
messageBuffer.addMessage("Turn aborted", "status");
|
|
1450
|
+
session.onThinkingChange(false);
|
|
1451
|
+
diffProcessor.reset();
|
|
1452
|
+
if (shouldEmitCodexReadyOnTurnEnd({ shouldExit, exitReason })) {
|
|
1453
|
+
sendReady();
|
|
1454
|
+
readyAlreadySent = true;
|
|
1455
|
+
}
|
|
1456
|
+
}
|
|
1457
|
+
if (msg.type === "agent_reasoning_section_break") {
|
|
1458
|
+
reasoningProcessor.handleSectionBreak();
|
|
1459
|
+
}
|
|
1460
|
+
if (msg.type === "agent_reasoning_delta") {
|
|
1461
|
+
reasoningProcessor.processDelta(msg.delta);
|
|
1462
|
+
}
|
|
1463
|
+
if (msg.type === "agent_reasoning") {
|
|
1464
|
+
reasoningProcessor.complete(msg.text);
|
|
1465
|
+
}
|
|
1466
|
+
if (msg.type === "agent_message") {
|
|
1467
|
+
session.runtimeSession.sendCodexMessage({
|
|
1468
|
+
type: "message",
|
|
1469
|
+
message: msg.message,
|
|
1470
|
+
id: node_crypto.randomUUID()
|
|
1471
|
+
});
|
|
1472
|
+
}
|
|
1473
|
+
if (msg.type === "exec_command_begin" || msg.type === "exec_approval_request") {
|
|
1474
|
+
const { call_id, type, ...inputs } = msg;
|
|
1475
|
+
session.runtimeSession.sendCodexMessage({
|
|
1476
|
+
type: "tool-call",
|
|
1477
|
+
name: "CodexBash",
|
|
1478
|
+
callId: call_id,
|
|
1479
|
+
input: inputs,
|
|
1480
|
+
id: node_crypto.randomUUID()
|
|
1481
|
+
});
|
|
1482
|
+
}
|
|
1483
|
+
if (msg.type === "exec_command_end") {
|
|
1484
|
+
const { call_id, type, ...output } = msg;
|
|
1485
|
+
session.runtimeSession.sendCodexMessage({
|
|
1486
|
+
type: "tool-call-result",
|
|
1487
|
+
callId: call_id,
|
|
1488
|
+
output,
|
|
1489
|
+
id: node_crypto.randomUUID()
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
if (msg.type === "token_count") {
|
|
1493
|
+
session.runtimeSession.sendCodexMessage({
|
|
1494
|
+
...msg,
|
|
1495
|
+
id: node_crypto.randomUUID()
|
|
1496
|
+
});
|
|
1497
|
+
}
|
|
1498
|
+
if (msg.type === "patch_apply_begin") {
|
|
1499
|
+
const { call_id, auto_approved, changes } = msg;
|
|
1500
|
+
const changeCount = Object.keys(changes).length;
|
|
1501
|
+
const filesMsg = changeCount === 1 ? "1 file" : `${changeCount} files`;
|
|
1502
|
+
messageBuffer.addMessage(`Modifying ${filesMsg}...`, "tool");
|
|
1503
|
+
session.runtimeSession.sendCodexMessage({
|
|
1504
|
+
type: "tool-call",
|
|
1505
|
+
name: "CodexPatch",
|
|
1506
|
+
callId: call_id,
|
|
1507
|
+
input: { auto_approved, changes },
|
|
1508
|
+
id: node_crypto.randomUUID()
|
|
1509
|
+
});
|
|
1510
|
+
}
|
|
1511
|
+
if (msg.type === "patch_apply_end") {
|
|
1512
|
+
const { call_id, stdout, stderr, success } = msg;
|
|
1513
|
+
if (success) {
|
|
1514
|
+
messageBuffer.addMessage((stdout || "Files modified successfully").substring(0, 200), "result");
|
|
1515
|
+
} else {
|
|
1516
|
+
messageBuffer.addMessage(`Error: ${(stderr || "Failed to modify files").substring(0, 200)}`, "result");
|
|
1517
|
+
}
|
|
1518
|
+
session.runtimeSession.sendCodexMessage({
|
|
1519
|
+
type: "tool-call-result",
|
|
1520
|
+
callId: call_id,
|
|
1521
|
+
output: { stdout, stderr, success },
|
|
1522
|
+
id: node_crypto.randomUUID()
|
|
1523
|
+
});
|
|
1524
|
+
}
|
|
1525
|
+
if (msg.type === "turn_diff" && msg.unified_diff) {
|
|
1526
|
+
diffProcessor.processDiff(msg.unified_diff);
|
|
1527
|
+
}
|
|
1528
|
+
});
|
|
1529
|
+
let readyAlreadySent = false;
|
|
1530
|
+
try {
|
|
1531
|
+
await client.connect();
|
|
1532
|
+
let wasCreated = false;
|
|
1533
|
+
let currentModeHash = null;
|
|
1534
|
+
let pending = null;
|
|
1535
|
+
while (!shouldExit) {
|
|
1536
|
+
let message = pending;
|
|
1537
|
+
pending = null;
|
|
1538
|
+
if (!message) {
|
|
1539
|
+
const waitSignal = abortController.signal;
|
|
1540
|
+
const batch = await session.queue.waitForMessagesAndGetAsString(waitSignal);
|
|
1541
|
+
if (!batch) {
|
|
1542
|
+
if (waitSignal.aborted && !shouldExit) {
|
|
1543
|
+
continue;
|
|
1544
|
+
}
|
|
1545
|
+
break;
|
|
1546
|
+
}
|
|
1547
|
+
message = batch;
|
|
1548
|
+
}
|
|
1549
|
+
if (!message) {
|
|
1550
|
+
break;
|
|
1551
|
+
}
|
|
1552
|
+
if (wasCreated && currentModeHash && message.hash !== currentModeHash) {
|
|
1553
|
+
messageBuffer.addMessage("\u2550".repeat(40), "status");
|
|
1554
|
+
messageBuffer.addMessage("Starting new Codex session (mode changed)...", "status");
|
|
1555
|
+
const prevSessionId = client.getSessionId();
|
|
1556
|
+
pendingModeResumeFile = findCodexResumeFile(prevSessionId);
|
|
1557
|
+
client.clearSession();
|
|
1558
|
+
session.clearSessionId();
|
|
1559
|
+
wasCreated = false;
|
|
1560
|
+
currentModeHash = null;
|
|
1561
|
+
pending = message;
|
|
1562
|
+
permissionHandler.reset();
|
|
1563
|
+
selectionHandler.reset();
|
|
1564
|
+
reasoningProcessor.abort();
|
|
1565
|
+
diffProcessor.reset();
|
|
1566
|
+
session.onThinkingChange(false);
|
|
1567
|
+
continue;
|
|
1568
|
+
}
|
|
1569
|
+
currentModeHash = message.hash;
|
|
1570
|
+
readyAlreadySent = false;
|
|
1571
|
+
messageBuffer.addMessage(message.message, "user");
|
|
1572
|
+
try {
|
|
1573
|
+
const approvalPolicy = (() => {
|
|
1574
|
+
switch (message.mode.permissionMode) {
|
|
1575
|
+
case "default":
|
|
1576
|
+
return "untrusted";
|
|
1577
|
+
case "read-only":
|
|
1578
|
+
return "never";
|
|
1579
|
+
case "safe-yolo":
|
|
1580
|
+
return "on-failure";
|
|
1581
|
+
case "yolo":
|
|
1582
|
+
return "on-failure";
|
|
1583
|
+
case "bypassPermissions":
|
|
1584
|
+
return "on-failure";
|
|
1585
|
+
case "acceptEdits":
|
|
1586
|
+
return "on-request";
|
|
1587
|
+
case "plan":
|
|
1588
|
+
return "untrusted";
|
|
1589
|
+
default:
|
|
1590
|
+
return "untrusted";
|
|
1591
|
+
}
|
|
1592
|
+
})();
|
|
1593
|
+
const sandbox = (() => {
|
|
1594
|
+
switch (message.mode.permissionMode) {
|
|
1595
|
+
case "default":
|
|
1596
|
+
return "workspace-write";
|
|
1597
|
+
case "read-only":
|
|
1598
|
+
return "read-only";
|
|
1599
|
+
case "safe-yolo":
|
|
1600
|
+
return "workspace-write";
|
|
1601
|
+
case "yolo":
|
|
1602
|
+
return "danger-full-access";
|
|
1603
|
+
case "bypassPermissions":
|
|
1604
|
+
return "danger-full-access";
|
|
1605
|
+
case "acceptEdits":
|
|
1606
|
+
return "workspace-write";
|
|
1607
|
+
case "plan":
|
|
1608
|
+
return "workspace-write";
|
|
1609
|
+
default:
|
|
1610
|
+
return "workspace-write";
|
|
1611
|
+
}
|
|
1612
|
+
})();
|
|
1613
|
+
if (!wasCreated) {
|
|
1614
|
+
const startConfig = {
|
|
1615
|
+
prompt: message.message,
|
|
1616
|
+
sandbox,
|
|
1617
|
+
"approval-policy": approvalPolicy,
|
|
1618
|
+
config: {}
|
|
1619
|
+
};
|
|
1620
|
+
if (message.mode.model) {
|
|
1621
|
+
startConfig.model = message.mode.model;
|
|
1622
|
+
}
|
|
1623
|
+
let resumeFile = null;
|
|
1624
|
+
if (pendingModeResumeFile) {
|
|
1625
|
+
resumeFile = pendingModeResumeFile;
|
|
1626
|
+
pendingModeResumeFile = null;
|
|
1627
|
+
} else if (storedSessionIdForResume) {
|
|
1628
|
+
resumeFile = findCodexResumeFile(storedSessionIdForResume);
|
|
1629
|
+
storedSessionIdForResume = null;
|
|
1630
|
+
} else if (session.sessionId) {
|
|
1631
|
+
resumeFile = findCodexResumeFile(session.sessionId);
|
|
1632
|
+
}
|
|
1633
|
+
if (resumeFile) {
|
|
1634
|
+
startConfig.config = {
|
|
1635
|
+
...startConfig.config || {},
|
|
1636
|
+
experimental_resume: resumeFile
|
|
1637
|
+
};
|
|
1638
|
+
}
|
|
1639
|
+
await client.startSession(startConfig, { signal: abortController.signal });
|
|
1640
|
+
const currentSessionId = client.getSessionId();
|
|
1641
|
+
if (currentSessionId) {
|
|
1642
|
+
session.onSessionFound(currentSessionId);
|
|
1643
|
+
}
|
|
1644
|
+
wasCreated = true;
|
|
1645
|
+
} else {
|
|
1646
|
+
await client.continueSession(message.message, { signal: abortController.signal });
|
|
1647
|
+
}
|
|
1648
|
+
} catch (error) {
|
|
1649
|
+
api.logger.warn("Error in codex session:", error);
|
|
1650
|
+
const isAbortError = error instanceof Error && error.name === "AbortError";
|
|
1651
|
+
const isExpectedInterruption = isAbortError || abortController.signal.aborted || shouldExit || exitReason === "switch";
|
|
1652
|
+
if (isExpectedInterruption) {
|
|
1653
|
+
messageBuffer.addMessage("Aborted by user", "status");
|
|
1654
|
+
session.runtimeSession.sendSessionEvent({ type: "message", message: "Aborted by user" });
|
|
1655
|
+
} else {
|
|
1656
|
+
messageBuffer.addMessage("Process exited unexpectedly", "status");
|
|
1657
|
+
session.runtimeSession.sendSessionEvent({ type: "message", message: "Process exited unexpectedly" });
|
|
1658
|
+
if (client.hasActiveSession()) {
|
|
1659
|
+
storedSessionIdForResume = client.storeSessionForResume();
|
|
1660
|
+
}
|
|
1661
|
+
}
|
|
1662
|
+
} finally {
|
|
1663
|
+
permissionHandler.reset();
|
|
1664
|
+
selectionHandler.reset();
|
|
1665
|
+
reasoningProcessor.abort();
|
|
1666
|
+
diffProcessor.reset();
|
|
1667
|
+
session.onThinkingChange(false);
|
|
1668
|
+
if (shouldEmitCodexReadyAfterWait({
|
|
1669
|
+
pending: Boolean(pending),
|
|
1670
|
+
queueSize: session.queue.size(),
|
|
1671
|
+
shouldExit,
|
|
1672
|
+
readyAlreadySent
|
|
1673
|
+
})) {
|
|
1674
|
+
sendReady();
|
|
1675
|
+
readyAlreadySent = true;
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
} finally {
|
|
1680
|
+
client.setHandler(null);
|
|
1681
|
+
client.setPermissionHandler(null);
|
|
1682
|
+
client.setSelectionHandler(null);
|
|
1683
|
+
if (exitReason === "switch") {
|
|
1684
|
+
api.logger.debug("[Codex] Remote launcher exiting via switch; waiting for MCP transport disconnect");
|
|
1685
|
+
await client.disconnect();
|
|
1686
|
+
} else {
|
|
1687
|
+
await client.forceCloseSession();
|
|
1688
|
+
}
|
|
1689
|
+
permissionHandler.reset();
|
|
1690
|
+
selectionHandler.reset();
|
|
1691
|
+
session.setPendingInteractionSuperseder(null);
|
|
1692
|
+
session.removeClientSwapCallback(handleClientSwap);
|
|
1693
|
+
if (process.stdin.isTTY) {
|
|
1694
|
+
try {
|
|
1695
|
+
process.stdin.setRawMode(false);
|
|
1696
|
+
} catch {
|
|
1697
|
+
}
|
|
1698
|
+
}
|
|
1699
|
+
if (hasTTY) {
|
|
1700
|
+
try {
|
|
1701
|
+
process.stdin.pause();
|
|
1702
|
+
} catch {
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
session.client.rpcHandlerManager.registerHandler("abort", async () => {
|
|
1706
|
+
});
|
|
1707
|
+
session.client.rpcHandlerManager.registerHandler("switch", async () => {
|
|
1708
|
+
});
|
|
1709
|
+
if (inkInstance) {
|
|
1710
|
+
inkInstance.unmount();
|
|
1711
|
+
}
|
|
1712
|
+
messageBuffer.clear();
|
|
1713
|
+
}
|
|
1714
|
+
api.logger.debug(`[Codex] Remote launcher returning: ${exitReason || "exit"}`);
|
|
1715
|
+
return exitReason || "exit";
|
|
1716
|
+
}
|
|
1717
|
+
|
|
1718
|
+
async function codexLoop(opts) {
|
|
1719
|
+
let mode = opts.startingMode ?? "local";
|
|
1720
|
+
opts.session.onModeChange(mode);
|
|
1721
|
+
while (true) {
|
|
1722
|
+
api.logger.debug(`[codex-loop] Iteration with mode: ${mode}`);
|
|
1723
|
+
if (mode === "local") {
|
|
1724
|
+
const result = await codexLocalLauncher(opts.session);
|
|
1725
|
+
if (result.type === "switch") {
|
|
1726
|
+
mode = "remote";
|
|
1727
|
+
opts.session.onModeChange(mode);
|
|
1728
|
+
continue;
|
|
1729
|
+
}
|
|
1730
|
+
return result.code;
|
|
1731
|
+
}
|
|
1732
|
+
const reason = await codexRemoteLauncher(opts.session);
|
|
1733
|
+
if (reason === "switch") {
|
|
1734
|
+
mode = "local";
|
|
1735
|
+
opts.session.onModeChange(mode);
|
|
1736
|
+
continue;
|
|
1737
|
+
}
|
|
1738
|
+
return 0;
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
|
|
1742
|
+
async function runCodex(opts) {
|
|
1743
|
+
const sessionTag = node_crypto.randomUUID();
|
|
1744
|
+
api.connectionState.setBackend("Codex");
|
|
1745
|
+
if (opts.startedBy === "daemon" && opts.startingMode === "local") {
|
|
1746
|
+
throw new Error("Daemon-spawned Codex sessions cannot use local mode.");
|
|
1747
|
+
}
|
|
1748
|
+
const api$1 = await api.ApiClient.create(opts.credentials);
|
|
1749
|
+
api.logger.debug(`[codex] Starting with options: startedBy=${opts.startedBy || "terminal"}, startingMode=${opts.startingMode || "local"}`);
|
|
1750
|
+
const settings = await persistence.readSettings();
|
|
1751
|
+
const machineId = settings?.machineId;
|
|
1752
|
+
if (!machineId) {
|
|
1753
|
+
console.error("[START] No machine ID found in settings.");
|
|
1754
|
+
process.exit(1);
|
|
1755
|
+
}
|
|
1756
|
+
await api$1.getOrCreateMachine({
|
|
1757
|
+
machineId,
|
|
1758
|
+
metadata: index.initialMachineMetadata
|
|
1759
|
+
});
|
|
1760
|
+
const { state, metadata } = BaseReasoningProcessor.createSessionMetadata({
|
|
1761
|
+
flavor: "codex",
|
|
1762
|
+
machineId,
|
|
1763
|
+
startedBy: opts.startedBy
|
|
1764
|
+
});
|
|
1765
|
+
const response = await api$1.getOrCreateSession({ tag: sessionTag, metadata, state });
|
|
1766
|
+
let sessionClient;
|
|
1767
|
+
let codexSession = null;
|
|
1768
|
+
const { session: initialSession, reconnectionHandle } = BaseReasoningProcessor.setupOfflineReconnection({
|
|
1769
|
+
api: api$1,
|
|
1770
|
+
sessionTag,
|
|
1771
|
+
metadata,
|
|
1772
|
+
state,
|
|
1773
|
+
response,
|
|
1774
|
+
onSessionSwap: (newSession) => {
|
|
1775
|
+
sessionClient = newSession;
|
|
1776
|
+
codexSession?.updateClient(newSession);
|
|
1777
|
+
}
|
|
1778
|
+
});
|
|
1779
|
+
sessionClient = initialSession;
|
|
1780
|
+
if (response) {
|
|
1781
|
+
try {
|
|
1782
|
+
const result = await index.notifyDaemonSessionStarted(response.id, metadata);
|
|
1783
|
+
if (result.error) {
|
|
1784
|
+
api.logger.debug("[START] Failed to report to daemon:", result.error);
|
|
1785
|
+
}
|
|
1786
|
+
} catch (error) {
|
|
1787
|
+
api.logger.debug("[START] Failed to report to daemon:", error);
|
|
1788
|
+
}
|
|
1789
|
+
}
|
|
1790
|
+
const messageQueue = new registerKillSessionHandler.MessageQueue2((mode) => registerKillSessionHandler.hashObject({
|
|
1791
|
+
permissionMode: mode.permissionMode,
|
|
1792
|
+
model: mode.model
|
|
1793
|
+
}));
|
|
1794
|
+
let currentPermissionMode;
|
|
1795
|
+
let currentModel;
|
|
1796
|
+
sessionClient.onUserMessage((message) => {
|
|
1797
|
+
let messagePermissionMode = currentPermissionMode;
|
|
1798
|
+
if (message.meta?.permissionMode) {
|
|
1799
|
+
messagePermissionMode = message.meta.permissionMode;
|
|
1800
|
+
currentPermissionMode = messagePermissionMode;
|
|
1801
|
+
}
|
|
1802
|
+
let messageModel = currentModel;
|
|
1803
|
+
if (message.meta?.hasOwnProperty("model")) {
|
|
1804
|
+
messageModel = message.meta.model || void 0;
|
|
1805
|
+
currentModel = messageModel;
|
|
1806
|
+
}
|
|
1807
|
+
if (opts.startingMode === "remote") {
|
|
1808
|
+
codexSession?.supersedePendingInteractions(BaseReasoningProcessor.INTERACTION_SUPERSEDED_ERROR);
|
|
1809
|
+
}
|
|
1810
|
+
messageQueue.push(message.content.text, {
|
|
1811
|
+
permissionMode: messagePermissionMode || "default",
|
|
1812
|
+
model: messageModel
|
|
1813
|
+
});
|
|
1814
|
+
});
|
|
1815
|
+
codexSession = new CodexSession({
|
|
1816
|
+
api: api$1,
|
|
1817
|
+
client: sessionClient,
|
|
1818
|
+
path: process.cwd(),
|
|
1819
|
+
logPath: api.logger.logFilePath,
|
|
1820
|
+
sessionId: null,
|
|
1821
|
+
mode: opts.startingMode ?? "local",
|
|
1822
|
+
messageQueue,
|
|
1823
|
+
codexArgs: opts.codexArgs,
|
|
1824
|
+
onModeChange: (mode) => {
|
|
1825
|
+
sessionClient.sendSessionEvent({ type: "switch", mode });
|
|
1826
|
+
sessionClient.updateAgentState((currentState) => ({
|
|
1827
|
+
...currentState,
|
|
1828
|
+
controlledByUser: mode === "local"
|
|
1829
|
+
}));
|
|
1830
|
+
}
|
|
1831
|
+
});
|
|
1832
|
+
try {
|
|
1833
|
+
const exitCode = await codexLoop({
|
|
1834
|
+
session: codexSession,
|
|
1835
|
+
startingMode: opts.startingMode ?? "local"
|
|
1836
|
+
});
|
|
1837
|
+
sessionClient.sendSessionDeath();
|
|
1838
|
+
await sessionClient.flush();
|
|
1839
|
+
await sessionClient.close();
|
|
1840
|
+
index.stopCaffeinate();
|
|
1841
|
+
process.exit(exitCode);
|
|
1842
|
+
} finally {
|
|
1843
|
+
reconnectionHandle?.cancel();
|
|
1844
|
+
codexSession.cleanup();
|
|
1845
|
+
}
|
|
1846
|
+
}
|
|
1847
|
+
|
|
1848
|
+
exports.runCodex = runCodex;
|