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.
- package/dist/ai/codex.d.ts +7 -0
- package/dist/ai/codex.js +189 -35
- package/package.json +1 -1
package/dist/ai/codex.d.ts
CHANGED
|
@@ -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
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
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,
|
|
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)
|
|
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
|
-
|
|
694
|
-
const
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
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
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
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
|
-
])
|
|
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: ${
|
|
1189
|
-
: `Path: ${
|
|
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"]);
|