@ccpocket/bridge 1.25.0 → 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.
@@ -8,6 +8,8 @@ export class CodexProcess extends EventEmitter {
8
8
  child = null;
9
9
  _status = "starting";
10
10
  _threadId = null;
11
+ _agentNickname = null;
12
+ _agentRole = null;
11
13
  stopped = false;
12
14
  startModel;
13
15
  inputResolve = null;
@@ -43,6 +45,12 @@ export class CodexProcess extends EventEmitter {
43
45
  get sessionId() {
44
46
  return this._threadId;
45
47
  }
48
+ get agentNickname() {
49
+ return this._agentNickname;
50
+ }
51
+ get agentRole() {
52
+ return this._agentRole;
53
+ }
46
54
  get isRunning() {
47
55
  return this.child !== null;
48
56
  }
@@ -89,12 +97,61 @@ export class CodexProcess extends EventEmitter {
89
97
  async archiveThread(threadId) {
90
98
  await this.request("thread/archive", { threadId });
91
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
+ }
92
117
  start(projectPath, options) {
93
118
  if (this.child) {
94
119
  this.stop();
95
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) {
96
151
  this.stopped = false;
97
152
  this._threadId = null;
153
+ this._agentNickname = null;
154
+ this._agentRole = null;
98
155
  this.pendingTurnId = null;
99
156
  this.pendingTurnCompletion = null;
100
157
  this.pendingApprovals.clear();
@@ -106,6 +163,9 @@ export class CodexProcess extends EventEmitter {
106
163
  this.lastPlanItemText = null;
107
164
  this.pendingPlanCompletion = null;
108
165
  this._pendingPlanInput = null;
166
+ this._projectPath = projectPath;
167
+ }
168
+ launchAppServer(projectPath, options) {
109
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})`);
110
170
  const child = spawn("codex", ["app-server", "--listen", "stdio://"], {
111
171
  cwd: projectPath,
@@ -142,23 +202,6 @@ export class CodexProcess extends EventEmitter {
142
202
  this.setStatus("idle");
143
203
  this.emit("exit", code);
144
204
  });
145
- void this.bootstrap(projectPath, options);
146
- }
147
- stop() {
148
- this.stopped = true;
149
- if (this.inputResolve) {
150
- this.inputResolve({ text: "" });
151
- this.inputResolve = null;
152
- }
153
- this.pendingApprovals.clear();
154
- this.pendingUserInputs.clear();
155
- this.rejectAllPending(new Error("stopped"));
156
- if (this.child) {
157
- this.child.kill("SIGTERM");
158
- this.child = null;
159
- }
160
- this.setStatus("idle");
161
- console.log("[codex-process] Stopped");
162
205
  }
163
206
  interrupt() {
164
207
  if (!this._threadId || !this.pendingTurnId)
@@ -211,9 +254,7 @@ export class CodexProcess extends EventEmitter {
211
254
  return;
212
255
  }
213
256
  this.pendingApprovals.delete(pending.toolUseId);
214
- this.respondToServerRequest(pending.requestId, {
215
- decision: "accept",
216
- });
257
+ this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, "accept"));
217
258
  this.emitToolResult(pending.toolUseId, "Approved");
218
259
  if (this.pendingApprovals.size === 0) {
219
260
  this.setStatus("running");
@@ -226,12 +267,7 @@ export class CodexProcess extends EventEmitter {
226
267
  return;
227
268
  }
228
269
  this.pendingApprovals.delete(pending.toolUseId);
229
- this.respondToServerRequest(pending.requestId, {
230
- decision: "accept",
231
- acceptSettings: {
232
- forSession: true,
233
- },
234
- });
270
+ this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, "acceptForSession"));
235
271
  this.emitToolResult(pending.toolUseId, "Approved (always)");
236
272
  if (this.pendingApprovals.size === 0) {
237
273
  this.setStatus("running");
@@ -249,9 +285,7 @@ export class CodexProcess extends EventEmitter {
249
285
  return;
250
286
  }
251
287
  this.pendingApprovals.delete(pending.toolUseId);
252
- this.respondToServerRequest(pending.requestId, {
253
- decision: "decline",
254
- });
288
+ this.respondToServerRequest(pending.requestId, buildApprovalResponse(pending, "decline"));
255
289
  this.emitToolResult(pending.toolUseId, "Rejected");
256
290
  if (this.pendingApprovals.size === 0) {
257
291
  this.setStatus("running");
@@ -264,9 +298,7 @@ export class CodexProcess extends EventEmitter {
264
298
  return;
265
299
  }
266
300
  this.pendingUserInputs.delete(pending.toolUseId);
267
- this.respondToServerRequest(pending.requestId, {
268
- answers: buildUserInputAnswers(pending.questions, result),
269
- });
301
+ this.respondToServerRequest(pending.requestId, buildUserInputResponse(pending, result));
270
302
  this.emitToolResult(pending.toolUseId, "Answered");
271
303
  if (this.pendingApprovals.size === 0 && this.pendingUserInputs.size === 0) {
272
304
  this.setStatus("running");
@@ -378,21 +410,13 @@ export class CodexProcess extends EventEmitter {
378
410
  }
379
411
  async bootstrap(projectPath, options) {
380
412
  try {
381
- await this.request("initialize", {
382
- clientInfo: {
383
- name: "ccpocket_bridge",
384
- version: "1.0.0",
385
- title: "ccpocket bridge",
386
- },
387
- capabilities: {
388
- experimentalApi: true,
389
- },
390
- });
391
- this.notify("initialized", {});
413
+ await this.initializeRpcConnection();
392
414
  const threadParams = {
393
415
  cwd: projectPath,
394
416
  approvalPolicy: normalizeApprovalPolicy(options?.approvalPolicy ?? "never"),
395
417
  sandbox: normalizeSandboxMode(options?.sandboxMode ?? "workspace-write"),
418
+ experimentalRawEvents: false,
419
+ persistExtendedHistory: true,
396
420
  };
397
421
  if (options?.model)
398
422
  threadParams.model = options.model;
@@ -412,6 +436,12 @@ export class CodexProcess extends EventEmitter {
412
436
  if (options?.threadId) {
413
437
  threadParams.threadId = options.threadId;
414
438
  }
439
+ else {
440
+ threadParams.experimentalRawEvents = false;
441
+ }
442
+ if (options?.threadId) {
443
+ threadParams.persistExtendedHistory = true;
444
+ }
415
445
  const response = await this.request(method, threadParams);
416
446
  const thread = response.thread;
417
447
  const threadId = typeof thread?.id === "string"
@@ -448,6 +478,19 @@ export class CodexProcess extends EventEmitter {
448
478
  this.emit("exit", 1);
449
479
  }
450
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
+ }
451
494
  /**
452
495
  * Fetch skills from Codex app-server via `skills/list` RPC and emit them
453
496
  * as a `supported_commands` system message so the Flutter client can display
@@ -639,6 +682,11 @@ export class CodexProcess extends EventEmitter {
639
682
  ...(typeof params.cwd === "string" ? { cwd: params.cwd } : {}),
640
683
  ...(params.commandActions ? { commandActions: params.commandActions } : {}),
641
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 } : {}),
642
690
  ...(typeof params.reason === "string" ? { reason: params.reason } : {}),
643
691
  };
644
692
  this.pendingApprovals.set(toolUseId, {
@@ -646,6 +694,7 @@ export class CodexProcess extends EventEmitter {
646
694
  toolUseId,
647
695
  toolName: "Bash",
648
696
  input,
697
+ kind: "command",
649
698
  });
650
699
  this.emitMessage({
651
700
  type: "permission_request",
@@ -660,6 +709,7 @@ export class CodexProcess extends EventEmitter {
660
709
  const toolUseId = this.extractToolUseId(params, id);
661
710
  const input = {
662
711
  ...(Array.isArray(params.changes) ? { changes: params.changes } : {}),
712
+ ...(typeof params.grantRoot === "string" ? { grantRoot: params.grantRoot } : {}),
663
713
  ...(typeof params.reason === "string" ? { reason: params.reason } : {}),
664
714
  };
665
715
  this.pendingApprovals.set(toolUseId, {
@@ -667,6 +717,7 @@ export class CodexProcess extends EventEmitter {
667
717
  toolUseId,
668
718
  toolName: "FileChange",
669
719
  input,
720
+ kind: "file",
670
721
  });
671
722
  this.emitMessage({
672
723
  type: "permission_request",
@@ -694,11 +745,13 @@ export class CodexProcess extends EventEmitter {
694
745
  this.pendingUserInputs.set(toolUseId, {
695
746
  requestId: id,
696
747
  toolUseId,
748
+ toolName: "AskUserQuestion",
697
749
  questions: questions.map((q) => ({
698
750
  id: q.id,
699
751
  question: q.question,
700
752
  })),
701
753
  input,
754
+ kind: "questions",
702
755
  });
703
756
  this.emitMessage({
704
757
  type: "permission_request",
@@ -709,6 +762,50 @@ export class CodexProcess extends EventEmitter {
709
762
  this.setStatus("waiting_approval");
710
763
  break;
711
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
+ }
712
809
  default:
713
810
  this.respondToServerRequest(id, {});
714
811
  break;
@@ -721,6 +818,8 @@ export class CodexProcess extends EventEmitter {
721
818
  if (typeof thread?.id === "string") {
722
819
  this._threadId = thread.id;
723
820
  }
821
+ this._agentNickname = stringOrNull(thread?.agentNickname);
822
+ this._agentRole = stringOrNull(thread?.agentRole);
724
823
  break;
725
824
  }
726
825
  case "turn/started": {
@@ -811,6 +910,10 @@ export class CodexProcess extends EventEmitter {
811
910
  });
812
911
  break;
813
912
  }
913
+ case "serverRequest/resolved": {
914
+ this.handleServerRequestResolved(params);
915
+ break;
916
+ }
814
917
  default:
815
918
  break;
816
919
  }
@@ -928,6 +1031,56 @@ export class CodexProcess extends EventEmitter {
928
1031
  });
929
1032
  break;
930
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
+ }
931
1084
  default:
932
1085
  break;
933
1086
  }
@@ -990,6 +1143,7 @@ export class CodexProcess extends EventEmitter {
990
1143
  const tool = typeof item.tool === "string" ? item.tool : "unknown";
991
1144
  const toolName = `mcp:${server}/${tool}`;
992
1145
  const result = item.result ?? item.error ?? "MCP call completed";
1146
+ const normalized = normalizeMcpToolResult(result);
993
1147
  this.emitMessage({
994
1148
  type: "assistant",
995
1149
  message: {
@@ -1009,8 +1163,22 @@ export class CodexProcess extends EventEmitter {
1009
1163
  this.emitMessage({
1010
1164
  type: "tool_result",
1011
1165
  toolUseId: itemId,
1012
- content: typeof result === "string" ? result : JSON.stringify(result),
1166
+ content: normalized.content,
1013
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,
1014
1182
  });
1015
1183
  break;
1016
1184
  }
@@ -1040,6 +1208,27 @@ export class CodexProcess extends EventEmitter {
1040
1208
  });
1041
1209
  break;
1042
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
+ }
1043
1232
  case "plan": {
1044
1233
  // Plan item completed — save text for plan approval emission in handleTurnCompleted()
1045
1234
  const planText = typeof item.text === "string" ? item.text : "";
@@ -1150,12 +1339,51 @@ export class CodexProcess extends EventEmitter {
1150
1339
  extractToolUseId(params, requestId) {
1151
1340
  if (typeof params.approvalId === "string")
1152
1341
  return params.approvalId;
1342
+ if (typeof params.elicitationId === "string")
1343
+ return params.elicitationId;
1153
1344
  if (typeof params.itemId === "string")
1154
1345
  return params.itemId;
1155
1346
  if (typeof requestId === "string")
1156
1347
  return requestId;
1157
1348
  return `approval-${requestId}`;
1158
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);
1159
1387
  }
1160
1388
  function normalizeApprovalPolicy(value) {
1161
1389
  switch (value) {
@@ -1197,6 +1425,9 @@ function normalizeItemType(raw) {
1197
1425
  function numberOrUndefined(value) {
1198
1426
  return typeof value === "number" && Number.isFinite(value) ? value : undefined;
1199
1427
  }
1428
+ function stringOrNull(value) {
1429
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
1430
+ }
1200
1431
  function summarizeFileChanges(changes) {
1201
1432
  if (!Array.isArray(changes) || changes.length === 0) {
1202
1433
  return "No file changes";
@@ -1212,6 +1443,127 @@ function summarizeFileChanges(changes) {
1212
1443
  })
1213
1444
  .join("\n");
1214
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
+ }
1215
1567
  /**
1216
1568
  * Format file changes including unified diff content for display in chat.
1217
1569
  * Falls back to `kind: path` summary when no diff is available.
@@ -1367,6 +1719,170 @@ function normalizeAnswerValues(value) {
1367
1719
  const normalized = String(value).trim();
1368
1720
  return normalized ? [normalized] : [];
1369
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
+ }
1370
1886
  function formatPlanUpdateText(params) {
1371
1887
  const stepsRaw = params.plan;
1372
1888
  if (!Array.isArray(stepsRaw) || stepsRaw.length === 0)