lunel-cli 0.1.114 → 0.1.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/ai/codex.js CHANGED
@@ -6,6 +6,18 @@ import * as path from "path";
6
6
  import { spawn } from "child_process";
7
7
  import { createInterface } from "readline";
8
8
  const THREAD_LIST_SOURCE_KINDS = ["cli", "vscode", "appServer", "exec", "unknown"];
9
+ const CODEX_AGENTS = [
10
+ {
11
+ name: "Build",
12
+ mode: "default",
13
+ description: "Default Codex collaboration mode.",
14
+ },
15
+ {
16
+ name: "plan",
17
+ mode: "plan",
18
+ description: "Plan-first Codex collaboration mode.",
19
+ },
20
+ ];
9
21
  const DEBUG_MODE = process.env.LUNEL_DEBUG === "1" || process.env.LUNEL_DEBUG_AI === "1";
10
22
  function joinStreamingText(previousText, nextChunk) {
11
23
  if (!previousText) {
@@ -29,12 +41,14 @@ export class CodexProvider {
29
41
  proc = null;
30
42
  shuttingDown = false;
31
43
  emitter = null;
44
+ defaultModelContextWindow = null;
32
45
  nextId = 1;
33
46
  pending = new Map();
34
47
  sessions = new Map();
35
48
  deletedThreadIds = new Set();
36
49
  resumedThreadIds = new Set();
37
50
  pendingPermissionRequestIds = new Map();
51
+ pendingQuestionRequestIds = new Map();
38
52
  assistantMessageIdByTurnId = new Map();
39
53
  partTextById = new Map();
40
54
  async init() {
@@ -61,7 +75,15 @@ export class CodexProvider {
61
75
  this.emitter?.({ type: "sse_dead", properties: { error: msg } });
62
76
  }
63
77
  });
64
- await this.call("initialize", { clientInfo: { name: "lunel", version: "1.0" } });
78
+ await this.call("initialize", {
79
+ clientInfo: { name: "lunel", version: "1.0" },
80
+ capabilities: {
81
+ experimentalApi: true,
82
+ },
83
+ });
84
+ await this.refreshConfigDefaults().catch(() => {
85
+ // Config defaults are best-effort metadata only.
86
+ });
65
87
  if (DEBUG_MODE)
66
88
  console.log("Codex ready.\n");
67
89
  }
@@ -143,6 +165,46 @@ export class CodexProvider {
143
165
  }
144
166
  return { deleted: true };
145
167
  }
168
+ async renameSession(id, title) {
169
+ const trimmed = title.trim();
170
+ if (!trimmed) {
171
+ throw new Error("Session title cannot be empty");
172
+ }
173
+ const methodsToTry = [
174
+ { method: "thread/name/set", params: { threadId: id, name: trimmed } },
175
+ { method: "thread/name/set", params: { threadId: id, title: trimmed } },
176
+ { method: "thread/name/update", params: { threadId: id, name: trimmed } },
177
+ { method: "thread/name/update", params: { threadId: id, title: trimmed } },
178
+ { method: "thread/metadata/update", params: { threadId: id, title: trimmed } },
179
+ { method: "thread/update", params: { threadId: id, title: trimmed } },
180
+ { method: "thread/rename", params: { threadId: id, title: trimmed } },
181
+ ];
182
+ let renamed = false;
183
+ let lastError = null;
184
+ for (const entry of methodsToTry) {
185
+ try {
186
+ await this.call(entry.method, entry.params);
187
+ renamed = true;
188
+ break;
189
+ }
190
+ catch (err) {
191
+ lastError = err;
192
+ }
193
+ }
194
+ if (!renamed && lastError) {
195
+ throw lastError instanceof Error ? lastError : new Error(String(lastError));
196
+ }
197
+ const existing = this.sessions.get(id) ?? this.ensureLocalSession(id);
198
+ const session = this.upsertSession({
199
+ id,
200
+ title: trimmed,
201
+ createdAt: existing.createdAt,
202
+ updatedAt: Date.now(),
203
+ archived: existing.archived,
204
+ cwd: existing.cwd,
205
+ }, true);
206
+ return { session: this.toSessionInfo(session) };
207
+ }
146
208
  async getMessages(sessionId) {
147
209
  const session = this.ensureLocalSession(sessionId);
148
210
  const result = await this.call("thread/read", {
@@ -165,6 +227,17 @@ export class CodexProvider {
165
227
  archived: false,
166
228
  cwd: this.extractThreadCwd(threadObject) ?? session.cwd,
167
229
  });
230
+ if (this.defaultModelContextWindow && this.defaultModelContextWindow > 0) {
231
+ this.emitter?.({
232
+ type: "session.usage",
233
+ properties: {
234
+ sessionID: sessionId,
235
+ tokenUsage: {
236
+ modelContextWindow: this.defaultModelContextWindow,
237
+ },
238
+ },
239
+ });
240
+ }
168
241
  return { messages: session.messages };
169
242
  }
170
243
  async prompt(sessionId, text, model, agent, files = [], codexOptions) {
@@ -172,26 +245,31 @@ export class CodexProvider {
172
245
  session.updatedAt = Date.now();
173
246
  (async () => {
174
247
  try {
175
- await this.ensureThreadResumed(session.id);
248
+ const effortLevels = ["low", "medium", "high"];
249
+ const speedDelta = {
250
+ fast: -1,
251
+ balanced: 0,
252
+ quality: 1,
253
+ };
254
+ const baseEffort = codexOptions?.reasoningEffort ?? "medium";
255
+ const baseIndex = effortLevels.indexOf(baseEffort);
256
+ const adjustedIndex = Math.max(0, Math.min(effortLevels.length - 1, baseIndex + (codexOptions?.speed ? speedDelta[codexOptions.speed] : 0)));
257
+ const reasoningEffort = effortLevels[adjustedIndex];
258
+ const modelId = await this.resolveModelId(model);
259
+ const collaborationMode = this.buildCollaborationMode(agent, modelId, reasoningEffort);
260
+ // Freshly created sessions already have a live backend thread from
261
+ // thread/start. Forcing thread/resume here can attach stale state to a
262
+ // brand-new session and trigger rollout lookup failures on turn/start.
263
+ await this.ensureThreadResumed(session.id, false, codexOptions, collaborationMode);
176
264
  let imageUrlKey = "url";
177
265
  while (true) {
178
266
  try {
179
- const effortLevels = ["low", "medium", "high"];
180
- const speedDelta = {
181
- fast: -1,
182
- balanced: 0,
183
- quality: 1,
184
- };
185
- const baseEffort = codexOptions?.reasoningEffort ?? "medium";
186
- const baseIndex = effortLevels.indexOf(baseEffort);
187
- const adjustedIndex = Math.max(0, Math.min(effortLevels.length - 1, baseIndex + (codexOptions?.speed ? speedDelta[codexOptions.speed] : 0)));
188
- const reasoningEffort = effortLevels[adjustedIndex];
189
267
  await this.call("turn/start", {
190
268
  threadId: session.id,
191
269
  input: this.makeTurnInputPayload(text, files, imageUrlKey),
192
- ...(model ? { model: model.providerID === "codex" ? model.modelID : `${model.providerID}/${model.modelID}` } : {}),
193
- ...(agent ? { agent } : {}),
270
+ ...(modelId ? { model: modelId } : {}),
194
271
  ...(reasoningEffort ? { reasoningEffort } : {}),
272
+ ...(collaborationMode ? { collaborationMode } : {}),
195
273
  });
196
274
  break;
197
275
  }
@@ -225,7 +303,7 @@ export class CodexProvider {
225
303
  return {};
226
304
  }
227
305
  async agents() {
228
- return { agents: [] };
306
+ return { agents: [...CODEX_AGENTS] };
229
307
  }
230
308
  async providers() {
231
309
  const items = await this.fetchModels();
@@ -283,11 +361,53 @@ export class CodexProvider {
283
361
  });
284
362
  return {};
285
363
  }
286
- async questionReply() {
287
- throw new Error("Codex structured user input is not supported by Lunel yet");
364
+ async questionReply(sessionId, questionId, answers) {
365
+ const pending = this.pendingQuestionRequestIds.get(questionId);
366
+ if (!pending) {
367
+ this.emitter?.({
368
+ type: "question.replied",
369
+ properties: { sessionID: sessionId, requestID: questionId, answers, skipped: true },
370
+ });
371
+ return {};
372
+ }
373
+ const responseAnswers = {};
374
+ pending.questionIds.forEach((id, index) => {
375
+ responseAnswers[id] = {
376
+ answers: Array.isArray(answers[index]) ? answers[index].filter((value) => typeof value === "string") : [],
377
+ };
378
+ });
379
+ this.send({
380
+ jsonrpc: "2.0",
381
+ id: pending.requestId,
382
+ result: { answers: responseAnswers },
383
+ });
384
+ this.pendingQuestionRequestIds.delete(questionId);
385
+ this.emitter?.({
386
+ type: "question.replied",
387
+ properties: { sessionID: sessionId, requestID: questionId, answers },
388
+ });
389
+ return {};
288
390
  }
289
- async questionReject() {
290
- throw new Error("Codex structured user input is not supported by Lunel yet");
391
+ async questionReject(sessionId, questionId) {
392
+ const pending = this.pendingQuestionRequestIds.get(questionId);
393
+ if (!pending) {
394
+ this.emitter?.({
395
+ type: "question.rejected",
396
+ properties: { sessionID: sessionId, requestID: questionId, skipped: true },
397
+ });
398
+ return {};
399
+ }
400
+ this.send({
401
+ jsonrpc: "2.0",
402
+ id: pending.requestId,
403
+ result: { answers: {} },
404
+ });
405
+ this.pendingQuestionRequestIds.delete(questionId);
406
+ this.emitter?.({
407
+ type: "question.rejected",
408
+ properties: { sessionID: sessionId, requestID: questionId },
409
+ });
410
+ return {};
291
411
  }
292
412
  send(req) {
293
413
  if (!this.proc?.stdin?.writable)
@@ -344,10 +464,28 @@ export class CodexProvider {
344
464
  handleServerRequest(method, requestId, params) {
345
465
  const session = this.resolveSessionFromPayload(params);
346
466
  if (method === "item/tool/requestUserInput") {
347
- this.send({
348
- jsonrpc: "2.0",
349
- id: requestId,
350
- error: { code: -32601, message: "Structured user input is not supported by Lunel yet" },
467
+ const questionRequestId = String(requestId);
468
+ const callId = this.extractItemId(params) ?? undefined;
469
+ const messageId = session ? this.ensureAssistantMessage(session, this.extractTurnId(params) ?? `session:${session.id}`) : undefined;
470
+ const questions = this.extractStructuredUserInputQuestions(params);
471
+ this.pendingQuestionRequestIds.set(questionRequestId, {
472
+ sessionId: session?.id ?? "",
473
+ requestId,
474
+ messageId,
475
+ callId,
476
+ questionIds: questions.map((question) => this.readString(this.asRecord(question).id)).filter((value) => Boolean(value)),
477
+ });
478
+ this.emitter?.({
479
+ type: "question.asked",
480
+ properties: {
481
+ id: questionRequestId,
482
+ sessionID: session?.id,
483
+ questions,
484
+ tool: {
485
+ ...(messageId ? { messageID: messageId } : {}),
486
+ ...(callId ? { callID: callId } : {}),
487
+ },
488
+ },
351
489
  });
352
490
  return;
353
491
  }
@@ -427,6 +565,17 @@ export class CodexProvider {
427
565
  });
428
566
  }
429
567
  return;
568
+ case "thread/tokenUsage/updated":
569
+ if (session) {
570
+ this.emitter?.({
571
+ type: "session.usage",
572
+ properties: {
573
+ sessionID: session.id,
574
+ tokenUsage: params.tokenUsage ?? null,
575
+ },
576
+ });
577
+ }
578
+ return;
430
579
  case "turn/completed":
431
580
  case "turn/failed":
432
581
  if (session) {
@@ -503,6 +652,9 @@ export class CodexProvider {
503
652
  this.pendingPermissionRequestIds.delete(permissionId);
504
653
  this.emitter?.({ type: "permission.replied", properties: { permissionId } });
505
654
  }
655
+ if (permissionId && this.pendingQuestionRequestIds.has(permissionId)) {
656
+ this.pendingQuestionRequestIds.delete(permissionId);
657
+ }
506
658
  return;
507
659
  }
508
660
  default:
@@ -621,7 +773,7 @@ export class CodexProvider {
621
773
  const itemId = this.extractItemId(params) ?? this.readString(item.id) ?? normalizedType ?? "tool";
622
774
  const fileChangeLike = this.isFileChangeStructuredItem(normalizedType, item, params);
623
775
  const emittedPartType = normalizedType === "plan"
624
- ? "reasoning"
776
+ ? "plan"
625
777
  : (fileChangeLike ? "file-change" : "tool");
626
778
  const partId = `${messageId}:${emittedPartType}:${itemId}`;
627
779
  const nextText = this.extractStructuredOutput(params, item, normalizedType);
@@ -644,8 +796,8 @@ export class CodexProvider {
644
796
  sessionID: session.id,
645
797
  messageID: messageId,
646
798
  type: emittedPartType,
647
- ...(emittedPartType === "reasoning"
648
- ? { text: outputValue ?? "Planning..." }
799
+ ...(emittedPartType === "plan"
800
+ ? { text: outputValue ?? (emittedPartType === "plan" ? "Planning..." : "") }
649
801
  : { name, toolName: name, input, output: outputValue, state, ...(patch ? { patch } : {}) }),
650
802
  };
651
803
  this.upsertLocalMessagePart(session, messageId, part);
@@ -720,6 +872,22 @@ export class CodexProvider {
720
872
  async fetchServerThreads() {
721
873
  return this.fetchServerThreadsByArchiveState(false);
722
874
  }
875
+ async refreshConfigDefaults() {
876
+ const result = await this.call("config/read", undefined);
877
+ const payload = this.asRecord(result);
878
+ const config = this.asRecord(payload.config ?? result);
879
+ const raw = config.model_context_window;
880
+ if (typeof raw === "number" && Number.isFinite(raw) && raw > 0) {
881
+ this.defaultModelContextWindow = raw;
882
+ return;
883
+ }
884
+ if (typeof raw === "string" && raw.trim()) {
885
+ const parsed = Number(raw);
886
+ if (Number.isFinite(parsed) && parsed > 0) {
887
+ this.defaultModelContextWindow = parsed;
888
+ }
889
+ }
890
+ }
723
891
  async refreshSessionMetadata(sessionId) {
724
892
  const session = this.sessions.get(sessionId);
725
893
  const result = await this.call("thread/read", {
@@ -800,6 +968,30 @@ export class CodexProvider {
800
968
  })
801
969
  .filter((value) => Boolean(value));
802
970
  }
971
+ async resolveModelId(model) {
972
+ if (model) {
973
+ return model.providerID === "codex" ? model.modelID : `${model.providerID}/${model.modelID}`;
974
+ }
975
+ const items = await this.fetchModels();
976
+ return items.find((item) => item.isDefault)?.model ?? items[0]?.model;
977
+ }
978
+ buildCollaborationMode(agent, modelId, reasoningEffort) {
979
+ const normalizedAgent = (agent ?? "").trim().toLowerCase();
980
+ const mode = normalizedAgent === "build" ? "default" : normalizedAgent;
981
+ if (mode !== "default" && mode !== "plan") {
982
+ return undefined;
983
+ }
984
+ if (!modelId) {
985
+ return undefined;
986
+ }
987
+ return {
988
+ mode,
989
+ settings: {
990
+ model: modelId,
991
+ reasoning_effort: reasoningEffort,
992
+ },
993
+ };
994
+ }
803
995
  parseThreadListEntry(value, archived = false) {
804
996
  const obj = this.asRecord(value);
805
997
  const id = this.extractThreadId(obj);
@@ -909,7 +1101,7 @@ export class CodexProvider {
909
1101
  return existing;
910
1102
  return this.upsertSession({ id: sessionId, title: "Conversation", createdAt: Date.now(), updatedAt: Date.now() });
911
1103
  }
912
- async ensureThreadResumed(threadId, force = false) {
1104
+ async ensureThreadResumed(threadId, force = false, codexOptions, collaborationMode) {
913
1105
  if (!threadId || this.resumedThreadIds.has(threadId)) {
914
1106
  if (!force)
915
1107
  return;
@@ -919,6 +1111,14 @@ export class CodexProvider {
919
1111
  if (session?.cwd) {
920
1112
  params.cwd = session.cwd;
921
1113
  }
1114
+ const permissionMode = codexOptions?.permissionMode ?? "default";
1115
+ if (permissionMode === "full-access") {
1116
+ params.approvalPolicy = "never";
1117
+ params.sandbox = "danger-full-access";
1118
+ }
1119
+ if (collaborationMode) {
1120
+ params.collaborationMode = collaborationMode;
1121
+ }
922
1122
  const result = await this.call("thread/resume", params);
923
1123
  const payload = this.asRecord(result);
924
1124
  const threadValue = payload.thread;
@@ -971,6 +1171,9 @@ export class CodexProvider {
971
1171
  const turnId = this.readString(turnObject.id);
972
1172
  const turnTime = this.extractUpdatedAt(turnObject) ?? this.extractCreatedAt(turnObject) ?? Date.now();
973
1173
  const items = this.readArray(turnObject.items);
1174
+ const assistantParts = [];
1175
+ let assistantMessageId;
1176
+ let assistantTimestamp = turnTime;
974
1177
  for (const item of items) {
975
1178
  const itemObject = this.asRecord(item);
976
1179
  const type = this.normalizedItemType(this.readString(itemObject.type) ?? "");
@@ -992,88 +1195,208 @@ export class CodexProvider {
992
1195
  const text = this.decodeItemText(itemObject);
993
1196
  if (!text)
994
1197
  continue;
995
- messages.push({
996
- id: itemId,
997
- role: "assistant",
998
- parts: [{ id: `${itemId}:text`, type: "text", text, sessionID: threadId, messageID: itemId }],
999
- time: timestamp,
1198
+ assistantMessageId = assistantMessageId ?? itemId;
1199
+ assistantTimestamp = Math.max(assistantTimestamp, timestamp);
1200
+ assistantParts.push({
1201
+ id: `${assistantMessageId}:text:${itemId}`,
1202
+ type: "text",
1203
+ text,
1204
+ sessionID: threadId,
1205
+ messageID: assistantMessageId,
1000
1206
  });
1001
- if (turnId)
1002
- this.assistantMessageIdByTurnId.set(turnId, itemId);
1003
1207
  continue;
1004
1208
  }
1005
1209
  if (type === "reasoning") {
1006
1210
  const text = this.decodeReasoningItemText(itemObject);
1007
- messages.push({
1008
- id: itemId,
1009
- role: "assistant",
1010
- parts: [{ id: `${itemId}:reasoning`, type: "reasoning", text, sessionID: threadId, messageID: itemId }],
1011
- time: timestamp,
1211
+ assistantMessageId = assistantMessageId ?? (turnId ? `assistant:${turnId}` : itemId);
1212
+ assistantTimestamp = Math.max(assistantTimestamp, timestamp);
1213
+ assistantParts.push({
1214
+ id: `${assistantMessageId}:reasoning:${itemId}`,
1215
+ type: "reasoning",
1216
+ text,
1217
+ sessionID: threadId,
1218
+ messageID: assistantMessageId,
1012
1219
  });
1013
1220
  continue;
1014
1221
  }
1015
1222
  if (type === "plan") {
1016
1223
  const text = this.decodePlanItemText(itemObject);
1017
- messages.push({
1018
- id: itemId,
1019
- role: "assistant",
1020
- parts: [{ id: `${itemId}:plan`, type: "reasoning", text, sessionID: threadId, messageID: itemId }],
1021
- time: timestamp,
1224
+ assistantMessageId = assistantMessageId ?? (turnId ? `assistant:${turnId}` : itemId);
1225
+ assistantTimestamp = Math.max(assistantTimestamp, timestamp);
1226
+ assistantParts.push({
1227
+ id: `${assistantMessageId}:plan:${itemId}`,
1228
+ type: "plan",
1229
+ text,
1230
+ sessionID: threadId,
1231
+ messageID: assistantMessageId,
1022
1232
  });
1023
1233
  continue;
1024
1234
  }
1025
- if (type === "commandexecution" || type === "enteredreviewmode" || type === "contextcompaction") {
1026
- const output = this.decodeCommandExecutionItemText(itemObject, type);
1027
- const input = this.extractCommandExecutionInput(itemObject);
1028
- messages.push({
1029
- id: itemId,
1030
- role: "assistant",
1031
- parts: [{
1032
- id: `${itemId}:tool`,
1033
- type: "tool",
1034
- name: type === "commandexecution" ? "command" : "system",
1035
- toolName: type === "commandexecution" ? "command" : "system",
1036
- ...(input ? { input } : {}),
1037
- output,
1038
- state: "completed",
1039
- sessionID: threadId,
1040
- messageID: itemId,
1041
- }],
1042
- time: timestamp,
1043
- });
1235
+ if (type === "commandexecution"
1236
+ || type === "enteredreviewmode"
1237
+ || type === "exitedreviewmode"
1238
+ || type === "contextcompaction"
1239
+ || type === "mcptoolcall"
1240
+ || type === "dynamictoolcall"
1241
+ || type === "collabtoolcall"
1242
+ || type === "websearch"
1243
+ || type === "imageview") {
1244
+ assistantMessageId = assistantMessageId ?? (turnId ? `assistant:${turnId}` : itemId);
1245
+ assistantTimestamp = Math.max(assistantTimestamp, timestamp);
1246
+ assistantParts.push(this.decodeStoredToolLikePart(type, itemObject, threadId, assistantMessageId, itemId));
1044
1247
  continue;
1045
1248
  }
1046
1249
  if (type === "filechange" || type === "toolcall" || type === "diff") {
1047
- const output = this.decodeFileLikeItemText(itemObject);
1048
- if (!output)
1049
- continue;
1050
- const emittedPartType = this.isFileChangeStructuredItem(type, itemObject)
1051
- ? "file-change"
1052
- : "tool";
1053
- const patch = emittedPartType === "file-change"
1054
- ? this.extractCanonicalPatch({}, itemObject)
1055
- : undefined;
1056
- messages.push({
1057
- id: itemId,
1058
- role: "assistant",
1059
- parts: [{
1060
- id: `${itemId}:${emittedPartType}`,
1061
- type: emittedPartType,
1062
- name: type,
1063
- toolName: type,
1064
- output,
1065
- state: "completed",
1066
- ...(patch ? { patch } : {}),
1067
- sessionID: threadId,
1068
- messageID: itemId,
1069
- }],
1070
- time: timestamp,
1071
- });
1250
+ assistantMessageId = assistantMessageId ?? (turnId ? `assistant:${turnId}` : itemId);
1251
+ assistantTimestamp = Math.max(assistantTimestamp, timestamp);
1252
+ assistantParts.push(this.decodeStoredToolLikePart(type, itemObject, threadId, assistantMessageId, itemId));
1253
+ }
1254
+ }
1255
+ if (assistantParts.length > 0) {
1256
+ const resolvedAssistantMessageId = assistantMessageId ?? (turnId ? `assistant:${turnId}` : crypto.randomUUID());
1257
+ messages.push({
1258
+ id: resolvedAssistantMessageId,
1259
+ role: "assistant",
1260
+ parts: assistantParts.map((part) => ({
1261
+ ...part,
1262
+ sessionID: threadId,
1263
+ messageID: resolvedAssistantMessageId,
1264
+ })),
1265
+ time: assistantTimestamp,
1266
+ });
1267
+ if (turnId) {
1268
+ this.assistantMessageIdByTurnId.set(turnId, resolvedAssistantMessageId);
1072
1269
  }
1073
1270
  }
1074
1271
  }
1075
1272
  return messages;
1076
1273
  }
1274
+ decodeStoredToolLikePart(type, itemObject, threadId, messageId, itemId) {
1275
+ if (type === "filechange" || type === "diff" || this.isFileChangeStructuredItem(type, itemObject)) {
1276
+ const output = this.decodeFileLikeItemText(itemObject) ?? this.describeCompletedItemOutput(itemObject, itemObject, type) ?? "File changes";
1277
+ const patch = this.extractCanonicalPatch(itemObject, itemObject);
1278
+ return {
1279
+ id: `${messageId}:file-change:${itemId}`,
1280
+ type: "file-change",
1281
+ name: this.describeToolPart(type, type, itemObject),
1282
+ toolName: this.describeToolPart(type, type, itemObject),
1283
+ output,
1284
+ state: "completed",
1285
+ ...(patch ? { patch } : {}),
1286
+ sessionID: threadId,
1287
+ messageID: messageId,
1288
+ };
1289
+ }
1290
+ const name = this.describeStoredToolName(type, itemObject);
1291
+ const input = this.extractStoredToolInput(type, itemObject);
1292
+ const output = this.extractStoredToolOutput(type, itemObject);
1293
+ return {
1294
+ id: `${messageId}:tool:${itemId}`,
1295
+ type: "tool",
1296
+ name,
1297
+ toolName: name,
1298
+ ...(input !== undefined ? { input } : {}),
1299
+ ...(output !== undefined ? { output } : {}),
1300
+ state: "completed",
1301
+ sessionID: threadId,
1302
+ messageID: messageId,
1303
+ };
1304
+ }
1305
+ describeStoredToolName(type, itemObject) {
1306
+ if (type === "commandexecution") {
1307
+ return "command";
1308
+ }
1309
+ if (type === "websearch") {
1310
+ return "web-search";
1311
+ }
1312
+ if (type === "imageview") {
1313
+ return "image-view";
1314
+ }
1315
+ if (type === "enteredreviewmode") {
1316
+ return "review";
1317
+ }
1318
+ if (type === "exitedreviewmode") {
1319
+ return "review";
1320
+ }
1321
+ if (type === "contextcompaction") {
1322
+ return "context";
1323
+ }
1324
+ return this.firstString(itemObject, [
1325
+ "name",
1326
+ "toolName",
1327
+ "tool_name",
1328
+ "title",
1329
+ "serverToolName",
1330
+ "server_tool_name",
1331
+ "kind",
1332
+ ]) ?? type;
1333
+ }
1334
+ extractStoredToolInput(type, itemObject) {
1335
+ if (type === "commandexecution") {
1336
+ return this.extractCommandExecutionInput(itemObject, itemObject);
1337
+ }
1338
+ return itemObject.input
1339
+ ?? itemObject.arguments
1340
+ ?? itemObject.args
1341
+ ?? itemObject.query
1342
+ ?? itemObject.path
1343
+ ?? itemObject.url
1344
+ ?? itemObject.command
1345
+ ?? itemObject.pattern
1346
+ ?? undefined;
1347
+ }
1348
+ extractStoredToolOutput(type, itemObject) {
1349
+ if (type === "commandexecution") {
1350
+ return this.decodeStoredCommandExecutionOutput(itemObject);
1351
+ }
1352
+ if (type === "enteredreviewmode") {
1353
+ return `Reviewing ${this.readString(itemObject.review) ?? "changes"}...`;
1354
+ }
1355
+ if (type === "exitedreviewmode") {
1356
+ return this.firstString(itemObject, ["summary", "text", "message"]) ?? "Exited review mode";
1357
+ }
1358
+ if (type === "contextcompaction") {
1359
+ return "Context compacted";
1360
+ }
1361
+ if (type === "imageview") {
1362
+ const path = this.firstString(itemObject, ["path", "file", "filePath", "file_path", "url"]);
1363
+ return path ? `Viewed image ${path}` : "Viewed image";
1364
+ }
1365
+ const direct = this.firstString(itemObject, [
1366
+ "aggregatedOutput",
1367
+ "output",
1368
+ "outputText",
1369
+ "output_text",
1370
+ "text",
1371
+ "message",
1372
+ "summary",
1373
+ "result",
1374
+ "content",
1375
+ ]);
1376
+ if (direct?.trim()) {
1377
+ return direct.trim();
1378
+ }
1379
+ const flattened = this.flattenTextValue(itemObject.output ?? itemObject.result ?? itemObject.content).trim();
1380
+ return flattened || undefined;
1381
+ }
1382
+ decodeStoredCommandExecutionOutput(itemObject) {
1383
+ const aggregatedOutput = this.firstString(itemObject, [
1384
+ "aggregatedOutput",
1385
+ "aggregated_output",
1386
+ "output",
1387
+ "outputText",
1388
+ "output_text",
1389
+ "stdout",
1390
+ "stderr",
1391
+ "text",
1392
+ "message",
1393
+ "summary",
1394
+ ]);
1395
+ if (aggregatedOutput?.trim()) {
1396
+ return aggregatedOutput.trim();
1397
+ }
1398
+ return this.decodeCommandExecutionItemText(itemObject, "commandexecution");
1399
+ }
1077
1400
  decodeUserMessageParts(itemObject, threadId, itemId) {
1078
1401
  const parts = [];
1079
1402
  const content = this.readArray(itemObject.content);
@@ -1306,6 +1629,41 @@ export class CodexProvider {
1306
1629
  ?? this.readRawString(this.asRecord(payload.event).delta)
1307
1630
  ?? this.readRawString(this.asRecord(payload.event).message));
1308
1631
  }
1632
+ extractStructuredUserInputQuestions(payload) {
1633
+ const rawQuestions = this.readArray(payload.questions);
1634
+ const questions = [];
1635
+ for (const value of rawQuestions) {
1636
+ const question = this.asRecord(value);
1637
+ const options = this.readArray(question.options)
1638
+ .map((option) => {
1639
+ const optionObject = this.asRecord(option);
1640
+ const label = this.readString(optionObject.label);
1641
+ const description = this.readString(optionObject.description);
1642
+ if (!label)
1643
+ return undefined;
1644
+ return {
1645
+ label,
1646
+ ...(description ? { description } : {}),
1647
+ };
1648
+ })
1649
+ .filter((value) => Boolean(value));
1650
+ const id = this.readString(question.id);
1651
+ const header = this.readString(question.header);
1652
+ const prompt = this.readString(question.question);
1653
+ if (!id || !header || !prompt) {
1654
+ continue;
1655
+ }
1656
+ questions.push({
1657
+ id,
1658
+ header,
1659
+ question: prompt,
1660
+ ...(options.length > 0 ? { options } : {}),
1661
+ ...(typeof question.isOther === "boolean" ? { isOther: question.isOther } : {}),
1662
+ ...(typeof question.isSecret === "boolean" ? { isSecret: question.isSecret } : {}),
1663
+ });
1664
+ }
1665
+ return questions;
1666
+ }
1309
1667
  extractThreadId(payload) {
1310
1668
  if (!payload || typeof payload !== "object")
1311
1669
  return undefined;