@ccpocket/bridge 0.2.0 → 1.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/codex-process.d.ts +63 -6
- package/dist/codex-process.js +1081 -205
- package/dist/codex-process.js.map +1 -1
- package/dist/parser.d.ts +0 -2
- package/dist/parser.js.map +1 -1
- package/dist/session.js +11 -4
- package/dist/session.js.map +1 -1
- package/dist/websocket.js +44 -36
- package/dist/websocket.js.map +1 -1
- package/package.json +1 -1
package/dist/codex-process.js
CHANGED
|
@@ -3,17 +3,29 @@ import { randomUUID } from "node:crypto";
|
|
|
3
3
|
import { tmpdir } from "node:os";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { rm, writeFile } from "node:fs/promises";
|
|
6
|
-
import {
|
|
6
|
+
import { spawn } from "node:child_process";
|
|
7
7
|
export class CodexProcess extends EventEmitter {
|
|
8
|
-
|
|
9
|
-
thread = null;
|
|
8
|
+
child = null;
|
|
10
9
|
_status = "starting";
|
|
11
10
|
_threadId = null;
|
|
12
11
|
stopped = false;
|
|
13
12
|
startModel;
|
|
14
|
-
// User input channel
|
|
15
13
|
inputResolve = null;
|
|
16
|
-
|
|
14
|
+
pendingTurnId = null;
|
|
15
|
+
pendingTurnCompletion = null;
|
|
16
|
+
pendingApprovals = new Map();
|
|
17
|
+
pendingUserInputs = new Map();
|
|
18
|
+
lastTokenUsage = null;
|
|
19
|
+
rpcSeq = 1;
|
|
20
|
+
pendingRpc = new Map();
|
|
21
|
+
stdoutBuffer = "";
|
|
22
|
+
// Collaboration mode & plan completion state
|
|
23
|
+
_approvalPolicy = "never";
|
|
24
|
+
_collaborationMode = "default";
|
|
25
|
+
lastPlanItemText = null;
|
|
26
|
+
pendingPlanCompletion = null;
|
|
27
|
+
/** Queued plan execution text when inputResolve wasn't ready at approval time. */
|
|
28
|
+
_pendingPlanInput = null;
|
|
17
29
|
get status() {
|
|
18
30
|
return this._status;
|
|
19
31
|
}
|
|
@@ -24,67 +36,112 @@ export class CodexProcess extends EventEmitter {
|
|
|
24
36
|
return this._threadId;
|
|
25
37
|
}
|
|
26
38
|
get isRunning() {
|
|
27
|
-
return this.
|
|
39
|
+
return this.child !== null;
|
|
28
40
|
}
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
get approvalPolicy() {
|
|
42
|
+
return this._approvalPolicy;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Update approval policy at runtime.
|
|
46
|
+
* Takes effect on the next `turn/start` RPC call.
|
|
47
|
+
*/
|
|
48
|
+
setApprovalPolicy(policy) {
|
|
49
|
+
this._approvalPolicy = policy;
|
|
50
|
+
console.log(`[codex-process] Approval policy changed to: ${policy}`);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Set collaboration mode ("plan" or "default").
|
|
54
|
+
* Takes effect on the next `turn/start` RPC call.
|
|
55
|
+
*/
|
|
56
|
+
setCollaborationMode(mode) {
|
|
57
|
+
this._collaborationMode = mode;
|
|
58
|
+
console.log(`[codex-process] Collaboration mode changed to: ${mode}`);
|
|
59
|
+
}
|
|
60
|
+
get collaborationMode() {
|
|
61
|
+
return this._collaborationMode;
|
|
32
62
|
}
|
|
33
63
|
start(projectPath, options) {
|
|
34
|
-
if (this.
|
|
64
|
+
if (this.child) {
|
|
35
65
|
this.stop();
|
|
36
66
|
}
|
|
37
67
|
this.stopped = false;
|
|
38
68
|
this._threadId = null;
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
skipGitRepoCheck: true,
|
|
45
|
-
...(options?.model ? { model: options.model } : {}),
|
|
46
|
-
...(options?.modelReasoningEffort ? { modelReasoningEffort: options.modelReasoningEffort } : {}),
|
|
47
|
-
...(options?.webSearchMode ? { webSearchMode: options.webSearchMode } : {}),
|
|
48
|
-
};
|
|
49
|
-
console.log(`[codex-process] Starting (cwd: ${projectPath}, sandbox: ${threadOpts.sandboxMode}, approval: ${threadOpts.approvalPolicy}, model: ${threadOpts.model ?? "default"}, reasoning: ${threadOpts.modelReasoningEffort ?? "default"}, network: ${threadOpts.networkAccessEnabled}, webSearch: ${threadOpts.webSearchMode ?? "default"})`);
|
|
50
|
-
this.thread = options?.threadId
|
|
51
|
-
? this.codex.resumeThread(options.threadId, threadOpts)
|
|
52
|
-
: this.codex.startThread(threadOpts);
|
|
53
|
-
this.setStatus("idle");
|
|
69
|
+
this.pendingTurnId = null;
|
|
70
|
+
this.pendingTurnCompletion = null;
|
|
71
|
+
this.pendingApprovals.clear();
|
|
72
|
+
this.pendingUserInputs.clear();
|
|
73
|
+
this.lastTokenUsage = null;
|
|
54
74
|
this.startModel = options?.model;
|
|
55
|
-
|
|
56
|
-
this.
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
75
|
+
this._approvalPolicy = options?.approvalPolicy ?? "never";
|
|
76
|
+
this._collaborationMode = options?.collaborationMode ?? "default";
|
|
77
|
+
this.lastPlanItemText = null;
|
|
78
|
+
this.pendingPlanCompletion = null;
|
|
79
|
+
this._pendingPlanInput = null;
|
|
80
|
+
console.log(`[codex-process] Starting app-server (cwd: ${projectPath}, sandbox: ${options?.sandboxMode ?? "workspace-write"}, approval: ${options?.approvalPolicy ?? "never"}, model: ${options?.model ?? "default"}, collaboration: ${this._collaborationMode})`);
|
|
81
|
+
const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
|
|
82
|
+
cwd: projectPath,
|
|
83
|
+
stdio: "pipe",
|
|
84
|
+
env: process.env,
|
|
85
|
+
});
|
|
86
|
+
this.child = child;
|
|
87
|
+
child.stdout.setEncoding("utf8");
|
|
88
|
+
child.stdout.on("data", (chunk) => {
|
|
89
|
+
this.handleStdoutChunk(chunk);
|
|
90
|
+
});
|
|
91
|
+
child.stderr.setEncoding("utf8");
|
|
92
|
+
child.stderr.on("data", (chunk) => {
|
|
93
|
+
const line = chunk.trim();
|
|
94
|
+
if (line) {
|
|
95
|
+
console.log(`[codex-process] stderr: ${line}`);
|
|
63
96
|
}
|
|
97
|
+
});
|
|
98
|
+
child.on("error", (err) => {
|
|
99
|
+
if (this.stopped)
|
|
100
|
+
return;
|
|
101
|
+
console.error("[codex-process] app-server process error:", err);
|
|
102
|
+
this.emitMessage({ type: "error", message: `Failed to start codex app-server: ${err.message}` });
|
|
64
103
|
this.setStatus("idle");
|
|
65
104
|
this.emit("exit", 1);
|
|
66
105
|
});
|
|
106
|
+
child.on("exit", (code) => {
|
|
107
|
+
const exitCode = code ?? 0;
|
|
108
|
+
this.child = null;
|
|
109
|
+
this.rejectAllPending(new Error("codex app-server exited"));
|
|
110
|
+
if (!this.stopped && exitCode !== 0) {
|
|
111
|
+
this.emitMessage({ type: "error", message: `codex app-server exited with code ${exitCode}` });
|
|
112
|
+
}
|
|
113
|
+
this.setStatus("idle");
|
|
114
|
+
this.emit("exit", code);
|
|
115
|
+
});
|
|
116
|
+
void this.bootstrap(projectPath, options);
|
|
67
117
|
}
|
|
68
118
|
stop() {
|
|
69
119
|
this.stopped = true;
|
|
70
|
-
if (this.pendingAbort) {
|
|
71
|
-
this.pendingAbort.abort();
|
|
72
|
-
this.pendingAbort = null;
|
|
73
|
-
}
|
|
74
|
-
// Unblock pending input wait
|
|
75
120
|
if (this.inputResolve) {
|
|
76
121
|
this.inputResolve({ text: "" });
|
|
77
122
|
this.inputResolve = null;
|
|
78
123
|
}
|
|
79
|
-
this.
|
|
124
|
+
this.pendingApprovals.clear();
|
|
125
|
+
this.pendingUserInputs.clear();
|
|
126
|
+
this.rejectAllPending(new Error("stopped"));
|
|
127
|
+
if (this.child) {
|
|
128
|
+
this.child.kill("SIGTERM");
|
|
129
|
+
this.child = null;
|
|
130
|
+
}
|
|
80
131
|
this.setStatus("idle");
|
|
81
132
|
console.log("[codex-process] Stopped");
|
|
82
133
|
}
|
|
83
134
|
interrupt() {
|
|
84
|
-
if (this.
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
135
|
+
if (!this._threadId || !this.pendingTurnId)
|
|
136
|
+
return;
|
|
137
|
+
void this.request("turn/interrupt", {
|
|
138
|
+
threadId: this._threadId,
|
|
139
|
+
turnId: this.pendingTurnId,
|
|
140
|
+
}).catch((err) => {
|
|
141
|
+
if (!this.stopped) {
|
|
142
|
+
console.warn(`[codex-process] turn/interrupt failed: ${err instanceof Error ? err.message : String(err)}`);
|
|
143
|
+
}
|
|
144
|
+
});
|
|
88
145
|
}
|
|
89
146
|
sendInput(text) {
|
|
90
147
|
if (!this.inputResolve) {
|
|
@@ -102,201 +159,744 @@ export class CodexProcess extends EventEmitter {
|
|
|
102
159
|
}
|
|
103
160
|
const resolve = this.inputResolve;
|
|
104
161
|
this.inputResolve = null;
|
|
105
|
-
resolve({
|
|
106
|
-
|
|
107
|
-
|
|
162
|
+
resolve({ text, images });
|
|
163
|
+
}
|
|
164
|
+
approve(toolUseId, _updatedInput) {
|
|
165
|
+
// Check if this is a plan completion approval
|
|
166
|
+
if (this.pendingPlanCompletion && toolUseId === this.pendingPlanCompletion.toolUseId) {
|
|
167
|
+
this.handlePlanApproved(_updatedInput);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
const pending = this.resolvePendingApproval(toolUseId);
|
|
171
|
+
if (!pending) {
|
|
172
|
+
console.log("[codex-process] approve() called but no pending permission requests");
|
|
173
|
+
return;
|
|
174
|
+
}
|
|
175
|
+
this.pendingApprovals.delete(pending.toolUseId);
|
|
176
|
+
this.respondToServerRequest(pending.requestId, {
|
|
177
|
+
decision: "accept",
|
|
178
|
+
});
|
|
179
|
+
this.emitToolResult(pending.toolUseId, "Approved");
|
|
180
|
+
if (this.pendingApprovals.size === 0) {
|
|
181
|
+
this.setStatus("running");
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
approveAlways(toolUseId) {
|
|
185
|
+
const pending = this.resolvePendingApproval(toolUseId);
|
|
186
|
+
if (!pending) {
|
|
187
|
+
console.log("[codex-process] approveAlways() called but no pending permission requests");
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
this.pendingApprovals.delete(pending.toolUseId);
|
|
191
|
+
this.respondToServerRequest(pending.requestId, {
|
|
192
|
+
decision: "accept",
|
|
193
|
+
acceptSettings: {
|
|
194
|
+
forSession: true,
|
|
195
|
+
},
|
|
108
196
|
});
|
|
197
|
+
this.emitToolResult(pending.toolUseId, "Approved (always)");
|
|
198
|
+
if (this.pendingApprovals.size === 0) {
|
|
199
|
+
this.setStatus("running");
|
|
200
|
+
}
|
|
109
201
|
}
|
|
110
|
-
|
|
111
|
-
|
|
202
|
+
reject(toolUseId, _message) {
|
|
203
|
+
// Check if this is a plan completion rejection
|
|
204
|
+
if (this.pendingPlanCompletion && toolUseId === this.pendingPlanCompletion.toolUseId) {
|
|
205
|
+
this.handlePlanRejected(_message);
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
const pending = this.resolvePendingApproval(toolUseId);
|
|
209
|
+
if (!pending) {
|
|
210
|
+
console.log("[codex-process] reject() called but no pending permission requests");
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
this.pendingApprovals.delete(pending.toolUseId);
|
|
214
|
+
this.respondToServerRequest(pending.requestId, {
|
|
215
|
+
decision: "decline",
|
|
216
|
+
});
|
|
217
|
+
this.emitToolResult(pending.toolUseId, "Rejected");
|
|
218
|
+
if (this.pendingApprovals.size === 0) {
|
|
219
|
+
this.setStatus("running");
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
answer(toolUseId, result) {
|
|
223
|
+
const pending = this.resolvePendingUserInput(toolUseId);
|
|
224
|
+
if (!pending) {
|
|
225
|
+
console.log("[codex-process] answer() called but no pending AskUserQuestion");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
this.pendingUserInputs.delete(pending.toolUseId);
|
|
229
|
+
this.respondToServerRequest(pending.requestId, {
|
|
230
|
+
answers: buildUserInputAnswers(pending.questions, result),
|
|
231
|
+
});
|
|
232
|
+
this.emitToolResult(pending.toolUseId, "Answered");
|
|
233
|
+
if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
|
|
234
|
+
this.setStatus("running");
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
getPendingPermission(toolUseId) {
|
|
238
|
+
// Check plan completion first
|
|
239
|
+
if (this.pendingPlanCompletion) {
|
|
240
|
+
if (!toolUseId || toolUseId === this.pendingPlanCompletion.toolUseId) {
|
|
241
|
+
return {
|
|
242
|
+
toolUseId: this.pendingPlanCompletion.toolUseId,
|
|
243
|
+
toolName: "ExitPlanMode",
|
|
244
|
+
input: { plan: this.pendingPlanCompletion.planText },
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
const pending = this.resolvePendingApproval(toolUseId);
|
|
249
|
+
if (pending) {
|
|
250
|
+
return {
|
|
251
|
+
toolUseId: pending.toolUseId,
|
|
252
|
+
toolName: pending.toolName,
|
|
253
|
+
input: { ...pending.input },
|
|
254
|
+
};
|
|
255
|
+
}
|
|
256
|
+
const pendingAsk = this.resolvePendingUserInput(toolUseId);
|
|
257
|
+
if (!pendingAsk)
|
|
258
|
+
return undefined;
|
|
259
|
+
return {
|
|
260
|
+
toolUseId: pendingAsk.toolUseId,
|
|
261
|
+
toolName: "AskUserQuestion",
|
|
262
|
+
input: { ...pendingAsk.input },
|
|
263
|
+
};
|
|
264
|
+
}
|
|
265
|
+
/** Emit a synthetic tool_result so history replay can match it to a permission_request. */
|
|
266
|
+
emitToolResult(toolUseId, content) {
|
|
267
|
+
this.emitMessage({
|
|
268
|
+
type: "tool_result",
|
|
269
|
+
toolUseId,
|
|
270
|
+
content,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
resolvePendingApproval(toolUseId) {
|
|
274
|
+
if (toolUseId)
|
|
275
|
+
return this.pendingApprovals.get(toolUseId);
|
|
276
|
+
const first = this.pendingApprovals.values().next();
|
|
277
|
+
return first.done ? undefined : first.value;
|
|
278
|
+
}
|
|
279
|
+
resolvePendingUserInput(toolUseId) {
|
|
280
|
+
if (toolUseId)
|
|
281
|
+
return this.pendingUserInputs.get(toolUseId);
|
|
282
|
+
const first = this.pendingUserInputs.values().next();
|
|
283
|
+
return first.done ? undefined : first.value;
|
|
284
|
+
}
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Plan completion handlers (native collaboration_mode)
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
/**
|
|
289
|
+
* Plan approved → switch to Default mode and auto-start execution.
|
|
290
|
+
*/
|
|
291
|
+
handlePlanApproved(updatedInput) {
|
|
292
|
+
const planText = updatedInput?.plan ?? this.pendingPlanCompletion?.planText ?? "";
|
|
293
|
+
const resolvedToolUseId = this.pendingPlanCompletion?.toolUseId;
|
|
294
|
+
this.pendingPlanCompletion = null;
|
|
295
|
+
this._collaborationMode = "default";
|
|
296
|
+
console.log("[codex-process] Plan approved, switching to Default mode");
|
|
297
|
+
// Emit synthetic tool_result so history replay knows this approval is resolved
|
|
298
|
+
if (resolvedToolUseId) {
|
|
299
|
+
this.emitToolResult(resolvedToolUseId, "Plan approved");
|
|
300
|
+
}
|
|
301
|
+
// Resolve inputResolve to start the next turn (Default mode) automatically
|
|
302
|
+
if (this.inputResolve) {
|
|
303
|
+
const resolve = this.inputResolve;
|
|
304
|
+
this.inputResolve = null;
|
|
305
|
+
resolve({ text: `Execute the following plan:\n\n${planText}` });
|
|
306
|
+
}
|
|
307
|
+
else {
|
|
308
|
+
// inputResolve may not be ready yet if approval comes before the next
|
|
309
|
+
// input loop iteration. Queue the text so sendInput() can pick it up.
|
|
310
|
+
console.warn("[codex-process] Plan approved but inputResolve not ready, queuing as pending input");
|
|
311
|
+
this._pendingPlanInput = `Execute the following plan:\n\n${planText}`;
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
/**
|
|
315
|
+
* Plan rejected → stay in Plan mode and re-plan with feedback.
|
|
316
|
+
*/
|
|
317
|
+
handlePlanRejected(feedback) {
|
|
318
|
+
const resolvedToolUseId = this.pendingPlanCompletion?.toolUseId;
|
|
319
|
+
this.pendingPlanCompletion = null;
|
|
320
|
+
console.log("[codex-process] Plan rejected, continuing in Plan mode");
|
|
321
|
+
// Stay in Plan mode
|
|
322
|
+
// Emit synthetic tool_result so history replay knows this approval is resolved
|
|
323
|
+
if (resolvedToolUseId) {
|
|
324
|
+
this.emitToolResult(resolvedToolUseId, "Plan rejected");
|
|
325
|
+
}
|
|
326
|
+
if (feedback) {
|
|
327
|
+
if (this.inputResolve) {
|
|
328
|
+
const resolve = this.inputResolve;
|
|
329
|
+
this.inputResolve = null;
|
|
330
|
+
resolve({ text: feedback });
|
|
331
|
+
}
|
|
332
|
+
else {
|
|
333
|
+
console.warn("[codex-process] Plan rejected but inputResolve not ready, queuing feedback");
|
|
334
|
+
this._pendingPlanInput = feedback;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
else {
|
|
338
|
+
this.setStatus("idle");
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
async bootstrap(projectPath, options) {
|
|
342
|
+
try {
|
|
343
|
+
await this.request("initialize", {
|
|
344
|
+
clientInfo: {
|
|
345
|
+
name: "ccpocket_bridge",
|
|
346
|
+
version: "1.0.0",
|
|
347
|
+
title: "ccpocket bridge",
|
|
348
|
+
},
|
|
349
|
+
capabilities: {
|
|
350
|
+
experimentalApi: true,
|
|
351
|
+
},
|
|
352
|
+
});
|
|
353
|
+
this.notify("initialized", {});
|
|
354
|
+
const threadParams = {
|
|
355
|
+
cwd: projectPath,
|
|
356
|
+
approvalPolicy: normalizeApprovalPolicy(options?.approvalPolicy ?? "never"),
|
|
357
|
+
sandbox: normalizeSandboxMode(options?.sandboxMode ?? "workspace-write"),
|
|
358
|
+
};
|
|
359
|
+
if (options?.model)
|
|
360
|
+
threadParams.model = options.model;
|
|
361
|
+
if (options?.modelReasoningEffort) {
|
|
362
|
+
threadParams.effort = normalizeReasoningEffort(options.modelReasoningEffort);
|
|
363
|
+
}
|
|
364
|
+
if (options?.networkAccessEnabled !== undefined) {
|
|
365
|
+
threadParams.sandboxPolicy = {
|
|
366
|
+
type: normalizeSandboxMode(options?.sandboxMode ?? "workspace-write"),
|
|
367
|
+
networkAccess: options.networkAccessEnabled,
|
|
368
|
+
};
|
|
369
|
+
}
|
|
370
|
+
if (options?.webSearchMode) {
|
|
371
|
+
threadParams.webSearchMode = options.webSearchMode;
|
|
372
|
+
}
|
|
373
|
+
const method = options?.threadId ? "thread/resume" : "thread/start";
|
|
374
|
+
if (options?.threadId) {
|
|
375
|
+
threadParams.threadId = options.threadId;
|
|
376
|
+
}
|
|
377
|
+
const response = await this.request(method, threadParams);
|
|
378
|
+
const thread = response.thread;
|
|
379
|
+
const threadId = typeof thread?.id === "string"
|
|
380
|
+
? thread.id
|
|
381
|
+
: options?.threadId;
|
|
382
|
+
if (!threadId) {
|
|
383
|
+
throw new Error(`${method} returned no thread id`);
|
|
384
|
+
}
|
|
385
|
+
// Capture the resolved model name from thread response
|
|
386
|
+
if (typeof thread?.model === "string" && thread.model) {
|
|
387
|
+
this.startModel = thread.model;
|
|
388
|
+
}
|
|
389
|
+
this._threadId = threadId;
|
|
390
|
+
this.emitMessage({
|
|
391
|
+
type: "system",
|
|
392
|
+
subtype: "init",
|
|
393
|
+
sessionId: threadId,
|
|
394
|
+
model: this.startModel ?? "codex",
|
|
395
|
+
});
|
|
396
|
+
this.setStatus("idle");
|
|
397
|
+
await this.runInputLoop(options);
|
|
398
|
+
}
|
|
399
|
+
catch (err) {
|
|
400
|
+
if (!this.stopped) {
|
|
401
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
402
|
+
console.error("[codex-process] bootstrap error:", err);
|
|
403
|
+
this.emitMessage({ type: "error", message: `Codex error: ${message}` });
|
|
404
|
+
this.emitMessage({ type: "result", subtype: "error", error: message, sessionId: this._threadId ?? undefined });
|
|
405
|
+
}
|
|
406
|
+
this.setStatus("idle");
|
|
407
|
+
this.emit("exit", 1);
|
|
408
|
+
}
|
|
409
|
+
}
|
|
410
|
+
async runInputLoop(options) {
|
|
112
411
|
while (!this.stopped) {
|
|
113
|
-
// Wait for user input
|
|
114
412
|
const pendingInput = await new Promise((resolve) => {
|
|
115
413
|
this.inputResolve = resolve;
|
|
414
|
+
// If plan approval arrived before inputResolve was ready, drain it now.
|
|
415
|
+
if (this._pendingPlanInput) {
|
|
416
|
+
const text = this._pendingPlanInput;
|
|
417
|
+
this._pendingPlanInput = null;
|
|
418
|
+
this.inputResolve = null;
|
|
419
|
+
resolve({ text });
|
|
420
|
+
}
|
|
116
421
|
});
|
|
117
|
-
if (this.stopped || !pendingInput.text
|
|
422
|
+
if (this.stopped || !pendingInput.text)
|
|
118
423
|
break;
|
|
119
|
-
|
|
424
|
+
if (!this._threadId) {
|
|
425
|
+
this.emitMessage({ type: "error", message: "Codex thread is not initialized" });
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
const { input, tempPaths } = await this.toRpcInput(pendingInput);
|
|
120
429
|
if (!input) {
|
|
121
430
|
continue;
|
|
122
431
|
}
|
|
123
|
-
// Execute turn
|
|
124
432
|
this.setStatus("running");
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
const
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
433
|
+
this.lastTokenUsage = null;
|
|
434
|
+
const completion = await new Promise((resolve, reject) => {
|
|
435
|
+
this.pendingTurnCompletion = { resolve, reject };
|
|
436
|
+
const params = {
|
|
437
|
+
threadId: this._threadId,
|
|
438
|
+
input,
|
|
439
|
+
approvalPolicy: normalizeApprovalPolicy(this._approvalPolicy),
|
|
440
|
+
};
|
|
441
|
+
if (options?.model)
|
|
442
|
+
params.model = options.model;
|
|
443
|
+
if (options?.modelReasoningEffort) {
|
|
444
|
+
params.effort = normalizeReasoningEffort(options.modelReasoningEffort);
|
|
135
445
|
}
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
446
|
+
// Always send collaborationMode so the server switches modes correctly.
|
|
447
|
+
// Omitting it causes the server to persist the previous turn's mode.
|
|
448
|
+
const modeSettings = {
|
|
449
|
+
model: options?.model || this.startModel || "gpt-5.3-codex",
|
|
450
|
+
};
|
|
451
|
+
if (this._collaborationMode === "plan") {
|
|
452
|
+
modeSettings.reasoning_effort = "medium";
|
|
453
|
+
}
|
|
454
|
+
params.collaborationMode = {
|
|
455
|
+
mode: this._collaborationMode,
|
|
456
|
+
settings: modeSettings,
|
|
457
|
+
};
|
|
458
|
+
console.log(`[codex-process] turn/start: approval=${params.approvalPolicy}, collaboration=${this._collaborationMode}`);
|
|
459
|
+
void this.request("turn/start", params)
|
|
460
|
+
.then((result) => {
|
|
461
|
+
const turn = result.turn;
|
|
462
|
+
if (typeof turn?.id === "string") {
|
|
463
|
+
this.pendingTurnId = turn.id;
|
|
143
464
|
}
|
|
465
|
+
})
|
|
466
|
+
.catch((err) => {
|
|
467
|
+
this.pendingTurnCompletion = null;
|
|
468
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
469
|
+
});
|
|
470
|
+
}).catch((err) => {
|
|
471
|
+
if (!this.stopped) {
|
|
472
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
473
|
+
this.emitMessage({ type: "error", message });
|
|
144
474
|
this.emitMessage({
|
|
145
475
|
type: "result",
|
|
146
|
-
subtype:
|
|
147
|
-
error:
|
|
476
|
+
subtype: "error",
|
|
477
|
+
error: message,
|
|
148
478
|
sessionId: this._threadId ?? undefined,
|
|
149
479
|
});
|
|
480
|
+
this.setStatus("idle");
|
|
150
481
|
}
|
|
482
|
+
});
|
|
483
|
+
await Promise.all(tempPaths.map((path) => rm(path, { force: true }).catch(() => { })));
|
|
484
|
+
void completion;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
handleStdoutChunk(chunk) {
|
|
488
|
+
this.stdoutBuffer += chunk;
|
|
489
|
+
while (true) {
|
|
490
|
+
const newlineIndex = this.stdoutBuffer.indexOf("\n");
|
|
491
|
+
if (newlineIndex < 0)
|
|
492
|
+
break;
|
|
493
|
+
const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
|
|
494
|
+
this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
|
|
495
|
+
if (!line)
|
|
496
|
+
continue;
|
|
497
|
+
try {
|
|
498
|
+
const envelope = JSON.parse(line);
|
|
499
|
+
this.handleRpcEnvelope(envelope);
|
|
151
500
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
await rm(path, { force: true }).catch(() => { });
|
|
155
|
-
}
|
|
156
|
-
this.pendingAbort = null;
|
|
501
|
+
catch (err) {
|
|
502
|
+
console.warn(`[codex-process] failed to parse app-server JSON line: ${line.slice(0, 200)}`);
|
|
157
503
|
if (!this.stopped) {
|
|
158
|
-
this.
|
|
504
|
+
this.emitMessage({
|
|
505
|
+
type: "error",
|
|
506
|
+
message: `Failed to parse codex app-server output: ${err instanceof Error ? err.message : String(err)}`,
|
|
507
|
+
});
|
|
159
508
|
}
|
|
160
509
|
}
|
|
161
510
|
}
|
|
162
511
|
}
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
512
|
+
handleRpcEnvelope(envelope) {
|
|
513
|
+
if (envelope.id != null && envelope.method && envelope.result === undefined && envelope.error === undefined) {
|
|
514
|
+
this.handleServerRequest(envelope.id, envelope.method, envelope.params ?? {});
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (envelope.id != null && (envelope.result !== undefined || envelope.error)) {
|
|
518
|
+
this.handleRpcResponse(envelope);
|
|
519
|
+
return;
|
|
520
|
+
}
|
|
521
|
+
if (envelope.method) {
|
|
522
|
+
this.handleNotification(envelope.method, envelope.params ?? {});
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
handleRpcResponse(envelope) {
|
|
526
|
+
if (typeof envelope.id !== "number") {
|
|
527
|
+
return;
|
|
528
|
+
}
|
|
529
|
+
const pending = this.pendingRpc.get(envelope.id);
|
|
530
|
+
if (!pending)
|
|
531
|
+
return;
|
|
532
|
+
this.pendingRpc.delete(envelope.id);
|
|
533
|
+
if ("error" in envelope && envelope.error) {
|
|
534
|
+
const message = envelope.error.message ?? `RPC error ${envelope.error.code ?? ""}`;
|
|
535
|
+
pending.reject(new Error(message));
|
|
536
|
+
return;
|
|
537
|
+
}
|
|
538
|
+
pending.resolve(envelope.result);
|
|
539
|
+
}
|
|
540
|
+
handleServerRequest(id, method, params) {
|
|
541
|
+
switch (method) {
|
|
542
|
+
case "item/commandExecution/requestApproval": {
|
|
543
|
+
const toolUseId = this.extractToolUseId(params, id);
|
|
544
|
+
const input = {
|
|
545
|
+
...(typeof params.command === "string" ? { command: params.command } : {}),
|
|
546
|
+
...(typeof params.cwd === "string" ? { cwd: params.cwd } : {}),
|
|
547
|
+
...(params.commandActions ? { commandActions: params.commandActions } : {}),
|
|
548
|
+
...(params.networkApprovalContext ? { networkApprovalContext: params.networkApprovalContext } : {}),
|
|
549
|
+
...(typeof params.reason === "string" ? { reason: params.reason } : {}),
|
|
550
|
+
};
|
|
551
|
+
this.pendingApprovals.set(toolUseId, {
|
|
552
|
+
requestId: id,
|
|
553
|
+
toolUseId,
|
|
554
|
+
toolName: "Bash",
|
|
555
|
+
input,
|
|
556
|
+
});
|
|
557
|
+
this.emitMessage({
|
|
558
|
+
type: "permission_request",
|
|
559
|
+
toolUseId,
|
|
560
|
+
toolName: "Bash",
|
|
561
|
+
input,
|
|
562
|
+
});
|
|
563
|
+
this.setStatus("waiting_approval");
|
|
564
|
+
break;
|
|
565
|
+
}
|
|
566
|
+
case "item/fileChange/requestApproval": {
|
|
567
|
+
const toolUseId = this.extractToolUseId(params, id);
|
|
568
|
+
const input = {
|
|
569
|
+
...(Array.isArray(params.changes) ? { changes: params.changes } : {}),
|
|
570
|
+
...(typeof params.reason === "string" ? { reason: params.reason } : {}),
|
|
571
|
+
};
|
|
572
|
+
this.pendingApprovals.set(toolUseId, {
|
|
573
|
+
requestId: id,
|
|
574
|
+
toolUseId,
|
|
575
|
+
toolName: "FileChange",
|
|
576
|
+
input,
|
|
577
|
+
});
|
|
578
|
+
this.emitMessage({
|
|
579
|
+
type: "permission_request",
|
|
580
|
+
toolUseId,
|
|
581
|
+
toolName: "FileChange",
|
|
582
|
+
input,
|
|
583
|
+
});
|
|
584
|
+
this.setStatus("waiting_approval");
|
|
585
|
+
break;
|
|
586
|
+
}
|
|
587
|
+
case "item/tool/requestUserInput": {
|
|
588
|
+
const toolUseId = this.extractToolUseId(params, id);
|
|
589
|
+
const questions = normalizeUserInputQuestions(params.questions);
|
|
590
|
+
const input = {
|
|
591
|
+
questions: questions.map((q) => ({
|
|
592
|
+
id: q.id,
|
|
593
|
+
question: q.question,
|
|
594
|
+
header: q.header,
|
|
595
|
+
options: q.options,
|
|
596
|
+
multiSelect: false,
|
|
597
|
+
isOther: q.isOther,
|
|
598
|
+
isSecret: q.isSecret,
|
|
599
|
+
})),
|
|
600
|
+
};
|
|
601
|
+
this.pendingUserInputs.set(toolUseId, {
|
|
602
|
+
requestId: id,
|
|
603
|
+
toolUseId,
|
|
604
|
+
questions: questions.map((q) => ({
|
|
605
|
+
id: q.id,
|
|
606
|
+
question: q.question,
|
|
607
|
+
})),
|
|
608
|
+
input,
|
|
609
|
+
});
|
|
168
610
|
this.emitMessage({
|
|
169
|
-
type: "
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
611
|
+
type: "permission_request",
|
|
612
|
+
toolUseId,
|
|
613
|
+
toolName: "AskUserQuestion",
|
|
614
|
+
input,
|
|
173
615
|
});
|
|
616
|
+
this.setStatus("waiting_approval");
|
|
174
617
|
break;
|
|
175
|
-
|
|
618
|
+
}
|
|
619
|
+
default:
|
|
620
|
+
this.respondToServerRequest(id, {});
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
handleNotification(method, params) {
|
|
625
|
+
switch (method) {
|
|
626
|
+
case "thread/started": {
|
|
627
|
+
const thread = params.thread;
|
|
628
|
+
if (typeof thread?.id === "string") {
|
|
629
|
+
this._threadId = thread.id;
|
|
630
|
+
}
|
|
631
|
+
break;
|
|
632
|
+
}
|
|
633
|
+
case "turn/started": {
|
|
634
|
+
const turn = params.turn;
|
|
635
|
+
if (typeof turn?.id === "string") {
|
|
636
|
+
this.pendingTurnId = turn.id;
|
|
637
|
+
}
|
|
176
638
|
this.setStatus("running");
|
|
177
639
|
break;
|
|
178
|
-
|
|
179
|
-
|
|
640
|
+
}
|
|
641
|
+
case "turn/completed": {
|
|
642
|
+
this.handleTurnCompleted(params.turn);
|
|
180
643
|
break;
|
|
181
|
-
|
|
182
|
-
|
|
644
|
+
}
|
|
645
|
+
case "thread/tokenUsage/updated": {
|
|
646
|
+
const usage = params.usage;
|
|
647
|
+
if (usage) {
|
|
648
|
+
this.lastTokenUsage = {
|
|
649
|
+
input: numberOrUndefined(usage.inputTokens ?? usage.input_tokens),
|
|
650
|
+
cachedInput: numberOrUndefined(usage.cachedInputTokens ?? usage.cached_input_tokens),
|
|
651
|
+
output: numberOrUndefined(usage.outputTokens ?? usage.output_tokens),
|
|
652
|
+
};
|
|
653
|
+
}
|
|
183
654
|
break;
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
this.
|
|
655
|
+
}
|
|
656
|
+
case "item/started": {
|
|
657
|
+
this.processItemStarted(params.item);
|
|
187
658
|
break;
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
659
|
+
}
|
|
660
|
+
case "item/completed": {
|
|
661
|
+
this.processItemCompleted(params.item);
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
664
|
+
case "item/agentMessage/delta": {
|
|
665
|
+
const delta = typeof params.delta === "string"
|
|
666
|
+
? params.delta
|
|
667
|
+
: typeof params.textDelta === "string"
|
|
668
|
+
? params.textDelta
|
|
669
|
+
: "";
|
|
670
|
+
if (delta) {
|
|
671
|
+
this.emitMessage({ type: "stream_delta", text: delta });
|
|
672
|
+
}
|
|
673
|
+
break;
|
|
674
|
+
}
|
|
675
|
+
case "item/reasoning/summaryTextDelta":
|
|
676
|
+
case "item/reasoning/textDelta": {
|
|
677
|
+
const delta = typeof params.delta === "string"
|
|
678
|
+
? params.delta
|
|
679
|
+
: typeof params.textDelta === "string"
|
|
680
|
+
? params.textDelta
|
|
681
|
+
: "";
|
|
682
|
+
if (delta) {
|
|
683
|
+
this.emitMessage({ type: "thinking_delta", text: delta });
|
|
684
|
+
}
|
|
197
685
|
break;
|
|
198
|
-
|
|
686
|
+
}
|
|
687
|
+
case "item/plan/delta": {
|
|
688
|
+
const delta = typeof params.delta === "string" ? params.delta : "";
|
|
689
|
+
if (delta) {
|
|
690
|
+
this.emitMessage({ type: "thinking_delta", text: delta });
|
|
691
|
+
}
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
case "turn/plan/updated": {
|
|
695
|
+
// Default mode's update_plan tool output — always show as informational text
|
|
696
|
+
const text = formatPlanUpdateText(params);
|
|
697
|
+
if (!text)
|
|
698
|
+
break;
|
|
199
699
|
this.emitMessage({
|
|
200
|
-
type: "
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
700
|
+
type: "assistant",
|
|
701
|
+
message: {
|
|
702
|
+
id: randomUUID(),
|
|
703
|
+
role: "assistant",
|
|
704
|
+
content: [{ type: "text", text }],
|
|
705
|
+
model: "codex",
|
|
706
|
+
},
|
|
204
707
|
});
|
|
205
708
|
break;
|
|
206
|
-
|
|
207
|
-
|
|
709
|
+
}
|
|
710
|
+
default:
|
|
208
711
|
break;
|
|
209
712
|
}
|
|
210
713
|
}
|
|
714
|
+
handleTurnCompleted(turn) {
|
|
715
|
+
const status = String(turn?.status ?? "completed");
|
|
716
|
+
const usage = this.lastTokenUsage;
|
|
717
|
+
this.lastTokenUsage = null;
|
|
718
|
+
if (status === "failed") {
|
|
719
|
+
const errorObj = turn?.error;
|
|
720
|
+
const message = typeof errorObj?.message === "string"
|
|
721
|
+
? errorObj.message
|
|
722
|
+
: "Turn failed";
|
|
723
|
+
this.emitMessage({
|
|
724
|
+
type: "result",
|
|
725
|
+
subtype: "error",
|
|
726
|
+
error: message,
|
|
727
|
+
sessionId: this._threadId ?? undefined,
|
|
728
|
+
});
|
|
729
|
+
}
|
|
730
|
+
else if (status === "interrupted") {
|
|
731
|
+
this.emitMessage({
|
|
732
|
+
type: "result",
|
|
733
|
+
subtype: "interrupted",
|
|
734
|
+
sessionId: this._threadId ?? undefined,
|
|
735
|
+
});
|
|
736
|
+
}
|
|
737
|
+
else {
|
|
738
|
+
this.emitMessage({
|
|
739
|
+
type: "result",
|
|
740
|
+
subtype: "success",
|
|
741
|
+
sessionId: this._threadId ?? undefined,
|
|
742
|
+
...(usage?.input != null ? { inputTokens: usage.input } : {}),
|
|
743
|
+
...(usage?.cachedInput != null ? { cachedInputTokens: usage.cachedInput } : {}),
|
|
744
|
+
...(usage?.output != null ? { outputTokens: usage.output } : {}),
|
|
745
|
+
});
|
|
746
|
+
}
|
|
747
|
+
this.pendingTurnId = null;
|
|
748
|
+
// Plan mode: emit synthetic plan approval and wait for user decision
|
|
749
|
+
if (this._collaborationMode === "plan" && this.lastPlanItemText) {
|
|
750
|
+
const toolUseId = `plan_${randomUUID()}`;
|
|
751
|
+
this.pendingPlanCompletion = {
|
|
752
|
+
toolUseId,
|
|
753
|
+
planText: this.lastPlanItemText,
|
|
754
|
+
};
|
|
755
|
+
this.lastPlanItemText = null;
|
|
756
|
+
this.emitMessage({
|
|
757
|
+
type: "permission_request",
|
|
758
|
+
toolUseId,
|
|
759
|
+
toolName: "ExitPlanMode",
|
|
760
|
+
input: { plan: this.pendingPlanCompletion.planText },
|
|
761
|
+
});
|
|
762
|
+
this.setStatus("waiting_approval");
|
|
763
|
+
// Do NOT set idle — waiting for plan approval
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
this.lastPlanItemText = null;
|
|
767
|
+
if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
|
|
768
|
+
this.setStatus("idle");
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
if (this.pendingTurnCompletion) {
|
|
772
|
+
this.pendingTurnCompletion.resolve();
|
|
773
|
+
this.pendingTurnCompletion = null;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
211
776
|
processItemStarted(item) {
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
777
|
+
if (!item || typeof item !== "object")
|
|
778
|
+
return;
|
|
779
|
+
const itemId = typeof item.id === "string" ? item.id : randomUUID();
|
|
780
|
+
const itemType = normalizeItemType(item.type);
|
|
781
|
+
switch (itemType) {
|
|
782
|
+
case "commandexecution": {
|
|
783
|
+
const commandText = typeof item.command === "string"
|
|
784
|
+
? item.command
|
|
785
|
+
: Array.isArray(item.command)
|
|
786
|
+
? item.command.map((part) => String(part)).join(" ")
|
|
787
|
+
: "";
|
|
215
788
|
this.emitMessage({
|
|
216
789
|
type: "assistant",
|
|
217
790
|
message: {
|
|
218
|
-
id:
|
|
791
|
+
id: itemId,
|
|
219
792
|
role: "assistant",
|
|
220
793
|
content: [
|
|
221
794
|
{
|
|
222
795
|
type: "tool_use",
|
|
223
|
-
id:
|
|
796
|
+
id: itemId,
|
|
224
797
|
name: "Bash",
|
|
225
|
-
input: { command:
|
|
798
|
+
input: { command: commandText },
|
|
799
|
+
},
|
|
800
|
+
],
|
|
801
|
+
model: "codex",
|
|
802
|
+
},
|
|
803
|
+
});
|
|
804
|
+
break;
|
|
805
|
+
}
|
|
806
|
+
case "filechange": {
|
|
807
|
+
this.emitMessage({
|
|
808
|
+
type: "assistant",
|
|
809
|
+
message: {
|
|
810
|
+
id: itemId,
|
|
811
|
+
role: "assistant",
|
|
812
|
+
content: [
|
|
813
|
+
{
|
|
814
|
+
type: "tool_use",
|
|
815
|
+
id: itemId,
|
|
816
|
+
name: "FileChange",
|
|
817
|
+
input: {
|
|
818
|
+
changes: Array.isArray(item.changes) ? item.changes : [],
|
|
819
|
+
},
|
|
226
820
|
},
|
|
227
821
|
],
|
|
228
822
|
model: "codex",
|
|
229
823
|
},
|
|
230
824
|
});
|
|
231
825
|
break;
|
|
232
|
-
|
|
826
|
+
}
|
|
827
|
+
default:
|
|
828
|
+
break;
|
|
233
829
|
}
|
|
234
830
|
}
|
|
235
831
|
processItemCompleted(item) {
|
|
236
|
-
|
|
237
|
-
|
|
832
|
+
if (!item || typeof item !== "object")
|
|
833
|
+
return;
|
|
834
|
+
const itemId = typeof item.id === "string" ? item.id : randomUUID();
|
|
835
|
+
const itemType = normalizeItemType(item.type);
|
|
836
|
+
switch (itemType) {
|
|
837
|
+
case "agentmessage": {
|
|
838
|
+
const text = extractAgentText(item);
|
|
839
|
+
if (!text)
|
|
840
|
+
return;
|
|
238
841
|
this.emitMessage({
|
|
239
842
|
type: "assistant",
|
|
240
843
|
message: {
|
|
241
|
-
id:
|
|
844
|
+
id: itemId,
|
|
242
845
|
role: "assistant",
|
|
243
|
-
content: [{ type: "text", text
|
|
846
|
+
content: [{ type: "text", text }],
|
|
244
847
|
model: "codex",
|
|
245
848
|
},
|
|
246
849
|
});
|
|
247
850
|
break;
|
|
248
|
-
|
|
249
|
-
|
|
851
|
+
}
|
|
852
|
+
case "reasoning": {
|
|
853
|
+
const text = extractReasoningText(item);
|
|
854
|
+
if (text) {
|
|
855
|
+
this.emitMessage({ type: "thinking_delta", text });
|
|
856
|
+
}
|
|
250
857
|
break;
|
|
251
|
-
|
|
858
|
+
}
|
|
859
|
+
case "commandexecution": {
|
|
860
|
+
const output = typeof item.aggregatedOutput === "string"
|
|
861
|
+
? item.aggregatedOutput
|
|
862
|
+
: typeof item.output === "string"
|
|
863
|
+
? item.output
|
|
864
|
+
: "";
|
|
865
|
+
const exitCode = numberOrUndefined(item.exitCode ?? item.exit_code);
|
|
252
866
|
this.emitMessage({
|
|
253
867
|
type: "tool_result",
|
|
254
|
-
toolUseId:
|
|
255
|
-
content:
|
|
868
|
+
toolUseId: itemId,
|
|
869
|
+
content: output || `exit code: ${exitCode ?? "unknown"}`,
|
|
256
870
|
toolName: "Bash",
|
|
257
871
|
});
|
|
258
872
|
break;
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
type: "assistant",
|
|
263
|
-
message: {
|
|
264
|
-
id: item.id,
|
|
265
|
-
role: "assistant",
|
|
266
|
-
content: [
|
|
267
|
-
{
|
|
268
|
-
type: "tool_use",
|
|
269
|
-
id: item.id,
|
|
270
|
-
name: "FileChange",
|
|
271
|
-
input: { changes: item.changes },
|
|
272
|
-
},
|
|
273
|
-
],
|
|
274
|
-
model: "codex",
|
|
275
|
-
},
|
|
276
|
-
});
|
|
873
|
+
}
|
|
874
|
+
case "filechange": {
|
|
875
|
+
const content = formatFileChangesWithDiff(item.changes);
|
|
277
876
|
this.emitMessage({
|
|
278
877
|
type: "tool_result",
|
|
279
|
-
toolUseId:
|
|
280
|
-
content
|
|
281
|
-
.map((c) => `${c.kind}: ${c.path}`)
|
|
282
|
-
.join("\n"),
|
|
878
|
+
toolUseId: itemId,
|
|
879
|
+
content,
|
|
283
880
|
toolName: "FileChange",
|
|
284
881
|
});
|
|
285
882
|
break;
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
883
|
+
}
|
|
884
|
+
case "mcptoolcall": {
|
|
885
|
+
const server = typeof item.server === "string" ? item.server : "mcp";
|
|
886
|
+
const tool = typeof item.tool === "string" ? item.tool : "unknown";
|
|
887
|
+
const toolName = `mcp:${server}/${tool}`;
|
|
888
|
+
const result = item.result ?? item.error ?? "MCP call completed";
|
|
289
889
|
this.emitMessage({
|
|
290
890
|
type: "assistant",
|
|
291
891
|
message: {
|
|
292
|
-
id:
|
|
892
|
+
id: itemId,
|
|
293
893
|
role: "assistant",
|
|
294
894
|
content: [
|
|
295
895
|
{
|
|
296
896
|
type: "tool_use",
|
|
297
|
-
id:
|
|
897
|
+
id: itemId,
|
|
298
898
|
name: toolName,
|
|
299
|
-
input: item.arguments,
|
|
899
|
+
input: item.arguments ?? {},
|
|
300
900
|
},
|
|
301
901
|
],
|
|
302
902
|
model: "codex",
|
|
@@ -304,26 +904,25 @@ export class CodexProcess extends EventEmitter {
|
|
|
304
904
|
});
|
|
305
905
|
this.emitMessage({
|
|
306
906
|
type: "tool_result",
|
|
307
|
-
toolUseId:
|
|
308
|
-
content:
|
|
309
|
-
? JSON.stringify(item.result)
|
|
310
|
-
: item.error?.message ?? "MCP call completed",
|
|
907
|
+
toolUseId: itemId,
|
|
908
|
+
content: typeof result === "string" ? result : JSON.stringify(result),
|
|
311
909
|
toolName,
|
|
312
910
|
});
|
|
313
911
|
break;
|
|
314
912
|
}
|
|
315
|
-
case "
|
|
913
|
+
case "websearch": {
|
|
914
|
+
const query = typeof item.query === "string" ? item.query : "";
|
|
316
915
|
this.emitMessage({
|
|
317
916
|
type: "assistant",
|
|
318
917
|
message: {
|
|
319
|
-
id:
|
|
918
|
+
id: itemId,
|
|
320
919
|
role: "assistant",
|
|
321
920
|
content: [
|
|
322
921
|
{
|
|
323
922
|
type: "tool_use",
|
|
324
|
-
id:
|
|
923
|
+
id: itemId,
|
|
325
924
|
name: "WebSearch",
|
|
326
|
-
input: { query
|
|
925
|
+
input: { query },
|
|
327
926
|
},
|
|
328
927
|
],
|
|
329
928
|
model: "codex",
|
|
@@ -331,53 +930,33 @@ export class CodexProcess extends EventEmitter {
|
|
|
331
930
|
});
|
|
332
931
|
this.emitMessage({
|
|
333
932
|
type: "tool_result",
|
|
334
|
-
toolUseId:
|
|
335
|
-
content: `Web search: ${
|
|
933
|
+
toolUseId: itemId,
|
|
934
|
+
content: query ? `Web search: ${query}` : "Web search completed",
|
|
336
935
|
toolName: "WebSearch",
|
|
337
936
|
});
|
|
338
937
|
break;
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
role: "assistant",
|
|
345
|
-
content: [
|
|
346
|
-
{
|
|
347
|
-
type: "text",
|
|
348
|
-
text: item.items
|
|
349
|
-
.map((t) => `${t.completed ? "\u2705" : "\u2B1C"} ${t.text}`)
|
|
350
|
-
.join("\n"),
|
|
351
|
-
},
|
|
352
|
-
],
|
|
353
|
-
model: "codex",
|
|
354
|
-
},
|
|
355
|
-
});
|
|
938
|
+
}
|
|
939
|
+
case "plan": {
|
|
940
|
+
// Plan item completed — save text for plan approval emission in handleTurnCompleted()
|
|
941
|
+
const planText = typeof item.text === "string" ? item.text : "";
|
|
942
|
+
this.lastPlanItemText = planText;
|
|
356
943
|
break;
|
|
357
|
-
|
|
358
|
-
|
|
944
|
+
}
|
|
945
|
+
case "error": {
|
|
946
|
+
const message = typeof item.message === "string" ? item.message : "Codex item error";
|
|
947
|
+
this.emitMessage({ type: "error", message });
|
|
948
|
+
break;
|
|
949
|
+
}
|
|
950
|
+
default:
|
|
359
951
|
break;
|
|
360
952
|
}
|
|
361
953
|
}
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
this.emit("status", status);
|
|
366
|
-
this.emitMessage({ type: "status", status });
|
|
367
|
-
}
|
|
368
|
-
}
|
|
369
|
-
emitMessage(msg) {
|
|
370
|
-
this.emit("message", msg);
|
|
371
|
-
}
|
|
372
|
-
async toSdkInput(pendingInput) {
|
|
954
|
+
async toRpcInput(pendingInput) {
|
|
955
|
+
const input = [{ type: "text", text: pendingInput.text }];
|
|
956
|
+
const tempPaths = [];
|
|
373
957
|
if (!pendingInput.images || pendingInput.images.length === 0) {
|
|
374
|
-
return { input
|
|
958
|
+
return { input, tempPaths };
|
|
375
959
|
}
|
|
376
|
-
const inputParts = [];
|
|
377
|
-
const tempPaths = [];
|
|
378
|
-
// Add text first
|
|
379
|
-
inputParts.push({ type: "text", text: pendingInput.text });
|
|
380
|
-
// Add each image
|
|
381
960
|
for (const image of pendingInput.images) {
|
|
382
961
|
const ext = extensionFromMime(image.mimeType);
|
|
383
962
|
if (!ext) {
|
|
@@ -400,14 +979,311 @@ export class CodexProcess extends EventEmitter {
|
|
|
400
979
|
}
|
|
401
980
|
const tempPath = join(tmpdir(), `ccpocket-codex-image-${randomUUID()}.${ext}`);
|
|
402
981
|
await writeFile(tempPath, buffer);
|
|
403
|
-
inputParts.push({ type: "local_image", path: tempPath });
|
|
404
982
|
tempPaths.push(tempPath);
|
|
983
|
+
input.push({ type: "localImage", path: tempPath });
|
|
984
|
+
}
|
|
985
|
+
return { input, tempPaths };
|
|
986
|
+
}
|
|
987
|
+
request(method, params) {
|
|
988
|
+
const id = this.rpcSeq++;
|
|
989
|
+
const envelope = { id, method, params };
|
|
990
|
+
return new Promise((resolve, reject) => {
|
|
991
|
+
this.pendingRpc.set(id, { resolve, reject, method });
|
|
992
|
+
try {
|
|
993
|
+
this.writeEnvelope(envelope);
|
|
994
|
+
}
|
|
995
|
+
catch (err) {
|
|
996
|
+
this.pendingRpc.delete(id);
|
|
997
|
+
reject(err instanceof Error ? err : new Error(String(err)));
|
|
998
|
+
}
|
|
999
|
+
});
|
|
1000
|
+
}
|
|
1001
|
+
notify(method, params) {
|
|
1002
|
+
this.writeEnvelope({ method, params });
|
|
1003
|
+
}
|
|
1004
|
+
respondToServerRequest(id, result) {
|
|
1005
|
+
try {
|
|
1006
|
+
this.writeEnvelope({ id, result });
|
|
1007
|
+
}
|
|
1008
|
+
catch (err) {
|
|
1009
|
+
if (!this.stopped) {
|
|
1010
|
+
console.warn(`[codex-process] failed to respond to server request: ${err instanceof Error ? err.message : String(err)}`);
|
|
1011
|
+
}
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
writeEnvelope(envelope) {
|
|
1015
|
+
if (!this.child || this.child.killed) {
|
|
1016
|
+
throw new Error("codex app-server is not running");
|
|
1017
|
+
}
|
|
1018
|
+
const line = `${JSON.stringify(envelope)}\n`;
|
|
1019
|
+
this.child.stdin.write(line);
|
|
1020
|
+
}
|
|
1021
|
+
rejectAllPending(error) {
|
|
1022
|
+
for (const pending of this.pendingRpc.values()) {
|
|
1023
|
+
pending.reject(error);
|
|
1024
|
+
}
|
|
1025
|
+
this.pendingRpc.clear();
|
|
1026
|
+
if (this.pendingTurnCompletion) {
|
|
1027
|
+
this.pendingTurnCompletion.reject(error);
|
|
1028
|
+
this.pendingTurnCompletion = null;
|
|
1029
|
+
}
|
|
1030
|
+
}
|
|
1031
|
+
setStatus(status) {
|
|
1032
|
+
if (this._status !== status) {
|
|
1033
|
+
this._status = status;
|
|
1034
|
+
this.emit("status", status);
|
|
1035
|
+
this.emitMessage({ type: "status", status });
|
|
1036
|
+
}
|
|
1037
|
+
}
|
|
1038
|
+
emitMessage(msg) {
|
|
1039
|
+
this.emit("message", msg);
|
|
1040
|
+
}
|
|
1041
|
+
extractToolUseId(params, requestId) {
|
|
1042
|
+
if (typeof params.approvalId === "string")
|
|
1043
|
+
return params.approvalId;
|
|
1044
|
+
if (typeof params.itemId === "string")
|
|
1045
|
+
return params.itemId;
|
|
1046
|
+
if (typeof requestId === "string")
|
|
1047
|
+
return requestId;
|
|
1048
|
+
return `approval-${requestId}`;
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
function normalizeApprovalPolicy(value) {
|
|
1052
|
+
switch (value) {
|
|
1053
|
+
case "on-request":
|
|
1054
|
+
return "on-request";
|
|
1055
|
+
case "on-failure":
|
|
1056
|
+
return "on-failure";
|
|
1057
|
+
case "untrusted":
|
|
1058
|
+
return "untrusted";
|
|
1059
|
+
case "never":
|
|
1060
|
+
default:
|
|
1061
|
+
return "never";
|
|
1062
|
+
}
|
|
1063
|
+
}
|
|
1064
|
+
function normalizeSandboxMode(value) {
|
|
1065
|
+
switch (value) {
|
|
1066
|
+
case "read-only":
|
|
1067
|
+
return "read-only";
|
|
1068
|
+
case "danger-full-access":
|
|
1069
|
+
return "danger-full-access";
|
|
1070
|
+
case "workspace-write":
|
|
1071
|
+
default:
|
|
1072
|
+
return "workspace-write";
|
|
1073
|
+
}
|
|
1074
|
+
}
|
|
1075
|
+
function normalizeReasoningEffort(value) {
|
|
1076
|
+
switch (value) {
|
|
1077
|
+
case "xhigh":
|
|
1078
|
+
return "high";
|
|
1079
|
+
default:
|
|
1080
|
+
return value;
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
function normalizeItemType(raw) {
|
|
1084
|
+
if (typeof raw !== "string")
|
|
1085
|
+
return "";
|
|
1086
|
+
return raw.replace(/[_\s-]/g, "").toLowerCase();
|
|
1087
|
+
}
|
|
1088
|
+
function numberOrUndefined(value) {
|
|
1089
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
1090
|
+
}
|
|
1091
|
+
function summarizeFileChanges(changes) {
|
|
1092
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
1093
|
+
return "No file changes";
|
|
1094
|
+
}
|
|
1095
|
+
return changes
|
|
1096
|
+
.map((entry) => {
|
|
1097
|
+
if (!entry || typeof entry !== "object")
|
|
1098
|
+
return "changed";
|
|
1099
|
+
const record = entry;
|
|
1100
|
+
const kind = typeof record.kind === "string" ? record.kind : "changed";
|
|
1101
|
+
const path = typeof record.path === "string" ? record.path : "(unknown)";
|
|
1102
|
+
return `${kind}: ${path}`;
|
|
1103
|
+
})
|
|
1104
|
+
.join("\n");
|
|
1105
|
+
}
|
|
1106
|
+
/**
|
|
1107
|
+
* Format file changes including unified diff content for display in chat.
|
|
1108
|
+
* Falls back to `kind: path` summary when no diff is available.
|
|
1109
|
+
*/
|
|
1110
|
+
function formatFileChangesWithDiff(changes) {
|
|
1111
|
+
if (!Array.isArray(changes) || changes.length === 0) {
|
|
1112
|
+
return "No file changes";
|
|
1113
|
+
}
|
|
1114
|
+
return changes
|
|
1115
|
+
.map((entry) => {
|
|
1116
|
+
if (!entry || typeof entry !== "object")
|
|
1117
|
+
return "changed";
|
|
1118
|
+
const record = entry;
|
|
1119
|
+
const kind = typeof record.kind === "string" ? record.kind : "changed";
|
|
1120
|
+
const path = typeof record.path === "string" ? record.path : "(unknown)";
|
|
1121
|
+
const diff = typeof record.diff === "string" ? record.diff.trim() : "";
|
|
1122
|
+
if (diff) {
|
|
1123
|
+
// If diff already has unified headers, use as-is; otherwise add them
|
|
1124
|
+
if (diff.startsWith("---") || diff.startsWith("@@")) {
|
|
1125
|
+
return `--- a/${path}\n+++ b/${path}\n${diff}`;
|
|
1126
|
+
}
|
|
1127
|
+
return diff;
|
|
405
1128
|
}
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
1129
|
+
return `${kind}: ${path}`;
|
|
1130
|
+
})
|
|
1131
|
+
.join("\n\n");
|
|
1132
|
+
}
|
|
1133
|
+
function extractAgentText(item) {
|
|
1134
|
+
if (typeof item.text === "string")
|
|
1135
|
+
return item.text;
|
|
1136
|
+
const parts = item.content;
|
|
1137
|
+
if (Array.isArray(parts)) {
|
|
1138
|
+
const text = parts
|
|
1139
|
+
.filter((part) => part && typeof part === "object")
|
|
1140
|
+
.map((part) => {
|
|
1141
|
+
const record = part;
|
|
1142
|
+
if (record.type === "text" && typeof record.text === "string") {
|
|
1143
|
+
return record.text;
|
|
1144
|
+
}
|
|
1145
|
+
return "";
|
|
1146
|
+
})
|
|
1147
|
+
.filter((part) => part.length > 0)
|
|
1148
|
+
.join("\n");
|
|
1149
|
+
if (text)
|
|
1150
|
+
return text;
|
|
1151
|
+
}
|
|
1152
|
+
return "";
|
|
1153
|
+
}
|
|
1154
|
+
function extractReasoningText(item) {
|
|
1155
|
+
if (typeof item.text === "string")
|
|
1156
|
+
return item.text;
|
|
1157
|
+
const summary = item.summary;
|
|
1158
|
+
if (Array.isArray(summary)) {
|
|
1159
|
+
const text = summary
|
|
1160
|
+
.map((entry) => {
|
|
1161
|
+
if (!entry || typeof entry !== "object")
|
|
1162
|
+
return "";
|
|
1163
|
+
const record = entry;
|
|
1164
|
+
return typeof record.text === "string" ? record.text : "";
|
|
1165
|
+
})
|
|
1166
|
+
.filter((part) => part.length > 0)
|
|
1167
|
+
.join("\n");
|
|
1168
|
+
if (text)
|
|
1169
|
+
return text;
|
|
1170
|
+
}
|
|
1171
|
+
return "";
|
|
1172
|
+
}
|
|
1173
|
+
function normalizeUserInputQuestions(raw) {
|
|
1174
|
+
if (!Array.isArray(raw))
|
|
1175
|
+
return [];
|
|
1176
|
+
return raw
|
|
1177
|
+
.filter((entry) => !!entry && typeof entry === "object")
|
|
1178
|
+
.map((entry, index) => {
|
|
1179
|
+
const id = typeof entry.id === "string" ? entry.id : `question_${index + 1}`;
|
|
1180
|
+
const question = typeof entry.question === "string" ? entry.question : "";
|
|
1181
|
+
const header = typeof entry.header === "string" ? entry.header : `Question ${index + 1}`;
|
|
1182
|
+
const optionsRaw = Array.isArray(entry.options) ? entry.options : [];
|
|
1183
|
+
const options = optionsRaw
|
|
1184
|
+
.filter((option) => !!option && typeof option === "object")
|
|
1185
|
+
.map((option) => ({
|
|
1186
|
+
label: typeof option.label === "string" ? option.label : "",
|
|
1187
|
+
description: typeof option.description === "string" ? option.description : "",
|
|
1188
|
+
}))
|
|
1189
|
+
.filter((option) => option.label.length > 0);
|
|
1190
|
+
return {
|
|
1191
|
+
id,
|
|
1192
|
+
question,
|
|
1193
|
+
header,
|
|
1194
|
+
options,
|
|
1195
|
+
isOther: Boolean(entry.isOther),
|
|
1196
|
+
isSecret: Boolean(entry.isSecret),
|
|
1197
|
+
};
|
|
1198
|
+
})
|
|
1199
|
+
.filter((question) => question.question.length > 0);
|
|
1200
|
+
}
|
|
1201
|
+
function buildUserInputAnswers(questions, rawResult) {
|
|
1202
|
+
const parsed = parseResultObject(rawResult);
|
|
1203
|
+
const answerMap = {};
|
|
1204
|
+
for (const question of questions) {
|
|
1205
|
+
const candidate = parsed.byId[question.id] ?? parsed.byQuestion[question.question];
|
|
1206
|
+
const answers = normalizeAnswerValues(candidate);
|
|
1207
|
+
if (answers.length > 0) {
|
|
1208
|
+
answerMap[question.id] = { answers };
|
|
1209
|
+
}
|
|
1210
|
+
}
|
|
1211
|
+
if (Object.keys(answerMap).length === 0 && questions.length > 0) {
|
|
1212
|
+
answerMap[questions[0].id] = { answers: normalizeAnswerValues(rawResult) };
|
|
1213
|
+
}
|
|
1214
|
+
return answerMap;
|
|
1215
|
+
}
|
|
1216
|
+
function parseResultObject(rawResult) {
|
|
1217
|
+
try {
|
|
1218
|
+
const parsed = JSON.parse(rawResult);
|
|
1219
|
+
const byId = {};
|
|
1220
|
+
const byQuestion = {};
|
|
1221
|
+
if (parsed && typeof parsed === "object") {
|
|
1222
|
+
const answers = parsed.answers;
|
|
1223
|
+
if (answers && typeof answers === "object" && !Array.isArray(answers)) {
|
|
1224
|
+
for (const [key, value] of Object.entries(answers)) {
|
|
1225
|
+
byId[key] = value;
|
|
1226
|
+
byQuestion[key] = value;
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
409
1229
|
}
|
|
410
|
-
return {
|
|
1230
|
+
return { byId, byQuestion };
|
|
1231
|
+
}
|
|
1232
|
+
catch {
|
|
1233
|
+
return { byId: {}, byQuestion: {} };
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
function normalizeAnswerValues(value) {
|
|
1237
|
+
if (typeof value === "string") {
|
|
1238
|
+
return value
|
|
1239
|
+
.split(",")
|
|
1240
|
+
.map((part) => part.trim())
|
|
1241
|
+
.filter((part) => part.length > 0);
|
|
1242
|
+
}
|
|
1243
|
+
if (Array.isArray(value)) {
|
|
1244
|
+
return value
|
|
1245
|
+
.map((entry) => String(entry).trim())
|
|
1246
|
+
.filter((entry) => entry.length > 0);
|
|
1247
|
+
}
|
|
1248
|
+
if (value && typeof value === "object") {
|
|
1249
|
+
const record = value;
|
|
1250
|
+
if (Array.isArray(record.answers)) {
|
|
1251
|
+
return record.answers
|
|
1252
|
+
.map((entry) => String(entry).trim())
|
|
1253
|
+
.filter((entry) => entry.length > 0);
|
|
1254
|
+
}
|
|
1255
|
+
}
|
|
1256
|
+
if (value == null)
|
|
1257
|
+
return [];
|
|
1258
|
+
const normalized = String(value).trim();
|
|
1259
|
+
return normalized ? [normalized] : [];
|
|
1260
|
+
}
|
|
1261
|
+
function formatPlanUpdateText(params) {
|
|
1262
|
+
const stepsRaw = params.plan;
|
|
1263
|
+
if (!Array.isArray(stepsRaw) || stepsRaw.length === 0)
|
|
1264
|
+
return "";
|
|
1265
|
+
const explanation = typeof params.explanation === "string" ? params.explanation.trim() : "";
|
|
1266
|
+
const lines = stepsRaw
|
|
1267
|
+
.filter((entry) => !!entry && typeof entry === "object")
|
|
1268
|
+
.map((entry, index) => {
|
|
1269
|
+
const step = typeof entry.step === "string" ? entry.step : `Step ${index + 1}`;
|
|
1270
|
+
const status = normalizePlanStatus(entry.status);
|
|
1271
|
+
return `${index + 1}. [${status}] ${step}`;
|
|
1272
|
+
});
|
|
1273
|
+
if (lines.length === 0)
|
|
1274
|
+
return "";
|
|
1275
|
+
const header = explanation ? `Plan update: ${explanation}` : "Plan update:";
|
|
1276
|
+
return `${header}\n${lines.join("\n")}`;
|
|
1277
|
+
}
|
|
1278
|
+
function normalizePlanStatus(raw) {
|
|
1279
|
+
switch (raw) {
|
|
1280
|
+
case "inProgress":
|
|
1281
|
+
return "in progress";
|
|
1282
|
+
case "completed":
|
|
1283
|
+
return "completed";
|
|
1284
|
+
case "pending":
|
|
1285
|
+
default:
|
|
1286
|
+
return "pending";
|
|
411
1287
|
}
|
|
412
1288
|
}
|
|
413
1289
|
function extensionFromMime(mimeType) {
|