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