@ccpocket/bridge 1.53.1 → 1.54.0

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.
@@ -1,7 +1,7 @@
1
1
  import { readdir, readFile, writeFile, appendFile, stat, open } from "node:fs/promises";
2
2
  import { createReadStream } from "node:fs";
3
3
  import { createInterface } from "node:readline";
4
- import { basename, join } from "node:path";
4
+ import { basename, extname, join } from "node:path";
5
5
  import { homedir } from "node:os";
6
6
  import { isAutoRenamePromptText } from "./auto-rename.js";
7
7
  import { CODEX_ASSIST_MODEL } from "./codex-assist.js";
@@ -2104,46 +2104,221 @@ async function extractCodexMessageImages(sessionId, messageUuid) {
2104
2104
  catch {
2105
2105
  return [];
2106
2106
  }
2107
- // Codex doesn't have per-message UUIDs in the same way.
2108
- // We scan for event_msg with user_message that has images and match by line index
2109
- // encoded in the UUID (format: "codex-line-{index}").
2107
+ // Codex doesn't have per-message UUIDs in the same way. Newer app history
2108
+ // uses a stable turn ordinal (codex:user-turn:N); older builds encoded the
2109
+ // JSONL line index (codex-line:N).
2110
2110
  const lineIndex = messageUuid.startsWith("codex-line-")
2111
2111
  ? parseInt(messageUuid.slice("codex-line-".length), 10)
2112
2112
  : -1;
2113
- if (lineIndex < 0)
2114
- return [];
2115
2113
  const lines = raw.split("\n");
2116
- if (lineIndex >= lines.length)
2117
- return [];
2118
- const line = lines[lineIndex];
2119
- if (!line?.trim())
2120
- return [];
2121
- let entry;
2122
- try {
2123
- entry = JSON.parse(line);
2114
+ if (lineIndex >= 0) {
2115
+ if (lineIndex >= lines.length)
2116
+ return [];
2117
+ const line = lines[lineIndex];
2118
+ if (!line?.trim())
2119
+ return [];
2120
+ let entry;
2121
+ try {
2122
+ entry = JSON.parse(line);
2123
+ }
2124
+ catch {
2125
+ return [];
2126
+ }
2127
+ if (entry.type !== "event_msg")
2128
+ return [];
2129
+ const payload = asObject(entry.payload);
2130
+ if (!payload || payload.type !== "user_message")
2131
+ return [];
2132
+ return extractCodexUserMessagePayloadImages(payload);
2124
2133
  }
2125
- catch {
2134
+ const turnMatch = messageUuid.match(/^codex:user-turn:(\d+)$/);
2135
+ const targetOrdinal = turnMatch ? Number(turnMatch[1]) : -1;
2136
+ if (!Number.isInteger(targetOrdinal) || targetOrdinal <= 0)
2126
2137
  return [];
2138
+ const responseItemImagesByOrdinal = collectCodexUserResponseItemImagesByOrdinal(lines);
2139
+ let ordinal = 0;
2140
+ for (const line of lines) {
2141
+ if (!line.trim())
2142
+ continue;
2143
+ let entry;
2144
+ try {
2145
+ entry = JSON.parse(line);
2146
+ }
2147
+ catch {
2148
+ continue;
2149
+ }
2150
+ if (entry.type !== "event_msg")
2151
+ continue;
2152
+ const payload = asObject(entry.payload);
2153
+ if (!payload || payload.type !== "user_message")
2154
+ continue;
2155
+ if (!codexUserMessagePayloadHasDisplayContent(payload))
2156
+ continue;
2157
+ ordinal += 1;
2158
+ if (ordinal === targetOrdinal) {
2159
+ const images = await extractCodexUserMessagePayloadImages(payload);
2160
+ return images.length > 0
2161
+ ? images
2162
+ : (responseItemImagesByOrdinal.get(targetOrdinal) ?? []);
2163
+ }
2127
2164
  }
2128
- if (entry.type !== "event_msg")
2129
- return [];
2130
- const payload = asObject(entry.payload);
2131
- if (!payload || payload.type !== "user_message")
2132
- return [];
2165
+ return [];
2166
+ }
2167
+ async function extractCodexUserMessagePayloadImages(payload) {
2133
2168
  const images = [];
2134
- // Parse payload.images (Data URI format: "data:image/png;base64,...")
2135
2169
  if (Array.isArray(payload.images)) {
2136
2170
  for (const img of payload.images) {
2137
- if (typeof img !== "string")
2171
+ if (typeof img === "string") {
2172
+ const match = img.match(/^data:(image\/[^;]+);base64,(.+)$/);
2173
+ if (match) {
2174
+ images.push({ base64: match[2], mimeType: match[1] });
2175
+ }
2138
2176
  continue;
2139
- const match = img.match(/^data:(image\/[^;]+);base64,(.+)$/);
2140
- if (match) {
2141
- images.push({ base64: match[2], mimeType: match[1] });
2177
+ }
2178
+ const item = asObject(img);
2179
+ if (!item)
2180
+ continue;
2181
+ const base64 = typeof item.base64 === "string"
2182
+ ? item.base64
2183
+ : typeof item.data === "string"
2184
+ ? item.data
2185
+ : undefined;
2186
+ const mimeType = typeof item.mimeType === "string"
2187
+ ? item.mimeType
2188
+ : typeof item.mime_type === "string"
2189
+ ? item.mime_type
2190
+ : typeof item.media_type === "string"
2191
+ ? item.media_type
2192
+ : undefined;
2193
+ if (base64 && mimeType) {
2194
+ images.push({ base64, mimeType });
2142
2195
  }
2143
2196
  }
2144
2197
  }
2198
+ if (Array.isArray(payload.local_images)) {
2199
+ for (const imagePath of payload.local_images) {
2200
+ if (typeof imagePath !== "string" || imagePath.length === 0)
2201
+ continue;
2202
+ const image = await readLocalImageAsBase64(imagePath);
2203
+ if (image)
2204
+ images.push(image);
2205
+ }
2206
+ }
2145
2207
  return images;
2146
2208
  }
2209
+ function collectCodexUserResponseItemImagesByOrdinal(lines) {
2210
+ const imagesByOrdinal = new Map();
2211
+ let ordinal = 0;
2212
+ for (const line of lines) {
2213
+ if (!line.trim())
2214
+ continue;
2215
+ let entry;
2216
+ try {
2217
+ entry = JSON.parse(line);
2218
+ }
2219
+ catch {
2220
+ continue;
2221
+ }
2222
+ if (entry.type !== "response_item")
2223
+ continue;
2224
+ const payload = asObject(entry.payload);
2225
+ if (!payload ||
2226
+ payload.type !== "message" ||
2227
+ payload.role !== "user" ||
2228
+ !codexUserResponseItemHasDisplayContent(payload)) {
2229
+ continue;
2230
+ }
2231
+ ordinal += 1;
2232
+ const images = extractCodexUserResponseItemImages(payload);
2233
+ if (images.length > 0) {
2234
+ imagesByOrdinal.set(ordinal, images);
2235
+ }
2236
+ }
2237
+ return imagesByOrdinal;
2238
+ }
2239
+ function codexUserResponseItemHasDisplayContent(payload) {
2240
+ const texts = [];
2241
+ let hasImage = false;
2242
+ for (const item of arrayValue(payload.content)) {
2243
+ const content = asObject(item);
2244
+ if (!content)
2245
+ continue;
2246
+ if (content.type === "input_image") {
2247
+ hasImage = true;
2248
+ continue;
2249
+ }
2250
+ if (content.type !== "input_text" || typeof content.text !== "string") {
2251
+ continue;
2252
+ }
2253
+ texts.push(content.text);
2254
+ }
2255
+ const userText = texts
2256
+ .filter((text) => !isCodexImageWrapperText(text.trim()))
2257
+ .join("\n")
2258
+ .trim();
2259
+ if (userText && isCodexInjectedUserContext(userText))
2260
+ return false;
2261
+ return userText.length > 0 || hasImage;
2262
+ }
2263
+ function extractCodexUserResponseItemImages(payload) {
2264
+ const images = [];
2265
+ for (const item of arrayValue(payload.content)) {
2266
+ const content = asObject(item);
2267
+ if (!content || content.type !== "input_image")
2268
+ continue;
2269
+ const imageUrl = typeof content.image_url === "string"
2270
+ ? content.image_url
2271
+ : typeof content.url === "string"
2272
+ ? content.url
2273
+ : undefined;
2274
+ const image = extractDataUriImage(imageUrl);
2275
+ if (image)
2276
+ images.push(image);
2277
+ }
2278
+ return images;
2279
+ }
2280
+ function extractDataUriImage(value) {
2281
+ const match = value?.match(/^data:(image\/[^;]+);base64,(.+)$/);
2282
+ return match ? { base64: match[2], mimeType: match[1] } : null;
2283
+ }
2284
+ function isCodexImageWrapperText(text) {
2285
+ return /^<image(?:\s[^>]*)?>$/.test(text) || text === "</image>";
2286
+ }
2287
+ async function readLocalImageAsBase64(imagePath) {
2288
+ const mimeType = mimeTypeForLocalImagePath(imagePath);
2289
+ if (!mimeType)
2290
+ return null;
2291
+ try {
2292
+ const buffer = await readFile(imagePath);
2293
+ return { base64: buffer.toString("base64"), mimeType };
2294
+ }
2295
+ catch {
2296
+ return null;
2297
+ }
2298
+ }
2299
+ function mimeTypeForLocalImagePath(imagePath) {
2300
+ switch (extname(imagePath).toLowerCase()) {
2301
+ case ".png":
2302
+ return "image/png";
2303
+ case ".jpg":
2304
+ case ".jpeg":
2305
+ return "image/jpeg";
2306
+ case ".gif":
2307
+ return "image/gif";
2308
+ case ".webp":
2309
+ return "image/webp";
2310
+ default:
2311
+ return null;
2312
+ }
2313
+ }
2314
+ function codexUserMessagePayloadHasDisplayContent(payload) {
2315
+ const message = typeof payload.message === "string" ? payload.message : "";
2316
+ const images = Array.isArray(payload.images) ? payload.images.length : 0;
2317
+ const localImages = Array.isArray(payload.local_images)
2318
+ ? payload.local_images.length
2319
+ : 0;
2320
+ return message.trim().length > 0 || images + localImages > 0;
2321
+ }
2147
2322
  export async function getCodexSessionHistory(threadId) {
2148
2323
  const jsonlPath = await findCodexSessionJsonlPath(threadId);
2149
2324
  if (!jsonlPath)
@@ -2250,6 +2425,9 @@ export async function getCodexSessionHistory(threadId) {
2250
2425
  continue;
2251
2426
  }
2252
2427
  if (payload.role === "user") {
2428
+ if (content.some((item) => item.type === "input_image")) {
2429
+ continue;
2430
+ }
2253
2431
  const text = content
2254
2432
  .filter((item) => item.type === "input_text" && typeof item.text === "string")
2255
2433
  .map((item) => item.text)