lunel-cli 0.1.52 → 0.1.54

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.
@@ -58,10 +58,13 @@ export declare class CodexProvider implements AIProvider {
58
58
  private ensureAssistantMessage;
59
59
  private upsertLocalMessagePart;
60
60
  private fetchServerThreads;
61
+ private fetchServerThreadsByArchiveState;
61
62
  private fetchModels;
62
63
  private parseThreadListEntry;
63
64
  private hasNextCursor;
64
65
  private ingestThreadMetadata;
66
+ private reconcileSessionsWithServer;
67
+ private mergeSession;
65
68
  private upsertSession;
66
69
  private ensureLocalSession;
67
70
  private resolveSessionFromPayload;
@@ -87,6 +90,7 @@ export declare class CodexProvider implements AIProvider {
87
90
  private extractUpdatedAt;
88
91
  private readTimestamp;
89
92
  private firstString;
93
+ private firstStringFromSources;
90
94
  private readArray;
91
95
  private readString;
92
96
  private asRecord;
@@ -101,7 +105,10 @@ export declare class CodexProvider implements AIProvider {
101
105
  private extractDiffLikePayload;
102
106
  private renderFileChangeEntriesBody;
103
107
  private renderUnifiedDiffBody;
108
+ private extractCanonicalPatch;
109
+ private normalizeDisplayPath;
104
110
  private normalizedFileChangeStatus;
105
111
  private extractToolInput;
112
+ private extractCommandExecutionInput;
106
113
  private describeCompletedItemOutput;
107
114
  }
package/dist/ai/codex.js CHANGED
@@ -2,6 +2,7 @@
2
2
  // over stdin/stdout. Maps Codex's thread/turn model onto Lunel's AIProvider
3
3
  // contract using the same thread/list + thread/read flow used by Remodex.
4
4
  import * as crypto from "crypto";
5
+ import * as path from "path";
5
6
  import { spawn } from "child_process";
6
7
  import { createInterface } from "readline";
7
8
  const THREAD_LIST_SOURCE_KINDS = ["cli", "vscode", "appServer", "exec", "unknown"];
@@ -61,14 +62,20 @@ export class CodexProvider {
61
62
  title: threadTitle,
62
63
  createdAt: this.extractCreatedAt(result) ?? Date.now(),
63
64
  updatedAt: this.extractUpdatedAt(result) ?? Date.now(),
65
+ archived: false,
64
66
  });
65
67
  return { session: this.toSessionInfo(session) };
66
68
  }
67
69
  async listSessions() {
68
- const remoteThreads = await this.fetchServerThreads();
69
- for (const thread of remoteThreads) {
70
- this.upsertSession(thread);
70
+ const activeThreads = await this.fetchServerThreads();
71
+ let archivedThreads = [];
72
+ try {
73
+ archivedThreads = await this.fetchServerThreadsByArchiveState(true);
74
+ }
75
+ catch {
76
+ // Some Codex runtimes may not support archived thread listing.
71
77
  }
78
+ this.reconcileSessionsWithServer(activeThreads, archivedThreads);
72
79
  const sessions = Array.from(this.sessions.values())
73
80
  .sort((a, b) => a.updatedAt - b.updatedAt)
74
81
  .map((session) => this.toSessionInfo(session));
@@ -103,12 +110,14 @@ export class CodexProvider {
103
110
  const historyMessages = this.decodeMessagesFromThreadRead(sessionId, threadObject);
104
111
  if (historyMessages.length > 0) {
105
112
  session.messages = historyMessages;
106
- session.updatedAt = this.extractUpdatedAt(threadObject) ?? session.updatedAt;
107
- session.createdAt = this.extractCreatedAt(threadObject) ?? session.createdAt;
108
- const title = this.extractThreadTitle(threadObject);
109
- if (title)
110
- session.title = title;
111
113
  }
114
+ this.upsertSession({
115
+ id: sessionId,
116
+ title: this.extractThreadTitle(threadObject),
117
+ createdAt: this.extractCreatedAt(threadObject) ?? session.createdAt,
118
+ updatedAt: this.extractUpdatedAt(threadObject) ?? session.updatedAt,
119
+ archived: false,
120
+ });
112
121
  return { messages: session.messages };
113
122
  }
114
123
  async prompt(sessionId, text, model, agent) {
@@ -297,15 +306,24 @@ export class CodexProvider {
297
306
  case "thread/started":
298
307
  case "thread/name/updated":
299
308
  if (session) {
300
- const title = this.extractThreadTitle(params);
301
- if (title)
302
- session.title = title;
303
- session.updatedAt = this.extractUpdatedAt(params) ?? Date.now();
304
- this.emitter?.({ type: "session.updated", properties: { info: this.toSessionInfo(session) } });
309
+ this.upsertSession({
310
+ id: session.id,
311
+ title: this.extractThreadTitle(params),
312
+ createdAt: this.extractCreatedAt(params) ?? session.createdAt,
313
+ updatedAt: this.extractUpdatedAt(params) ?? Date.now(),
314
+ archived: false,
315
+ }, true);
305
316
  }
306
317
  return;
307
318
  case "thread/status/changed":
308
319
  if (session) {
320
+ this.upsertSession({
321
+ id: session.id,
322
+ title: this.extractThreadTitle(params),
323
+ createdAt: this.extractCreatedAt(params) ?? session.createdAt,
324
+ updatedAt: this.extractUpdatedAt(params) ?? Date.now(),
325
+ archived: session.archived,
326
+ }, true);
309
327
  this.emitter?.({
310
328
  type: "session.status",
311
329
  properties: { sessionID: session.id, status: params.status ?? params },
@@ -374,10 +392,12 @@ export class CodexProvider {
374
392
  case "codex/event/read":
375
393
  case "codex/event/search":
376
394
  case "codex/event/list_files":
395
+ this.emitStructuredToolPart(session, method, params, false);
396
+ return;
377
397
  case "turn/diff/updated":
378
398
  case "codex/event/turn_diff_updated":
379
399
  case "codex/event/turn_diff":
380
- this.emitStructuredToolPart(session, method, params, false);
400
+ this.emitStructuredToolPart(session, method, params, true);
381
401
  return;
382
402
  case "item/completed":
383
403
  case "codex/event/item_completed":
@@ -521,6 +541,9 @@ export class CodexProvider {
521
541
  const name = this.describeToolPart(normalizedType, method, item);
522
542
  const input = this.extractToolInput(item, params);
523
543
  const outputValue = output || this.describeCompletedItemOutput(item, params, normalizedType) || undefined;
544
+ const patch = emittedPartType === "file-change"
545
+ ? this.extractCanonicalPatch(params, item)
546
+ : undefined;
524
547
  const part = {
525
548
  id: partId,
526
549
  sessionID: session.id,
@@ -528,7 +551,7 @@ export class CodexProvider {
528
551
  type: emittedPartType,
529
552
  ...(emittedPartType === "reasoning"
530
553
  ? { text: outputValue ?? "Planning..." }
531
- : { name, toolName: name, input, output: outputValue, state }),
554
+ : { name, toolName: name, input, output: outputValue, state, ...(patch ? { patch } : {}) }),
532
555
  };
533
556
  this.upsertLocalMessagePart(session, messageId, part);
534
557
  this.emitMessagePartEvent(session.id, messageId, "assistant", part);
@@ -600,12 +623,16 @@ export class CodexProvider {
600
623
  message.time = Date.now();
601
624
  }
602
625
  async fetchServerThreads() {
626
+ return this.fetchServerThreadsByArchiveState(false);
627
+ }
628
+ async fetchServerThreadsByArchiveState(archived) {
603
629
  const threads = [];
604
630
  let nextCursor = null;
605
631
  let hasRequestedFirstPage = false;
606
632
  do {
607
633
  const result = await this.call("thread/list", {
608
634
  sourceKinds: THREAD_LIST_SOURCE_KINDS,
635
+ archived,
609
636
  cursor: nextCursor,
610
637
  });
611
638
  const payload = this.asRecord(result);
@@ -617,7 +644,7 @@ export class CodexProvider {
617
644
  ? payload.threads
618
645
  : [];
619
646
  for (const entry of page) {
620
- const parsed = this.parseThreadListEntry(entry);
647
+ const parsed = this.parseThreadListEntry(entry, archived);
621
648
  if (parsed)
622
649
  threads.push(parsed);
623
650
  }
@@ -659,7 +686,7 @@ export class CodexProvider {
659
686
  })
660
687
  .filter((value) => Boolean(value));
661
688
  }
662
- parseThreadListEntry(value) {
689
+ parseThreadListEntry(value, archived = false) {
663
690
  const obj = this.asRecord(value);
664
691
  const id = this.extractThreadId(obj);
665
692
  if (!id)
@@ -669,6 +696,7 @@ export class CodexProvider {
669
696
  title: this.extractThreadTitle(obj),
670
697
  createdAt: this.extractCreatedAt(obj),
671
698
  updatedAt: this.extractUpdatedAt(obj),
699
+ archived,
672
700
  };
673
701
  }
674
702
  hasNextCursor(value) {
@@ -682,7 +710,7 @@ export class CodexProvider {
682
710
  const threadId = this.extractThreadId(payload);
683
711
  if (!threadId)
684
712
  return;
685
- const title = this.extractThreadTitleFromUnknown(payload) ?? "Conversation";
713
+ const title = this.extractThreadTitleFromUnknown(payload);
686
714
  this.upsertSession({
687
715
  id: threadId,
688
716
  title,
@@ -690,22 +718,65 @@ export class CodexProvider {
690
718
  updatedAt: this.extractUpdatedAt(payload) ?? Date.now(),
691
719
  });
692
720
  }
693
- upsertSession(input) {
694
- const existing = this.sessions.get(input.id);
695
- if (existing) {
696
- existing.title = input.title ?? existing.title;
697
- existing.createdAt = input.createdAt ?? existing.createdAt;
698
- existing.updatedAt = input.updatedAt ?? existing.updatedAt;
699
- return existing;
721
+ reconcileSessionsWithServer(activeThreads, archivedThreads = []) {
722
+ const localSessions = this.sessions;
723
+ const merged = new Map();
724
+ for (const thread of activeThreads) {
725
+ const session = this.mergeSession(localSessions.get(thread.id), { ...thread, archived: false });
726
+ merged.set(thread.id, session);
700
727
  }
701
- const session = {
702
- id: input.id,
703
- title: input.title ?? "Conversation",
704
- createdAt: input.createdAt ?? Date.now(),
705
- updatedAt: input.updatedAt ?? Date.now(),
706
- messages: [],
707
- };
728
+ for (const thread of archivedThreads) {
729
+ if (merged.has(thread.id))
730
+ continue;
731
+ const session = this.mergeSession(localSessions.get(thread.id), { ...thread, archived: true });
732
+ merged.set(thread.id, session);
733
+ }
734
+ for (const [id, session] of localSessions.entries()) {
735
+ if (!merged.has(id)) {
736
+ merged.set(id, session);
737
+ }
738
+ }
739
+ this.sessions = merged;
740
+ }
741
+ mergeSession(existing, input) {
742
+ if (!existing) {
743
+ return {
744
+ id: input.id,
745
+ title: input.title ?? "Conversation",
746
+ createdAt: input.createdAt ?? Date.now(),
747
+ updatedAt: input.updatedAt ?? Date.now(),
748
+ archived: input.archived ?? false,
749
+ messages: [],
750
+ };
751
+ }
752
+ existing.title = input.title ?? existing.title;
753
+ existing.createdAt = input.createdAt ?? existing.createdAt;
754
+ existing.updatedAt = input.updatedAt != null
755
+ ? Math.max(existing.updatedAt, input.updatedAt)
756
+ : existing.updatedAt;
757
+ existing.archived = input.archived ?? existing.archived ?? false;
758
+ return existing;
759
+ }
760
+ upsertSession(input, emitUpdated = false) {
761
+ const existing = this.sessions.get(input.id);
762
+ const before = existing
763
+ ? {
764
+ title: existing.title,
765
+ createdAt: existing.createdAt,
766
+ updatedAt: existing.updatedAt,
767
+ archived: existing.archived ?? false,
768
+ }
769
+ : null;
770
+ const session = this.mergeSession(existing, input);
708
771
  this.sessions.set(input.id, session);
772
+ const changed = !before
773
+ || before.title !== session.title
774
+ || before.createdAt !== session.createdAt
775
+ || before.updatedAt !== session.updatedAt
776
+ || before.archived !== (session.archived ?? false);
777
+ if (emitUpdated && changed) {
778
+ this.emitter?.({ type: "session.updated", properties: { info: this.toSessionInfo(session) } });
779
+ }
709
780
  return session;
710
781
  }
711
782
  ensureLocalSession(sessionId) {
@@ -797,6 +868,7 @@ export class CodexProvider {
797
868
  }
798
869
  if (type === "commandexecution" || type === "enteredreviewmode" || type === "contextcompaction") {
799
870
  const output = this.decodeCommandExecutionItemText(itemObject, type);
871
+ const input = this.extractCommandExecutionInput(itemObject);
800
872
  messages.push({
801
873
  id: itemId,
802
874
  role: "assistant",
@@ -805,6 +877,7 @@ export class CodexProvider {
805
877
  type: "tool",
806
878
  name: type === "commandexecution" ? "command" : "system",
807
879
  toolName: type === "commandexecution" ? "command" : "system",
880
+ ...(input ? { input } : {}),
808
881
  output,
809
882
  state: "completed",
810
883
  sessionID: threadId,
@@ -821,6 +894,9 @@ export class CodexProvider {
821
894
  const emittedPartType = this.isFileChangeStructuredItem(type, itemObject)
822
895
  ? "file-change"
823
896
  : "tool";
897
+ const patch = emittedPartType === "file-change"
898
+ ? this.extractCanonicalPatch({}, itemObject)
899
+ : undefined;
824
900
  messages.push({
825
901
  id: itemId,
826
902
  role: "assistant",
@@ -831,6 +907,7 @@ export class CodexProvider {
831
907
  toolName: type,
832
908
  output,
833
909
  state: "completed",
910
+ ...(patch ? { patch } : {}),
834
911
  sessionID: threadId,
835
912
  messageID: itemId,
836
913
  }],
@@ -1045,6 +1122,14 @@ export class CodexProvider {
1045
1122
  }
1046
1123
  return undefined;
1047
1124
  }
1125
+ firstStringFromSources(sources, keys) {
1126
+ for (const source of sources) {
1127
+ const value = this.firstString(source, keys);
1128
+ if (value)
1129
+ return value;
1130
+ }
1131
+ return undefined;
1132
+ }
1048
1133
  readArray(value) {
1049
1134
  return Array.isArray(value) ? value : [];
1050
1135
  }
@@ -1181,12 +1266,13 @@ export class CodexProvider {
1181
1266
  "old_path",
1182
1267
  "oldPath",
1183
1268
  "from",
1184
- ]) ?? "file";
1269
+ ]);
1270
+ const normalizedPath = this.normalizeDisplayPath(path) ?? "file";
1185
1271
  const kind = this.firstString(obj, ["kind", "type", "action", "status"]) ?? "change";
1186
1272
  const diff = this.firstString(obj, ["diff", "unified_diff", "unifiedDiff", "patch"]) ?? "";
1187
1273
  return diff
1188
- ? `Path: ${path}\nKind: ${kind}\n\n\`\`\`diff\n${diff}\n\`\`\``
1189
- : `Path: ${path}\nKind: ${kind}`;
1274
+ ? `Path: ${normalizedPath}\nKind: ${kind}\n\n\`\`\`diff\n${diff}\n\`\`\``
1275
+ : `Path: ${normalizedPath}\nKind: ${kind}`;
1190
1276
  })
1191
1277
  .filter(Boolean);
1192
1278
  return rendered.length > 0 ? rendered.join("\n\n---\n\n") : undefined;
@@ -1194,6 +1280,42 @@ export class CodexProvider {
1194
1280
  renderUnifiedDiffBody(diff, status) {
1195
1281
  return `Status: ${status}\n\n\`\`\`diff\n${diff.trim()}\n\`\`\``;
1196
1282
  }
1283
+ extractCanonicalPatch(params, item) {
1284
+ const event = this.asRecord(params.event);
1285
+ const nestedItem = this.asRecord(event.item);
1286
+ const sources = [item, params, event, nestedItem];
1287
+ for (const source of sources) {
1288
+ const diff = this.firstString(source, ["diff", "unified_diff", "unifiedDiff", "patch"]);
1289
+ if (diff?.trim()) {
1290
+ return diff.trim();
1291
+ }
1292
+ }
1293
+ const changes = this.readArray(item.changes ?? params.changes ?? event.changes ?? nestedItem.changes);
1294
+ if (changes.length === 0)
1295
+ return undefined;
1296
+ const patch = changes
1297
+ .map((change) => {
1298
+ const obj = this.asRecord(change);
1299
+ return this.firstString(obj, ["diff", "unified_diff", "unifiedDiff", "patch"]) ?? "";
1300
+ })
1301
+ .filter((value) => value.trim().length > 0)
1302
+ .join("\n");
1303
+ return patch.trim() || undefined;
1304
+ }
1305
+ normalizeDisplayPath(rawPath) {
1306
+ if (!rawPath)
1307
+ return undefined;
1308
+ const trimmed = rawPath.trim();
1309
+ if (!trimmed)
1310
+ return undefined;
1311
+ if (!path.isAbsolute(trimmed))
1312
+ return trimmed;
1313
+ const relative = path.relative(process.cwd(), trimmed);
1314
+ if (!relative || relative.startsWith("..")) {
1315
+ return trimmed;
1316
+ }
1317
+ return relative;
1318
+ }
1197
1319
  normalizedFileChangeStatus(itemObject, isCompleted) {
1198
1320
  const status = this.readString(itemObject.status)
1199
1321
  ?? this.readString(this.asRecord(itemObject.status).type)
@@ -1204,8 +1326,40 @@ export class CodexProvider {
1204
1326
  return isCompleted ? "completed" : "inProgress";
1205
1327
  }
1206
1328
  extractToolInput(item, params) {
1329
+ const normalizedType = this.normalizeStructuredType(this.readString(item.type) ?? "");
1330
+ if (normalizedType === "commandexecution") {
1331
+ return this.extractCommandExecutionInput(item, params);
1332
+ }
1207
1333
  return item.input ?? item.command ?? item.path ?? item.args ?? params.command ?? params.path ?? undefined;
1208
1334
  }
1335
+ extractCommandExecutionInput(item, params) {
1336
+ const event = this.asRecord(params?.event);
1337
+ const nestedItem = this.asRecord(event.item);
1338
+ const sources = [item, params ?? {}, event, nestedItem];
1339
+ const command = this.firstStringFromSources(sources, [
1340
+ "command",
1341
+ "cmd",
1342
+ "raw_command",
1343
+ "rawCommand",
1344
+ "invocation",
1345
+ "input",
1346
+ "fullCommand",
1347
+ "full_command",
1348
+ ]);
1349
+ const cwd = this.firstStringFromSources(sources, [
1350
+ "cwd",
1351
+ "workdir",
1352
+ "workingDirectory",
1353
+ "working_directory",
1354
+ ]);
1355
+ if (command && cwd)
1356
+ return { command, cwd };
1357
+ if (command)
1358
+ return { command };
1359
+ if (cwd)
1360
+ return { cwd };
1361
+ return undefined;
1362
+ }
1209
1363
  describeCompletedItemOutput(item, params, normalizedType) {
1210
1364
  if (normalizedType === "commandexecution") {
1211
1365
  return this.firstString(item, ["stdout", "stderr", "text", "message", "summary"]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.52",
3
+ "version": "0.1.54",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",