@ccpocket-base-auth/bridge 1.26.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.
Files changed (89) hide show
  1. package/README.md +67 -0
  2. package/dist/archive-store.d.ts +28 -0
  3. package/dist/archive-store.js +68 -0
  4. package/dist/archive-store.js.map +1 -0
  5. package/dist/cli.d.ts +2 -0
  6. package/dist/cli.js +82 -0
  7. package/dist/cli.js.map +1 -0
  8. package/dist/codex-process.d.ts +171 -0
  9. package/dist/codex-process.js +1928 -0
  10. package/dist/codex-process.js.map +1 -0
  11. package/dist/debug-trace-store.d.ts +15 -0
  12. package/dist/debug-trace-store.js +78 -0
  13. package/dist/debug-trace-store.js.map +1 -0
  14. package/dist/doctor.d.ts +58 -0
  15. package/dist/doctor.js +663 -0
  16. package/dist/doctor.js.map +1 -0
  17. package/dist/firebase-auth.d.ts +35 -0
  18. package/dist/firebase-auth.js +132 -0
  19. package/dist/firebase-auth.js.map +1 -0
  20. package/dist/gallery-store.d.ts +67 -0
  21. package/dist/gallery-store.js +333 -0
  22. package/dist/gallery-store.js.map +1 -0
  23. package/dist/image-store.d.ts +23 -0
  24. package/dist/image-store.js +142 -0
  25. package/dist/image-store.js.map +1 -0
  26. package/dist/index.d.ts +1 -0
  27. package/dist/index.js +191 -0
  28. package/dist/index.js.map +1 -0
  29. package/dist/mdns.d.ts +7 -0
  30. package/dist/mdns.js +49 -0
  31. package/dist/mdns.js.map +1 -0
  32. package/dist/parser.d.ts +465 -0
  33. package/dist/parser.js +251 -0
  34. package/dist/parser.js.map +1 -0
  35. package/dist/project-history.d.ts +10 -0
  36. package/dist/project-history.js +73 -0
  37. package/dist/project-history.js.map +1 -0
  38. package/dist/prompt-history-backup.d.ts +15 -0
  39. package/dist/prompt-history-backup.js +46 -0
  40. package/dist/prompt-history-backup.js.map +1 -0
  41. package/dist/proxy.d.ts +15 -0
  42. package/dist/proxy.js +95 -0
  43. package/dist/proxy.js.map +1 -0
  44. package/dist/push-i18n.d.ts +7 -0
  45. package/dist/push-i18n.js +75 -0
  46. package/dist/push-i18n.js.map +1 -0
  47. package/dist/push-relay.d.ts +29 -0
  48. package/dist/push-relay.js +70 -0
  49. package/dist/push-relay.js.map +1 -0
  50. package/dist/recording-store.d.ts +51 -0
  51. package/dist/recording-store.js +158 -0
  52. package/dist/recording-store.js.map +1 -0
  53. package/dist/screenshot.d.ts +28 -0
  54. package/dist/screenshot.js +98 -0
  55. package/dist/screenshot.js.map +1 -0
  56. package/dist/sdk-process.d.ts +180 -0
  57. package/dist/sdk-process.js +937 -0
  58. package/dist/sdk-process.js.map +1 -0
  59. package/dist/session.d.ts +142 -0
  60. package/dist/session.js +615 -0
  61. package/dist/session.js.map +1 -0
  62. package/dist/sessions-index.d.ts +128 -0
  63. package/dist/sessions-index.js +1767 -0
  64. package/dist/sessions-index.js.map +1 -0
  65. package/dist/setup-launchd.d.ts +8 -0
  66. package/dist/setup-launchd.js +109 -0
  67. package/dist/setup-launchd.js.map +1 -0
  68. package/dist/setup-systemd.d.ts +8 -0
  69. package/dist/setup-systemd.js +118 -0
  70. package/dist/setup-systemd.js.map +1 -0
  71. package/dist/startup-info.d.ts +8 -0
  72. package/dist/startup-info.js +92 -0
  73. package/dist/startup-info.js.map +1 -0
  74. package/dist/usage.d.ts +69 -0
  75. package/dist/usage.js +545 -0
  76. package/dist/usage.js.map +1 -0
  77. package/dist/version.d.ts +13 -0
  78. package/dist/version.js +43 -0
  79. package/dist/version.js.map +1 -0
  80. package/dist/websocket.d.ts +127 -0
  81. package/dist/websocket.js +2482 -0
  82. package/dist/websocket.js.map +1 -0
  83. package/dist/worktree-store.d.ts +25 -0
  84. package/dist/worktree-store.js +59 -0
  85. package/dist/worktree-store.js.map +1 -0
  86. package/dist/worktree.d.ts +47 -0
  87. package/dist/worktree.js +313 -0
  88. package/dist/worktree.js.map +1 -0
  89. package/package.json +68 -0
@@ -0,0 +1,1928 @@
1
+ import { EventEmitter } from "node:events";
2
+ import { randomUUID } from "node:crypto";
3
+ import { tmpdir } from "node:os";
4
+ import { join } from "node:path";
5
+ import { rm, writeFile } from "node:fs/promises";
6
+ import { spawn } from "node:child_process";
7
+ export class CodexProcess extends EventEmitter {
8
+ child = null;
9
+ _status = "starting";
10
+ _threadId = null;
11
+ _agentNickname = null;
12
+ _agentRole = null;
13
+ stopped = false;
14
+ startModel;
15
+ inputResolve = null;
16
+ pendingTurnId = null;
17
+ pendingTurnCompletion = null;
18
+ pendingApprovals = new Map();
19
+ pendingUserInputs = new Map();
20
+ lastTokenUsage = null;
21
+ /** Full skill metadata from the last `skills/list` response. */
22
+ _skills = [];
23
+ /** Project path stored for re-fetching skills on `skills/changed`. */
24
+ _projectPath = null;
25
+ /** Expose skill metadata so session/websocket can access it. */
26
+ get skills() {
27
+ return this._skills;
28
+ }
29
+ rpcSeq = 1;
30
+ pendingRpc = new Map();
31
+ stdoutBuffer = "";
32
+ // Collaboration mode & plan completion state
33
+ _approvalPolicy = "never";
34
+ _collaborationMode = "default";
35
+ lastPlanItemText = null;
36
+ pendingPlanCompletion = null;
37
+ /** Queued plan execution text when inputResolve wasn't ready at approval time. */
38
+ _pendingPlanInput = null;
39
+ get status() {
40
+ return this._status;
41
+ }
42
+ get isWaitingForInput() {
43
+ return this.inputResolve !== null;
44
+ }
45
+ get sessionId() {
46
+ return this._threadId;
47
+ }
48
+ get agentNickname() {
49
+ return this._agentNickname;
50
+ }
51
+ get agentRole() {
52
+ return this._agentRole;
53
+ }
54
+ get isRunning() {
55
+ return this.child !== null;
56
+ }
57
+ get approvalPolicy() {
58
+ return this._approvalPolicy;
59
+ }
60
+ /**
61
+ * Update approval policy at runtime.
62
+ * Takes effect on the next `turn/start` RPC call.
63
+ */
64
+ setApprovalPolicy(policy) {
65
+ this._approvalPolicy = policy;
66
+ console.log(`[codex-process] Approval policy changed to: ${policy}`);
67
+ }
68
+ /**
69
+ * Set collaboration mode ("plan" or "default").
70
+ * Takes effect on the next `turn/start` RPC call.
71
+ */
72
+ setCollaborationMode(mode) {
73
+ this._collaborationMode = mode;
74
+ console.log(`[codex-process] Collaboration mode changed to: ${mode}`);
75
+ }
76
+ get collaborationMode() {
77
+ return this._collaborationMode;
78
+ }
79
+ /**
80
+ * Rename a thread via the app-server RPC.
81
+ * Sends thread/name/set which persists to ~/.codex/session_index.jsonl.
82
+ */
83
+ async renameThread(name) {
84
+ if (!this._threadId) {
85
+ throw new Error("No thread ID available for rename");
86
+ }
87
+ await this.request("thread/name/set", {
88
+ threadId: this._threadId,
89
+ name,
90
+ });
91
+ }
92
+ /**
93
+ * Archive a Codex thread via the app-server `thread/archive` RPC.
94
+ * Accepts an explicit threadId so that historical (non-active) sessions
95
+ * can be archived without requiring a running process.
96
+ */
97
+ async archiveThread(threadId) {
98
+ await this.request("thread/archive", { threadId });
99
+ }
100
+ async listThreads(params = {}) {
101
+ const result = await this.request("thread/list", {
102
+ sortKey: "updated_at",
103
+ archived: false,
104
+ ...(params.limit != null ? { limit: params.limit } : {}),
105
+ ...(params.cursor !== undefined ? { cursor: params.cursor } : {}),
106
+ ...(params.cwd ? { cwd: params.cwd } : {}),
107
+ ...(params.searchTerm ? { searchTerm: params.searchTerm } : {}),
108
+ });
109
+ const data = Array.isArray(result.data)
110
+ ? result.data.map((entry) => toCodexThreadSummary(entry))
111
+ : [];
112
+ return {
113
+ data,
114
+ nextCursor: typeof result.nextCursor === "string" ? result.nextCursor : null,
115
+ };
116
+ }
117
+ start(projectPath, options) {
118
+ if (this.child) {
119
+ this.stop();
120
+ }
121
+ this.prepareLaunch(projectPath, options);
122
+ this.launchAppServer(projectPath, options);
123
+ void this.bootstrap(projectPath, options);
124
+ }
125
+ async initializeOnly(projectPath) {
126
+ if (this.child) {
127
+ this.stop();
128
+ }
129
+ this.prepareLaunch(projectPath);
130
+ this.launchAppServer(projectPath);
131
+ await this.initializeRpcConnection();
132
+ this.setStatus("idle");
133
+ }
134
+ stop() {
135
+ this.stopped = true;
136
+ if (this.inputResolve) {
137
+ this.inputResolve({ text: "" });
138
+ this.inputResolve = null;
139
+ }
140
+ this.pendingApprovals.clear();
141
+ this.pendingUserInputs.clear();
142
+ this.rejectAllPending(new Error("stopped"));
143
+ if (this.child) {
144
+ this.child.kill("SIGTERM");
145
+ this.child = null;
146
+ }
147
+ this.setStatus("idle");
148
+ console.log("[codex-process] Stopped");
149
+ }
150
+ prepareLaunch(projectPath, options) {
151
+ this.stopped = false;
152
+ this._threadId = null;
153
+ this._agentNickname = null;
154
+ this._agentRole = null;
155
+ this.pendingTurnId = null;
156
+ this.pendingTurnCompletion = null;
157
+ this.pendingApprovals.clear();
158
+ this.pendingUserInputs.clear();
159
+ this.lastTokenUsage = null;
160
+ this.startModel = options?.model;
161
+ this._approvalPolicy = options?.approvalPolicy ?? "never";
162
+ this._collaborationMode = options?.collaborationMode ?? "default";
163
+ this.lastPlanItemText = null;
164
+ this.pendingPlanCompletion = null;
165
+ this._pendingPlanInput = null;
166
+ this._projectPath = projectPath;
167
+ }
168
+ launchAppServer(projectPath, options) {
169
+ 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})`);
170
+ const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
171
+ cwd: projectPath,
172
+ stdio: "pipe",
173
+ env: process.env,
174
+ });
175
+ this.child = child;
176
+ child.stdout.setEncoding("utf8");
177
+ child.stdout.on("data", (chunk) => {
178
+ this.handleStdoutChunk(chunk);
179
+ });
180
+ child.stderr.setEncoding("utf8");
181
+ child.stderr.on("data", (chunk) => {
182
+ const line = chunk.trim();
183
+ if (line) {
184
+ console.log(`[codex-process] stderr: ${line}`);
185
+ }
186
+ });
187
+ child.on("error", (err) => {
188
+ if (this.stopped)
189
+ return;
190
+ console.error("[codex-process] app-server process error:", err);
191
+ this.emitMessage({ type: "error", message: `Failed to start codex app-server: ${err.message}` });
192
+ this.setStatus("idle");
193
+ this.emit("exit", 1);
194
+ });
195
+ child.on("exit", (code) => {
196
+ const exitCode = code ?? 0;
197
+ this.child = null;
198
+ this.rejectAllPending(new Error("codex app-server exited"));
199
+ if (!this.stopped && exitCode !== 0) {
200
+ this.emitMessage({ type: "error", message: `codex app-server exited with code ${exitCode}` });
201
+ }
202
+ this.setStatus("idle");
203
+ this.emit("exit", code);
204
+ });
205
+ }
206
+ interrupt() {
207
+ if (!this._threadId || !this.pendingTurnId)
208
+ return;
209
+ void this.request("turn/interrupt", {
210
+ threadId: this._threadId,
211
+ turnId: this.pendingTurnId,
212
+ }).catch((err) => {
213
+ if (!this.stopped) {
214
+ console.warn(`[codex-process] turn/interrupt failed: ${err instanceof Error ? err.message : String(err)}`);
215
+ }
216
+ });
217
+ }
218
+ sendInput(text) {
219
+ if (!this.inputResolve) {
220
+ console.error("[codex-process] No pending input resolver for sendInput");
221
+ return;
222
+ }
223
+ const resolve = this.inputResolve;
224
+ this.inputResolve = null;
225
+ resolve({ text });
226
+ }
227
+ sendInputWithImages(text, images) {
228
+ if (!this.inputResolve) {
229
+ console.error("[codex-process] No pending input resolver for sendInputWithImages");
230
+ return;
231
+ }
232
+ const resolve = this.inputResolve;
233
+ this.inputResolve = null;
234
+ resolve({ text, images });
235
+ }
236
+ sendInputWithSkill(text, skill) {
237
+ if (!this.inputResolve) {
238
+ console.error("[codex-process] No pending input resolver for sendInputWithSkill");
239
+ return;
240
+ }
241
+ const resolve = this.inputResolve;
242
+ this.inputResolve = null;
243
+ resolve({ text, skill });
244
+ }
245
+ approve(toolUseId, _updatedInput) {
246
+ // Check if this is a plan completion approval
247
+ if (this.pendingPlanCompletion && toolUseId === this.pendingPlanCompletion.toolUseId) {
248
+ this.handlePlanApproved(_updatedInput);
249
+ return;
250
+ }
251
+ const pending = this.resolvePendingApproval(toolUseId);
252
+ if (!pending) {
253
+ console.log("[codex-process] approve() called but no pending permission requests");
254
+ return;
255
+ }
256
+ this.pendingApprovals.delete(pending.toolUseId);
257
+ this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, "accept"));
258
+ this.emitToolResult(pending.toolUseId, "Approved");
259
+ if (this.pendingApprovals.size === 0) {
260
+ this.setStatus("running");
261
+ }
262
+ }
263
+ approveAlways(toolUseId) {
264
+ const pending = this.resolvePendingApproval(toolUseId);
265
+ if (!pending) {
266
+ console.log("[codex-process] approveAlways() called but no pending permission requests");
267
+ return;
268
+ }
269
+ this.pendingApprovals.delete(pending.toolUseId);
270
+ this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, "acceptForSession"));
271
+ this.emitToolResult(pending.toolUseId, "Approved (always)");
272
+ if (this.pendingApprovals.size === 0) {
273
+ this.setStatus("running");
274
+ }
275
+ }
276
+ reject(toolUseId, _message) {
277
+ // Check if this is a plan completion rejection
278
+ if (this.pendingPlanCompletion && toolUseId === this.pendingPlanCompletion.toolUseId) {
279
+ this.handlePlanRejected(_message);
280
+ return;
281
+ }
282
+ const pending = this.resolvePendingApproval(toolUseId);
283
+ if (!pending) {
284
+ console.log("[codex-process] reject() called but no pending permission requests");
285
+ return;
286
+ }
287
+ this.pendingApprovals.delete(pending.toolUseId);
288
+ this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, "decline"));
289
+ this.emitToolResult(pending.toolUseId, "Rejected");
290
+ if (this.pendingApprovals.size === 0) {
291
+ this.setStatus("running");
292
+ }
293
+ }
294
+ answer(toolUseId, result) {
295
+ const pending = this.resolvePendingUserInput(toolUseId);
296
+ if (!pending) {
297
+ console.log("[codex-process] answer() called but no pending AskUserQuestion");
298
+ return;
299
+ }
300
+ this.pendingUserInputs.delete(pending.toolUseId);
301
+ this.respondToServerRequest(pending.requestId, buildUserInputResponse(pending, result));
302
+ this.emitToolResult(pending.toolUseId, "Answered");
303
+ if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
304
+ this.setStatus("running");
305
+ }
306
+ }
307
+ getPendingPermission(toolUseId) {
308
+ // Check plan completion first
309
+ if (this.pendingPlanCompletion) {
310
+ if (!toolUseId || toolUseId === this.pendingPlanCompletion.toolUseId) {
311
+ return {
312
+ toolUseId: this.pendingPlanCompletion.toolUseId,
313
+ toolName: "ExitPlanMode",
314
+ input: { plan: this.pendingPlanCompletion.planText },
315
+ };
316
+ }
317
+ }
318
+ const pending = this.resolvePendingApproval(toolUseId);
319
+ if (pending) {
320
+ return {
321
+ toolUseId: pending.toolUseId,
322
+ toolName: pending.toolName,
323
+ input: { ...pending.input },
324
+ };
325
+ }
326
+ const pendingAsk = this.resolvePendingUserInput(toolUseId);
327
+ if (!pendingAsk)
328
+ return undefined;
329
+ return {
330
+ toolUseId: pendingAsk.toolUseId,
331
+ toolName: "AskUserQuestion",
332
+ input: { ...pendingAsk.input },
333
+ };
334
+ }
335
+ /** Emit a synthetic tool_result so history replay can match it to a permission_request. */
336
+ emitToolResult(toolUseId, content) {
337
+ this.emitMessage({
338
+ type: "tool_result",
339
+ toolUseId,
340
+ content,
341
+ });
342
+ }
343
+ resolvePendingApproval(toolUseId) {
344
+ if (toolUseId)
345
+ return this.pendingApprovals.get(toolUseId);
346
+ const first = this.pendingApprovals.values().next();
347
+ return first.done ? undefined : first.value;
348
+ }
349
+ resolvePendingUserInput(toolUseId) {
350
+ if (toolUseId)
351
+ return this.pendingUserInputs.get(toolUseId);
352
+ const first = this.pendingUserInputs.values().next();
353
+ return first.done ? undefined : first.value;
354
+ }
355
+ // ---------------------------------------------------------------------------
356
+ // Plan completion handlers (native collaboration_mode)
357
+ // ---------------------------------------------------------------------------
358
+ /**
359
+ * Plan approved → switch to Default mode and auto-start execution.
360
+ */
361
+ handlePlanApproved(updatedInput) {
362
+ const planText = updatedInput?.plan ?? this.pendingPlanCompletion?.planText ?? "";
363
+ const resolvedToolUseId = this.pendingPlanCompletion?.toolUseId;
364
+ this.pendingPlanCompletion = null;
365
+ this._collaborationMode = "default";
366
+ console.log("[codex-process] Plan approved, switching to Default mode");
367
+ // Emit synthetic tool_result so history replay knows this approval is resolved
368
+ if (resolvedToolUseId) {
369
+ this.emitToolResult(resolvedToolUseId, "Plan approved");
370
+ }
371
+ // Resolve inputResolve to start the next turn (Default mode) automatically
372
+ if (this.inputResolve) {
373
+ const resolve = this.inputResolve;
374
+ this.inputResolve = null;
375
+ resolve({ text: `Execute the following plan:\n\n${planText}` });
376
+ }
377
+ else {
378
+ // inputResolve may not be ready yet if approval comes before the next
379
+ // input loop iteration. Queue the text so sendInput() can pick it up.
380
+ console.warn("[codex-process] Plan approved but inputResolve not ready, queuing as pending input");
381
+ this._pendingPlanInput = `Execute the following plan:\n\n${planText}`;
382
+ }
383
+ }
384
+ /**
385
+ * Plan rejected → stay in Plan mode and re-plan with feedback.
386
+ */
387
+ handlePlanRejected(feedback) {
388
+ const resolvedToolUseId = this.pendingPlanCompletion?.toolUseId;
389
+ this.pendingPlanCompletion = null;
390
+ console.log("[codex-process] Plan rejected, continuing in Plan mode");
391
+ // Stay in Plan mode
392
+ // Emit synthetic tool_result so history replay knows this approval is resolved
393
+ if (resolvedToolUseId) {
394
+ this.emitToolResult(resolvedToolUseId, "Plan rejected");
395
+ }
396
+ if (feedback) {
397
+ if (this.inputResolve) {
398
+ const resolve = this.inputResolve;
399
+ this.inputResolve = null;
400
+ resolve({ text: feedback });
401
+ }
402
+ else {
403
+ console.warn("[codex-process] Plan rejected but inputResolve not ready, queuing feedback");
404
+ this._pendingPlanInput = feedback;
405
+ }
406
+ }
407
+ else {
408
+ this.setStatus("idle");
409
+ }
410
+ }
411
+ async bootstrap(projectPath, options) {
412
+ try {
413
+ await this.initializeRpcConnection();
414
+ const threadParams = {
415
+ cwd: projectPath,
416
+ approvalPolicy: normalizeApprovalPolicy(options?.approvalPolicy ?? "never"),
417
+ sandbox: normalizeSandboxMode(options?.sandboxMode ?? "workspace-write"),
418
+ experimentalRawEvents: false,
419
+ persistExtendedHistory: true,
420
+ };
421
+ if (options?.model)
422
+ threadParams.model = options.model;
423
+ if (options?.modelReasoningEffort) {
424
+ threadParams.effort = normalizeReasoningEffort(options.modelReasoningEffort);
425
+ }
426
+ if (options?.networkAccessEnabled !== undefined) {
427
+ threadParams.sandboxPolicy = {
428
+ type: normalizeSandboxMode(options?.sandboxMode ?? "workspace-write"),
429
+ networkAccess: options.networkAccessEnabled,
430
+ };
431
+ }
432
+ if (options?.webSearchMode) {
433
+ threadParams.webSearchMode = options.webSearchMode;
434
+ }
435
+ const method = options?.threadId ? "thread/resume" : "thread/start";
436
+ if (options?.threadId) {
437
+ threadParams.threadId = options.threadId;
438
+ }
439
+ else {
440
+ threadParams.experimentalRawEvents = false;
441
+ }
442
+ if (options?.threadId) {
443
+ threadParams.persistExtendedHistory = true;
444
+ }
445
+ const response = await this.request(method, threadParams);
446
+ const thread = response.thread;
447
+ const threadId = typeof thread?.id === "string"
448
+ ? thread.id
449
+ : options?.threadId;
450
+ if (!threadId) {
451
+ throw new Error(`${method} returned no thread id`);
452
+ }
453
+ // Capture the resolved model name from thread response
454
+ if (typeof thread?.model === "string" && thread.model) {
455
+ this.startModel = thread.model;
456
+ }
457
+ this._threadId = threadId;
458
+ this.emitMessage({
459
+ type: "system",
460
+ subtype: "init",
461
+ sessionId: threadId,
462
+ model: this.startModel ?? "codex",
463
+ });
464
+ this.setStatus("idle");
465
+ // Fetch skills in background (non-blocking)
466
+ this._projectPath = projectPath;
467
+ void this.fetchSkills(projectPath);
468
+ await this.runInputLoop(options);
469
+ }
470
+ catch (err) {
471
+ if (!this.stopped) {
472
+ const message = err instanceof Error ? err.message : String(err);
473
+ console.error("[codex-process] bootstrap error:", err);
474
+ this.emitMessage({ type: "error", message: `Codex error: ${message}` });
475
+ this.emitMessage({ type: "result", subtype: "error", error: message, sessionId: this._threadId ?? undefined });
476
+ }
477
+ this.setStatus("idle");
478
+ this.emit("exit", 1);
479
+ }
480
+ }
481
+ async initializeRpcConnection() {
482
+ await this.request("initialize", {
483
+ clientInfo: {
484
+ name: "ccpocket_bridge",
485
+ version: "1.0.0",
486
+ title: "ccpocket bridge",
487
+ },
488
+ capabilities: {
489
+ experimentalApi: true,
490
+ },
491
+ });
492
+ this.notify("initialized", {});
493
+ }
494
+ /**
495
+ * Fetch skills from Codex app-server via `skills/list` RPC and emit them
496
+ * as a `supported_commands` system message so the Flutter client can display
497
+ * skill entries alongside built-in slash commands.
498
+ */
499
+ async fetchSkills(projectPath) {
500
+ const TIMEOUT_MS = 10_000;
501
+ try {
502
+ const result = await Promise.race([
503
+ this.request("skills/list", { cwds: [projectPath] }),
504
+ new Promise((resolve) => setTimeout(() => resolve(null), TIMEOUT_MS)),
505
+ ]);
506
+ if (this.stopped || !result?.data)
507
+ return;
508
+ const skills = [];
509
+ const slashCommands = [];
510
+ const skillMetadata = [];
511
+ for (const entry of result.data) {
512
+ for (const skill of entry.skills) {
513
+ if (skill.enabled) {
514
+ skills.push(skill.name);
515
+ slashCommands.push(skill.name);
516
+ skillMetadata.push({
517
+ name: skill.name,
518
+ path: skill.path,
519
+ description: skill.description,
520
+ shortDescription: skill.shortDescription ?? skill.interface?.shortDescription ?? undefined,
521
+ enabled: skill.enabled,
522
+ scope: skill.scope,
523
+ displayName: skill.interface?.displayName ?? undefined,
524
+ defaultPrompt: skill.interface?.defaultPrompt ?? undefined,
525
+ brandColor: skill.interface?.brandColor ?? undefined,
526
+ });
527
+ }
528
+ }
529
+ }
530
+ this._skills = skillMetadata;
531
+ if (slashCommands.length > 0) {
532
+ console.log(`[codex-process] skills/list returned ${slashCommands.length} skills`);
533
+ this.emitMessage({
534
+ type: "system",
535
+ subtype: "supported_commands",
536
+ slashCommands,
537
+ skills,
538
+ skillMetadata,
539
+ });
540
+ }
541
+ }
542
+ catch (err) {
543
+ console.log(`[codex-process] skills/list failed (non-fatal): ${err instanceof Error ? err.message : String(err)}`);
544
+ }
545
+ }
546
+ async runInputLoop(options) {
547
+ while (!this.stopped) {
548
+ const pendingInput = await new Promise((resolve) => {
549
+ this.inputResolve = resolve;
550
+ // If plan approval arrived before inputResolve was ready, drain it now.
551
+ if (this._pendingPlanInput) {
552
+ const text = this._pendingPlanInput;
553
+ this._pendingPlanInput = null;
554
+ this.inputResolve = null;
555
+ resolve({ text });
556
+ }
557
+ });
558
+ if (this.stopped || !pendingInput.text)
559
+ break;
560
+ if (!this._threadId) {
561
+ this.emitMessage({ type: "error", message: "Codex thread is not initialized" });
562
+ continue;
563
+ }
564
+ const { input, tempPaths } = await this.toRpcInput(pendingInput);
565
+ if (!input) {
566
+ continue;
567
+ }
568
+ this.setStatus("running");
569
+ this.lastTokenUsage = null;
570
+ const completion = await new Promise((resolve, reject) => {
571
+ this.pendingTurnCompletion = { resolve, reject };
572
+ const params = {
573
+ threadId: this._threadId,
574
+ input,
575
+ approvalPolicy: normalizeApprovalPolicy(this._approvalPolicy),
576
+ };
577
+ if (options?.model)
578
+ params.model = options.model;
579
+ if (options?.modelReasoningEffort) {
580
+ params.effort = normalizeReasoningEffort(options.modelReasoningEffort);
581
+ }
582
+ // Always send collaborationMode so the server switches modes correctly.
583
+ // Omitting it causes the server to persist the previous turn's mode.
584
+ const modeSettings = {
585
+ model: options?.model || this.startModel || "gpt-5.4",
586
+ };
587
+ if (this._collaborationMode === "plan") {
588
+ modeSettings.reasoning_effort = "medium";
589
+ }
590
+ params.collaborationMode = {
591
+ mode: this._collaborationMode,
592
+ settings: modeSettings,
593
+ };
594
+ console.log(`[codex-process] turn/start: approval=${params.approvalPolicy}, collaboration=${this._collaborationMode}`);
595
+ void this.request("turn/start", params)
596
+ .then((result) => {
597
+ const turn = result.turn;
598
+ if (typeof turn?.id === "string") {
599
+ this.pendingTurnId = turn.id;
600
+ }
601
+ })
602
+ .catch((err) => {
603
+ this.pendingTurnCompletion = null;
604
+ reject(err instanceof Error ? err : new Error(String(err)));
605
+ });
606
+ }).catch((err) => {
607
+ if (!this.stopped) {
608
+ const message = err instanceof Error ? err.message : String(err);
609
+ this.emitMessage({ type: "error", message });
610
+ this.emitMessage({
611
+ type: "result",
612
+ subtype: "error",
613
+ error: message,
614
+ sessionId: this._threadId ?? undefined,
615
+ });
616
+ this.setStatus("idle");
617
+ }
618
+ });
619
+ await Promise.all(tempPaths.map((path) => rm(path, { force: true }).catch(() => { })));
620
+ void completion;
621
+ }
622
+ }
623
+ handleStdoutChunk(chunk) {
624
+ this.stdoutBuffer += chunk;
625
+ while (true) {
626
+ const newlineIndex = this.stdoutBuffer.indexOf("\n");
627
+ if (newlineIndex < 0)
628
+ break;
629
+ const line = this.stdoutBuffer.slice(0, newlineIndex).trim();
630
+ this.stdoutBuffer = this.stdoutBuffer.slice(newlineIndex + 1);
631
+ if (!line)
632
+ continue;
633
+ try {
634
+ const envelope = JSON.parse(line);
635
+ this.handleRpcEnvelope(envelope);
636
+ }
637
+ catch (err) {
638
+ console.warn(`[codex-process] failed to parse app-server JSON line: ${line.slice(0, 200)}`);
639
+ if (!this.stopped) {
640
+ this.emitMessage({
641
+ type: "error",
642
+ message: `Failed to parse codex app-server output: ${err instanceof Error ? err.message : String(err)}`,
643
+ });
644
+ }
645
+ }
646
+ }
647
+ }
648
+ handleRpcEnvelope(envelope) {
649
+ if (envelope.id != null && envelope.method && envelope.result === undefined && envelope.error === undefined) {
650
+ this.handleServerRequest(envelope.id, envelope.method, envelope.params ?? {});
651
+ return;
652
+ }
653
+ if (envelope.id != null && (envelope.result !== undefined || envelope.error)) {
654
+ this.handleRpcResponse(envelope);
655
+ return;
656
+ }
657
+ if (envelope.method) {
658
+ this.handleNotification(envelope.method, envelope.params ?? {});
659
+ }
660
+ }
661
+ handleRpcResponse(envelope) {
662
+ if (typeof envelope.id !== "number") {
663
+ return;
664
+ }
665
+ const pending = this.pendingRpc.get(envelope.id);
666
+ if (!pending)
667
+ return;
668
+ this.pendingRpc.delete(envelope.id);
669
+ if ("error" in envelope && envelope.error) {
670
+ const message = envelope.error.message ?? `RPC error ${envelope.error.code ?? ""}`;
671
+ pending.reject(new Error(message));
672
+ return;
673
+ }
674
+ pending.resolve(envelope.result);
675
+ }
676
+ handleServerRequest(id, method, params) {
677
+ switch (method) {
678
+ case "item/commandExecution/requestApproval": {
679
+ const toolUseId = this.extractToolUseId(params, id);
680
+ const input = {
681
+ ...(typeof params.command === "string" ? { command: params.command } : {}),
682
+ ...(typeof params.cwd === "string" ? { cwd: params.cwd } : {}),
683
+ ...(params.commandActions ? { commandActions: params.commandActions } : {}),
684
+ ...(params.networkApprovalContext ? { networkApprovalContext: params.networkApprovalContext } : {}),
685
+ ...(params.additionalPermissions ? { additionalPermissions: params.additionalPermissions } : {}),
686
+ ...(params.skillMetadata ? { skillMetadata: params.skillMetadata } : {}),
687
+ ...(params.proposedExecpolicyAmendment ? { proposedExecpolicyAmendment: params.proposedExecpolicyAmendment } : {}),
688
+ ...(params.proposedNetworkPolicyAmendments ? { proposedNetworkPolicyAmendments: params.proposedNetworkPolicyAmendments } : {}),
689
+ ...(params.availableDecisions ? { availableDecisions: params.availableDecisions } : {}),
690
+ ...(typeof params.reason === "string" ? { reason: params.reason } : {}),
691
+ };
692
+ this.pendingApprovals.set(toolUseId, {
693
+ requestId: id,
694
+ toolUseId,
695
+ toolName: "Bash",
696
+ input,
697
+ kind: "command",
698
+ });
699
+ this.emitMessage({
700
+ type: "permission_request",
701
+ toolUseId,
702
+ toolName: "Bash",
703
+ input,
704
+ });
705
+ this.setStatus("waiting_approval");
706
+ break;
707
+ }
708
+ case "item/fileChange/requestApproval": {
709
+ const toolUseId = this.extractToolUseId(params, id);
710
+ const input = {
711
+ ...(Array.isArray(params.changes) ? { changes: params.changes } : {}),
712
+ ...(typeof params.grantRoot === "string" ? { grantRoot: params.grantRoot } : {}),
713
+ ...(typeof params.reason === "string" ? { reason: params.reason } : {}),
714
+ };
715
+ this.pendingApprovals.set(toolUseId, {
716
+ requestId: id,
717
+ toolUseId,
718
+ toolName: "FileChange",
719
+ input,
720
+ kind: "file",
721
+ });
722
+ this.emitMessage({
723
+ type: "permission_request",
724
+ toolUseId,
725
+ toolName: "FileChange",
726
+ input,
727
+ });
728
+ this.setStatus("waiting_approval");
729
+ break;
730
+ }
731
+ case "item/tool/requestUserInput": {
732
+ const toolUseId = this.extractToolUseId(params, id);
733
+ const questions = normalizeUserInputQuestions(params.questions);
734
+ const input = {
735
+ questions: questions.map((q) => ({
736
+ id: q.id,
737
+ question: q.question,
738
+ header: q.header,
739
+ options: q.options,
740
+ multiSelect: false,
741
+ isOther: q.isOther,
742
+ isSecret: q.isSecret,
743
+ })),
744
+ };
745
+ this.pendingUserInputs.set(toolUseId, {
746
+ requestId: id,
747
+ toolUseId,
748
+ toolName: "AskUserQuestion",
749
+ questions: questions.map((q) => ({
750
+ id: q.id,
751
+ question: q.question,
752
+ })),
753
+ input,
754
+ kind: "questions",
755
+ });
756
+ this.emitMessage({
757
+ type: "permission_request",
758
+ toolUseId,
759
+ toolName: "AskUserQuestion",
760
+ input,
761
+ });
762
+ this.setStatus("waiting_approval");
763
+ break;
764
+ }
765
+ case "item/permissions/requestApproval": {
766
+ const toolUseId = this.extractToolUseId(params, id);
767
+ const requestedPermissions = asRecord(params.permissions) ?? {};
768
+ const input = {
769
+ permissions: requestedPermissions,
770
+ ...(typeof params.reason === "string" ? { reason: params.reason } : {}),
771
+ };
772
+ this.pendingApprovals.set(toolUseId, {
773
+ requestId: id,
774
+ toolUseId,
775
+ toolName: "Permissions",
776
+ input,
777
+ kind: "permissions",
778
+ requestedPermissions,
779
+ });
780
+ this.emitMessage({
781
+ type: "permission_request",
782
+ toolUseId,
783
+ toolName: "Permissions",
784
+ input,
785
+ });
786
+ this.setStatus("waiting_approval");
787
+ break;
788
+ }
789
+ case "mcpServer/elicitation/request": {
790
+ const toolUseId = this.extractToolUseId(params, id);
791
+ const elicitation = createElicitationInput(params);
792
+ this.pendingUserInputs.set(toolUseId, {
793
+ requestId: id,
794
+ toolUseId,
795
+ toolName: "McpElicitation",
796
+ questions: elicitation.questions,
797
+ input: elicitation.input,
798
+ kind: elicitation.kind,
799
+ });
800
+ this.emitMessage({
801
+ type: "permission_request",
802
+ toolUseId,
803
+ toolName: "McpElicitation",
804
+ input: elicitation.input,
805
+ });
806
+ this.setStatus("waiting_approval");
807
+ break;
808
+ }
809
+ default:
810
+ this.respondToServerRequest(id, {});
811
+ break;
812
+ }
813
+ }
814
+ handleNotification(method, params) {
815
+ switch (method) {
816
+ case "thread/started": {
817
+ const thread = params.thread;
818
+ if (typeof thread?.id === "string") {
819
+ this._threadId = thread.id;
820
+ }
821
+ this._agentNickname = stringOrNull(thread?.agentNickname);
822
+ this._agentRole = stringOrNull(thread?.agentRole);
823
+ break;
824
+ }
825
+ case "turn/started": {
826
+ const turn = params.turn;
827
+ if (typeof turn?.id === "string") {
828
+ this.pendingTurnId = turn.id;
829
+ }
830
+ this.setStatus("running");
831
+ break;
832
+ }
833
+ case "turn/completed": {
834
+ this.handleTurnCompleted(params.turn);
835
+ break;
836
+ }
837
+ case "thread/name/updated": {
838
+ // Name change notification — handled by session manager
839
+ break;
840
+ }
841
+ case "thread/tokenUsage/updated": {
842
+ const usage = params.usage;
843
+ if (usage) {
844
+ this.lastTokenUsage = {
845
+ input: numberOrUndefined(usage.inputTokens ?? usage.input_tokens),
846
+ cachedInput: numberOrUndefined(usage.cachedInputTokens ?? usage.cached_input_tokens),
847
+ output: numberOrUndefined(usage.outputTokens ?? usage.output_tokens),
848
+ };
849
+ }
850
+ break;
851
+ }
852
+ case "item/started": {
853
+ this.processItemStarted(params.item);
854
+ break;
855
+ }
856
+ case "item/completed": {
857
+ this.processItemCompleted(params.item);
858
+ break;
859
+ }
860
+ case "item/agentMessage/delta": {
861
+ const delta = typeof params.delta === "string"
862
+ ? params.delta
863
+ : typeof params.textDelta === "string"
864
+ ? params.textDelta
865
+ : "";
866
+ if (delta) {
867
+ this.emitMessage({ type: "stream_delta", text: delta });
868
+ }
869
+ break;
870
+ }
871
+ case "item/reasoning/summaryTextDelta":
872
+ case "item/reasoning/textDelta": {
873
+ const delta = typeof params.delta === "string"
874
+ ? params.delta
875
+ : typeof params.textDelta === "string"
876
+ ? params.textDelta
877
+ : "";
878
+ if (delta) {
879
+ this.emitMessage({ type: "thinking_delta", text: delta });
880
+ }
881
+ break;
882
+ }
883
+ case "item/plan/delta": {
884
+ const delta = typeof params.delta === "string" ? params.delta : "";
885
+ if (delta) {
886
+ this.emitMessage({ type: "thinking_delta", text: delta });
887
+ }
888
+ break;
889
+ }
890
+ case "skills/changed": {
891
+ // Re-fetch skills when Codex notifies us of changes
892
+ if (this._projectPath) {
893
+ void this.fetchSkills(this._projectPath);
894
+ }
895
+ break;
896
+ }
897
+ case "turn/plan/updated": {
898
+ // Default mode's update_plan tool output — always show as informational text
899
+ const text = formatPlanUpdateText(params);
900
+ if (!text)
901
+ break;
902
+ this.emitMessage({
903
+ type: "assistant",
904
+ message: {
905
+ id: randomUUID(),
906
+ role: "assistant",
907
+ content: [{ type: "text", text }],
908
+ model: "codex",
909
+ },
910
+ });
911
+ break;
912
+ }
913
+ case "serverRequest/resolved": {
914
+ this.handleServerRequestResolved(params);
915
+ break;
916
+ }
917
+ default:
918
+ break;
919
+ }
920
+ }
921
+ handleTurnCompleted(turn) {
922
+ const status = String(turn?.status ?? "completed");
923
+ const usage = this.lastTokenUsage;
924
+ this.lastTokenUsage = null;
925
+ if (status === "failed") {
926
+ const errorObj = turn?.error;
927
+ const message = typeof errorObj?.message === "string"
928
+ ? errorObj.message
929
+ : "Turn failed";
930
+ this.emitMessage({
931
+ type: "result",
932
+ subtype: "error",
933
+ error: message,
934
+ sessionId: this._threadId ?? undefined,
935
+ });
936
+ }
937
+ else if (status === "interrupted") {
938
+ this.emitMessage({
939
+ type: "result",
940
+ subtype: "interrupted",
941
+ sessionId: this._threadId ?? undefined,
942
+ });
943
+ }
944
+ else {
945
+ this.emitMessage({
946
+ type: "result",
947
+ subtype: "success",
948
+ sessionId: this._threadId ?? undefined,
949
+ ...(usage?.input != null ? { inputTokens: usage.input } : {}),
950
+ ...(usage?.cachedInput != null ? { cachedInputTokens: usage.cachedInput } : {}),
951
+ ...(usage?.output != null ? { outputTokens: usage.output } : {}),
952
+ });
953
+ }
954
+ this.pendingTurnId = null;
955
+ // Plan mode: emit synthetic plan approval and wait for user decision
956
+ if (this._collaborationMode === "plan" && this.lastPlanItemText) {
957
+ const toolUseId = `plan_${randomUUID()}`;
958
+ this.pendingPlanCompletion = {
959
+ toolUseId,
960
+ planText: this.lastPlanItemText,
961
+ };
962
+ this.lastPlanItemText = null;
963
+ this.emitMessage({
964
+ type: "permission_request",
965
+ toolUseId,
966
+ toolName: "ExitPlanMode",
967
+ input: { plan: this.pendingPlanCompletion.planText },
968
+ });
969
+ this.setStatus("waiting_approval");
970
+ // Do NOT set idle — waiting for plan approval
971
+ }
972
+ else {
973
+ this.lastPlanItemText = null;
974
+ if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
975
+ this.setStatus("idle");
976
+ }
977
+ }
978
+ if (this.pendingTurnCompletion) {
979
+ this.pendingTurnCompletion.resolve();
980
+ this.pendingTurnCompletion = null;
981
+ }
982
+ }
983
+ processItemStarted(item) {
984
+ if (!item || typeof item !== "object")
985
+ return;
986
+ const itemId = typeof item.id === "string" ? item.id : randomUUID();
987
+ const itemType = normalizeItemType(item.type);
988
+ switch (itemType) {
989
+ case "commandexecution": {
990
+ const commandText = typeof item.command === "string"
991
+ ? item.command
992
+ : Array.isArray(item.command)
993
+ ? item.command.map((part) => String(part)).join(" ")
994
+ : "";
995
+ this.emitMessage({
996
+ type: "assistant",
997
+ message: {
998
+ id: itemId,
999
+ role: "assistant",
1000
+ content: [
1001
+ {
1002
+ type: "tool_use",
1003
+ id: itemId,
1004
+ name: "Bash",
1005
+ input: { command: commandText },
1006
+ },
1007
+ ],
1008
+ model: "codex",
1009
+ },
1010
+ });
1011
+ break;
1012
+ }
1013
+ case "filechange": {
1014
+ this.emitMessage({
1015
+ type: "assistant",
1016
+ message: {
1017
+ id: itemId,
1018
+ role: "assistant",
1019
+ content: [
1020
+ {
1021
+ type: "tool_use",
1022
+ id: itemId,
1023
+ name: "FileChange",
1024
+ input: {
1025
+ changes: Array.isArray(item.changes) ? item.changes : [],
1026
+ },
1027
+ },
1028
+ ],
1029
+ model: "codex",
1030
+ },
1031
+ });
1032
+ break;
1033
+ }
1034
+ case "dynamictoolcall": {
1035
+ const tool = typeof item.tool === "string" ? item.tool : "DynamicTool";
1036
+ this.emitMessage({
1037
+ type: "assistant",
1038
+ message: {
1039
+ id: itemId,
1040
+ role: "assistant",
1041
+ content: [
1042
+ {
1043
+ type: "tool_use",
1044
+ id: itemId,
1045
+ name: tool,
1046
+ input: toToolUseInput(item.arguments),
1047
+ },
1048
+ ],
1049
+ model: "codex",
1050
+ },
1051
+ });
1052
+ break;
1053
+ }
1054
+ case "collabagenttoolcall": {
1055
+ const tool = typeof item.tool === "string" ? item.tool : "subagent";
1056
+ const toolName = "SubAgent";
1057
+ const input = {
1058
+ tool,
1059
+ ...(typeof item.prompt === "string" ? { prompt: item.prompt } : {}),
1060
+ ...(typeof item.senderThreadId === "string" ? { senderThreadId: item.senderThreadId } : {}),
1061
+ ...(Array.isArray(item.receiverThreadIds) ? { receiverThreadIds: item.receiverThreadIds } : {}),
1062
+ ...(typeof item.model === "string" ? { model: item.model } : {}),
1063
+ ...(typeof item.reasoningEffort === "string" ? { reasoningEffort: item.reasoningEffort } : {}),
1064
+ ...(item.agentsStates ? { agentsStates: item.agentsStates } : {}),
1065
+ };
1066
+ this.emitMessage({
1067
+ type: "assistant",
1068
+ message: {
1069
+ id: itemId,
1070
+ role: "assistant",
1071
+ content: [
1072
+ {
1073
+ type: "tool_use",
1074
+ id: itemId,
1075
+ name: toolName,
1076
+ input,
1077
+ },
1078
+ ],
1079
+ model: "codex",
1080
+ },
1081
+ });
1082
+ break;
1083
+ }
1084
+ default:
1085
+ break;
1086
+ }
1087
+ }
1088
+ processItemCompleted(item) {
1089
+ if (!item || typeof item !== "object")
1090
+ return;
1091
+ const itemId = typeof item.id === "string" ? item.id : randomUUID();
1092
+ const itemType = normalizeItemType(item.type);
1093
+ switch (itemType) {
1094
+ case "agentmessage": {
1095
+ const text = extractAgentText(item);
1096
+ if (!text)
1097
+ return;
1098
+ this.emitMessage({
1099
+ type: "assistant",
1100
+ message: {
1101
+ id: itemId,
1102
+ role: "assistant",
1103
+ content: [{ type: "text", text }],
1104
+ model: "codex",
1105
+ },
1106
+ });
1107
+ break;
1108
+ }
1109
+ case "reasoning": {
1110
+ const text = extractReasoningText(item);
1111
+ if (text) {
1112
+ this.emitMessage({ type: "thinking_delta", text });
1113
+ }
1114
+ break;
1115
+ }
1116
+ case "commandexecution": {
1117
+ const output = typeof item.aggregatedOutput === "string"
1118
+ ? item.aggregatedOutput
1119
+ : typeof item.output === "string"
1120
+ ? item.output
1121
+ : "";
1122
+ const exitCode = numberOrUndefined(item.exitCode ?? item.exit_code);
1123
+ this.emitMessage({
1124
+ type: "tool_result",
1125
+ toolUseId: itemId,
1126
+ content: output || `exit code: ${exitCode ?? "unknown"}`,
1127
+ toolName: "Bash",
1128
+ });
1129
+ break;
1130
+ }
1131
+ case "filechange": {
1132
+ const content = formatFileChangesWithDiff(item.changes);
1133
+ this.emitMessage({
1134
+ type: "tool_result",
1135
+ toolUseId: itemId,
1136
+ content,
1137
+ toolName: "FileChange",
1138
+ });
1139
+ break;
1140
+ }
1141
+ case "mcptoolcall": {
1142
+ const server = typeof item.server === "string" ? item.server : "mcp";
1143
+ const tool = typeof item.tool === "string" ? item.tool : "unknown";
1144
+ const toolName = `mcp:${server}/${tool}`;
1145
+ const result = item.result ?? item.error ?? "MCP call completed";
1146
+ const normalized = normalizeMcpToolResult(result);
1147
+ this.emitMessage({
1148
+ type: "assistant",
1149
+ message: {
1150
+ id: itemId,
1151
+ role: "assistant",
1152
+ content: [
1153
+ {
1154
+ type: "tool_use",
1155
+ id: itemId,
1156
+ name: toolName,
1157
+ input: item.arguments ?? {},
1158
+ },
1159
+ ],
1160
+ model: "codex",
1161
+ },
1162
+ });
1163
+ this.emitMessage({
1164
+ type: "tool_result",
1165
+ toolUseId: itemId,
1166
+ content: normalized.content,
1167
+ toolName,
1168
+ ...(normalized.rawContentBlocks.length > 0
1169
+ ? { rawContentBlocks: normalized.rawContentBlocks }
1170
+ : {}),
1171
+ });
1172
+ break;
1173
+ }
1174
+ case "dynamictoolcall": {
1175
+ const tool = typeof item.tool === "string" ? item.tool : "DynamicTool";
1176
+ const content = formatDynamicToolResult(item);
1177
+ this.emitMessage({
1178
+ type: "tool_result",
1179
+ toolUseId: itemId,
1180
+ content,
1181
+ toolName: tool,
1182
+ });
1183
+ break;
1184
+ }
1185
+ case "websearch": {
1186
+ const query = typeof item.query === "string" ? item.query : "";
1187
+ this.emitMessage({
1188
+ type: "assistant",
1189
+ message: {
1190
+ id: itemId,
1191
+ role: "assistant",
1192
+ content: [
1193
+ {
1194
+ type: "tool_use",
1195
+ id: itemId,
1196
+ name: "WebSearch",
1197
+ input: { query },
1198
+ },
1199
+ ],
1200
+ model: "codex",
1201
+ },
1202
+ });
1203
+ this.emitMessage({
1204
+ type: "tool_result",
1205
+ toolUseId: itemId,
1206
+ content: query ? `Web search: ${query}` : "Web search completed",
1207
+ toolName: "WebSearch",
1208
+ });
1209
+ break;
1210
+ }
1211
+ case "collabagenttoolcall": {
1212
+ const tool = typeof item.tool === "string" ? item.tool : "subagent";
1213
+ const status = typeof item.status === "string" ? item.status : "completed";
1214
+ const receiverThreadIds = Array.isArray(item.receiverThreadIds)
1215
+ ? item.receiverThreadIds.map((entry) => String(entry))
1216
+ : [];
1217
+ const content = [
1218
+ `tool: ${tool}`,
1219
+ `status: ${status}`,
1220
+ ...(receiverThreadIds.length > 0
1221
+ ? [`agents: ${receiverThreadIds.join(", ")}`]
1222
+ : []),
1223
+ ].join("\n");
1224
+ this.emitMessage({
1225
+ type: "tool_result",
1226
+ toolUseId: itemId,
1227
+ content,
1228
+ toolName: "SubAgent",
1229
+ });
1230
+ break;
1231
+ }
1232
+ case "plan": {
1233
+ // Plan item completed — save text for plan approval emission in handleTurnCompleted()
1234
+ const planText = typeof item.text === "string" ? item.text : "";
1235
+ this.lastPlanItemText = planText;
1236
+ break;
1237
+ }
1238
+ case "error": {
1239
+ const message = typeof item.message === "string" ? item.message : "Codex item error";
1240
+ this.emitMessage({ type: "error", message });
1241
+ break;
1242
+ }
1243
+ default:
1244
+ break;
1245
+ }
1246
+ }
1247
+ async toRpcInput(pendingInput) {
1248
+ const input = [];
1249
+ const tempPaths = [];
1250
+ // Prepend SkillUserInput if a skill reference is attached
1251
+ if (pendingInput.skill) {
1252
+ input.push({ type: "skill", name: pendingInput.skill.name, path: pendingInput.skill.path });
1253
+ }
1254
+ input.push({ type: "text", text: pendingInput.text });
1255
+ if (!pendingInput.images || pendingInput.images.length === 0) {
1256
+ return { input, tempPaths };
1257
+ }
1258
+ for (const image of pendingInput.images) {
1259
+ const ext = extensionFromMime(image.mimeType);
1260
+ if (!ext) {
1261
+ this.emitMessage({
1262
+ type: "error",
1263
+ message: `Unsupported image mime type for Codex: ${image.mimeType}`,
1264
+ });
1265
+ continue;
1266
+ }
1267
+ let buffer;
1268
+ try {
1269
+ buffer = Buffer.from(image.base64, "base64");
1270
+ }
1271
+ catch {
1272
+ this.emitMessage({
1273
+ type: "error",
1274
+ message: "Invalid base64 image data for Codex input",
1275
+ });
1276
+ continue;
1277
+ }
1278
+ const tempPath = join(tmpdir(), `ccpocket-codex-image-${randomUUID()}.${ext}`);
1279
+ await writeFile(tempPath, buffer);
1280
+ tempPaths.push(tempPath);
1281
+ input.push({ type: "localImage", path: tempPath });
1282
+ }
1283
+ return { input, tempPaths };
1284
+ }
1285
+ request(method, params) {
1286
+ const id = this.rpcSeq++;
1287
+ const envelope = { id, method, params };
1288
+ return new Promise((resolve, reject) => {
1289
+ this.pendingRpc.set(id, { resolve, reject, method });
1290
+ try {
1291
+ this.writeEnvelope(envelope);
1292
+ }
1293
+ catch (err) {
1294
+ this.pendingRpc.delete(id);
1295
+ reject(err instanceof Error ? err : new Error(String(err)));
1296
+ }
1297
+ });
1298
+ }
1299
+ notify(method, params) {
1300
+ this.writeEnvelope({ method, params });
1301
+ }
1302
+ respondToServerRequest(id, result) {
1303
+ try {
1304
+ this.writeEnvelope({ id, result });
1305
+ }
1306
+ catch (err) {
1307
+ if (!this.stopped) {
1308
+ console.warn(`[codex-process] failed to respond to server request: ${err instanceof Error ? err.message : String(err)}`);
1309
+ }
1310
+ }
1311
+ }
1312
+ writeEnvelope(envelope) {
1313
+ if (!this.child || this.child.killed) {
1314
+ throw new Error("codex app-server is not running");
1315
+ }
1316
+ const line = `${JSON.stringify(envelope)}\n`;
1317
+ this.child.stdin.write(line);
1318
+ }
1319
+ rejectAllPending(error) {
1320
+ for (const pending of this.pendingRpc.values()) {
1321
+ pending.reject(error);
1322
+ }
1323
+ this.pendingRpc.clear();
1324
+ if (this.pendingTurnCompletion) {
1325
+ this.pendingTurnCompletion.reject(error);
1326
+ this.pendingTurnCompletion = null;
1327
+ }
1328
+ }
1329
+ setStatus(status) {
1330
+ if (this._status !== status) {
1331
+ this._status = status;
1332
+ this.emit("status", status);
1333
+ this.emitMessage({ type: "status", status });
1334
+ }
1335
+ }
1336
+ emitMessage(msg) {
1337
+ this.emit("message", msg);
1338
+ }
1339
+ extractToolUseId(params, requestId) {
1340
+ if (typeof params.approvalId === "string")
1341
+ return params.approvalId;
1342
+ if (typeof params.elicitationId === "string")
1343
+ return params.elicitationId;
1344
+ if (typeof params.itemId === "string")
1345
+ return params.itemId;
1346
+ if (typeof requestId === "string")
1347
+ return requestId;
1348
+ return `approval-${requestId}`;
1349
+ }
1350
+ handleServerRequestResolved(params) {
1351
+ const requestId = params.requestId;
1352
+ if (requestId === undefined || requestId === null)
1353
+ return;
1354
+ const approval = [...this.pendingApprovals.values()].find((entry) => entry.requestId === requestId);
1355
+ if (approval) {
1356
+ this.pendingApprovals.delete(approval.toolUseId);
1357
+ this.emitMessage({ type: "permission_resolved", toolUseId: approval.toolUseId });
1358
+ }
1359
+ const inputRequest = [...this.pendingUserInputs.values()].find((entry) => entry.requestId === requestId);
1360
+ if (inputRequest) {
1361
+ this.pendingUserInputs.delete(inputRequest.toolUseId);
1362
+ this.emitMessage({ type: "permission_resolved", toolUseId: inputRequest.toolUseId });
1363
+ }
1364
+ if (!this.pendingPlanCompletion && this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
1365
+ this.setStatus(this.pendingTurnId ? "running" : "idle");
1366
+ }
1367
+ }
1368
+ }
1369
+ function buildApprovalResponse(pending, decision) {
1370
+ if (pending.kind === "permissions") {
1371
+ return {
1372
+ scope: decision === "acceptForSession" ? "session" : "turn",
1373
+ permissions: decision === "decline" ? {} : (pending.requestedPermissions ?? {}),
1374
+ };
1375
+ }
1376
+ return {
1377
+ decision,
1378
+ };
1379
+ }
1380
+ function buildUserInputResponse(pending, rawResult) {
1381
+ if (pending.kind === "questions") {
1382
+ return {
1383
+ answers: buildUserInputAnswers(pending.questions, rawResult),
1384
+ };
1385
+ }
1386
+ return buildElicitationResponse(pending, rawResult);
1387
+ }
1388
+ function normalizeApprovalPolicy(value) {
1389
+ switch (value) {
1390
+ case "on-request":
1391
+ return "on-request";
1392
+ case "on-failure":
1393
+ return "on-failure";
1394
+ case "untrusted":
1395
+ return "untrusted";
1396
+ case "never":
1397
+ default:
1398
+ return "never";
1399
+ }
1400
+ }
1401
+ function normalizeSandboxMode(value) {
1402
+ switch (value) {
1403
+ case "read-only":
1404
+ return "read-only";
1405
+ case "danger-full-access":
1406
+ return "danger-full-access";
1407
+ case "workspace-write":
1408
+ default:
1409
+ return "workspace-write";
1410
+ }
1411
+ }
1412
+ function normalizeReasoningEffort(value) {
1413
+ switch (value) {
1414
+ case "xhigh":
1415
+ return "high";
1416
+ default:
1417
+ return value;
1418
+ }
1419
+ }
1420
+ function normalizeItemType(raw) {
1421
+ if (typeof raw !== "string")
1422
+ return "";
1423
+ return raw.replace(/[_\s-]/g, "").toLowerCase();
1424
+ }
1425
+ function numberOrUndefined(value) {
1426
+ return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1427
+ }
1428
+ function stringOrNull(value) {
1429
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
1430
+ }
1431
+ function summarizeFileChanges(changes) {
1432
+ if (!Array.isArray(changes) || changes.length === 0) {
1433
+ return "No file changes";
1434
+ }
1435
+ return changes
1436
+ .map((entry) => {
1437
+ if (!entry || typeof entry !== "object")
1438
+ return "changed";
1439
+ const record = entry;
1440
+ const kind = typeof record.kind === "string" ? record.kind : "changed";
1441
+ const path = typeof record.path === "string" ? record.path : "(unknown)";
1442
+ return `${kind}: ${path}`;
1443
+ })
1444
+ .join("\n");
1445
+ }
1446
+ function toToolUseInput(value) {
1447
+ if (value && typeof value === "object" && !Array.isArray(value)) {
1448
+ return value;
1449
+ }
1450
+ if (Array.isArray(value)) {
1451
+ return { items: value };
1452
+ }
1453
+ if (value === undefined || value === null) {
1454
+ return {};
1455
+ }
1456
+ return { value };
1457
+ }
1458
+ function formatDynamicToolResult(item) {
1459
+ const status = typeof item.status === "string" ? item.status : "completed";
1460
+ const success = typeof item.success === "boolean" ? item.success : null;
1461
+ const contentItems = Array.isArray(item.contentItems) ? item.contentItems : null;
1462
+ const parts = [
1463
+ `status: ${status}`,
1464
+ ...(success != null ? [`success: ${success}`] : []),
1465
+ ];
1466
+ if (contentItems && contentItems.length > 0) {
1467
+ for (const entry of contentItems) {
1468
+ if (!entry || typeof entry !== "object")
1469
+ continue;
1470
+ const record = entry;
1471
+ const type = typeof record.type === "string" ? record.type : "item";
1472
+ if (type === "inputText" && typeof record.text === "string") {
1473
+ parts.push(record.text);
1474
+ continue;
1475
+ }
1476
+ if (type === "inputImage" && typeof record.imageUrl === "string") {
1477
+ parts.push(`image: ${record.imageUrl}`);
1478
+ continue;
1479
+ }
1480
+ parts.push(JSON.stringify(record));
1481
+ }
1482
+ }
1483
+ return parts.join("\n");
1484
+ }
1485
+ function normalizeMcpToolResult(result) {
1486
+ if (typeof result === "string") {
1487
+ return { content: result, rawContentBlocks: [] };
1488
+ }
1489
+ const record = result && typeof result === "object" && !Array.isArray(result)
1490
+ ? result
1491
+ : null;
1492
+ const contentItems = Array.isArray(record?.content) ? record.content : null;
1493
+ if (!contentItems) {
1494
+ return {
1495
+ content: result == null ? "MCP call completed" : JSON.stringify(result),
1496
+ rawContentBlocks: [],
1497
+ };
1498
+ }
1499
+ const textParts = [];
1500
+ const rawContentBlocks = [];
1501
+ for (const entry of contentItems) {
1502
+ if (!entry || typeof entry !== "object")
1503
+ continue;
1504
+ const item = entry;
1505
+ const type = typeof item.type === "string" ? item.type : "";
1506
+ if (type === "text" && typeof item.text === "string") {
1507
+ textParts.push(item.text);
1508
+ rawContentBlocks.push({ type: "text", text: item.text });
1509
+ continue;
1510
+ }
1511
+ if (type === "image" && typeof item.data === "string") {
1512
+ const mimeType = typeof item.mimeType === "string"
1513
+ ? item.mimeType
1514
+ : typeof item.mediaType === "string"
1515
+ ? item.mediaType
1516
+ : typeof item.media_type === "string"
1517
+ ? item.media_type
1518
+ : "image/png";
1519
+ rawContentBlocks.push({
1520
+ type: "image",
1521
+ source: {
1522
+ type: "base64",
1523
+ data: item.data,
1524
+ media_type: mimeType,
1525
+ },
1526
+ });
1527
+ continue;
1528
+ }
1529
+ rawContentBlocks.push(item);
1530
+ textParts.push(JSON.stringify(item));
1531
+ }
1532
+ const content = textParts.join("\n").trim();
1533
+ if (content.length > 0) {
1534
+ return { content, rawContentBlocks };
1535
+ }
1536
+ const imageCount = rawContentBlocks.filter((entry) => entry.type === "image").length;
1537
+ if (imageCount > 0) {
1538
+ return {
1539
+ content: imageCount === 1 ? "Generated 1 image" : `Generated ${imageCount} images`,
1540
+ rawContentBlocks,
1541
+ };
1542
+ }
1543
+ return {
1544
+ content: result == null ? "MCP call completed" : JSON.stringify(result),
1545
+ rawContentBlocks,
1546
+ };
1547
+ }
1548
+ function toCodexThreadSummary(entry) {
1549
+ const record = entry && typeof entry === "object"
1550
+ ? entry
1551
+ : {};
1552
+ const gitInfo = record.gitInfo && typeof record.gitInfo === "object"
1553
+ ? record.gitInfo
1554
+ : {};
1555
+ return {
1556
+ id: typeof record.id === "string" ? record.id : "",
1557
+ preview: typeof record.preview === "string" ? record.preview : "",
1558
+ createdAt: numberOrUndefined(record.createdAt) ?? 0,
1559
+ updatedAt: numberOrUndefined(record.updatedAt) ?? 0,
1560
+ cwd: typeof record.cwd === "string" ? record.cwd : "",
1561
+ agentNickname: stringOrNull(record.agentNickname),
1562
+ agentRole: stringOrNull(record.agentRole),
1563
+ gitBranch: stringOrNull(gitInfo.branch),
1564
+ name: stringOrNull(record.name),
1565
+ };
1566
+ }
1567
+ /**
1568
+ * Format file changes including unified diff content for display in chat.
1569
+ * Falls back to `kind: path` summary when no diff is available.
1570
+ */
1571
+ function formatFileChangesWithDiff(changes) {
1572
+ if (!Array.isArray(changes) || changes.length === 0) {
1573
+ return "No file changes";
1574
+ }
1575
+ return changes
1576
+ .map((entry) => {
1577
+ if (!entry || typeof entry !== "object")
1578
+ return "changed";
1579
+ const record = entry;
1580
+ const kind = typeof record.kind === "string" ? record.kind : "changed";
1581
+ const path = typeof record.path === "string" ? record.path : "(unknown)";
1582
+ const diff = typeof record.diff === "string" ? record.diff.trim() : "";
1583
+ if (diff) {
1584
+ // If diff already has unified headers, use as-is; otherwise add them
1585
+ if (diff.startsWith("---") || diff.startsWith("@@")) {
1586
+ return `--- a/${path}\n+++ b/${path}\n${diff}`;
1587
+ }
1588
+ return diff;
1589
+ }
1590
+ return `${kind}: ${path}`;
1591
+ })
1592
+ .join("\n\n");
1593
+ }
1594
+ function extractAgentText(item) {
1595
+ if (typeof item.text === "string")
1596
+ return item.text;
1597
+ const parts = item.content;
1598
+ if (Array.isArray(parts)) {
1599
+ const text = parts
1600
+ .filter((part) => part && typeof part === "object")
1601
+ .map((part) => {
1602
+ const record = part;
1603
+ if (record.type === "text" && typeof record.text === "string") {
1604
+ return record.text;
1605
+ }
1606
+ return "";
1607
+ })
1608
+ .filter((part) => part.length > 0)
1609
+ .join("\n");
1610
+ if (text)
1611
+ return text;
1612
+ }
1613
+ return "";
1614
+ }
1615
+ function extractReasoningText(item) {
1616
+ if (typeof item.text === "string")
1617
+ return item.text;
1618
+ const summary = item.summary;
1619
+ if (Array.isArray(summary)) {
1620
+ const text = summary
1621
+ .map((entry) => {
1622
+ if (!entry || typeof entry !== "object")
1623
+ return "";
1624
+ const record = entry;
1625
+ return typeof record.text === "string" ? record.text : "";
1626
+ })
1627
+ .filter((part) => part.length > 0)
1628
+ .join("\n");
1629
+ if (text)
1630
+ return text;
1631
+ }
1632
+ return "";
1633
+ }
1634
+ function normalizeUserInputQuestions(raw) {
1635
+ if (!Array.isArray(raw))
1636
+ return [];
1637
+ return raw
1638
+ .filter((entry) => !!entry && typeof entry === "object")
1639
+ .map((entry, index) => {
1640
+ const id = typeof entry.id === "string" ? entry.id : `question_${index + 1}`;
1641
+ const question = typeof entry.question === "string" ? entry.question : "";
1642
+ const header = typeof entry.header === "string" ? entry.header : `Question ${index + 1}`;
1643
+ const optionsRaw = Array.isArray(entry.options) ? entry.options : [];
1644
+ const options = optionsRaw
1645
+ .filter((option) => !!option && typeof option === "object")
1646
+ .map((option) => ({
1647
+ label: typeof option.label === "string" ? option.label : "",
1648
+ description: typeof option.description === "string" ? option.description : "",
1649
+ }))
1650
+ .filter((option) => option.label.length > 0);
1651
+ return {
1652
+ id,
1653
+ question,
1654
+ header,
1655
+ options,
1656
+ isOther: Boolean(entry.isOther),
1657
+ isSecret: Boolean(entry.isSecret),
1658
+ };
1659
+ })
1660
+ .filter((question) => question.question.length > 0);
1661
+ }
1662
+ function buildUserInputAnswers(questions, rawResult) {
1663
+ const parsed = parseResultObject(rawResult);
1664
+ const answerMap = {};
1665
+ for (const question of questions) {
1666
+ const candidate = parsed.byId[question.id] ?? parsed.byQuestion[question.question];
1667
+ const answers = normalizeAnswerValues(candidate);
1668
+ if (answers.length > 0) {
1669
+ answerMap[question.id] = { answers };
1670
+ }
1671
+ }
1672
+ if (Object.keys(answerMap).length === 0 && questions.length > 0) {
1673
+ answerMap[questions[0].id] = { answers: normalizeAnswerValues(rawResult) };
1674
+ }
1675
+ return answerMap;
1676
+ }
1677
+ function parseResultObject(rawResult) {
1678
+ try {
1679
+ const parsed = JSON.parse(rawResult);
1680
+ const byId = {};
1681
+ const byQuestion = {};
1682
+ if (parsed && typeof parsed === "object") {
1683
+ const answers = parsed.answers;
1684
+ if (answers && typeof answers === "object" && !Array.isArray(answers)) {
1685
+ for (const [key, value] of Object.entries(answers)) {
1686
+ byId[key] = value;
1687
+ byQuestion[key] = value;
1688
+ }
1689
+ }
1690
+ }
1691
+ return { byId, byQuestion };
1692
+ }
1693
+ catch {
1694
+ return { byId: {}, byQuestion: {} };
1695
+ }
1696
+ }
1697
+ function normalizeAnswerValues(value) {
1698
+ if (typeof value === "string") {
1699
+ return value
1700
+ .split(",")
1701
+ .map((part) => part.trim())
1702
+ .filter((part) => part.length > 0);
1703
+ }
1704
+ if (Array.isArray(value)) {
1705
+ return value
1706
+ .map((entry) => String(entry).trim())
1707
+ .filter((entry) => entry.length > 0);
1708
+ }
1709
+ if (value && typeof value === "object") {
1710
+ const record = value;
1711
+ if (Array.isArray(record.answers)) {
1712
+ return record.answers
1713
+ .map((entry) => String(entry).trim())
1714
+ .filter((entry) => entry.length > 0);
1715
+ }
1716
+ }
1717
+ if (value == null)
1718
+ return [];
1719
+ const normalized = String(value).trim();
1720
+ return normalized ? [normalized] : [];
1721
+ }
1722
+ function buildElicitationResponse(pending, rawResult) {
1723
+ if (pending.kind === "elicitation_url") {
1724
+ const action = parseElicitationAction(rawResult);
1725
+ return {
1726
+ action,
1727
+ content: null,
1728
+ _meta: null,
1729
+ };
1730
+ }
1731
+ const parsed = parseResultObject(rawResult);
1732
+ const content = {};
1733
+ for (const question of pending.questions) {
1734
+ const candidate = parsed.byId[question.id] ?? parsed.byQuestion[question.question];
1735
+ const answers = normalizeAnswerValues(candidate);
1736
+ if (answers.length === 1) {
1737
+ content[question.id] = answers[0];
1738
+ }
1739
+ else if (answers.length > 1) {
1740
+ content[question.id] = answers;
1741
+ }
1742
+ }
1743
+ if (Object.keys(content).length === 0 && pending.questions.length === 1) {
1744
+ const answers = normalizeAnswerValues(rawResult);
1745
+ if (answers.length === 1) {
1746
+ content[pending.questions[0].id] = answers[0];
1747
+ }
1748
+ else if (answers.length > 1) {
1749
+ content[pending.questions[0].id] = answers;
1750
+ }
1751
+ }
1752
+ return {
1753
+ action: "accept",
1754
+ content: Object.keys(content).length > 0 ? content : null,
1755
+ _meta: null,
1756
+ };
1757
+ }
1758
+ function parseElicitationAction(rawResult) {
1759
+ const normalized = rawResult.trim().toLowerCase();
1760
+ if (normalized.includes("cancel"))
1761
+ return "cancel";
1762
+ if (normalized.includes("decline") || normalized.includes("deny"))
1763
+ return "decline";
1764
+ try {
1765
+ const parsed = JSON.parse(rawResult);
1766
+ const answers = parsed.answers;
1767
+ if (answers && typeof answers === "object" && !Array.isArray(answers)) {
1768
+ const first = Object.values(answers)[0];
1769
+ const answer = normalizeAnswerValues(first).join(" ").toLowerCase();
1770
+ if (answer.includes("cancel"))
1771
+ return "cancel";
1772
+ if (answer.includes("decline") || answer.includes("deny"))
1773
+ return "decline";
1774
+ }
1775
+ }
1776
+ catch {
1777
+ // Fall through to accept.
1778
+ }
1779
+ return "accept";
1780
+ }
1781
+ function createElicitationInput(params) {
1782
+ const serverName = typeof params.serverName === "string" ? params.serverName : "MCP";
1783
+ const message = typeof params.message === "string" ? params.message : "Provide input";
1784
+ if (params.mode === "url") {
1785
+ const url = typeof params.url === "string" ? params.url : "";
1786
+ const question = url ? `${message}\n${url}` : message;
1787
+ return {
1788
+ kind: "elicitation_url",
1789
+ questions: [{ id: "elicitation_action", question }],
1790
+ input: {
1791
+ mode: "url",
1792
+ serverName,
1793
+ url,
1794
+ message,
1795
+ questions: [
1796
+ {
1797
+ id: "elicitation_action",
1798
+ header: serverName,
1799
+ question,
1800
+ options: [
1801
+ { label: "Accept", description: "Continue with this request" },
1802
+ { label: "Decline", description: "Reject this request" },
1803
+ { label: "Cancel", description: "Cancel without accepting" },
1804
+ ],
1805
+ multiSelect: false,
1806
+ isOther: false,
1807
+ isSecret: false,
1808
+ },
1809
+ ],
1810
+ },
1811
+ };
1812
+ }
1813
+ const schema = asRecord(params.requestedSchema);
1814
+ const properties = asRecord(schema?.properties) ?? {};
1815
+ const requiredFields = new Set(Array.isArray(schema?.required)
1816
+ ? schema.required.map((entry) => String(entry))
1817
+ : []);
1818
+ const questions = Object.entries(properties)
1819
+ .filter(([, value]) => value && typeof value === "object")
1820
+ .map(([key, value]) => {
1821
+ const field = value;
1822
+ const title = typeof field.title === "string" ? field.title : key;
1823
+ const description = typeof field.description === "string" ? field.description : message;
1824
+ const enumValues = Array.isArray(field.enum) ? field.enum.map((entry) => String(entry)) : [];
1825
+ const type = typeof field.type === "string" ? field.type : "";
1826
+ const options = enumValues.length > 0
1827
+ ? enumValues.map((entry, index) => ({
1828
+ label: entry,
1829
+ description: index === 0 ? description : "",
1830
+ }))
1831
+ : type === "boolean"
1832
+ ? [
1833
+ { label: "true", description: description },
1834
+ { label: "false", description: "" },
1835
+ ]
1836
+ : [];
1837
+ return {
1838
+ id: key,
1839
+ question: requiredFields.has(key) ? `${title} (required)` : title,
1840
+ header: serverName,
1841
+ options,
1842
+ isOther: options.length === 0,
1843
+ isSecret: false,
1844
+ };
1845
+ });
1846
+ const normalizedQuestions = questions.length > 0
1847
+ ? questions
1848
+ : [
1849
+ {
1850
+ id: "value",
1851
+ question: message,
1852
+ header: serverName,
1853
+ options: [],
1854
+ isOther: true,
1855
+ isSecret: false,
1856
+ },
1857
+ ];
1858
+ return {
1859
+ kind: "elicitation_form",
1860
+ questions: normalizedQuestions.map((question) => ({
1861
+ id: question.id,
1862
+ question: question.question,
1863
+ })),
1864
+ input: {
1865
+ mode: "form",
1866
+ serverName,
1867
+ message,
1868
+ requestedSchema: schema,
1869
+ questions: normalizedQuestions.map((question) => ({
1870
+ id: question.id,
1871
+ header: question.header,
1872
+ question: question.question,
1873
+ options: question.options,
1874
+ multiSelect: false,
1875
+ isOther: question.isOther,
1876
+ isSecret: question.isSecret,
1877
+ })),
1878
+ },
1879
+ };
1880
+ }
1881
+ function asRecord(value) {
1882
+ return value && typeof value === "object" && !Array.isArray(value)
1883
+ ? value
1884
+ : undefined;
1885
+ }
1886
+ function formatPlanUpdateText(params) {
1887
+ const stepsRaw = params.plan;
1888
+ if (!Array.isArray(stepsRaw) || stepsRaw.length === 0)
1889
+ return "";
1890
+ const explanation = typeof params.explanation === "string" ? params.explanation.trim() : "";
1891
+ const lines = stepsRaw
1892
+ .filter((entry) => !!entry && typeof entry === "object")
1893
+ .map((entry, index) => {
1894
+ const step = typeof entry.step === "string" ? entry.step : `Step ${index + 1}`;
1895
+ const status = normalizePlanStatus(entry.status);
1896
+ return `${index + 1}. [${status}] ${step}`;
1897
+ });
1898
+ if (lines.length === 0)
1899
+ return "";
1900
+ const header = explanation ? `Plan update: ${explanation}` : "Plan update:";
1901
+ return `${header}\n${lines.join("\n")}`;
1902
+ }
1903
+ function normalizePlanStatus(raw) {
1904
+ switch (raw) {
1905
+ case "inProgress":
1906
+ return "in progress";
1907
+ case "completed":
1908
+ return "completed";
1909
+ case "pending":
1910
+ default:
1911
+ return "pending";
1912
+ }
1913
+ }
1914
+ function extensionFromMime(mimeType) {
1915
+ switch (mimeType) {
1916
+ case "image/png":
1917
+ return "png";
1918
+ case "image/jpeg":
1919
+ return "jpg";
1920
+ case "image/webp":
1921
+ return "webp";
1922
+ case "image/gif":
1923
+ return "gif";
1924
+ default:
1925
+ return null;
1926
+ }
1927
+ }
1928
+ //# sourceMappingURL=codex-process.js.map