@bacnh85/pi-web 0.10.3 → 0.10.4

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/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.10.4 (2026-09-13)
4
+
5
+ ### Fixed
6
+
7
+ - **Truthful image extensions**: saved files are typed by magic bytes, not
8
+ URL/file extension — Z.ai GLM-Image serves JPEG behind a `.png` URL, which
9
+ previously produced mislabeled files and inline blocks.
10
+
3
11
  ## 0.10.3 (2026-09-13)
4
12
 
5
13
  ### Fixed
@@ -238,7 +238,7 @@ export async function apiGenerateImage(opts: {
238
238
  for (let i = 0; i < items.length; i++) {
239
239
  const item = items[i];
240
240
  if (typeof item?.b64_json === "string" && item.b64_json) {
241
- paths.push(writeB64(opts.outDir, item.b64_json, i));
241
+ paths.push(writeB64(opts.outDir, Buffer.from(item.b64_json, "base64"), i));
242
242
  } else if (typeof item?.url === "string" && item.url) {
243
243
  // A failed download must not waste the generation: surface the URL.
244
244
  try {
@@ -256,9 +256,19 @@ export async function apiGenerateImage(opts: {
256
256
  return { paths, urls, model: typeof payload?.model === "string" ? payload.model : opts.model };
257
257
  }
258
258
 
259
- function writeB64(outDir: string, b64: string, i: number): string {
260
- const file = path.join(outDir, `pi-web-image-${randomUUID().slice(0, 8)}-${i}.png`);
261
- fs.writeFileSync(file, Buffer.from(b64, "base64"));
259
+ // Some gateways serve JPEG/WebP bytes behind a .png URL (Z.ai GLM-Image does)
260
+ // trust the magic bytes, not the URL/file extension.
261
+ function extFromBytes(buf: Buffer, fallback: string): string {
262
+ if (buf.length >= 4 && buf[0] === 0x89 && buf[1] === 0x50 && buf[2] === 0x4e && buf[3] === 0x47) return ".png";
263
+ if (buf.length >= 3 && buf[0] === 0xff && buf[1] === 0xd8 && buf[2] === 0xff) return ".jpg";
264
+ if (buf.length >= 6 && buf.toString("ascii", 0, 3) === "GIF") return ".gif";
265
+ if (buf.length >= 12 && buf.toString("ascii", 0, 4) === "RIFF" && buf.toString("ascii", 8, 12) === "WEBP") return ".webp";
266
+ return fallback;
267
+ }
268
+
269
+ function writeB64(outDir: string, buf: Buffer, i: number): string {
270
+ const file = path.join(outDir, `pi-web-image-${randomUUID().slice(0, 8)}-${i}${extFromBytes(buf, ".png")}`);
271
+ fs.writeFileSync(file, buf);
262
272
  return file;
263
273
  }
264
274
 
@@ -290,7 +300,7 @@ async function downloadImage(
290
300
  if (!res.ok) throw new ImageApiError(res.status, `image download failed (HTTP ${res.status})`);
291
301
  const buf = Buffer.from(await res.arrayBuffer());
292
302
  if (buf.length > MAX_DOWNLOAD_BYTES) throw new Error(`image exceeds the ${MAX_DOWNLOAD_BYTES}-byte download cap`);
293
- const file = path.join(outDir, `pi-web-image-${randomUUID().slice(0, 8)}-${i}${extFor(url)}`);
303
+ const file = path.join(outDir, `pi-web-image-${randomUUID().slice(0, 8)}-${i}${extFromBytes(buf, extFor(url))}`);
294
304
  fs.writeFileSync(file, buf);
295
305
  return file;
296
306
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bacnh85/pi-web",
3
- "version": "0.10.3",
3
+ "version": "0.10.4",
4
4
  "description": "Pi extension for web search, page extraction, Firecrawl scraping/crawling, Crawl4AI headless browser crawling, Gemini web-tier research, free upstream image generation, and one-off gateway chat.",
5
5
  "type": "module",
6
6
  "license": "MIT",