lunel-cli 0.1.60 → 0.1.61

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.
@@ -74,7 +74,11 @@ export declare class CodexProvider implements AIProvider {
74
74
  private resolveSessionFromPayload;
75
75
  private resolveInFlightTurnId;
76
76
  private decodeMessagesFromThreadRead;
77
+ private decodeUserMessageParts;
77
78
  private decodeItemText;
79
+ private makeTurnInputPayload;
80
+ private shouldRetryTurnStartWithImageURLField;
81
+ private inferImageMimeFromDataUrl;
78
82
  private decodeReasoningItemText;
79
83
  private decodePlanItemText;
80
84
  private decodeCommandExecutionItemText;
package/dist/ai/codex.js CHANGED
@@ -121,19 +121,31 @@ export class CodexProvider {
121
121
  return { messages: session.messages };
122
122
  }
123
123
  async prompt(sessionId, text, model, agent, files = []) {
124
- if (files.length > 0) {
125
- throw new Error("Codex image attachments are not supported in Lunel yet");
126
- }
127
124
  const session = this.ensureLocalSession(sessionId);
128
125
  session.updatedAt = Date.now();
129
126
  (async () => {
130
127
  try {
131
- await this.call("turn/start", {
132
- threadId: session.id,
133
- input: [{ type: "text", text }],
134
- ...(model ? { model: model.providerID === "codex" ? model.modelID : `${model.providerID}/${model.modelID}` } : {}),
135
- ...(agent ? { agent } : {}),
136
- });
128
+ let imageUrlKey = "url";
129
+ while (true) {
130
+ try {
131
+ await this.call("turn/start", {
132
+ threadId: session.id,
133
+ input: this.makeTurnInputPayload(text, files, imageUrlKey),
134
+ ...(model ? { model: model.providerID === "codex" ? model.modelID : `${model.providerID}/${model.modelID}` } : {}),
135
+ ...(agent ? { agent } : {}),
136
+ });
137
+ break;
138
+ }
139
+ catch (err) {
140
+ if (imageUrlKey === "url"
141
+ && files.length > 0
142
+ && this.shouldRetryTurnStartWithImageURLField(err)) {
143
+ imageUrlKey = "image_url";
144
+ continue;
145
+ }
146
+ throw err;
147
+ }
148
+ }
137
149
  }
138
150
  catch (err) {
139
151
  const message = err.message;
@@ -860,13 +872,13 @@ export class CodexProvider {
860
872
  const itemId = this.readString(itemObject.id) ?? crypto.randomUUID();
861
873
  const timestamp = this.extractUpdatedAt(itemObject) ?? this.extractCreatedAt(itemObject) ?? (turnTime + orderOffset++);
862
874
  if (type === "usermessage") {
863
- const text = this.decodeItemText(itemObject);
864
- if (!text)
875
+ const parts = this.decodeUserMessageParts(itemObject, threadId, itemId);
876
+ if (parts.length === 0)
865
877
  continue;
866
878
  messages.push({
867
879
  id: itemId,
868
880
  role: "user",
869
- parts: [{ id: `${itemId}:text`, type: "text", text, sessionID: threadId, messageID: itemId }],
881
+ parts,
870
882
  time: timestamp,
871
883
  });
872
884
  continue;
@@ -957,6 +969,40 @@ export class CodexProvider {
957
969
  }
958
970
  return messages;
959
971
  }
972
+ decodeUserMessageParts(itemObject, threadId, itemId) {
973
+ const parts = [];
974
+ const content = this.readArray(itemObject.content);
975
+ let fileIndex = 0;
976
+ for (const entry of content) {
977
+ const obj = this.asRecord(entry);
978
+ const type = this.normalizedItemType(this.readString(obj.type) ?? "");
979
+ if (type === "image" || type === "localimage") {
980
+ const url = this.readString(obj.url) ?? this.readString(obj.image_url) ?? this.readString(obj.imageUrl);
981
+ if (!url)
982
+ continue;
983
+ parts.push({
984
+ id: `${itemId}:file:${fileIndex++}`,
985
+ type: "file",
986
+ mime: this.inferImageMimeFromDataUrl(url),
987
+ filename: this.readString(obj.filename) ?? this.readString(obj.name) ?? undefined,
988
+ url,
989
+ sessionID: threadId,
990
+ messageID: itemId,
991
+ });
992
+ }
993
+ }
994
+ const text = this.decodeItemText(itemObject);
995
+ if (text) {
996
+ parts.push({
997
+ id: `${itemId}:text`,
998
+ type: "text",
999
+ text,
1000
+ sessionID: threadId,
1001
+ messageID: itemId,
1002
+ });
1003
+ }
1004
+ return parts;
1005
+ }
960
1006
  decodeItemText(itemObject) {
961
1007
  const content = this.readArray(itemObject.content);
962
1008
  const parts = [];
@@ -977,6 +1023,40 @@ export class CodexProvider {
977
1023
  const joined = parts.join("\n").trim();
978
1024
  return joined || this.readString(itemObject.text) || this.readString(itemObject.message) || "";
979
1025
  }
1026
+ makeTurnInputPayload(text, files, imageUrlKey) {
1027
+ const items = [];
1028
+ for (const file of files) {
1029
+ const url = typeof file.url === "string" ? file.url.trim() : "";
1030
+ if (!url)
1031
+ continue;
1032
+ items.push({
1033
+ type: "image",
1034
+ [imageUrlKey]: url,
1035
+ });
1036
+ }
1037
+ const trimmedText = text.trim();
1038
+ if (trimmedText) {
1039
+ items.push({ type: "text", text: trimmedText });
1040
+ }
1041
+ return items;
1042
+ }
1043
+ shouldRetryTurnStartWithImageURLField(error) {
1044
+ const message = (error instanceof Error ? error.message : String(error)).toLowerCase();
1045
+ if (!message.includes("image_url")) {
1046
+ return false;
1047
+ }
1048
+ return (message.includes("missing")
1049
+ || message.includes("unknown field")
1050
+ || message.includes("expected")
1051
+ || message.includes("invalid"));
1052
+ }
1053
+ inferImageMimeFromDataUrl(url) {
1054
+ const match = /^data:([^;,]+)[;,]/i.exec(url);
1055
+ if (match?.[1]) {
1056
+ return match[1];
1057
+ }
1058
+ return "image/jpeg";
1059
+ }
980
1060
  decodeReasoningItemText(itemObject) {
981
1061
  const summary = this.flattenTextValue(itemObject.summary).trim();
982
1062
  const content = this.flattenTextValue(itemObject.content).trim();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lunel-cli",
3
- "version": "0.1.60",
3
+ "version": "0.1.61",
4
4
  "author": [
5
5
  {
6
6
  "name": "Soham Bharambe",