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