lunel-cli 0.1.53 → 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;
@@ -102,7 +106,9 @@ export declare class CodexProvider implements AIProvider {
102
106
  private renderFileChangeEntriesBody;
103
107
  private renderUnifiedDiffBody;
104
108
  private extractCanonicalPatch;
109
+ private normalizeDisplayPath;
105
110
  private normalizedFileChangeStatus;
106
111
  private extractToolInput;
112
+ private extractCommandExecutionInput;
107
113
  private describeCompletedItemOutput;
108
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 },
@@ -605,12 +623,16 @@ export class CodexProvider {
605
623
  message.time = Date.now();
606
624
  }
607
625
  async fetchServerThreads() {
626
+ return this.fetchServerThreadsByArchiveState(false);
627
+ }
628
+ async fetchServerThreadsByArchiveState(archived) {
608
629
  const threads = [];
609
630
  let nextCursor = null;
610
631
  let hasRequestedFirstPage = false;
611
632
  do {
612
633
  const result = await this.call("thread/list", {
613
634
  sourceKinds: THREAD_LIST_SOURCE_KINDS,
635
+ archived,
614
636
  cursor: nextCursor,
615
637
  });
616
638
  const payload = this.asRecord(result);
@@ -622,7 +644,7 @@ export class CodexProvider {
622
644
  ? payload.threads
623
645
  : [];
624
646
  for (const entry of page) {
625
- const parsed = this.parseThreadListEntry(entry);
647
+ const parsed = this.parseThreadListEntry(entry, archived);
626
648
  if (parsed)
627
649
  threads.push(parsed);
628
650
  }
@@ -664,7 +686,7 @@ export class CodexProvider {
664
686
  })
665
687
  .filter((value) => Boolean(value));
666
688
  }
667
- parseThreadListEntry(value) {
689
+ parseThreadListEntry(value, archived = false) {
668
690
  const obj = this.asRecord(value);
669
691
  const id = this.extractThreadId(obj);
670
692
  if (!id)
@@ -674,6 +696,7 @@ export class CodexProvider {
674
696
  title: this.extractThreadTitle(obj),
675
697
  createdAt: this.extractCreatedAt(obj),
676
698
  updatedAt: this.extractUpdatedAt(obj),
699
+ archived,
677
700
  };
678
701
  }
679
702
  hasNextCursor(value) {
@@ -687,7 +710,7 @@ export class CodexProvider {
687
710
  const threadId = this.extractThreadId(payload);
688
711
  if (!threadId)
689
712
  return;
690
- const title = this.extractThreadTitleFromUnknown(payload) ?? "Conversation";
713
+ const title = this.extractThreadTitleFromUnknown(payload);
691
714
  this.upsertSession({
692
715
  id: threadId,
693
716
  title,
@@ -695,22 +718,65 @@ export class CodexProvider {
695
718
  updatedAt: this.extractUpdatedAt(payload) ?? Date.now(),
696
719
  });
697
720
  }
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;
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);
705
727
  }
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
- };
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);
713
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
+ }
714
780
  return session;
715
781
  }
716
782
  ensureLocalSession(sessionId) {
@@ -802,6 +868,7 @@ export class CodexProvider {
802
868
  }
803
869
  if (type === "commandexecution" || type === "enteredreviewmode" || type === "contextcompaction") {
804
870
  const output = this.decodeCommandExecutionItemText(itemObject, type);
871
+ const input = this.extractCommandExecutionInput(itemObject);
805
872
  messages.push({
806
873
  id: itemId,
807
874
  role: "assistant",
@@ -810,6 +877,7 @@ export class CodexProvider {
810
877
  type: "tool",
811
878
  name: type === "commandexecution" ? "command" : "system",
812
879
  toolName: type === "commandexecution" ? "command" : "system",
880
+ ...(input ? { input } : {}),
813
881
  output,
814
882
  state: "completed",
815
883
  sessionID: threadId,
@@ -1054,6 +1122,14 @@ export class CodexProvider {
1054
1122
  }
1055
1123
  return undefined;
1056
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
+ }
1057
1133
  readArray(value) {
1058
1134
  return Array.isArray(value) ? value : [];
1059
1135
  }
@@ -1190,12 +1266,13 @@ export class CodexProvider {
1190
1266
  "old_path",
1191
1267
  "oldPath",
1192
1268
  "from",
1193
- ]) ?? "file";
1269
+ ]);
1270
+ const normalizedPath = this.normalizeDisplayPath(path) ?? "file";
1194
1271
  const kind = this.firstString(obj, ["kind", "type", "action", "status"]) ?? "change";
1195
1272
  const diff = this.firstString(obj, ["diff", "unified_diff", "unifiedDiff", "patch"]) ?? "";
1196
1273
  return diff
1197
- ? `Path: ${path}\nKind: ${kind}\n\n\`\`\`diff\n${diff}\n\`\`\``
1198
- : `Path: ${path}\nKind: ${kind}`;
1274
+ ? `Path: ${normalizedPath}\nKind: ${kind}\n\n\`\`\`diff\n${diff}\n\`\`\``
1275
+ : `Path: ${normalizedPath}\nKind: ${kind}`;
1199
1276
  })
1200
1277
  .filter(Boolean);
1201
1278
  return rendered.length > 0 ? rendered.join("\n\n---\n\n") : undefined;
@@ -1225,6 +1302,20 @@ export class CodexProvider {
1225
1302
  .join("\n");
1226
1303
  return patch.trim() || undefined;
1227
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
+ }
1228
1319
  normalizedFileChangeStatus(itemObject, isCompleted) {
1229
1320
  const status = this.readString(itemObject.status)
1230
1321
  ?? this.readString(this.asRecord(itemObject.status).type)
@@ -1235,8 +1326,40 @@ export class CodexProvider {
1235
1326
  return isCompleted ? "completed" : "inProgress";
1236
1327
  }
1237
1328
  extractToolInput(item, params) {
1329
+ const normalizedType = this.normalizeStructuredType(this.readString(item.type) ?? "");
1330
+ if (normalizedType === "commandexecution") {
1331
+ return this.extractCommandExecutionInput(item, params);
1332
+ }
1238
1333
  return item.input ?? item.command ?? item.path ?? item.args ?? params.command ?? params.path ?? undefined;
1239
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
+ }
1240
1363
  describeCompletedItemOutput(item, params, normalizedType) {
1241
1364
  if (normalizedType === "commandexecution") {
1242
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.53",
3
+ "version": "0.1.54",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",