greprag 5.61.1 → 5.62.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.
@@ -2501,6 +2501,588 @@ function activeProcedureAnnounces(store) {
2501
2501
  return lines;
2502
2502
  }
2503
2503
 
2504
+ // src/archive/glyph-font.ts
2505
+ var GLYPH_W = 8;
2506
+ var GLYPH_H = 8;
2507
+ var FIRST_CODE = 32;
2508
+ var LAST_CODE = 126;
2509
+ var FONT_HEX = [
2510
+ "0000000000000000",
2511
+ "183c3c1818001800",
2512
+ "3636000000000000",
2513
+ "36367f367f363600",
2514
+ "0c3e031e301f0c00",
2515
+ "006333180c666300",
2516
+ "1c361c6e3b336e00",
2517
+ "0606030000000000",
2518
+ "180c0606060c1800",
2519
+ "060c1818180c0600",
2520
+ "00663cff3c660000",
2521
+ "000c0c3f0c0c0000",
2522
+ "00000000000c0c06",
2523
+ "0000003f00000000",
2524
+ "00000000000c0c00",
2525
+ "6030180c06030100",
2526
+ "3e63737b6f673e00",
2527
+ "0c0e0c0c0c0c3f00",
2528
+ "1e33301c06333f00",
2529
+ "1e33301c30331e00",
2530
+ "383c36337f307800",
2531
+ "3f031f3030331e00",
2532
+ "1c06031f33331e00",
2533
+ "3f3330180c0c0c00",
2534
+ "1e33331e33331e00",
2535
+ "1e33333e30180e00",
2536
+ "000c0c00000c0c00",
2537
+ "000c0c00000c0c06",
2538
+ "180c0603060c1800",
2539
+ "00003f00003f0000",
2540
+ "060c1830180c0600",
2541
+ "1e3330180c000c00",
2542
+ "3e637b7b7b031e00",
2543
+ "0c1e33333f333300",
2544
+ "3f66663e66663f00",
2545
+ "3c66030303663c00",
2546
+ "1f36666666361f00",
2547
+ "7f46161e16467f00",
2548
+ "7f46161e16060f00",
2549
+ "3c66030373667c00",
2550
+ "3333333f33333300",
2551
+ "1e0c0c0c0c0c1e00",
2552
+ "7830303033331e00",
2553
+ "6766361e36666700",
2554
+ "0f06060646667f00",
2555
+ "63777f7f6b636300",
2556
+ "63676f7b73636300",
2557
+ "1c36636363361c00",
2558
+ "3f66663e06060f00",
2559
+ "1e3333333b1e3800",
2560
+ "3f66663e36666700",
2561
+ "1e33070e38331e00",
2562
+ "3f2d0c0c0c0c1e00",
2563
+ "3333333333333f00",
2564
+ "33333333331e0c00",
2565
+ "6363636b7f776300",
2566
+ "6363361c1c366300",
2567
+ "3333331e0c0c1e00",
2568
+ "7f6331184c667f00",
2569
+ "1e06060606061e00",
2570
+ "03060c1830604000",
2571
+ "1e18181818181e00",
2572
+ "081c366300000000",
2573
+ "00000000000000ff",
2574
+ "0c0c180000000000",
2575
+ "00001e303e336e00",
2576
+ "0706063e66663b00",
2577
+ "00001e3303331e00",
2578
+ "3830303e33336e00",
2579
+ "00001e333f031e00",
2580
+ "1c36060f06060f00",
2581
+ "00006e33333e301f",
2582
+ "0706366e66666700",
2583
+ "0c000e0c0c0c1e00",
2584
+ "300030303033331e",
2585
+ "070666361e366700",
2586
+ "0e0c0c0c0c0c1e00",
2587
+ "0000337f7f6b6300",
2588
+ "00001f3333333300",
2589
+ "00001e3333331e00",
2590
+ "00003b66663e060f",
2591
+ "00006e33333e3078",
2592
+ "00003b6e66060f00",
2593
+ "00003e031e301f00",
2594
+ "080c3e0c0c2c1800",
2595
+ "0000333333336e00",
2596
+ "00003333331e0c00",
2597
+ "0000636b7f7f3600",
2598
+ "000063361c366300",
2599
+ "00003333333e301f",
2600
+ "00003f190c263f00",
2601
+ "380c0c070c0c3800",
2602
+ "1818180018181800",
2603
+ "070c0c380c0c0700",
2604
+ "6e3b000000000000"
2605
+ ];
2606
+ function glyphRows(code) {
2607
+ if (code < FIRST_CODE || code > LAST_CODE)
2608
+ return null;
2609
+ const hex = FONT_HEX[code - FIRST_CODE];
2610
+ const out = [];
2611
+ for (let i = 0; i < 8; i++)
2612
+ out.push(parseInt(hex.slice(i * 2, i * 2 + 2), 16));
2613
+ return out;
2614
+ }
2615
+ var reverse = null;
2616
+ function codeForSignature(sigHex) {
2617
+ if (!reverse) {
2618
+ reverse = /* @__PURE__ */ new Map();
2619
+ for (let c = FIRST_CODE; c <= LAST_CODE; c++)
2620
+ reverse.set(FONT_HEX[c - FIRST_CODE], c);
2621
+ }
2622
+ return reverse.get(sigHex) ?? null;
2623
+ }
2624
+ function signatureForRows(bytes) {
2625
+ return bytes.map((b) => (b & 255).toString(16).padStart(2, "0")).join("");
2626
+ }
2627
+
2628
+ // src/archive/png-codec.ts
2629
+ var zlib = __toESM(require("zlib"));
2630
+ var PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
2631
+ var crcTable = null;
2632
+ function getCrcTable() {
2633
+ if (crcTable)
2634
+ return crcTable;
2635
+ crcTable = new Uint32Array(256);
2636
+ for (let n = 0; n < 256; n++) {
2637
+ let c = n;
2638
+ for (let k = 0; k < 8; k++) {
2639
+ c = c & 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
2640
+ }
2641
+ crcTable[n] = c >>> 0;
2642
+ }
2643
+ return crcTable;
2644
+ }
2645
+ function crc32(buf) {
2646
+ const table = getCrcTable();
2647
+ let c = 4294967295;
2648
+ for (let i = 0; i < buf.length; i++) {
2649
+ c = table[(c ^ buf[i]) & 255] ^ c >>> 8;
2650
+ }
2651
+ return (c ^ 4294967295) >>> 0;
2652
+ }
2653
+ function chunk(type, data) {
2654
+ const len = Buffer.alloc(4);
2655
+ len.writeUInt32BE(data.length, 0);
2656
+ const typeAndData = Buffer.concat([Buffer.from(type, "ascii"), data]);
2657
+ const crc = Buffer.alloc(4);
2658
+ crc.writeUInt32BE(crc32(typeAndData), 0);
2659
+ return Buffer.concat([len, typeAndData, crc]);
2660
+ }
2661
+ function encodePng(img) {
2662
+ const { width, height, pixels } = img;
2663
+ if (pixels.length !== width * height) {
2664
+ throw new Error(`pixel buffer ${pixels.length} != ${width}x${height}`);
2665
+ }
2666
+ const ihdr = Buffer.alloc(13);
2667
+ ihdr.writeUInt32BE(width, 0);
2668
+ ihdr.writeUInt32BE(height, 4);
2669
+ ihdr[8] = 8;
2670
+ ihdr[9] = 0;
2671
+ ihdr[10] = 0;
2672
+ ihdr[11] = 0;
2673
+ ihdr[12] = 0;
2674
+ const raw = Buffer.alloc(height * (width + 1));
2675
+ for (let y = 0; y < height; y++) {
2676
+ raw[y * (width + 1)] = 0;
2677
+ pixels.copy(raw, y * (width + 1) + 1, y * width, (y + 1) * width);
2678
+ }
2679
+ const idat = zlib.deflateSync(raw, { level: 9 });
2680
+ return Buffer.concat([
2681
+ PNG_SIGNATURE,
2682
+ chunk("IHDR", ihdr),
2683
+ chunk("IDAT", idat),
2684
+ chunk("IEND", Buffer.alloc(0))
2685
+ ]);
2686
+ }
2687
+ function paeth(a, b, c) {
2688
+ const p = a + b - c;
2689
+ const pa = Math.abs(p - a);
2690
+ const pb = Math.abs(p - b);
2691
+ const pc = Math.abs(p - c);
2692
+ if (pa <= pb && pa <= pc)
2693
+ return a;
2694
+ if (pb <= pc)
2695
+ return b;
2696
+ return c;
2697
+ }
2698
+ function decodePng(buf) {
2699
+ if (buf.length < 8 || !buf.subarray(0, 8).equals(PNG_SIGNATURE)) {
2700
+ throw new Error("not a PNG (bad signature)");
2701
+ }
2702
+ let width = 0;
2703
+ let height = 0;
2704
+ const idatParts = [];
2705
+ let off = 8;
2706
+ while (off + 8 <= buf.length) {
2707
+ const len = buf.readUInt32BE(off);
2708
+ const type = buf.toString("ascii", off + 4, off + 8);
2709
+ const data = buf.subarray(off + 8, off + 8 + len);
2710
+ if (type === "IHDR") {
2711
+ width = data.readUInt32BE(0);
2712
+ height = data.readUInt32BE(4);
2713
+ const bitDepth = data[8];
2714
+ const colorType = data[9];
2715
+ const interlace = data[12];
2716
+ if (bitDepth !== 8 || colorType !== 0 || interlace !== 0) {
2717
+ throw new Error(`unsupported PNG (bitDepth=${bitDepth} colorType=${colorType} interlace=${interlace})`);
2718
+ }
2719
+ } else if (type === "IDAT") {
2720
+ idatParts.push(data);
2721
+ } else if (type === "IEND") {
2722
+ break;
2723
+ }
2724
+ off += 8 + len + 4;
2725
+ }
2726
+ if (!width || !height)
2727
+ throw new Error("PNG missing IHDR");
2728
+ if (idatParts.length === 0)
2729
+ throw new Error("PNG missing IDAT");
2730
+ const raw = zlib.inflateSync(Buffer.concat(idatParts));
2731
+ const stride = width + 1;
2732
+ if (raw.length < height * stride) {
2733
+ throw new Error(`PNG data short: ${raw.length} < ${height * stride}`);
2734
+ }
2735
+ const pixels = Buffer.alloc(width * height);
2736
+ for (let y = 0; y < height; y++) {
2737
+ const filter = raw[y * stride];
2738
+ const rowIn = raw.subarray(y * stride + 1, y * stride + 1 + width);
2739
+ const rowOut = pixels.subarray(y * width, (y + 1) * width);
2740
+ for (let x = 0; x < width; x++) {
2741
+ const a = x > 0 ? rowOut[x - 1] : 0;
2742
+ const b = y > 0 ? pixels[(y - 1) * width + x] : 0;
2743
+ const c = x > 0 && y > 0 ? pixels[(y - 1) * width + x - 1] : 0;
2744
+ let v;
2745
+ switch (filter) {
2746
+ case 0:
2747
+ v = rowIn[x];
2748
+ break;
2749
+ case 1:
2750
+ v = rowIn[x] + a;
2751
+ break;
2752
+ case 2:
2753
+ v = rowIn[x] + b;
2754
+ break;
2755
+ case 3:
2756
+ v = rowIn[x] + (a + b >> 1);
2757
+ break;
2758
+ case 4:
2759
+ v = rowIn[x] + paeth(a, b, c);
2760
+ break;
2761
+ default:
2762
+ throw new Error(`unsupported PNG filter ${filter} on row ${y}`);
2763
+ }
2764
+ rowOut[x] = v & 255;
2765
+ }
2766
+ }
2767
+ return { width, height, pixels };
2768
+ }
2769
+
2770
+ // src/archive/page-renderer.ts
2771
+ var PAGE_SIZE = 1568;
2772
+ var CELL_W = GLYPH_W;
2773
+ var CELL_H = 10;
2774
+ var COLS = Math.floor(PAGE_SIZE / CELL_W);
2775
+ var ROWS = Math.floor(PAGE_SIZE / CELL_H);
2776
+ var CHARS_PER_PAGE = COLS * ROWS;
2777
+ var BG = 255;
2778
+ var FG = 0;
2779
+ function sanitizeForGrid(text) {
2780
+ const out = [];
2781
+ for (const ch of text.replace(/\r\n/g, "\n").replace(/\r/g, "\n")) {
2782
+ const code = ch.codePointAt(0);
2783
+ if (ch === "\n") {
2784
+ out.push("\n");
2785
+ } else if (ch === " ") {
2786
+ out.push("\\t");
2787
+ } else if (code >= 32 && code <= 126) {
2788
+ out.push(ch);
2789
+ } else if (code <= 65535) {
2790
+ out.push("\\u" + code.toString(16).padStart(4, "0"));
2791
+ } else {
2792
+ out.push("\\u{" + code.toString(16) + "}");
2793
+ }
2794
+ }
2795
+ return out.join("");
2796
+ }
2797
+ function layoutRows(sanitized, cols = COLS) {
2798
+ const rows = [];
2799
+ let offset = 0;
2800
+ for (const line of sanitized.split("\n")) {
2801
+ if (line.length === 0) {
2802
+ rows.push({ text: " ".repeat(cols), charStart: offset });
2803
+ } else {
2804
+ for (let i = 0; i < line.length; i += cols) {
2805
+ const slice = line.slice(i, i + cols);
2806
+ rows.push({ text: slice.padEnd(cols, " "), charStart: offset + i });
2807
+ }
2808
+ }
2809
+ offset += line.length + 1;
2810
+ }
2811
+ return rows;
2812
+ }
2813
+ function renderPage(pageRows, cols = COLS, rows = ROWS) {
2814
+ const rowCount = Math.max(1, Math.min(pageRows.length, rows));
2815
+ const width = cols * CELL_W;
2816
+ const height = rowCount * CELL_H;
2817
+ const pixels = Buffer.alloc(width * height, BG);
2818
+ for (let r = 0; r < Math.min(pageRows.length, rowCount); r++) {
2819
+ const row = pageRows[r];
2820
+ for (let c = 0; c < cols; c++) {
2821
+ const glyph = glyphRows(row.text.charCodeAt(c));
2822
+ if (!glyph)
2823
+ continue;
2824
+ const px = c * CELL_W;
2825
+ const py = r * CELL_H;
2826
+ for (let gy = 0; gy < GLYPH_H; gy++) {
2827
+ const bits = glyph[gy];
2828
+ if (bits === 0)
2829
+ continue;
2830
+ const base = (py + gy) * width + px;
2831
+ for (let gx = 0; gx < GLYPH_W; gx++) {
2832
+ if (bits & 1 << gx)
2833
+ pixels[base + gx] = FG;
2834
+ }
2835
+ }
2836
+ }
2837
+ }
2838
+ const img = { width, height, pixels };
2839
+ const charStart = pageRows.length ? pageRows[0].charStart : 0;
2840
+ const last = pageRows[pageRows.length - 1];
2841
+ const charEnd = last ? last.charStart + last.text.trimEnd().length : charStart;
2842
+ return {
2843
+ png: encodePng(img),
2844
+ gridText: pageRows.map((r) => r.text).join("\n"),
2845
+ charStart,
2846
+ charEnd
2847
+ };
2848
+ }
2849
+ function paginate(rowsIn, rowsPerPage = ROWS) {
2850
+ const pages = [];
2851
+ for (let i = 0; i < rowsIn.length; i += rowsPerPage) {
2852
+ pages.push(rowsIn.slice(i, i + rowsPerPage));
2853
+ }
2854
+ return pages;
2855
+ }
2856
+ function decodePageText(png, cols = COLS) {
2857
+ const img = decodePng(png);
2858
+ const rows = Math.floor(img.height / CELL_H);
2859
+ const usableCols = Math.min(cols, Math.floor(img.width / CELL_W));
2860
+ const lines = [];
2861
+ for (let r = 0; r < rows; r++) {
2862
+ const chars = [];
2863
+ for (let c = 0; c < usableCols; c++) {
2864
+ const bytes = [];
2865
+ for (let gy = 0; gy < GLYPH_H; gy++) {
2866
+ let b = 0;
2867
+ const base = (r * CELL_H + gy) * img.width + c * CELL_W;
2868
+ for (let gx = 0; gx < GLYPH_W; gx++) {
2869
+ if (img.pixels[base + gx] < 128)
2870
+ b |= 1 << gx;
2871
+ }
2872
+ bytes.push(b);
2873
+ }
2874
+ const code = codeForSignature(signatureForRows(bytes));
2875
+ chars.push(code === null ? "\uFFFD" : String.fromCharCode(code));
2876
+ }
2877
+ lines.push(chars.join(""));
2878
+ }
2879
+ return lines.join("\n");
2880
+ }
2881
+ function verifyRoundTrip(page) {
2882
+ const decoded = decodePageText(page.png);
2883
+ if (decoded === page.gridText)
2884
+ return null;
2885
+ const a = page.gridText;
2886
+ const b = decoded;
2887
+ const n = Math.min(a.length, b.length);
2888
+ for (let i = 0; i < n; i++) {
2889
+ if (a[i] !== b[i]) {
2890
+ return `mismatch at char ${i}: rendered ${JSON.stringify(a[i])} decoded ${JSON.stringify(b[i])}`;
2891
+ }
2892
+ }
2893
+ return `length mismatch: rendered ${a.length} decoded ${b.length}`;
2894
+ }
2895
+
2896
+ // src/opencode-plugin-png.ts
2897
+ var CCR_MARKER_HASH_LEN2 = 16;
2898
+ function ccrPngMarker(hash) {
2899
+ return `<<ccr:${hash.slice(0, CCR_MARKER_HASH_LEN2)}>>`;
2900
+ }
2901
+ var CCR_MARKER_RE2 = /<<ccr:/;
2902
+ var DEFAULT_THRESHOLD_CHARS = 4e5;
2903
+ var DEFAULT_MIN_PART_CHARS = 4e3;
2904
+ var DEFAULT_LIVE_TAIL_MESSAGES = 1;
2905
+ var DEFAULT_VISION_MODEL_SUBSTRINGS = [
2906
+ "gpt-4o",
2907
+ "gpt-4.1",
2908
+ "gpt-4-turbo",
2909
+ "gpt-5",
2910
+ "o1",
2911
+ "o3",
2912
+ "o4",
2913
+ "claude-3",
2914
+ "claude-4",
2915
+ "claude-opus",
2916
+ "claude-sonnet",
2917
+ "claude-haiku",
2918
+ "claude-fable",
2919
+ "gemini",
2920
+ "llama-3.2-vision",
2921
+ "llama-4",
2922
+ "qwen2.5-vl",
2923
+ "qwen2-vl",
2924
+ "qwen3-vl",
2925
+ "pixtral",
2926
+ "mistral-small-3",
2927
+ "grok-vision",
2928
+ "grok-4",
2929
+ "internvl",
2930
+ "-vl",
2931
+ "vision"
2932
+ ];
2933
+ function inputModalities(modalities) {
2934
+ if (!modalities)
2935
+ return null;
2936
+ if (Array.isArray(modalities))
2937
+ return modalities.map((m) => String(m));
2938
+ if (typeof modalities === "object" && Array.isArray(modalities.input)) {
2939
+ return modalities.input.map((m) => String(m));
2940
+ }
2941
+ return null;
2942
+ }
2943
+ function isVisionCapable(opts) {
2944
+ const mods = inputModalities(opts.modalities);
2945
+ if (mods) {
2946
+ return mods.some((m) => m.toLowerCase() === "image");
2947
+ }
2948
+ const id = (opts.modelId || "").toLowerCase();
2949
+ if (!id)
2950
+ return false;
2951
+ const allow = [...DEFAULT_VISION_MODEL_SUBSTRINGS, ...opts.visionAllowlist || []];
2952
+ return allow.some((sub) => sub && id.includes(String(sub).toLowerCase()));
2953
+ }
2954
+ function renderTextToImageParts(text, hash) {
2955
+ const sanitized = sanitizeForGrid(text);
2956
+ const pages = paginate(layoutRows(sanitized));
2957
+ const parts = [];
2958
+ let pngBytes = 0;
2959
+ let selfTestError = null;
2960
+ for (let i = 0; i < pages.length; i++) {
2961
+ const rendered = renderPage(pages[i]);
2962
+ const err = verifyRoundTrip(rendered);
2963
+ if (err && !selfTestError)
2964
+ selfTestError = `page ${i + 1}: ${err}`;
2965
+ pngBytes += rendered.png.length;
2966
+ parts.push({
2967
+ type: "file",
2968
+ mime: "image/png",
2969
+ url: `data:image/png;base64,${rendered.png.toString("base64")}`,
2970
+ filename: `greprag-png-${hash.slice(0, CCR_MARKER_HASH_LEN2)}-p${String(i + 1).padStart(3, "0")}.png`
2971
+ });
2972
+ }
2973
+ return { parts, pages: pages.length, pngBytes, selfTestError };
2974
+ }
2975
+ function partsOf(msg) {
2976
+ if (!msg || typeof msg !== "object")
2977
+ return null;
2978
+ const parts = msg.parts;
2979
+ return Array.isArray(parts) ? parts : null;
2980
+ }
2981
+ function partCharLen(p) {
2982
+ if (!p || typeof p !== "object")
2983
+ return 0;
2984
+ const part = p;
2985
+ if (part.type === "text" && typeof part.text === "string")
2986
+ return part.text.length;
2987
+ if (part.type === "file" && typeof part.url === "string")
2988
+ return part.url.length;
2989
+ if (part.type === "tool" && part.state && typeof part.state === "object") {
2990
+ const out = part.state.output;
2991
+ if (typeof out === "string")
2992
+ return out.length;
2993
+ }
2994
+ return 0;
2995
+ }
2996
+ function maybeRecodePart(p, minPartChars, stash) {
2997
+ if (!p || typeof p !== "object")
2998
+ return null;
2999
+ const part = p;
3000
+ if (part.type !== "text" || typeof part.text !== "string")
3001
+ return null;
3002
+ const text = part.text;
3003
+ if (CCR_MARKER_RE2.test(text))
3004
+ return null;
3005
+ if (text.length < minPartChars)
3006
+ return null;
3007
+ const hash = stash(text);
3008
+ const { parts: fileParts, pages, pngBytes, selfTestError } = renderTextToImageParts(text, hash);
3009
+ const breadcrumb = {
3010
+ type: "text",
3011
+ text: `[greprag-png-recode: ${pages} page(s) of prior history rendered as pixel-text image(s) above; verbatim original ${ccrPngMarker(hash)} (greprag retrieve ${hash.slice(0, CCR_MARKER_HASH_LEN2)})]`
3012
+ };
3013
+ return { fileParts, breadcrumb, pages, pngBytes, selfTestError };
3014
+ }
3015
+ function recodeMessagesToPng(messages, opts) {
3016
+ const thresholdChars = opts.thresholdChars ?? DEFAULT_THRESHOLD_CHARS;
3017
+ const minPartChars = opts.minPartChars ?? DEFAULT_MIN_PART_CHARS;
3018
+ const liveTail = opts.liveTailMessages ?? DEFAULT_LIVE_TAIL_MESSAGES;
3019
+ const stats = {
3020
+ fired: false,
3021
+ visionCapable: false,
3022
+ totalChars: 0,
3023
+ thresholdChars,
3024
+ partsRecoded: 0,
3025
+ pagesRendered: 0,
3026
+ pngBytes: 0,
3027
+ selfTestError: null,
3028
+ reason: ""
3029
+ };
3030
+ if (!Array.isArray(messages)) {
3031
+ stats.reason = "messages not an array";
3032
+ return stats;
3033
+ }
3034
+ for (const msg of messages) {
3035
+ const parts = partsOf(msg);
3036
+ if (!parts)
3037
+ continue;
3038
+ for (const p of parts)
3039
+ stats.totalChars += partCharLen(p);
3040
+ }
3041
+ const vision = isVisionCapable(opts);
3042
+ stats.visionCapable = vision;
3043
+ if (!vision) {
3044
+ stats.reason = `model not vision-capable (modelId=${opts.modelId || "?"}) \u2014 pass-through`;
3045
+ return stats;
3046
+ }
3047
+ if (stats.totalChars < thresholdChars) {
3048
+ stats.reason = `below threshold (${stats.totalChars} < ${thresholdChars} chars)`;
3049
+ return stats;
3050
+ }
3051
+ const lastRecodableIdx = Math.max(0, messages.length - Math.max(0, liveTail));
3052
+ for (let mi = 0; mi < lastRecodableIdx; mi++) {
3053
+ const msg = messages[mi];
3054
+ const parts = partsOf(msg);
3055
+ if (!parts)
3056
+ continue;
3057
+ let changed = false;
3058
+ const newParts = [];
3059
+ for (const p of parts) {
3060
+ let rec = null;
3061
+ try {
3062
+ rec = maybeRecodePart(p, minPartChars, opts.stash);
3063
+ } catch {
3064
+ rec = null;
3065
+ }
3066
+ if (rec) {
3067
+ newParts.push(...rec.fileParts, rec.breadcrumb);
3068
+ changed = true;
3069
+ stats.partsRecoded++;
3070
+ stats.pagesRendered += rec.pages;
3071
+ stats.pngBytes += rec.pngBytes;
3072
+ if (rec.selfTestError && !stats.selfTestError)
3073
+ stats.selfTestError = rec.selfTestError;
3074
+ } else {
3075
+ newParts.push(p);
3076
+ }
3077
+ }
3078
+ if (changed)
3079
+ msg.parts = newParts;
3080
+ }
3081
+ stats.fired = stats.partsRecoded > 0;
3082
+ stats.reason = stats.fired ? `recoded ${stats.partsRecoded} part(s) -> ${stats.pagesRendered} page(s), ${stats.pngBytes} png bytes` : "threshold crossed but no recodable text parts (all below per-part floor or already marked)";
3083
+ return stats;
3084
+ }
3085
+
2504
3086
  // src/opencode-plugin.ts
2505
3087
  var DEBUG_LOG_PATH = path3.join(os.homedir(), ".greprag", "opencode-plugin-debug.log");
2506
3088
  var _debugLogReady = false;
@@ -2749,11 +3331,11 @@ function startCaptureWatcher() {
2749
3331
  fs3.writeFileSync(WATCHER_LOCK, String(child.pid));
2750
3332
  } catch {
2751
3333
  }
2752
- child.stderr?.on("data", (chunk) => {
2753
- process.stderr.write(`[greprag-watch] ${chunk.toString("utf-8")}`);
3334
+ child.stderr?.on("data", (chunk2) => {
3335
+ process.stderr.write(`[greprag-watch] ${chunk2.toString("utf-8")}`);
2754
3336
  });
2755
- child.stdout?.on("data", (chunk) => {
2756
- process.stderr.write(`[greprag-watch] ${chunk.toString("utf-8")}`);
3337
+ child.stdout?.on("data", (chunk2) => {
3338
+ process.stderr.write(`[greprag-watch] ${chunk2.toString("utf-8")}`);
2757
3339
  });
2758
3340
  child.on("exit", (code, signal) => {
2759
3341
  dlog(`startCaptureWatcher: child pid=${child.pid ?? "<none>"} exited code=${code} signal=${signal || "none"}`);
@@ -2958,11 +3540,11 @@ function startSessionRelay(sessionId, serverUrl) {
2958
3540
  fs3.writeFileSync(lockPath, String(child.pid));
2959
3541
  } catch {
2960
3542
  }
2961
- child.stderr?.on("data", (chunk) => {
2962
- process.stderr.write(`[greprag-relay] ${chunk.toString("utf-8")}`);
3543
+ child.stderr?.on("data", (chunk2) => {
3544
+ process.stderr.write(`[greprag-relay] ${chunk2.toString("utf-8")}`);
2963
3545
  });
2964
- child.stdout?.on("data", (chunk) => {
2965
- process.stderr.write(`[greprag-relay] ${chunk.toString("utf-8")}`);
3546
+ child.stdout?.on("data", (chunk2) => {
3547
+ process.stderr.write(`[greprag-relay] ${chunk2.toString("utf-8")}`);
2966
3548
  });
2967
3549
  child.on("exit", (code, signal) => {
2968
3550
  try {
@@ -3052,6 +3634,39 @@ function inlineCrushEnabled() {
3052
3634
  const v = process.env.GREPRAG_OPENCODE_CRUSH;
3053
3635
  return v === "true" || v === "1";
3054
3636
  }
3637
+ function pngRecodeEnabled() {
3638
+ const v = process.env.GREPRAG_OPENCODE_PNG;
3639
+ return v === "true" || v === "1";
3640
+ }
3641
+ function intEnv(name, allowZero = false) {
3642
+ const n = parseInt(process.env[name] || "", 10);
3643
+ if (!Number.isFinite(n))
3644
+ return void 0;
3645
+ return n > 0 || allowZero && n === 0 ? n : void 0;
3646
+ }
3647
+ function runPngRecode(input, msgs) {
3648
+ const model = input?.model;
3649
+ const modelId = model?.modelID || model?.id || model?.name || null;
3650
+ const modalities = model?.modalities || model?.info?.modalities || null;
3651
+ const allowEnv = (process.env.GREPRAG_OPENCODE_PNG_VISION_MODELS || "").split(",").map((s) => s.trim()).filter(Boolean);
3652
+ const stash = makeCcrStash();
3653
+ const stats = recodeMessagesToPng(msgs, {
3654
+ stash: (original) => stash(original, "log"),
3655
+ modelId,
3656
+ modalities,
3657
+ thresholdChars: intEnv("GREPRAG_OPENCODE_PNG_THRESHOLD"),
3658
+ minPartChars: intEnv("GREPRAG_OPENCODE_PNG_MIN_PART"),
3659
+ liveTailMessages: intEnv("GREPRAG_OPENCODE_PNG_TAIL", true),
3660
+ visionAllowlist: allowEnv.length ? allowEnv : void 0
3661
+ });
3662
+ if (stats.partsRecoded > 0) {
3663
+ dlog(
3664
+ `messages.transform png: recoded=${stats.partsRecoded} pages=${stats.pagesRendered} pngBytes=${stats.pngBytes} totalChars=${stats.totalChars} vision=${stats.visionCapable}` + (stats.selfTestError ? ` selfTestError=${stats.selfTestError}` : "")
3665
+ );
3666
+ } else {
3667
+ dlog(`messages.transform png: no-op (${stats.reason})`);
3668
+ }
3669
+ }
3055
3670
  function makeCcrStash() {
3056
3671
  return (original, sourceType) => {
3057
3672
  const hash = crypto2.createHash("sha256").update(original, "utf8").digest("hex").slice(0, 16);
@@ -3190,7 +3805,16 @@ ALWAYS pass \`--from-session ${sid82}\` when you run \`greprag send\`, so recipi
3190
3805
  // Contract: we mutate ONLY this request-scoped `output.messages` copy,
3191
3806
  // never opencode's persisted session history. adr:
3192
3807
  // adr/opencode-context-compressor.md
3193
- "experimental.chat.messages.transform": async (_input, output) => {
3808
+ "experimental.chat.messages.transform": async (input, output) => {
3809
+ try {
3810
+ if (pngRecodeEnabled()) {
3811
+ const msgs0 = output && output.messages;
3812
+ if (Array.isArray(msgs0))
3813
+ runPngRecode(input, msgs0);
3814
+ }
3815
+ } catch (e) {
3816
+ dlog(`messages.transform png: swallowed error (turn unaffected): ${e.message}`);
3817
+ }
3194
3818
  try {
3195
3819
  if (!inlineCrushEnabled())
3196
3820
  return;