lunel-cli 0.1.53 → 0.1.55

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,14 @@ export declare class CodexProvider implements AIProvider {
58
58
  private ensureAssistantMessage;
59
59
  private upsertLocalMessagePart;
60
60
  private fetchServerThreads;
61
+ private refreshSessionMetadata;
62
+ private fetchServerThreadsByArchiveState;
61
63
  private fetchModels;
62
64
  private parseThreadListEntry;
63
65
  private hasNextCursor;
64
66
  private ingestThreadMetadata;
67
+ private reconcileSessionsWithServer;
68
+ private mergeSession;
65
69
  private upsertSession;
66
70
  private ensureLocalSession;
67
71
  private resolveSessionFromPayload;
@@ -87,6 +91,7 @@ export declare class CodexProvider implements AIProvider {
87
91
  private extractUpdatedAt;
88
92
  private readTimestamp;
89
93
  private firstString;
94
+ private firstStringFromSources;
90
95
  private readArray;
91
96
  private readString;
92
97
  private asRecord;
@@ -102,7 +107,9 @@ export declare class CodexProvider implements AIProvider {
102
107
  private renderFileChangeEntriesBody;
103
108
  private renderUnifiedDiffBody;
104
109
  private extractCanonicalPatch;
110
+ private normalizeDisplayPath;
105
111
  private normalizedFileChangeStatus;
106
112
  private extractToolInput;
113
+ private extractCommandExecutionInput;
107
114
  private describeCompletedItemOutput;
108
115
  }
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 },
@@ -330,6 +348,9 @@ export class CodexProvider {
330
348
  session.activeTurnId = undefined;
331
349
  session.updatedAt = Date.now();
332
350
  this.finishAssistantTurn(session, params, method === "turn/failed");
351
+ this.refreshSessionMetadata(session.id).catch(() => {
352
+ // Best-effort metadata refresh for title/preview updates after a turn ends.
353
+ });
333
354
  this.emitter?.({ type: "session.idle", properties: { sessionID: session.id } });
334
355
  if (method === "turn/failed") {
335
356
  const error = this.readString(this.asRecord(params.error).message) ?? "Turn failed";
@@ -605,12 +626,34 @@ export class CodexProvider {
605
626
  message.time = Date.now();
606
627
  }
607
628
  async fetchServerThreads() {
629
+ return this.fetchServerThreadsByArchiveState(false);
630
+ }
631
+ async refreshSessionMetadata(sessionId) {
632
+ const session = this.sessions.get(sessionId);
633
+ const result = await this.call("thread/read", {
634
+ threadId: sessionId,
635
+ includeTurns: false,
636
+ });
637
+ const threadObject = this.extractThreadObject(result);
638
+ if (!threadObject || Object.keys(threadObject).length === 0) {
639
+ return;
640
+ }
641
+ this.upsertSession({
642
+ id: sessionId,
643
+ title: this.extractThreadTitle(threadObject),
644
+ createdAt: this.extractCreatedAt(threadObject) ?? session?.createdAt ?? Date.now(),
645
+ updatedAt: this.extractUpdatedAt(threadObject) ?? session?.updatedAt ?? Date.now(),
646
+ archived: false,
647
+ }, true);
648
+ }
649
+ async fetchServerThreadsByArchiveState(archived) {
608
650
  const threads = [];
609
651
  let nextCursor = null;
610
652
  let hasRequestedFirstPage = false;
611
653
  do {
612
654
  const result = await this.call("thread/list", {
613
655
  sourceKinds: THREAD_LIST_SOURCE_KINDS,
656
+ archived,
614
657
  cursor: nextCursor,
615
658
  });
616
659
  const payload = this.asRecord(result);
@@ -622,7 +665,7 @@ export class CodexProvider {
622
665
  ? payload.threads
623
666
  : [];
624
667
  for (const entry of page) {
625
- const parsed = this.parseThreadListEntry(entry);
668
+ const parsed = this.parseThreadListEntry(entry, archived);
626
669
  if (parsed)
627
670
  threads.push(parsed);
628
671
  }
@@ -664,7 +707,7 @@ export class CodexProvider {
664
707
  })
665
708
  .filter((value) => Boolean(value));
666
709
  }
667
- parseThreadListEntry(value) {
710
+ parseThreadListEntry(value, archived = false) {
668
711
  const obj = this.asRecord(value);
669
712
  const id = this.extractThreadId(obj);
670
713
  if (!id)
@@ -674,6 +717,7 @@ export class CodexProvider {
674
717
  title: this.extractThreadTitle(obj),
675
718
  createdAt: this.extractCreatedAt(obj),
676
719
  updatedAt: this.extractUpdatedAt(obj),
720
+ archived,
677
721
  };
678
722
  }
679
723
  hasNextCursor(value) {
@@ -687,7 +731,7 @@ export class CodexProvider {
687
731
  const threadId = this.extractThreadId(payload);
688
732
  if (!threadId)
689
733
  return;
690
- const title = this.extractThreadTitleFromUnknown(payload) ?? "Conversation";
734
+ const title = this.extractThreadTitleFromUnknown(payload);
691
735
  this.upsertSession({
692
736
  id: threadId,
693
737
  title,
@@ -695,22 +739,65 @@ export class CodexProvider {
695
739
  updatedAt: this.extractUpdatedAt(payload) ?? Date.now(),
696
740
  });
697
741
  }
698
- upsertSession(input) {
699
- const existing = this.sessions.get(input.id);
700
- if (existing) {
701
- existing.title = input.title ?? existing.title;
702
- existing.createdAt = input.createdAt ?? existing.createdAt;
703
- existing.updatedAt = input.updatedAt ?? existing.updatedAt;
704
- return existing;
742
+ reconcileSessionsWithServer(activeThreads, archivedThreads = []) {
743
+ const localSessions = this.sessions;
744
+ const merged = new Map();
745
+ for (const thread of activeThreads) {
746
+ const session = this.mergeSession(localSessions.get(thread.id), { ...thread, archived: false });
747
+ merged.set(thread.id, session);
705
748
  }
706
- const session = {
707
- id: input.id,
708
- title: input.title ?? "Conversation",
709
- createdAt: input.createdAt ?? Date.now(),
710
- updatedAt: input.updatedAt ?? Date.now(),
711
- messages: [],
712
- };
749
+ for (const thread of archivedThreads) {
750
+ if (merged.has(thread.id))
751
+ continue;
752
+ const session = this.mergeSession(localSessions.get(thread.id), { ...thread, archived: true });
753
+ merged.set(thread.id, session);
754
+ }
755
+ for (const [id, session] of localSessions.entries()) {
756
+ if (!merged.has(id)) {
757
+ merged.set(id, session);
758
+ }
759
+ }
760
+ this.sessions = merged;
761
+ }
762
+ mergeSession(existing, input) {
763
+ if (!existing) {
764
+ return {
765
+ id: input.id,
766
+ title: input.title ?? "Conversation",
767
+ createdAt: input.createdAt ?? Date.now(),
768
+ updatedAt: input.updatedAt ?? Date.now(),
769
+ archived: input.archived ?? false,
770
+ messages: [],
771
+ };
772
+ }
773
+ existing.title = input.title ?? existing.title;
774
+ existing.createdAt = input.createdAt ?? existing.createdAt;
775
+ existing.updatedAt = input.updatedAt != null
776
+ ? Math.max(existing.updatedAt, input.updatedAt)
777
+ : existing.updatedAt;
778
+ existing.archived = input.archived ?? existing.archived ?? false;
779
+ return existing;
780
+ }
781
+ upsertSession(input, emitUpdated = false) {
782
+ const existing = this.sessions.get(input.id);
783
+ const before = existing
784
+ ? {
785
+ title: existing.title,
786
+ createdAt: existing.createdAt,
787
+ updatedAt: existing.updatedAt,
788
+ archived: existing.archived ?? false,
789
+ }
790
+ : null;
791
+ const session = this.mergeSession(existing, input);
713
792
  this.sessions.set(input.id, session);
793
+ const changed = !before
794
+ || before.title !== session.title
795
+ || before.createdAt !== session.createdAt
796
+ || before.updatedAt !== session.updatedAt
797
+ || before.archived !== (session.archived ?? false);
798
+ if (emitUpdated && changed) {
799
+ this.emitter?.({ type: "session.updated", properties: { info: this.toSessionInfo(session) } });
800
+ }
714
801
  return session;
715
802
  }
716
803
  ensureLocalSession(sessionId) {
@@ -802,6 +889,7 @@ export class CodexProvider {
802
889
  }
803
890
  if (type === "commandexecution" || type === "enteredreviewmode" || type === "contextcompaction") {
804
891
  const output = this.decodeCommandExecutionItemText(itemObject, type);
892
+ const input = this.extractCommandExecutionInput(itemObject);
805
893
  messages.push({
806
894
  id: itemId,
807
895
  role: "assistant",
@@ -810,6 +898,7 @@ export class CodexProvider {
810
898
  type: "tool",
811
899
  name: type === "commandexecution" ? "command" : "system",
812
900
  toolName: type === "commandexecution" ? "command" : "system",
901
+ ...(input ? { input } : {}),
813
902
  output,
814
903
  state: "completed",
815
904
  sessionID: threadId,
@@ -963,10 +1052,16 @@ export class CodexProvider {
963
1052
  }
964
1053
  extractThreadTitle(payload) {
965
1054
  const thread = this.asRecord(payload.thread);
966
- return this.readString(thread.title)
967
- ?? this.readString(thread.name)
968
- ?? this.readString(payload.title)
969
- ?? this.readString(payload.name);
1055
+ const explicitName = this.readString(thread.name) ?? this.readString(payload.name);
1056
+ if (explicitName)
1057
+ return explicitName;
1058
+ const explicitTitle = this.readString(thread.title) ?? this.readString(payload.title);
1059
+ if (explicitTitle)
1060
+ return explicitTitle;
1061
+ const preview = this.readString(thread.preview) ?? this.readString(payload.preview);
1062
+ if (!preview)
1063
+ return undefined;
1064
+ return preview.charAt(0).toUpperCase() + preview.slice(1);
970
1065
  }
971
1066
  extractTurnId(payload) {
972
1067
  return (this.readString(payload.turnId)
@@ -1054,6 +1149,14 @@ export class CodexProvider {
1054
1149
  }
1055
1150
  return undefined;
1056
1151
  }
1152
+ firstStringFromSources(sources, keys) {
1153
+ for (const source of sources) {
1154
+ const value = this.firstString(source, keys);
1155
+ if (value)
1156
+ return value;
1157
+ }
1158
+ return undefined;
1159
+ }
1057
1160
  readArray(value) {
1058
1161
  return Array.isArray(value) ? value : [];
1059
1162
  }
@@ -1190,12 +1293,13 @@ export class CodexProvider {
1190
1293
  "old_path",
1191
1294
  "oldPath",
1192
1295
  "from",
1193
- ]) ?? "file";
1296
+ ]);
1297
+ const normalizedPath = this.normalizeDisplayPath(path) ?? "file";
1194
1298
  const kind = this.firstString(obj, ["kind", "type", "action", "status"]) ?? "change";
1195
1299
  const diff = this.firstString(obj, ["diff", "unified_diff", "unifiedDiff", "patch"]) ?? "";
1196
1300
  return diff
1197
- ? `Path: ${path}\nKind: ${kind}\n\n\`\`\`diff\n${diff}\n\`\`\``
1198
- : `Path: ${path}\nKind: ${kind}`;
1301
+ ? `Path: ${normalizedPath}\nKind: ${kind}\n\n\`\`\`diff\n${diff}\n\`\`\``
1302
+ : `Path: ${normalizedPath}\nKind: ${kind}`;
1199
1303
  })
1200
1304
  .filter(Boolean);
1201
1305
  return rendered.length > 0 ? rendered.join("\n\n---\n\n") : undefined;
@@ -1225,6 +1329,20 @@ export class CodexProvider {
1225
1329
  .join("\n");
1226
1330
  return patch.trim() || undefined;
1227
1331
  }
1332
+ normalizeDisplayPath(rawPath) {
1333
+ if (!rawPath)
1334
+ return undefined;
1335
+ const trimmed = rawPath.trim();
1336
+ if (!trimmed)
1337
+ return undefined;
1338
+ if (!path.isAbsolute(trimmed))
1339
+ return trimmed;
1340
+ const relative = path.relative(process.cwd(), trimmed);
1341
+ if (!relative || relative.startsWith("..")) {
1342
+ return trimmed;
1343
+ }
1344
+ return relative;
1345
+ }
1228
1346
  normalizedFileChangeStatus(itemObject, isCompleted) {
1229
1347
  const status = this.readString(itemObject.status)
1230
1348
  ?? this.readString(this.asRecord(itemObject.status).type)
@@ -1235,8 +1353,40 @@ export class CodexProvider {
1235
1353
  return isCompleted ? "completed" : "inProgress";
1236
1354
  }
1237
1355
  extractToolInput(item, params) {
1356
+ const normalizedType = this.normalizeStructuredType(this.readString(item.type) ?? "");
1357
+ if (normalizedType === "commandexecution") {
1358
+ return this.extractCommandExecutionInput(item, params);
1359
+ }
1238
1360
  return item.input ?? item.command ?? item.path ?? item.args ?? params.command ?? params.path ?? undefined;
1239
1361
  }
1362
+ extractCommandExecutionInput(item, params) {
1363
+ const event = this.asRecord(params?.event);
1364
+ const nestedItem = this.asRecord(event.item);
1365
+ const sources = [item, params ?? {}, event, nestedItem];
1366
+ const command = this.firstStringFromSources(sources, [
1367
+ "command",
1368
+ "cmd",
1369
+ "raw_command",
1370
+ "rawCommand",
1371
+ "invocation",
1372
+ "input",
1373
+ "fullCommand",
1374
+ "full_command",
1375
+ ]);
1376
+ const cwd = this.firstStringFromSources(sources, [
1377
+ "cwd",
1378
+ "workdir",
1379
+ "workingDirectory",
1380
+ "working_directory",
1381
+ ]);
1382
+ if (command && cwd)
1383
+ return { command, cwd };
1384
+ if (command)
1385
+ return { command };
1386
+ if (cwd)
1387
+ return { cwd };
1388
+ return undefined;
1389
+ }
1240
1390
  describeCompletedItemOutput(item, params, normalizedType) {
1241
1391
  if (normalizedType === "commandexecution") {
1242
1392
  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.53",
3
+ "version": "0.1.55",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",