@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.
@@ -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 { Codex } from "@openai/codex-sdk";
6
+ import { spawn } from "node:child_process";
7
7
  export class CodexProcess extends EventEmitter {
8
- codex;
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
- pendingAbort = null;
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.thread !== null;
39
+ return this.child !== null;
28
40
  }
29
- constructor() {
30
- super();
31
- this.codex = new Codex();
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.thread) {
64
+ if (this.child) {
35
65
  this.stop();
36
66
  }
37
67
  this.stopped = false;
38
68
  this._threadId = null;
39
- const threadOpts = {
40
- workingDirectory: projectPath,
41
- approvalPolicy: options?.approvalPolicy ?? "never",
42
- sandboxMode: options?.sandboxMode ?? "workspace-write",
43
- networkAccessEnabled: options?.networkAccessEnabled ?? true,
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
- // Start input loop
56
- this.runInputLoop().catch((err) => {
57
- if (!this.stopped) {
58
- console.error("[codex-process] Input loop error:", err);
59
- this.emitMessage({
60
- type: "error",
61
- message: `Codex error: ${err instanceof Error ? err.message : String(err)}`,
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.thread = null;
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.pendingAbort) {
85
- console.log("[codex-process] Interrupting current turn");
86
- this.pendingAbort.abort();
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
- text,
107
- images,
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
- // ---- Private ----
111
- async runInputLoop() {
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 || !this.thread)
422
+ if (this.stopped || !pendingInput.text)
118
423
  break;
119
- const { input, tempPaths } = await this.toSdkInput(pendingInput);
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
- const controller = new AbortController();
126
- this.pendingAbort = controller;
127
- try {
128
- const streamed = await this.thread.runStreamed(input, {
129
- signal: controller.signal,
130
- });
131
- for await (const event of streamed.events) {
132
- if (this.stopped)
133
- break;
134
- this.processEvent(event);
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
- catch (err) {
138
- if (!this.stopped) {
139
- const msg = err instanceof Error ? err.message : String(err);
140
- // Don't emit error for abort (user-initiated interrupt)
141
- if (!controller.signal.aborted) {
142
- this.emitMessage({ type: "error", message: msg });
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: controller.signal.aborted ? "interrupted" : "error",
147
- error: controller.signal.aborted ? undefined : msg,
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
- finally {
153
- for (const path of tempPaths) {
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.setStatus("idle");
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
- processEvent(event) {
164
- switch (event.type) {
165
- case "thread.started":
166
- this._threadId = event.thread_id;
167
- console.log(`[codex-process] Thread started: ${event.thread_id}`);
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: "system",
170
- subtype: "init",
171
- sessionId: event.thread_id,
172
- model: this.startModel ?? "codex",
611
+ type: "permission_request",
612
+ toolUseId,
613
+ toolName: "AskUserQuestion",
614
+ input,
173
615
  });
616
+ this.setStatus("waiting_approval");
174
617
  break;
175
- case "turn.started":
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
- case "item.started":
179
- this.processItemStarted(event.item);
640
+ }
641
+ case "turn/completed": {
642
+ this.handleTurnCompleted(params.turn);
180
643
  break;
181
- case "item.completed":
182
- this.processItemCompleted(event.item);
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
- case "item.updated":
185
- // Not fired in current SDK version, but handle for future compatibility
186
- this.processItemCompleted(event.item);
655
+ }
656
+ case "item/started": {
657
+ this.processItemStarted(params.item);
187
658
  break;
188
- case "turn.completed":
189
- this.emitMessage({
190
- type: "result",
191
- subtype: "success",
192
- sessionId: this._threadId ?? undefined,
193
- inputTokens: event.usage.input_tokens,
194
- cachedInputTokens: event.usage.cached_input_tokens,
195
- outputTokens: event.usage.output_tokens,
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
- case "turn.failed":
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: "result",
201
- subtype: "error",
202
- error: event.error.message,
203
- sessionId: this._threadId ?? undefined,
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
- case "error":
207
- this.emitMessage({ type: "error", message: event.message });
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
- switch (item.type) {
213
- case "command_execution":
214
- // Emit tool_use for the command (shown before result)
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: item.id,
791
+ id: itemId,
219
792
  role: "assistant",
220
793
  content: [
221
794
  {
222
795
  type: "tool_use",
223
- id: item.id,
796
+ id: itemId,
224
797
  name: "Bash",
225
- input: { command: item.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
- // Other item types: nothing to show on start
826
+ }
827
+ default:
828
+ break;
233
829
  }
234
830
  }
235
831
  processItemCompleted(item) {
236
- switch (item.type) {
237
- case "agent_message":
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: item.id,
844
+ id: itemId,
242
845
  role: "assistant",
243
- content: [{ type: "text", text: item.text }],
846
+ content: [{ type: "text", text }],
244
847
  model: "codex",
245
848
  },
246
849
  });
247
850
  break;
248
- case "reasoning":
249
- this.emitMessage({ type: "thinking_delta", text: item.text });
851
+ }
852
+ case "reasoning": {
853
+ const text = extractReasoningText(item);
854
+ if (text) {
855
+ this.emitMessage({ type: "thinking_delta", text });
856
+ }
250
857
  break;
251
- case "command_execution":
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: item.id,
255
- content: item.aggregated_output || `exit code: ${item.exit_code}`,
868
+ toolUseId: itemId,
869
+ content: output || `exit code: ${exitCode ?? "unknown"}`,
256
870
  toolName: "Bash",
257
871
  });
258
872
  break;
259
- case "file_change":
260
- // Emit tool_use first (for display)
261
- this.emitMessage({
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: item.id,
280
- content: item.changes
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
- case "mcp_tool_call": {
287
- const toolName = `mcp:${item.server}/${item.tool}`;
288
- // Emit tool_use first
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: item.id,
892
+ id: itemId,
293
893
  role: "assistant",
294
894
  content: [
295
895
  {
296
896
  type: "tool_use",
297
- id: item.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: item.id,
308
- content: item.result
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 "web_search":
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: item.id,
918
+ id: itemId,
320
919
  role: "assistant",
321
920
  content: [
322
921
  {
323
922
  type: "tool_use",
324
- id: item.id,
923
+ id: itemId,
325
924
  name: "WebSearch",
326
- input: { query: item.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: item.id,
335
- content: `Web search: ${item.query}`,
933
+ toolUseId: itemId,
934
+ content: query ? `Web search: ${query}` : "Web search completed",
336
935
  toolName: "WebSearch",
337
936
  });
338
937
  break;
339
- case "todo_list":
340
- this.emitMessage({
341
- type: "assistant",
342
- message: {
343
- id: item.id,
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
- case "error":
358
- this.emitMessage({ type: "error", message: item.message });
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
- setStatus(status) {
363
- if (this._status !== status) {
364
- this._status = status;
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: pendingInput.text, tempPaths: [] };
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
- if (tempPaths.length === 0) {
407
- // All images failed, send text only
408
- return { input: pendingInput.text, tempPaths: [] };
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 { input: inputParts, tempPaths };
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) {