lunel-cli 0.1.113 → 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,32 +6,49 @@ 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) {
12
24
  return nextChunk;
13
25
  }
14
- if (/[\s]$/.test(previousText) || /^[\s]/.test(nextChunk)) {
15
- return `${previousText}${nextChunk}`;
26
+ if (nextChunk.startsWith(previousText)) {
27
+ return nextChunk;
16
28
  }
17
- if (/^[.,!?;:)\]%}"'`]/.test(nextChunk)) {
18
- return `${previousText}${nextChunk}`;
29
+ if (previousText.endsWith(nextChunk)) {
30
+ return previousText;
19
31
  }
20
- if (/[(\[{/"'`]$/.test(previousText)) {
21
- return `${previousText}${nextChunk}`;
32
+ const maxOverlap = Math.min(previousText.length, nextChunk.length);
33
+ for (let overlap = maxOverlap; overlap > 0; overlap -= 1) {
34
+ if (previousText.slice(-overlap) === nextChunk.slice(0, overlap)) {
35
+ return previousText + nextChunk.slice(overlap);
36
+ }
22
37
  }
23
- return `${previousText} ${nextChunk}`;
38
+ return previousText + nextChunk;
24
39
  }
25
40
  export class CodexProvider {
26
41
  proc = null;
27
42
  shuttingDown = false;
28
43
  emitter = null;
44
+ defaultModelContextWindow = null;
29
45
  nextId = 1;
30
46
  pending = new Map();
31
47
  sessions = new Map();
32
48
  deletedThreadIds = new Set();
33
49
  resumedThreadIds = new Set();
34
50
  pendingPermissionRequestIds = new Map();
51
+ pendingQuestionRequestIds = new Map();
35
52
  assistantMessageIdByTurnId = new Map();
36
53
  partTextById = new Map();
37
54
  async init() {
@@ -58,7 +75,15 @@ export class CodexProvider {
58
75
  this.emitter?.({ type: "sse_dead", properties: { error: msg } });
59
76
  }
60
77
  });
61
- 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
+ });
62
87
  if (DEBUG_MODE)
63
88
  console.log("Codex ready.\n");
64
89
  }
@@ -140,6 +165,46 @@ export class CodexProvider {
140
165
  }
141
166
  return { deleted: true };
142
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
+ }
143
208
  async getMessages(sessionId) {
144
209
  const session = this.ensureLocalSession(sessionId);
145
210
  const result = await this.call("thread/read", {
@@ -162,22 +227,49 @@ export class CodexProvider {
162
227
  archived: false,
163
228
  cwd: this.extractThreadCwd(threadObject) ?? session.cwd,
164
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
+ }
165
241
  return { messages: session.messages };
166
242
  }
167
- async prompt(sessionId, text, model, agent, files = []) {
243
+ async prompt(sessionId, text, model, agent, files = [], codexOptions) {
168
244
  const session = this.ensureLocalSession(sessionId);
169
245
  session.updatedAt = Date.now();
170
246
  (async () => {
171
247
  try {
172
- 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);
173
264
  let imageUrlKey = "url";
174
265
  while (true) {
175
266
  try {
176
267
  await this.call("turn/start", {
177
268
  threadId: session.id,
178
269
  input: this.makeTurnInputPayload(text, files, imageUrlKey),
179
- ...(model ? { model: model.providerID === "codex" ? model.modelID : `${model.providerID}/${model.modelID}` } : {}),
180
- ...(agent ? { agent } : {}),
270
+ ...(modelId ? { model: modelId } : {}),
271
+ ...(reasoningEffort ? { reasoningEffort } : {}),
272
+ ...(collaborationMode ? { collaborationMode } : {}),
181
273
  });
182
274
  break;
183
275
  }
@@ -211,7 +303,7 @@ export class CodexProvider {
211
303
  return {};
212
304
  }
213
305
  async agents() {
214
- return { agents: [] };
306
+ return { agents: [...CODEX_AGENTS] };
215
307
  }
216
308
  async providers() {
217
309
  const items = await this.fetchModels();
@@ -269,11 +361,53 @@ export class CodexProvider {
269
361
  });
270
362
  return {};
271
363
  }
272
- async questionReply() {
273
- 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 {};
274
390
  }
275
- async questionReject() {
276
- 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 {};
277
411
  }
278
412
  send(req) {
279
413
  if (!this.proc?.stdin?.writable)
@@ -330,10 +464,28 @@ export class CodexProvider {
330
464
  handleServerRequest(method, requestId, params) {
331
465
  const session = this.resolveSessionFromPayload(params);
332
466
  if (method === "item/tool/requestUserInput") {
333
- this.send({
334
- jsonrpc: "2.0",
335
- id: requestId,
336
- 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
+ },
337
489
  });
338
490
  return;
339
491
  }
@@ -413,6 +565,17 @@ export class CodexProvider {
413
565
  });
414
566
  }
415
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;
416
579
  case "turn/completed":
417
580
  case "turn/failed":
418
581
  if (session) {
@@ -489,6 +652,9 @@ export class CodexProvider {
489
652
  this.pendingPermissionRequestIds.delete(permissionId);
490
653
  this.emitter?.({ type: "permission.replied", properties: { permissionId } });
491
654
  }
655
+ if (permissionId && this.pendingQuestionRequestIds.has(permissionId)) {
656
+ this.pendingQuestionRequestIds.delete(permissionId);
657
+ }
492
658
  return;
493
659
  }
494
660
  default:
@@ -607,7 +773,7 @@ export class CodexProvider {
607
773
  const itemId = this.extractItemId(params) ?? this.readString(item.id) ?? normalizedType ?? "tool";
608
774
  const fileChangeLike = this.isFileChangeStructuredItem(normalizedType, item, params);
609
775
  const emittedPartType = normalizedType === "plan"
610
- ? "reasoning"
776
+ ? "plan"
611
777
  : (fileChangeLike ? "file-change" : "tool");
612
778
  const partId = `${messageId}:${emittedPartType}:${itemId}`;
613
779
  const nextText = this.extractStructuredOutput(params, item, normalizedType);
@@ -630,8 +796,8 @@ export class CodexProvider {
630
796
  sessionID: session.id,
631
797
  messageID: messageId,
632
798
  type: emittedPartType,
633
- ...(emittedPartType === "reasoning"
634
- ? { text: outputValue ?? "Planning..." }
799
+ ...(emittedPartType === "plan"
800
+ ? { text: outputValue ?? (emittedPartType === "plan" ? "Planning..." : "") }
635
801
  : { name, toolName: name, input, output: outputValue, state, ...(patch ? { patch } : {}) }),
636
802
  };
637
803
  this.upsertLocalMessagePart(session, messageId, part);
@@ -706,6 +872,22 @@ export class CodexProvider {
706
872
  async fetchServerThreads() {
707
873
  return this.fetchServerThreadsByArchiveState(false);
708
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
+ }
709
891
  async refreshSessionMetadata(sessionId) {
710
892
  const session = this.sessions.get(sessionId);
711
893
  const result = await this.call("thread/read", {
@@ -786,6 +968,30 @@ export class CodexProvider {
786
968
  })
787
969
  .filter((value) => Boolean(value));
788
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
+ }
789
995
  parseThreadListEntry(value, archived = false) {
790
996
  const obj = this.asRecord(value);
791
997
  const id = this.extractThreadId(obj);
@@ -895,7 +1101,7 @@ export class CodexProvider {
895
1101
  return existing;
896
1102
  return this.upsertSession({ id: sessionId, title: "Conversation", createdAt: Date.now(), updatedAt: Date.now() });
897
1103
  }
898
- async ensureThreadResumed(threadId, force = false) {
1104
+ async ensureThreadResumed(threadId, force = false, codexOptions, collaborationMode) {
899
1105
  if (!threadId || this.resumedThreadIds.has(threadId)) {
900
1106
  if (!force)
901
1107
  return;
@@ -905,6 +1111,14 @@ export class CodexProvider {
905
1111
  if (session?.cwd) {
906
1112
  params.cwd = session.cwd;
907
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
+ }
908
1122
  const result = await this.call("thread/resume", params);
909
1123
  const payload = this.asRecord(result);
910
1124
  const threadValue = payload.thread;
@@ -957,6 +1171,9 @@ export class CodexProvider {
957
1171
  const turnId = this.readString(turnObject.id);
958
1172
  const turnTime = this.extractUpdatedAt(turnObject) ?? this.extractCreatedAt(turnObject) ?? Date.now();
959
1173
  const items = this.readArray(turnObject.items);
1174
+ const assistantParts = [];
1175
+ let assistantMessageId;
1176
+ let assistantTimestamp = turnTime;
960
1177
  for (const item of items) {
961
1178
  const itemObject = this.asRecord(item);
962
1179
  const type = this.normalizedItemType(this.readString(itemObject.type) ?? "");
@@ -978,88 +1195,208 @@ export class CodexProvider {
978
1195
  const text = this.decodeItemText(itemObject);
979
1196
  if (!text)
980
1197
  continue;
981
- messages.push({
982
- id: itemId,
983
- role: "assistant",
984
- parts: [{ id: `${itemId}:text`, type: "text", text, sessionID: threadId, messageID: itemId }],
985
- 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,
986
1206
  });
987
- if (turnId)
988
- this.assistantMessageIdByTurnId.set(turnId, itemId);
989
1207
  continue;
990
1208
  }
991
1209
  if (type === "reasoning") {
992
1210
  const text = this.decodeReasoningItemText(itemObject);
993
- messages.push({
994
- id: itemId,
995
- role: "assistant",
996
- parts: [{ id: `${itemId}:reasoning`, type: "reasoning", text, sessionID: threadId, messageID: itemId }],
997
- 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,
998
1219
  });
999
1220
  continue;
1000
1221
  }
1001
1222
  if (type === "plan") {
1002
1223
  const text = this.decodePlanItemText(itemObject);
1003
- messages.push({
1004
- id: itemId,
1005
- role: "assistant",
1006
- parts: [{ id: `${itemId}:plan`, type: "reasoning", text, sessionID: threadId, messageID: itemId }],
1007
- 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,
1008
1232
  });
1009
1233
  continue;
1010
1234
  }
1011
- if (type === "commandexecution" || type === "enteredreviewmode" || type === "contextcompaction") {
1012
- const output = this.decodeCommandExecutionItemText(itemObject, type);
1013
- const input = this.extractCommandExecutionInput(itemObject);
1014
- messages.push({
1015
- id: itemId,
1016
- role: "assistant",
1017
- parts: [{
1018
- id: `${itemId}:tool`,
1019
- type: "tool",
1020
- name: type === "commandexecution" ? "command" : "system",
1021
- toolName: type === "commandexecution" ? "command" : "system",
1022
- ...(input ? { input } : {}),
1023
- output,
1024
- state: "completed",
1025
- sessionID: threadId,
1026
- messageID: itemId,
1027
- }],
1028
- time: timestamp,
1029
- });
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));
1030
1247
  continue;
1031
1248
  }
1032
1249
  if (type === "filechange" || type === "toolcall" || type === "diff") {
1033
- const output = this.decodeFileLikeItemText(itemObject);
1034
- if (!output)
1035
- continue;
1036
- const emittedPartType = this.isFileChangeStructuredItem(type, itemObject)
1037
- ? "file-change"
1038
- : "tool";
1039
- const patch = emittedPartType === "file-change"
1040
- ? this.extractCanonicalPatch({}, itemObject)
1041
- : undefined;
1042
- messages.push({
1043
- id: itemId,
1044
- role: "assistant",
1045
- parts: [{
1046
- id: `${itemId}:${emittedPartType}`,
1047
- type: emittedPartType,
1048
- name: type,
1049
- toolName: type,
1050
- output,
1051
- state: "completed",
1052
- ...(patch ? { patch } : {}),
1053
- sessionID: threadId,
1054
- messageID: itemId,
1055
- }],
1056
- time: timestamp,
1057
- });
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);
1058
1269
  }
1059
1270
  }
1060
1271
  }
1061
1272
  return messages;
1062
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
+ }
1063
1400
  decodeUserMessageParts(itemObject, threadId, itemId) {
1064
1401
  const parts = [];
1065
1402
  const content = this.readArray(itemObject.content);
@@ -1282,15 +1619,50 @@ export class CodexProvider {
1282
1619
  ?? this.readString(this.asRecord(this.asRecord(payload.event).item).id));
1283
1620
  }
1284
1621
  extractTextPayload(payload) {
1285
- return (this.readString(payload.delta)
1286
- ?? this.readString(payload.text)
1287
- ?? this.readString(payload.message)
1288
- ?? this.readString(this.asRecord(payload.item).text)
1289
- ?? this.readString(this.asRecord(payload.item).delta)
1290
- ?? this.readString(this.asRecord(payload.item).message)
1291
- ?? this.readString(this.asRecord(payload.event).text)
1292
- ?? this.readString(this.asRecord(payload.event).delta)
1293
- ?? this.readString(this.asRecord(payload.event).message));
1622
+ return (this.readRawString(payload.delta)
1623
+ ?? this.readRawString(payload.text)
1624
+ ?? this.readRawString(payload.message)
1625
+ ?? this.readRawString(this.asRecord(payload.item).text)
1626
+ ?? this.readRawString(this.asRecord(payload.item).delta)
1627
+ ?? this.readRawString(this.asRecord(payload.item).message)
1628
+ ?? this.readRawString(this.asRecord(payload.event).text)
1629
+ ?? this.readRawString(this.asRecord(payload.event).delta)
1630
+ ?? this.readRawString(this.asRecord(payload.event).message));
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;
1294
1666
  }
1295
1667
  extractThreadId(payload) {
1296
1668
  if (!payload || typeof payload !== "object")
@@ -1362,6 +1734,9 @@ export class CodexProvider {
1362
1734
  readArray(value) {
1363
1735
  return Array.isArray(value) ? value : [];
1364
1736
  }
1737
+ readRawString(value) {
1738
+ return typeof value === "string" ? value : undefined;
1739
+ }
1365
1740
  readString(value) {
1366
1741
  return typeof value === "string" && value.trim() ? value.trim() : undefined;
1367
1742
  }