cdp-toolkit 1.9.1 → 1.9.5

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/mcp.js CHANGED
@@ -1121,6 +1121,8 @@ async function clearMocks(args = {}) {
1121
1121
  }
1122
1122
 
1123
1123
  // src/driver.ts
1124
+ import { mkdir as mkdir3, readFile as readFile3, unlink as unlink3, writeFile as writeFile3 } from "node:fs/promises";
1125
+ import { join as join3 } from "node:path";
1124
1126
  var UID_STAMP_ATTR = "data-cdp-uid";
1125
1127
  var LEGACY_NUMERIC_UID = /^[0-9]+$/;
1126
1128
  var REQUIRED_CAPABILITIES = {
@@ -1150,6 +1152,41 @@ function interpolatePoints(from, to, steps) {
1150
1152
  out.push({ x: to.x, y: to.y });
1151
1153
  return out;
1152
1154
  }
1155
+ function isTiledCapture(delivery) {
1156
+ return delivery.tiled === true;
1157
+ }
1158
+ function viewportRecordFile(scheme, targetId) {
1159
+ return join3(leaseDir(), `viewport-${scheme}-${targetId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
1160
+ }
1161
+ async function readViewportRecord(scheme, targetId) {
1162
+ try {
1163
+ const raw = JSON.parse(await readFile3(viewportRecordFile(scheme, targetId), "utf8"));
1164
+ if (typeof raw?.width !== "number" || typeof raw?.height !== "number")
1165
+ return;
1166
+ return {
1167
+ width: raw.width,
1168
+ height: raw.height,
1169
+ ...typeof raw.deviceScaleFactor === "number" ? { deviceScaleFactor: raw.deviceScaleFactor } : {},
1170
+ ...typeof raw.mobile === "boolean" ? { mobile: raw.mobile } : {}
1171
+ };
1172
+ } catch {
1173
+ return;
1174
+ }
1175
+ }
1176
+ async function writeViewportRecord(scheme, targetId, rec) {
1177
+ try {
1178
+ await mkdir3(leaseDir(), { recursive: true });
1179
+ await writeFile3(viewportRecordFile(scheme, targetId), JSON.stringify({ scheme, targetId, ...rec }), "utf8");
1180
+ } catch {}
1181
+ }
1182
+ function renderRestoreFailureNote(size, why) {
1183
+ return `— AND the ${size.width}x${size.height} render-size override could NOT be restored (${why}), so this page is STILL ` + `emulated at ${size.width}x${size.height}: put it back with emulate{width,height} or emulate{clearOverrides:true}.`;
1184
+ }
1185
+ async function clearViewportRecord(scheme, targetId) {
1186
+ try {
1187
+ await unlink3(viewportRecordFile(scheme, targetId));
1188
+ } catch {}
1189
+ }
1153
1190
 
1154
1191
  // src/cdp/driver.ts
1155
1192
  function driverError(code, message, data) {
@@ -1381,14 +1418,73 @@ async function backendNodeIdOf(conn, loc) {
1381
1418
  }
1382
1419
  return searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
1383
1420
  }
1384
- async function fullPageClip(conn) {
1385
- const m = await conn.send("Page.getLayoutMetrics");
1421
+ async function layoutMetrics(conn) {
1422
+ return conn.send("Page.getLayoutMetrics");
1423
+ }
1424
+ function contentClip(m) {
1386
1425
  const size = m.cssContentSize ?? m.contentSize;
1387
1426
  if (!size)
1388
1427
  throw driverError("page-error", "Page.getLayoutMetrics returned no content size");
1389
1428
  return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height) };
1390
1429
  }
1391
- async function elementClip(conn, loc) {
1430
+ function devicePixelsPerCssPx(m) {
1431
+ const css = m.cssVisualViewport?.clientWidth ?? m.cssLayoutViewport?.clientWidth;
1432
+ const dev = m.visualViewport?.clientWidth ?? m.layoutViewport?.clientWidth;
1433
+ if (!css || !dev)
1434
+ return 1;
1435
+ const ratio = dev / css;
1436
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : 1;
1437
+ }
1438
+ function scrollOffsetCssPx(m, devicePx) {
1439
+ const css = m.cssVisualViewport ?? m.cssLayoutViewport;
1440
+ if (css)
1441
+ return { x: css.pageX ?? 0, y: css.pageY ?? 0 };
1442
+ const legacy = m.layoutViewport;
1443
+ if (legacy)
1444
+ return { x: (legacy.pageX ?? 0) / devicePx, y: (legacy.pageY ?? 0) / devicePx };
1445
+ return { x: 0, y: 0 };
1446
+ }
1447
+ var MAX_ENCODED_SIDE_PX = 16384;
1448
+ function assertClipEncodable(clip, scale, devicePx, renderSize) {
1449
+ const outWidth = Math.ceil(clip.width * scale * devicePx);
1450
+ const outHeight = Math.ceil(clip.height * scale * devicePx);
1451
+ if (outWidth <= MAX_ENCODED_SIDE_PX && outHeight <= MAX_ENCODED_SIDE_PX)
1452
+ return;
1453
+ const at = renderSize ? ` at render size ${renderSize.width}x${renderSize.height}` : "";
1454
+ const projection = `(clip ${clip.width}x${clip.height} css px${at} x scale ${scale} x device pixel ratio ${devicePx}), ` + `past Chrome's ${MAX_ENCODED_SIDE_PX} px per-side limit; `;
1455
+ if (scale !== 1) {
1456
+ const fits = Math.floor(MAX_ENCODED_SIDE_PX / (Math.max(clip.width, clip.height) * devicePx) * 100) / 100;
1457
+ throw driverError("page-error", `screenshot scale ${scale} would encode ${outWidth}x${outHeight} px ${projection}` + (fits >= 0.01 ? `the largest scale that fits this clip is ${fits}.` : `this clip is already past the limit at scale 1 — capture a smaller region.`));
1458
+ }
1459
+ const maxCssSide = Math.floor(MAX_ENCODED_SIDE_PX / devicePx);
1460
+ throw driverError("page-error", `this capture would encode ${outWidth}x${outHeight} px ${projection}` + `at device pixel ratio ${devicePx} the clip may be at most ${maxCssSide} css px per side` + (renderSize ? ` — a narrow renderWidth reflows the document TALLER, so widen it, shorten the page, or drop fullPage.` : ` — capture a smaller region or drop fullPage.`));
1461
+ }
1462
+ function projectedSide(cssLength, scale, devicePx) {
1463
+ return Math.ceil(cssLength * scale * devicePx);
1464
+ }
1465
+ function planTiledCapture(clip, scale, devicePx, tile) {
1466
+ const untiled = { tiled: false, bandCount: 1, bandHeightCss: clip.height };
1467
+ if (tile === false)
1468
+ return untiled;
1469
+ const fits = projectedSide(clip.width, scale, devicePx) <= MAX_ENCODED_SIDE_PX && projectedSide(clip.height, scale, devicePx) <= MAX_ENCODED_SIDE_PX;
1470
+ if (fits && tile !== true)
1471
+ return untiled;
1472
+ const outWidth = projectedSide(clip.width, scale, devicePx);
1473
+ if (outWidth > MAX_ENCODED_SIDE_PX) {
1474
+ const maxCssWidth = Math.floor(MAX_ENCODED_SIDE_PX / (scale * devicePx));
1475
+ throw driverError("page-error", `this capture is ${outWidth} px WIDE (clip ${clip.width} css px x scale ${scale} x device pixel ratio ${devicePx}), ` + `past Chrome's ${MAX_ENCODED_SIDE_PX} px per-side limit, and width cannot be tiled: bands are stitched ` + `vertically only. Lower scale (at most ${Math.floor(MAX_ENCODED_SIDE_PX / (clip.width * devicePx) * 100) / 100}), ` + `or set renderWidth at most ${maxCssWidth} css px, or clip to a narrower element.`);
1476
+ }
1477
+ let bandHeightCss = Math.floor(MAX_ENCODED_SIDE_PX / (scale * devicePx));
1478
+ while (bandHeightCss > 1 && projectedSide(bandHeightCss, scale, devicePx) > MAX_ENCODED_SIDE_PX)
1479
+ bandHeightCss -= 1;
1480
+ if (bandHeightCss < 1) {
1481
+ throw driverError("page-error", `scale ${scale} at device pixel ratio ${devicePx} projects a single css pixel to ${scale * devicePx} device px, ` + `past the ${MAX_ENCODED_SIDE_PX} px limit — no band height can fit. Lower scale.`);
1482
+ }
1483
+ if (tile === true && clip.height <= bandHeightCss)
1484
+ bandHeightCss = Math.max(1, Math.ceil(clip.height / 2));
1485
+ return { tiled: true, bandCount: Math.max(1, Math.ceil(clip.height / bandHeightCss)), bandHeightCss };
1486
+ }
1487
+ async function elementBox(conn, loc) {
1392
1488
  const backendNodeId = await backendNodeIdOf(conn, loc);
1393
1489
  await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => {
1394
1490
  return;
@@ -1405,6 +1501,10 @@ async function elementClip(conn, loc) {
1405
1501
  throw driverError("page-error", "resolved element has zero area");
1406
1502
  return { x: minX, y: minY, width, height };
1407
1503
  }
1504
+ function documentClip(box, m, devicePx) {
1505
+ const scroll = scrollOffsetCssPx(m, devicePx);
1506
+ return { x: box.x + scroll.x, y: box.y + scroll.y, width: box.width, height: box.height };
1507
+ }
1408
1508
  var CDP_CAPABILITIES = new Set([
1409
1509
  "trace.performance",
1410
1510
  "heap.snapshot",
@@ -1415,6 +1515,9 @@ var CDP_CAPABILITIES = new Set([
1415
1515
  "emulate.networkConditions",
1416
1516
  "screenshot.fullPage",
1417
1517
  "screenshot.element",
1518
+ "screenshot.scale",
1519
+ "screenshot.renderSize",
1520
+ "screenshot.tile",
1418
1521
  "network.intercept",
1419
1522
  "snapshot.accessibilityTree",
1420
1523
  "input.insertTextAtomic",
@@ -1705,31 +1808,128 @@ class CdpPageDriver {
1705
1808
  const { objectId } = await resolveElementLocator(this.conn, loc);
1706
1809
  await this.conn.send("DOM.setFileInputFiles", { files: [...files], objectId });
1707
1810
  }
1708
- async screenshot(opts) {
1811
+ async screenshot(opts, consumeBands) {
1812
+ const renderSize = opts?.renderWidth !== undefined && opts?.renderHeight !== undefined ? { width: opts.renderWidth, height: opts.renderHeight } : undefined;
1813
+ if (!renderSize)
1814
+ return this.capture(opts, consumeBands);
1815
+ const previous = await readViewportRecord(this.browser.scheme, this.info.id);
1816
+ await this.conn.send("Emulation.setDeviceMetricsOverride", {
1817
+ width: renderSize.width,
1818
+ height: renderSize.height,
1819
+ deviceScaleFactor: previous?.deviceScaleFactor ?? 0,
1820
+ mobile: previous?.mobile ?? false
1821
+ });
1822
+ let shot;
1823
+ let captureError;
1824
+ try {
1825
+ shot = await this.capture(opts, consumeBands);
1826
+ } catch (e) {
1827
+ captureError = e;
1828
+ }
1829
+ let restoreError;
1830
+ try {
1831
+ if (previous) {
1832
+ await this.conn.send("Emulation.setDeviceMetricsOverride", {
1833
+ width: previous.width,
1834
+ height: previous.height,
1835
+ deviceScaleFactor: previous.deviceScaleFactor ?? 0,
1836
+ mobile: previous.mobile ?? false
1837
+ });
1838
+ } else {
1839
+ await this.conn.send("Emulation.clearDeviceMetricsOverride");
1840
+ }
1841
+ } catch (e) {
1842
+ restoreError = e instanceof Error ? e.message : String(e);
1843
+ }
1844
+ if (!shot) {
1845
+ if (restoreError !== undefined && captureError instanceof Error) {
1846
+ captureError.message = `${captureError.message} ${renderRestoreFailureNote(renderSize, restoreError)}`;
1847
+ }
1848
+ throw captureError;
1849
+ }
1850
+ const result = Object.assign(shot, { renderSize, renderRestored: restoreError === undefined });
1851
+ if (restoreError !== undefined)
1852
+ result.renderRestoreError = restoreError;
1853
+ return result;
1854
+ }
1855
+ async capture(opts, consumeBands) {
1709
1856
  await this.ensureDomain("Page");
1710
1857
  const format = opts?.format ?? "png";
1711
1858
  const quality = format === "jpeg" ? opts?.quality ?? 80 : undefined;
1859
+ const scale = opts?.scale ?? 1;
1860
+ const renderSize = opts?.renderWidth !== undefined && opts?.renderHeight !== undefined ? { width: opts.renderWidth, height: opts.renderHeight } : undefined;
1861
+ const box = opts?.clip ? await elementBox(this.conn, opts.clip) : undefined;
1862
+ const metrics = await layoutMetrics(this.conn);
1863
+ const devicePx = devicePixelsPerCssPx(metrics);
1864
+ const clip = box ? documentClip(box, metrics, devicePx) : contentClip(metrics);
1865
+ const sendsClip = opts?.clip !== undefined || opts?.fullPage === true || scale !== 1 || renderSize !== undefined;
1866
+ if (opts?.tile === true && !consumeBands) {
1867
+ throw driverError("unsupported", "screenshot tile:true needs a caller that can consume bands; this one passed no band consumer");
1868
+ }
1869
+ const plan = planTiledCapture(clip, scale, devicePx, consumeBands ? opts?.tile : false);
1870
+ if (plan.tiled)
1871
+ return this.captureTiled(clip, plan, scale, format, consumeBands);
1872
+ assertClipEncodable(clip, scale, devicePx, renderSize);
1712
1873
  const params = { format, captureBeyondViewport: true };
1713
1874
  if (quality != null)
1714
1875
  params.quality = quality;
1715
- if (opts?.clip)
1716
- params.clip = { ...await elementClip(this.conn, opts.clip), scale: 1 };
1717
- else if (opts?.fullPage)
1718
- params.clip = { ...await fullPageClip(this.conn), scale: 1 };
1876
+ if (sendsClip)
1877
+ params.clip = { ...clip, scale };
1719
1878
  const { data } = await this.conn.send("Page.captureScreenshot", params);
1720
1879
  return { data: new Uint8Array(Buffer.from(data, "base64")), format };
1721
1880
  }
1881
+ async captureTiled(clip, plan, scale, format, consumeBands) {
1882
+ const capture = {
1883
+ bands: this.bands(clip, plan, scale, format),
1884
+ bandCount: plan.bandCount,
1885
+ bandHeightCss: plan.bandHeightCss,
1886
+ clip,
1887
+ format: "png"
1888
+ };
1889
+ const consumed = await consumeBands(capture);
1890
+ return { tiled: true, format: "png", bandCount: plan.bandCount, bandHeightCss: plan.bandHeightCss, consumed };
1891
+ }
1892
+ async* bands(clip, plan, scale, format) {
1893
+ if (format !== "png") {
1894
+ throw driverError("unsupported", `a tiled capture is PNG-only (bands are stitched as PNG scanlines); got format "${format}"`);
1895
+ }
1896
+ const bottom = clip.y + clip.height;
1897
+ for (let i = 0;i < plan.bandCount; i++) {
1898
+ const y = clip.y + i * plan.bandHeightCss;
1899
+ const height = Math.min(plan.bandHeightCss, bottom - y);
1900
+ const { data } = await this.conn.send("Page.captureScreenshot", {
1901
+ format: "png",
1902
+ captureBeyondViewport: true,
1903
+ clip: { x: clip.x, y, width: clip.width, height, scale }
1904
+ });
1905
+ yield new Uint8Array(Buffer.from(data, "base64"));
1906
+ }
1907
+ }
1722
1908
  async emulate(opts) {
1723
1909
  if (opts.clearOverrides) {
1724
1910
  const c = () => {
1725
1911
  return;
1726
1912
  };
1727
- await this.conn.send("Emulation.clearDeviceMetricsOverride").catch(c);
1913
+ let metricsClearError;
1914
+ try {
1915
+ await this.ensureDomain("Page");
1916
+ const metrics = await layoutMetrics(this.conn);
1917
+ const width = metrics.cssVisualViewport?.clientWidth ?? metrics.cssLayoutViewport?.clientWidth ?? metrics.visualViewport?.clientWidth ?? metrics.layoutViewport?.clientWidth ?? 1;
1918
+ const height = metrics.cssVisualViewport?.clientHeight ?? metrics.cssLayoutViewport?.clientHeight ?? metrics.visualViewport?.clientHeight ?? metrics.layoutViewport?.clientHeight ?? 1;
1919
+ await this.conn.send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: 0, mobile: false });
1920
+ await this.conn.send("Emulation.clearDeviceMetricsOverride");
1921
+ } catch (e) {
1922
+ metricsClearError = e instanceof Error ? e.message : String(e);
1923
+ }
1728
1924
  await this.conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(c);
1729
1925
  await this.conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(c);
1730
1926
  await this.conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(c);
1731
1927
  await this.ensureDomainSoft("Network");
1732
1928
  await this.conn.send("Network.emulateNetworkConditions", { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }).catch(c);
1929
+ if (metricsClearError !== undefined) {
1930
+ throw driverError("page-error", `emulate: could not verify the device-metrics override was cleared (${metricsClearError})`);
1931
+ }
1932
+ await clearViewportRecord(this.browser.scheme, this.info.id);
1733
1933
  return { applied: [] };
1734
1934
  }
1735
1935
  const applied = [];
@@ -1743,6 +1943,12 @@ class CdpPageDriver {
1743
1943
  deviceScaleFactor: opts.deviceScaleFactor ?? 0,
1744
1944
  mobile: opts.mobile ?? false
1745
1945
  });
1946
+ await writeViewportRecord(this.browser.scheme, this.info.id, {
1947
+ width: opts.width,
1948
+ height: opts.height,
1949
+ deviceScaleFactor: opts.deviceScaleFactor ?? 0,
1950
+ mobile: opts.mobile ?? false
1951
+ });
1746
1952
  applied.push("deviceMetrics");
1747
1953
  }
1748
1954
  if (opts.userAgent != null) {
@@ -2130,8 +2336,522 @@ async function reapStaleAgentTabs(driver, backend) {
2130
2336
  }
2131
2337
 
2132
2338
  // src/shared-tools.ts
2133
- import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
2134
- import { dirname, join as join3 } from "node:path";
2339
+ import { mkdir as mkdir5, writeFile as writeFile4 } from "node:fs/promises";
2340
+ import { dirname as dirname2, join as join4 } from "node:path";
2341
+
2342
+ // src/png.ts
2343
+ import { once } from "node:events";
2344
+ import { mkdir as mkdir4, open, rename, rm } from "node:fs/promises";
2345
+ import { dirname } from "node:path";
2346
+ import { createDeflate, createInflate, deflateSync } from "node:zlib";
2347
+ var PNG_SIGNATURE = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
2348
+ var ZLIB_CHUNK = 1 << 18;
2349
+ var BLOCK_RAW_BYTES = 1 << 22;
2350
+ var MAX_BLOCK_ROWS = 4096;
2351
+ var DECIDE_LEVEL = 6;
2352
+ var CARRIED_CHUNKS = ["gAMA", "cHRM", "sRGB", "iCCP", "pHYs"];
2353
+ var CHANNELS = { 0: 1, 2: 3, 4: 2, 6: 4 };
2354
+ var LEGAL_DEPTHS = {
2355
+ 0: [1, 2, 4, 8, 16],
2356
+ 2: [8, 16],
2357
+ 4: [8, 16],
2358
+ 6: [8, 16]
2359
+ };
2360
+ var COLOUR_TYPE_NAMES = {
2361
+ 0: "greyscale",
2362
+ 2: "truecolour RGB",
2363
+ 3: "palette",
2364
+ 4: "greyscale+alpha",
2365
+ 6: "RGBA"
2366
+ };
2367
+
2368
+ class PngStitchError extends Error {
2369
+ constructor(message) {
2370
+ super(message);
2371
+ this.name = "PngStitchError";
2372
+ }
2373
+ }
2374
+ var CRC_TABLE = (() => {
2375
+ const table = new Uint32Array(256);
2376
+ for (let n = 0;n < 256; n++) {
2377
+ let c = n;
2378
+ for (let k = 0;k < 8; k++)
2379
+ c = (c & 1) !== 0 ? 3988292384 ^ c >>> 1 : c >>> 1;
2380
+ table[n] = c >>> 0;
2381
+ }
2382
+ return table;
2383
+ })();
2384
+ function crcUpdate(crc, bytes) {
2385
+ let c = crc;
2386
+ for (let i = 0;i < bytes.length; i++)
2387
+ c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
2388
+ return c >>> 0;
2389
+ }
2390
+ function crc32(bytes) {
2391
+ return (crcUpdate(4294967295, bytes) ^ 4294967295) >>> 0;
2392
+ }
2393
+ function readU32(b, off) {
2394
+ return (b[off] << 24 | b[off + 1] << 16 | b[off + 2] << 8 | b[off + 3]) >>> 0;
2395
+ }
2396
+ function chunkTypeAt(b, off) {
2397
+ return String.fromCharCode(b[off], b[off + 1], b[off + 2], b[off + 3]);
2398
+ }
2399
+ function sameBytes(a, b) {
2400
+ if (a.length !== b.length)
2401
+ return false;
2402
+ for (let i = 0;i < a.length; i++)
2403
+ if (a[i] !== b[i])
2404
+ return false;
2405
+ return true;
2406
+ }
2407
+ function paeth(a, b, c) {
2408
+ const p = a + b - c;
2409
+ const pa = p > a ? p - a : a - p;
2410
+ const pb = p > b ? p - b : b - p;
2411
+ const pc = p > c ? p - c : c - p;
2412
+ if (pa <= pb && pa <= pc)
2413
+ return a;
2414
+ return pb <= pc ? b : c;
2415
+ }
2416
+ function unfilterRow(type, row, prev, bpp, at) {
2417
+ const n = row.length;
2418
+ switch (type) {
2419
+ case 0:
2420
+ return;
2421
+ case 1:
2422
+ for (let i = bpp;i < n; i++)
2423
+ row[i] = row[i] + row[i - bpp] & 255;
2424
+ return;
2425
+ case 2:
2426
+ for (let i = 0;i < n; i++)
2427
+ row[i] = row[i] + prev[i] & 255;
2428
+ return;
2429
+ case 3:
2430
+ for (let i = 0;i < n; i++) {
2431
+ const a = i >= bpp ? row[i - bpp] : 0;
2432
+ row[i] = row[i] + (a + prev[i] >> 1) & 255;
2433
+ }
2434
+ return;
2435
+ case 4:
2436
+ for (let i = 0;i < n; i++) {
2437
+ const a = i >= bpp ? row[i - bpp] : 0;
2438
+ const c = i >= bpp ? prev[i - bpp] : 0;
2439
+ row[i] = row[i] + paeth(a, prev[i], c) & 255;
2440
+ }
2441
+ return;
2442
+ default:
2443
+ throw new PngStitchError(`${at}: unknown row filter type ${type} (PNG defines 0-4)`);
2444
+ }
2445
+ }
2446
+ function absSigned(v) {
2447
+ const u = v & 255;
2448
+ return u < 128 ? u : 256 - u;
2449
+ }
2450
+ function chooseFilter(row, prev, bpp) {
2451
+ const n = row.length;
2452
+ let s0 = 0, s1 = 0, s2 = 0, s3 = 0, s4 = 0;
2453
+ for (let i = 0;i < n; i++) {
2454
+ const x = row[i];
2455
+ const b = prev[i];
2456
+ const a = i >= bpp ? row[i - bpp] : 0;
2457
+ const c = i >= bpp ? prev[i - bpp] : 0;
2458
+ s0 += absSigned(x);
2459
+ s1 += absSigned(x - a);
2460
+ s2 += absSigned(x - b);
2461
+ s3 += absSigned(x - (a + b >> 1));
2462
+ s4 += absSigned(x - paeth(a, b, c));
2463
+ }
2464
+ let best = 0, bestSum = s0;
2465
+ if (s1 < bestSum) {
2466
+ best = 1;
2467
+ bestSum = s1;
2468
+ }
2469
+ if (s2 < bestSum) {
2470
+ best = 2;
2471
+ bestSum = s2;
2472
+ }
2473
+ if (s3 < bestSum) {
2474
+ best = 3;
2475
+ bestSum = s3;
2476
+ }
2477
+ if (s4 < bestSum) {
2478
+ best = 4;
2479
+ }
2480
+ return best;
2481
+ }
2482
+ function writeFilteredRow(type, row, prev, bpp, out, off) {
2483
+ const n = row.length;
2484
+ switch (type) {
2485
+ case 0:
2486
+ out.set(row, off);
2487
+ return;
2488
+ case 1:
2489
+ for (let i = 0;i < n; i++)
2490
+ out[off + i] = row[i] - (i >= bpp ? row[i - bpp] : 0) & 255;
2491
+ return;
2492
+ case 2:
2493
+ for (let i = 0;i < n; i++)
2494
+ out[off + i] = row[i] - prev[i] & 255;
2495
+ return;
2496
+ case 3:
2497
+ for (let i = 0;i < n; i++) {
2498
+ const a = i >= bpp ? row[i - bpp] : 0;
2499
+ out[off + i] = row[i] - (a + prev[i] >> 1) & 255;
2500
+ }
2501
+ return;
2502
+ default:
2503
+ for (let i = 0;i < n; i++) {
2504
+ const a = i >= bpp ? row[i - bpp] : 0;
2505
+ const c = i >= bpp ? prev[i - bpp] : 0;
2506
+ out[off + i] = row[i] - paeth(a, prev[i], c) & 255;
2507
+ }
2508
+ return;
2509
+ }
2510
+ }
2511
+ function filterUnitBytes(h) {
2512
+ return Math.max(1, Math.ceil(CHANNELS[h.colourType] * h.bitDepth / 8));
2513
+ }
2514
+ function bytesPerRow(h) {
2515
+ return Math.ceil(h.width * CHANNELS[h.colourType] * h.bitDepth / 8);
2516
+ }
2517
+ function readIhdr(band, dataStart, at) {
2518
+ const width = readU32(band, dataStart);
2519
+ const height = readU32(band, dataStart + 4);
2520
+ const bitDepth = band[dataStart + 8];
2521
+ const colourType = band[dataStart + 9];
2522
+ const compression = band[dataStart + 10];
2523
+ const filterMethod = band[dataStart + 11];
2524
+ const interlace = band[dataStart + 12];
2525
+ if (width < 1 || height < 1)
2526
+ throw new PngStitchError(`${at}: IHDR declares ${width}x${height}; both dimensions must be >= 1`);
2527
+ if (compression !== 0)
2528
+ throw new PngStitchError(`${at}: IHDR compression method ${compression}; PNG defines only 0 (deflate)`);
2529
+ if (filterMethod !== 0)
2530
+ throw new PngStitchError(`${at}: IHDR filter method ${filterMethod}; PNG defines only 0`);
2531
+ if (interlace === 1) {
2532
+ throw new PngStitchError(`${at}: interlaced (Adam7) PNG is not supported. Its seven reduced passes are not scanlines of the final image, ` + `so bands of them cannot be concatenated; re-capture without interlacing (Chrome's Page.captureScreenshot always does).`);
2533
+ }
2534
+ if (interlace !== 0)
2535
+ throw new PngStitchError(`${at}: IHDR interlace method ${interlace}; PNG defines only 0 (none) and 1 (Adam7)`);
2536
+ if (colourType === 3) {
2537
+ throw new PngStitchError(`${at}: colour type 3 (palette) is not supported. Concatenating palettized bands requires every band's PLTE/tRNS to ` + `be identical or a full palette remap; Chrome emits colour type 2, so this path is refused rather than guessed.`);
2538
+ }
2539
+ if (CHANNELS[colourType] === undefined)
2540
+ throw new PngStitchError(`${at}: IHDR colour type ${colourType}; PNG defines 0, 2, 3, 4 and 6`);
2541
+ if (!LEGAL_DEPTHS[colourType].includes(bitDepth)) {
2542
+ throw new PngStitchError(`${at}: bit depth ${bitDepth} is not legal for colour type ${colourType} (${COLOUR_TYPE_NAMES[colourType]}); ` + `allowed: ${LEGAL_DEPTHS[colourType].join(", ")}`);
2543
+ }
2544
+ return { width, height, bitDepth, colourType };
2545
+ }
2546
+ function parseBand(band, index) {
2547
+ const at = `band ${index}`;
2548
+ if (band.length < 8)
2549
+ throw new PngStitchError(`${at}: ${band.length} bytes is too short to be a PNG`);
2550
+ for (let i = 0;i < 8; i++) {
2551
+ if (band[i] !== PNG_SIGNATURE[i])
2552
+ throw new PngStitchError(`${at}: not a PNG (bad 8-byte signature)`);
2553
+ }
2554
+ let header;
2555
+ const idat = [];
2556
+ const carried = [];
2557
+ let sawIend = false;
2558
+ let off = 8;
2559
+ while (off + 8 <= band.length) {
2560
+ const len = readU32(band, off);
2561
+ const type = chunkTypeAt(band, off + 4);
2562
+ const dataStart = off + 8;
2563
+ const dataEnd = dataStart + len;
2564
+ if (dataEnd + 4 > band.length) {
2565
+ throw new PngStitchError(`${at}: chunk "${type}" at byte ${off} claims ${len} payload bytes but only ${band.length - dataStart} remain (truncated PNG)`);
2566
+ }
2567
+ if (type === "IHDR") {
2568
+ if (off !== 8)
2569
+ throw new PngStitchError(`${at}: IHDR must be the first chunk, found it at byte ${off}`);
2570
+ if (len !== 13)
2571
+ throw new PngStitchError(`${at}: IHDR payload is ${len} bytes, must be 13`);
2572
+ const want = readU32(band, dataEnd);
2573
+ const got = crc32(band.subarray(off + 4, dataEnd));
2574
+ if (want !== got)
2575
+ throw new PngStitchError(`${at}: IHDR CRC mismatch (file says 0x${want.toString(16)}, computed 0x${got.toString(16)})`);
2576
+ header = readIhdr(band, dataStart, at);
2577
+ } else if (!header) {
2578
+ throw new PngStitchError(`${at}: first chunk is "${type}", expected IHDR`);
2579
+ } else if (type === "IDAT") {
2580
+ idat.push([dataStart, dataEnd]);
2581
+ } else if (type === "IEND") {
2582
+ sawIend = true;
2583
+ break;
2584
+ } else if (CARRIED_CHUNKS.includes(type)) {
2585
+ carried.push({ type, data: band.subarray(dataStart, dataEnd) });
2586
+ }
2587
+ off = dataEnd + 4;
2588
+ }
2589
+ if (!header)
2590
+ throw new PngStitchError(`${at}: no IHDR chunk`);
2591
+ if (idat.length === 0)
2592
+ throw new PngStitchError(`${at}: no IDAT chunk`);
2593
+ if (!sawIend)
2594
+ throw new PngStitchError(`${at}: no IEND chunk (truncated PNG)`);
2595
+ return { header, idat, carried };
2596
+ }
2597
+ function assertCompatible(first, next, firstCarried, nextCarried, index) {
2598
+ const at = `band ${index}`;
2599
+ if (next.width !== first.width)
2600
+ throw new PngStitchError(`${at}: width ${next.width} does not match band 1's width ${first.width}`);
2601
+ if (next.bitDepth !== first.bitDepth)
2602
+ throw new PngStitchError(`${at}: bit depth ${next.bitDepth} does not match band 1's bit depth ${first.bitDepth}`);
2603
+ if (next.colourType !== first.colourType) {
2604
+ throw new PngStitchError(`${at}: colour type ${next.colourType} (${COLOUR_TYPE_NAMES[next.colourType] ?? "?"}) does not match band 1's ` + `colour type ${first.colourType} (${COLOUR_TYPE_NAMES[first.colourType] ?? "?"})`);
2605
+ }
2606
+ for (const type of CARRIED_CHUNKS) {
2607
+ const a = firstCarried.find((c) => c.type === type);
2608
+ const b = nextCarried.find((c) => c.type === type);
2609
+ if (!a && !b)
2610
+ continue;
2611
+ if (!a || !b)
2612
+ throw new PngStitchError(`${at}: ${b ? "has" : "is missing"} a "${type}" chunk that band 1 ${b ? "does not have" : "has"}; the bands disagree about how their samples are interpreted`);
2613
+ if (!sameBytes(a.data, b.data))
2614
+ throw new PngStitchError(`${at}: its "${type}" chunk differs from band 1's; the bands disagree about how their samples are interpreted`);
2615
+ }
2616
+ }
2617
+
2618
+ class ChunkWriter {
2619
+ fh;
2620
+ pos = 0;
2621
+ constructor(fh) {
2622
+ this.fh = fh;
2623
+ }
2624
+ get bytes() {
2625
+ return this.pos;
2626
+ }
2627
+ async writeRaw(b) {
2628
+ await this.fh.write(b, 0, b.length, this.pos);
2629
+ this.pos += b.length;
2630
+ }
2631
+ async writeChunk(type, payload) {
2632
+ const head = Buffer.allocUnsafe(8);
2633
+ head.writeUInt32BE(payload.length, 0);
2634
+ head.write(type, 4, "latin1");
2635
+ const crc = (crcUpdate(crcUpdate(4294967295, head.subarray(4)), payload) ^ 4294967295) >>> 0;
2636
+ const tail = Buffer.allocUnsafe(4);
2637
+ tail.writeUInt32BE(crc, 0);
2638
+ await this.fh.writev([head, payload, tail], this.pos);
2639
+ this.pos += 8 + payload.length + 4;
2640
+ }
2641
+ async patchAt(position, b) {
2642
+ await this.fh.write(b, 0, b.length, position);
2643
+ }
2644
+ }
2645
+ function ihdrChunk(h, height) {
2646
+ const buf = Buffer.allocUnsafe(25);
2647
+ buf.writeUInt32BE(13, 0);
2648
+ buf.write("IHDR", 4, "latin1");
2649
+ buf.writeUInt32BE(h.width, 8);
2650
+ buf.writeUInt32BE(height, 12);
2651
+ buf[16] = h.bitDepth;
2652
+ buf[17] = h.colourType;
2653
+ buf[18] = 0;
2654
+ buf[19] = 0;
2655
+ buf[20] = 0;
2656
+ buf.writeUInt32BE(crc32(buf.subarray(4, 21)), 21);
2657
+ return buf;
2658
+ }
2659
+
2660
+ class RowEncoder {
2661
+ writer;
2662
+ bpr;
2663
+ bpp;
2664
+ deflate = createDeflate({ chunkSize: ZLIB_CHUNK });
2665
+ pump;
2666
+ rowsPerBlock;
2667
+ block;
2668
+ blockRows = 0;
2669
+ candNone;
2670
+ candHeuristic;
2671
+ prevRaw;
2672
+ constructor(writer, bpr, bpp) {
2673
+ this.writer = writer;
2674
+ this.bpr = bpr;
2675
+ this.bpp = bpp;
2676
+ this.rowsPerBlock = Math.min(MAX_BLOCK_ROWS, Math.max(1, Math.floor(BLOCK_RAW_BYTES / bpr)));
2677
+ this.block = Buffer.allocUnsafe(this.rowsPerBlock * bpr);
2678
+ this.candNone = Buffer.allocUnsafe(this.rowsPerBlock * (bpr + 1));
2679
+ this.candHeuristic = Buffer.allocUnsafe(this.rowsPerBlock * (bpr + 1));
2680
+ this.prevRaw = new Uint8Array(bpr);
2681
+ this.pump = (async () => {
2682
+ for await (const chunk of this.deflate)
2683
+ await this.writer.writeChunk("IDAT", chunk);
2684
+ })();
2685
+ this.pump.catch(() => {});
2686
+ }
2687
+ async addRow(raw) {
2688
+ this.block.set(raw, this.blockRows * this.bpr);
2689
+ this.blockRows += 1;
2690
+ if (this.blockRows === this.rowsPerBlock)
2691
+ await this.emitBlock();
2692
+ }
2693
+ async emitBlock() {
2694
+ const n = this.blockRows;
2695
+ if (n === 0)
2696
+ return;
2697
+ const { bpr, bpp } = this;
2698
+ const stride = bpr + 1;
2699
+ let prev = this.prevRaw;
2700
+ for (let r = 0;r < n; r++) {
2701
+ const row = this.block.subarray(r * bpr, (r + 1) * bpr);
2702
+ const at = r * stride;
2703
+ this.candNone[at] = 0;
2704
+ this.candNone.set(row, at + 1);
2705
+ const filter = chooseFilter(row, prev, bpp);
2706
+ this.candHeuristic[at] = filter;
2707
+ writeFilteredRow(filter, row, prev, bpp, this.candHeuristic, at + 1);
2708
+ prev = row;
2709
+ }
2710
+ const used = n * stride;
2711
+ const a = this.candNone.subarray(0, used);
2712
+ const b = this.candHeuristic.subarray(0, used);
2713
+ const heuristicWins = deflateSync(b, { level: DECIDE_LEVEL }).length < deflateSync(a, { level: DECIDE_LEVEL }).length;
2714
+ this.prevRaw.set(prev);
2715
+ this.blockRows = 0;
2716
+ const out = Buffer.from(heuristicWins ? b : a);
2717
+ if (!this.deflate.write(out))
2718
+ await once(this.deflate, "drain");
2719
+ }
2720
+ async finish() {
2721
+ await this.emitBlock();
2722
+ this.deflate.end();
2723
+ await this.pump;
2724
+ }
2725
+ async abort() {
2726
+ this.deflate.destroy();
2727
+ await this.pump.catch(() => {});
2728
+ }
2729
+ }
2730
+ async function* inflateBandIdat(band, slices, at) {
2731
+ const inf = createInflate({ chunkSize: ZLIB_CHUNK });
2732
+ const feed = (async () => {
2733
+ for (const [start, end] of slices) {
2734
+ const view = Buffer.from(band.buffer, band.byteOffset + start, end - start);
2735
+ if (!inf.write(view))
2736
+ await once(inf, "drain");
2737
+ }
2738
+ inf.end();
2739
+ })();
2740
+ feed.catch(() => {});
2741
+ try {
2742
+ for await (const chunk of inf)
2743
+ yield chunk;
2744
+ } catch (err) {
2745
+ throw new PngStitchError(`${at}: IDAT stream would not inflate (${err.message})`);
2746
+ } finally {
2747
+ inf.destroy();
2748
+ }
2749
+ }
2750
+ function isAsyncIterable(x) {
2751
+ return typeof x === "object" && x !== null && Symbol.asyncIterator in x;
2752
+ }
2753
+ async function* eachBand(bands) {
2754
+ if (isAsyncIterable(bands)) {
2755
+ for await (const b of bands)
2756
+ yield b;
2757
+ } else {
2758
+ for (const b of bands)
2759
+ yield b;
2760
+ }
2761
+ }
2762
+ async function stitchPngBandsToFile(bands, outPath) {
2763
+ await mkdir4(dirname(outPath), { recursive: true });
2764
+ const partPath = `${outPath}.stitch-partial`;
2765
+ const fh = await open(partPath, "w");
2766
+ let done = false;
2767
+ try {
2768
+ const result = await stitchInto(fh, bands);
2769
+ await fh.close();
2770
+ await rename(partPath, outPath);
2771
+ done = true;
2772
+ return result;
2773
+ } finally {
2774
+ if (!done) {
2775
+ await fh.close().catch(() => {});
2776
+ await rm(partPath, { force: true }).catch(() => {});
2777
+ }
2778
+ }
2779
+ }
2780
+ async function stitchInto(fh, bands) {
2781
+ const writer = new ChunkWriter(fh);
2782
+ let first;
2783
+ let firstCarried = [];
2784
+ let encoder;
2785
+ let prevRow = new Uint8Array(0);
2786
+ let rowBuf = new Uint8Array(0);
2787
+ let bpp = 0;
2788
+ let bpr = 0;
2789
+ let totalRows = 0;
2790
+ let index = 0;
2791
+ try {
2792
+ for await (const band of eachBand(bands)) {
2793
+ index += 1;
2794
+ const parsed = parseBand(band, index);
2795
+ const at = `band ${index}`;
2796
+ if (!first) {
2797
+ first = parsed.header;
2798
+ firstCarried = parsed.carried;
2799
+ bpp = filterUnitBytes(first);
2800
+ bpr = bytesPerRow(first);
2801
+ prevRow = new Uint8Array(bpr);
2802
+ rowBuf = new Uint8Array(1 + bpr);
2803
+ await writer.writeRaw(PNG_SIGNATURE);
2804
+ await writer.writeRaw(ihdrChunk(first, 1));
2805
+ for (const c of parsed.carried)
2806
+ await writer.writeChunk(c.type, c.data);
2807
+ encoder = new RowEncoder(writer, bpr, bpp);
2808
+ } else {
2809
+ assertCompatible(first, parsed.header, firstCarried, parsed.carried, index);
2810
+ }
2811
+ const enc = encoder;
2812
+ const raw = rowBuf.subarray(1);
2813
+ prevRow.fill(0);
2814
+ let rows = 0;
2815
+ let filled = 0;
2816
+ for await (const chunk of inflateBandIdat(band, parsed.idat, at)) {
2817
+ let off = 0;
2818
+ while (off < chunk.length) {
2819
+ const take = Math.min(rowBuf.length - filled, chunk.length - off);
2820
+ rowBuf.set(chunk.subarray(off, off + take), filled);
2821
+ filled += take;
2822
+ off += take;
2823
+ if (filled !== rowBuf.length)
2824
+ continue;
2825
+ if (rows >= parsed.header.height) {
2826
+ throw new PngStitchError(`${at}: IDAT holds more scanlines than its IHDR height of ${parsed.header.height}`);
2827
+ }
2828
+ unfilterRow(rowBuf[0], raw, prevRow, bpp, at);
2829
+ await enc.addRow(raw);
2830
+ prevRow.set(raw);
2831
+ rows += 1;
2832
+ filled = 0;
2833
+ }
2834
+ }
2835
+ if (filled !== 0 || rows !== parsed.header.height) {
2836
+ throw new PngStitchError(`${at}: IDAT decoded to ${rows} complete scanlines${filled > 0 ? ` plus ${filled} trailing bytes` : ""}, but its IHDR declares ${parsed.header.height}`);
2837
+ }
2838
+ totalRows += parsed.header.height;
2839
+ if (totalRows > 2147483647)
2840
+ throw new PngStitchError(`stitched height would be ${totalRows}, past PNG's 2^31-1 limit`);
2841
+ }
2842
+ if (!first || !encoder)
2843
+ throw new PngStitchError("no bands to stitch");
2844
+ await encoder.finish();
2845
+ await writer.writeChunk("IEND", new Uint8Array(0));
2846
+ await writer.patchAt(8, ihdrChunk(first, totalRows));
2847
+ return { width: first.width, height: totalRows, bytes: writer.bytes, bands: index };
2848
+ } catch (err) {
2849
+ await encoder?.abort();
2850
+ throw err;
2851
+ }
2852
+ }
2853
+
2854
+ // src/shared-tools.ts
2135
2855
  var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2136
2856
  var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
2137
2857
 
@@ -2276,8 +2996,8 @@ async function closePage(driver, args) {
2276
2996
  return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
2277
2997
  }
2278
2998
  async function writeSelectedFile(id) {
2279
- await mkdir3(STATE_DIR, { recursive: true });
2280
- await writeFile3(join3(STATE_DIR, "selected"), id, "utf8");
2999
+ await mkdir5(STATE_DIR, { recursive: true });
3000
+ await writeFile4(join4(STATE_DIR, "selected"), id, "utf8");
2281
3001
  }
2282
3002
  async function selectPage(driver, args) {
2283
3003
  if (args.target === undefined || args.target === "")
@@ -2341,10 +3061,10 @@ function serializeForSink(value) {
2341
3061
  return json ?? "null";
2342
3062
  }
2343
3063
  async function writeJsonSink(savePath, value) {
2344
- const path = savePath.startsWith("/") ? savePath : join3(ARTIFACT_DIR, savePath);
2345
- await mkdir3(dirname(path), { recursive: true });
3064
+ const path = savePath.startsWith("/") ? savePath : join4(ARTIFACT_DIR, savePath);
3065
+ await mkdir5(dirname2(path), { recursive: true });
2346
3066
  const json = serializeForSink(value);
2347
- await writeFile3(path, json, "utf8");
3067
+ await writeFile4(path, json, "utf8");
2348
3068
  return { path, bytes: Buffer.byteLength(json, "utf8") };
2349
3069
  }
2350
3070
  function sinkRequested(savePath) {
@@ -2648,30 +3368,166 @@ async function uploadFile(driver, args) {
2648
3368
  function stamp() {
2649
3369
  return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
2650
3370
  }
3371
+ var MAX_SCREENSHOT_SCALE = 8;
3372
+ function resolveScreenshotScale(scale, capabilities) {
3373
+ if (scale === undefined)
3374
+ return 1;
3375
+ if (typeof scale !== "number" || !Number.isFinite(scale) || scale <= 0 || scale > MAX_SCREENSHOT_SCALE) {
3376
+ throw new SharedToolError(`take_screenshot: 'scale' must be a finite number greater than 0 and at most ${MAX_SCREENSHOT_SCALE} (default 1)`);
3377
+ }
3378
+ if (scale !== 1 && !capabilities.has("screenshot.scale")) {
3379
+ throw new SharedToolError("take_screenshot: 'scale' is not supported by this backend (WebDriver BiDi's browsingContext.captureScreenshot has no scale parameter). " + `Call emulate with deviceScaleFactor:${scale} first and capture at scale 1, or use --browser chrome for a per-capture scale.`);
3380
+ }
3381
+ return scale;
3382
+ }
3383
+ var MAX_RENDER_SIZE_PX = 16384;
3384
+ function resolveRenderSize(renderWidth, renderHeight, capabilities) {
3385
+ if (renderWidth === undefined && renderHeight === undefined)
3386
+ return;
3387
+ if (renderWidth === undefined || renderHeight === undefined) {
3388
+ throw new SharedToolError("take_screenshot: 'renderWidth' and 'renderHeight' are required together (a viewport is two numbers; " + `got ${renderWidth === undefined ? "only renderHeight" : "only renderWidth"})`);
3389
+ }
3390
+ for (const [name, value] of [["renderWidth", renderWidth], ["renderHeight", renderHeight]]) {
3391
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > MAX_RENDER_SIZE_PX) {
3392
+ throw new SharedToolError(`take_screenshot: '${name}' must be an integer between 1 and ${MAX_RENDER_SIZE_PX} css px (got ${JSON.stringify(value)})`);
3393
+ }
3394
+ }
3395
+ if (!capabilities.has("screenshot.renderSize")) {
3396
+ throw new SharedToolError("take_screenshot: 'renderWidth'/'renderHeight' are not supported by this backend (it cannot emulate a viewport for a capture). " + `Call emulate with width:${renderWidth}, height:${renderHeight} first, capture, then restore the viewport yourself.`);
3397
+ }
3398
+ return { width: renderWidth, height: renderHeight };
3399
+ }
3400
+ function resolveTileMode(tile, capabilities, format, returnBase64) {
3401
+ if (tile === undefined)
3402
+ return;
3403
+ if (typeof tile !== "boolean")
3404
+ throw new SharedToolError(`take_screenshot: 'tile' must be true, false, or omitted for automatic (got ${JSON.stringify(tile)})`);
3405
+ if (!tile)
3406
+ return false;
3407
+ if (!capabilities.has("screenshot.tile")) {
3408
+ throw new SharedToolError("take_screenshot: 'tile' is not supported by this backend (band tiling is declared only where the band arithmetic was measured against a real browser; " + "Firefox was not driven for it). Capture a smaller region or a single element, or use --browser chrome for a tiled capture.");
3409
+ }
3410
+ if (format !== "png")
3411
+ throw tiledCaptureRefusal("format");
3412
+ if (returnBase64)
3413
+ throw tiledCaptureRefusal("base64");
3414
+ return true;
3415
+ }
3416
+ function tiledCaptureRefusal(reason, banded) {
3417
+ const because = banded ? ` This capture needs ${banded.bandCount} bands (${banded.clip.width}x${banded.clip.height} css px is past what one image can encode).` : "";
3418
+ if (reason === "format") {
3419
+ return new SharedToolError("take_screenshot: a tiled capture is PNG-only — bands are stitched by concatenating PNG scanlines, and there is no lossless " + `JPEG equivalent to concatenate.${because} Pass format:"png", or lower scale / narrow the region until one JPEG fits.`);
3420
+ }
3421
+ return new SharedToolError("take_screenshot: 'returnBase64' cannot be combined with a tiled capture — a tiled image runs to hundreds of megabytes and base64 " + `inflates it by a third, into the caller's response.${because} Read the file at the returned path instead, or capture a region small enough for one shot.`);
3422
+ }
3423
+ function pngPixelSize(b, view) {
3424
+ if (b.length < 24)
3425
+ return;
3426
+ if (view.getUint32(0) !== 2303741511 || view.getUint32(4) !== 218765834)
3427
+ return;
3428
+ if (String.fromCharCode(b[12], b[13], b[14], b[15]) !== "IHDR")
3429
+ return;
3430
+ return { width: view.getUint32(16), height: view.getUint32(20) };
3431
+ }
3432
+ var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
3433
+ function jpegPixelSize(b, view) {
3434
+ if (b.length < 4 || b[0] !== 255 || b[1] !== 216)
3435
+ return;
3436
+ let i = 2;
3437
+ while (i + 3 < b.length) {
3438
+ if (b[i] !== 255) {
3439
+ i += 1;
3440
+ continue;
3441
+ }
3442
+ const marker = b[i + 1];
3443
+ if (marker === 255) {
3444
+ i += 1;
3445
+ continue;
3446
+ }
3447
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
3448
+ i += 2;
3449
+ continue;
3450
+ }
3451
+ const segLen = view.getUint16(i + 2);
3452
+ if (JPEG_SOF.has(marker)) {
3453
+ if (i + 9 > b.length)
3454
+ return;
3455
+ return { width: view.getUint16(i + 7), height: view.getUint16(i + 5) };
3456
+ }
3457
+ if (segLen < 2)
3458
+ return;
3459
+ i += 2 + segLen;
3460
+ }
3461
+ return;
3462
+ }
3463
+ function imagePixelSize(bytes) {
3464
+ if (bytes.byteLength < 4)
3465
+ return;
3466
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
3467
+ return pngPixelSize(bytes, view) ?? jpegPixelSize(bytes, view);
3468
+ }
2651
3469
  async function takeScreenshot(driver, args = {}) {
2652
3470
  if (args.uid !== undefined && args.uid !== "" && args.selector)
2653
3471
  throw new SharedToolError("provide exactly one of uid or selector, not both");
2654
3472
  const format = args.format ?? "png";
3473
+ const scale = resolveScreenshotScale(args.scale, driver.capabilities);
3474
+ const renderSize = resolveRenderSize(args.renderWidth, args.renderHeight, driver.capabilities);
3475
+ const tile = resolveTileMode(args.tile, driver.capabilities, format, args.returnBase64);
2655
3476
  return withPage2(driver, args.target, async (page) => {
2656
3477
  const clip = args.uid !== undefined || args.selector ? locatorOf(args) : undefined;
2657
- const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
2658
- if (data.byteLength === 0)
2659
- throw new SharedToolError("captureScreenshot returned empty data");
2660
- await mkdir3(ARTIFACT_DIR, { recursive: true });
2661
- const ext = outFormat === "jpeg" ? "jpg" : "png";
2662
- const path = args.savePath ?? join3(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
2663
- await writeFile3(path, Buffer.from(data));
2664
- const result = {
2665
- path,
2666
- bytes: data.byteLength,
2667
- format: outFormat,
2668
- target: target3(page.info)
3478
+ const ext = format === "jpeg" ? "jpg" : "png";
3479
+ const path = args.savePath ?? join4(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
3480
+ const stitch = async (banded) => {
3481
+ if (format !== "png")
3482
+ throw tiledCaptureRefusal("format", banded);
3483
+ if (args.returnBase64)
3484
+ throw tiledCaptureRefusal("base64", banded);
3485
+ return stitchPngBandsToFile(banded.bands, path);
2669
3486
  };
2670
- if (args.returnBase64)
2671
- result.base64 = Buffer.from(data).toString("base64");
3487
+ const shot = await page.screenshot({
3488
+ format,
3489
+ quality: args.quality,
3490
+ fullPage: args.fullPage,
3491
+ scale,
3492
+ ...clip ? { clip } : {},
3493
+ ...renderSize ? { renderWidth: renderSize.width, renderHeight: renderSize.height } : {},
3494
+ ...tile !== undefined ? { tile } : {}
3495
+ }, stitch);
3496
+ const result = isTiledCapture(shot) ? {
3497
+ path,
3498
+ bytes: shot.consumed.bytes,
3499
+ format: "png",
3500
+ target: target3(page.info),
3501
+ scale,
3502
+ width: shot.consumed.width,
3503
+ height: shot.consumed.height,
3504
+ tiled: true,
3505
+ bands: shot.consumed.bands
3506
+ } : await writeOneShot(shot.data, shot.format, path, target3(page.info), scale, args.returnBase64);
3507
+ if (renderSize && shot.renderSize) {
3508
+ result.renderSize = shot.renderSize;
3509
+ result.renderRestored = shot.renderRestored ?? false;
3510
+ if (shot.renderRestoreError !== undefined)
3511
+ result.renderRestoreError = shot.renderRestoreError;
3512
+ }
2672
3513
  return result;
2673
3514
  });
2674
3515
  }
3516
+ async function writeOneShot(data, outFormat, path, target, scale, returnBase64) {
3517
+ if (data.byteLength === 0)
3518
+ throw new SharedToolError("captureScreenshot returned empty data");
3519
+ await mkdir5(dirname2(path), { recursive: true });
3520
+ await writeFile4(path, Buffer.from(data));
3521
+ const size = imagePixelSize(data);
3522
+ const result = { path, bytes: data.byteLength, format: outFormat, target, scale };
3523
+ if (size) {
3524
+ result.width = size.width;
3525
+ result.height = size.height;
3526
+ }
3527
+ if (returnBase64)
3528
+ result.base64 = Buffer.from(data).toString("base64");
3529
+ return result;
3530
+ }
2675
3531
  async function emulate(driver, args = {}) {
2676
3532
  return withPage2(driver, args.target, async (page) => {
2677
3533
  if (args.clearOverrides) {
@@ -2842,10 +3698,10 @@ var LEASE_TOOLS = {
2842
3698
  };
2843
3699
 
2844
3700
  // src/tools/console.ts
2845
- import { readFile as readFile3 } from "node:fs/promises";
3701
+ import { readFile as readFile4 } from "node:fs/promises";
2846
3702
 
2847
3703
  // src/tools/recorder.ts
2848
- import { mkdir as mkdir4, appendFile, writeFile as writeFile4, copyFile } from "node:fs/promises";
3704
+ import { mkdir as mkdir6, appendFile, writeFile as writeFile5, copyFile } from "node:fs/promises";
2849
3705
  var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2850
3706
  function recFile(targetId) {
2851
3707
  return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
@@ -2891,11 +3747,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
2891
3747
  async function startRecorder(target, opts = {}) {
2892
3748
  const network = opts.network ?? true;
2893
3749
  const consoleCap = opts.console ?? true;
2894
- await mkdir4(ARTIFACT_DIR2, { recursive: true });
3750
+ await mkdir6(ARTIFACT_DIR2, { recursive: true });
2895
3751
  const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
2896
3752
  const file = opts.file ?? recFile(resolved.id);
2897
3753
  if (opts.truncate) {
2898
- await writeFile4(file, "");
3754
+ await writeFile5(file, "");
2899
3755
  }
2900
3756
  let chain = Promise.resolve();
2901
3757
  let dropped = 0;
@@ -3041,7 +3897,7 @@ function lineToEntry(line, index) {
3041
3897
  async function readConsoleEntries(file) {
3042
3898
  let raw;
3043
3899
  try {
3044
- raw = await readFile3(file, "utf8");
3900
+ raw = await readFile4(file, "utf8");
3045
3901
  } catch {
3046
3902
  return [];
3047
3903
  }
@@ -3098,7 +3954,7 @@ async function getConsoleMessage(args = {}) {
3098
3954
  }
3099
3955
 
3100
3956
  // src/tools/network.ts
3101
- import { readFile as readFile4 } from "node:fs/promises";
3957
+ import { readFile as readFile5 } from "node:fs/promises";
3102
3958
  var DEFAULT_CAPTURE_MS2 = 2500;
3103
3959
  function asHeaders(v) {
3104
3960
  if (!v || typeof v !== "object")
@@ -3165,7 +4021,7 @@ function correlate(lines) {
3165
4021
  async function readNetworkRequests(file) {
3166
4022
  let raw;
3167
4023
  try {
3168
- raw = await readFile4(file, "utf8");
4024
+ raw = await readFile5(file, "utf8");
3169
4025
  } catch {
3170
4026
  return [];
3171
4027
  }
@@ -3249,8 +4105,8 @@ async function getNetworkRequest(args = {}) {
3249
4105
  }
3250
4106
 
3251
4107
  // src/tools/performance.ts
3252
- import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, stat, unlink as unlink3 } from "node:fs/promises";
3253
- import { join as join4 } from "node:path";
4108
+ import { mkdir as mkdir7, readFile as readFile6, writeFile as writeFile6, stat, unlink as unlink4 } from "node:fs/promises";
4109
+ import { join as join5 } from "node:path";
3254
4110
  var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3255
4111
  var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
3256
4112
  var DEFAULT_CATEGORIES = [
@@ -3268,7 +4124,7 @@ function shortId(id) {
3268
4124
  return id.slice(0, 8);
3269
4125
  }
3270
4126
  function stateFilePath() {
3271
- return join4(STATE_DIR2, "trace-state.json");
4127
+ return join5(STATE_DIR2, "trace-state.json");
3272
4128
  }
3273
4129
  function bufferTraceData(conn, sink) {
3274
4130
  return conn.on("Tracing.dataCollected", (params) => {
@@ -3290,10 +4146,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
3290
4146
  return sink;
3291
4147
  }
3292
4148
  async function writeTrace(events, targetId) {
3293
- await mkdir5(ARTIFACT_DIR3, { recursive: true });
3294
- const path = join4(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
4149
+ await mkdir7(ARTIFACT_DIR3, { recursive: true });
4150
+ const path = join5(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
3295
4151
  const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
3296
- await writeFile5(path, body, "utf8");
4152
+ await writeFile6(path, body, "utf8");
3297
4153
  const { size } = await stat(path);
3298
4154
  return { path, bytes: size };
3299
4155
  }
@@ -3313,7 +4169,7 @@ async function performanceStartTrace(args = {}) {
3313
4169
  throw err;
3314
4170
  }
3315
4171
  liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
3316
- await mkdir5(STATE_DIR2, { recursive: true });
4172
+ await mkdir7(STATE_DIR2, { recursive: true });
3317
4173
  const state = {
3318
4174
  targetId: target.id,
3319
4175
  url: target.url,
@@ -3321,7 +4177,7 @@ async function performanceStartTrace(args = {}) {
3321
4177
  pid: process.pid,
3322
4178
  categories
3323
4179
  };
3324
- await writeFile5(stateFilePath(), JSON.stringify(state), "utf8");
4180
+ await writeFile6(stateFilePath(), JSON.stringify(state), "utf8");
3325
4181
  return {
3326
4182
  target: { id: target.id, url: target.url, title: target.title },
3327
4183
  categories,
@@ -3334,7 +4190,7 @@ async function performanceStopTrace(args = {}) {
3334
4190
  if (!entry) {
3335
4191
  let hint = "";
3336
4192
  try {
3337
- const raw = await readFile5(stateFilePath(), "utf8");
4193
+ const raw = await readFile6(stateFilePath(), "utf8");
3338
4194
  const state = JSON.parse(raw);
3339
4195
  hint = state.pid === process.pid ? ` (state file lists target ${shortId(state.targetId)} but its live connection is gone)` : ` (trace was started by a DIFFERENT process pid=${state.pid}; live recordings cannot cross process boundaries)`;
3340
4196
  } catch {}
@@ -3346,7 +4202,7 @@ async function performanceStopTrace(args = {}) {
3346
4202
  } finally {
3347
4203
  conn.close();
3348
4204
  liveTraces.delete(target.id);
3349
- await unlink3(stateFilePath()).catch(() => {});
4205
+ await unlink4(stateFilePath()).catch(() => {});
3350
4206
  }
3351
4207
  const written = await writeTrace(events, target.id);
3352
4208
  const metrics = analyzeEvents(events);
@@ -3464,7 +4320,7 @@ async function performanceAnalyzeInsight(args = {}) {
3464
4320
  }
3465
4321
  let raw;
3466
4322
  try {
3467
- raw = await readFile5(args.tracePath, "utf8");
4323
+ raw = await readFile6(args.tracePath, "utf8");
3468
4324
  } catch (err) {
3469
4325
  throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
3470
4326
  }
@@ -3482,8 +4338,8 @@ async function performanceAnalyzeInsight(args = {}) {
3482
4338
 
3483
4339
  // src/tools/screencast.ts
3484
4340
  import { spawn } from "node:child_process";
3485
- import { mkdir as mkdir6, rm, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3486
- import { join as join5 } from "node:path";
4341
+ import { mkdir as mkdir8, rm as rm2, stat as stat2, writeFile as writeFile7 } from "node:fs/promises";
4342
+ import { join as join6 } from "node:path";
3487
4343
  var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3488
4344
  var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
3489
4345
  function frameDurationsMs(ledger, stopWallclockMs) {
@@ -3539,7 +4395,7 @@ function pngSize(buf) {
3539
4395
  return;
3540
4396
  return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
3541
4397
  }
3542
- var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
4398
+ var JPEG_SOF2 = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
3543
4399
  function jpegSize(buf) {
3544
4400
  if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
3545
4401
  return;
@@ -3561,7 +4417,7 @@ function jpegSize(buf) {
3561
4417
  if (i + 3 >= buf.length)
3562
4418
  return;
3563
4419
  const segLen = buf.readUInt16BE(i + 2);
3564
- if (JPEG_SOF.has(marker)) {
4420
+ if (JPEG_SOF2.has(marker)) {
3565
4421
  if (i + 9 > buf.length)
3566
4422
  return;
3567
4423
  return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
@@ -3704,9 +4560,9 @@ async function startScreenRecording(args = {}) {
3704
4560
  conn.close();
3705
4561
  throw err;
3706
4562
  }
3707
- const spoolDir = join5(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
4563
+ const spoolDir = join6(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
3708
4564
  try {
3709
- await mkdir6(spoolDir, { recursive: true });
4565
+ await mkdir8(spoolDir, { recursive: true });
3710
4566
  } catch (err) {
3711
4567
  conn.close();
3712
4568
  throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
@@ -3747,7 +4603,7 @@ async function startScreenRecording(args = {}) {
3747
4603
  seq += 1;
3748
4604
  const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
3749
4605
  rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
3750
- rec.writes = rec.writes.then(() => writeFile6(join5(spoolDir, file), buf)).catch(() => {
4606
+ rec.writes = rec.writes.then(() => writeFile7(join6(spoolDir, file), buf)).catch(() => {
3751
4607
  rec.dropped += 1;
3752
4608
  rec.unwritten.add(file);
3753
4609
  });
@@ -3769,7 +4625,7 @@ async function startScreenRecording(args = {}) {
3769
4625
  } catch (err) {
3770
4626
  rec.off();
3771
4627
  conn.close();
3772
- await rm(spoolDir, { recursive: true, force: true }).catch(() => {});
4628
+ await rm2(spoolDir, { recursive: true, force: true }).catch(() => {});
3773
4629
  throw err;
3774
4630
  }
3775
4631
  liveRecordings.set(target.id, rec);
@@ -3807,14 +4663,14 @@ async function stopScreenRecording(args = {}) {
3807
4663
  liveRecordings.delete(rec.target.id);
3808
4664
  const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
3809
4665
  if (!ledger.length) {
3810
- await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
4666
+ await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
3811
4667
  throw new CdpError(`screen recording captured 0 frames for target ${shortId2(rec.target.id)}. Chrome emits a screencast frame only on repaint: a fully backgrounded or occluded tab may never paint. Retry with bringToFront:true, or drive the page while recording.`);
3812
4668
  }
3813
- await mkdir6(ARTIFACT_DIR4, { recursive: true });
3814
- const outPath = args.savePath ?? join5(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
4669
+ await mkdir8(ARTIFACT_DIR4, { recursive: true });
4670
+ const outPath = args.savePath ?? join6(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
3815
4671
  const frames = coalesceToConcatGrid(ledger, stoppedAt);
3816
- const manifestPath = join5(rec.spoolDir, "frames.ffconcat");
3817
- await writeFile6(manifestPath, buildConcatManifest(frames), "utf8");
4672
+ const manifestPath = join6(rec.spoolDir, "frames.ffconcat");
4673
+ await writeFile7(manifestPath, buildConcatManifest(frames), "utf8");
3818
4674
  const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
3819
4675
  const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
3820
4676
  let res;
@@ -3828,7 +4684,7 @@ async function stopScreenRecording(args = {}) {
3828
4684
  ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
3829
4685
  }
3830
4686
  const { size } = await stat2(outPath);
3831
- await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
4687
+ await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
3832
4688
  return {
3833
4689
  path: outPath,
3834
4690
  bytes: size,
@@ -3845,8 +4701,8 @@ ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
3845
4701
  }
3846
4702
 
3847
4703
  // src/tools/heap.ts
3848
- import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
3849
- import { join as join6 } from "node:path";
4704
+ import { mkdir as mkdir9, writeFile as writeFile8 } from "node:fs/promises";
4705
+ import { join as join7 } from "node:path";
3850
4706
  var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3851
4707
  function stamp4(targetId) {
3852
4708
  const short = targetId.slice(0, 8);
@@ -3871,9 +4727,9 @@ async function takeHeapsnapshot(args = {}) {
3871
4727
  if (snapshot.length === 0) {
3872
4728
  throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
3873
4729
  }
3874
- await mkdir7(ARTIFACT_DIR5, { recursive: true });
3875
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join6(ARTIFACT_DIR5, args.savePath) : join6(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
3876
- await writeFile7(path, snapshot, "utf8");
4730
+ await mkdir9(ARTIFACT_DIR5, { recursive: true });
4731
+ const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(ARTIFACT_DIR5, args.savePath) : join7(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
4732
+ await writeFile8(path, snapshot, "utf8");
3877
4733
  const bytes = Buffer.byteLength(snapshot, "utf8");
3878
4734
  return {
3879
4735
  path,
@@ -3886,7 +4742,7 @@ async function takeHeapsnapshot(args = {}) {
3886
4742
 
3887
4743
  // src/tools/lighthouse.ts
3888
4744
  import { spawn as spawn2 } from "node:child_process";
3889
- import { mkdir as mkdir8, readFile as readFile6, stat as stat3 } from "node:fs/promises";
4745
+ import { mkdir as mkdir10, readFile as readFile7, stat as stat3 } from "node:fs/promises";
3890
4746
  var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3891
4747
  function cdpPort() {
3892
4748
  const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
@@ -3947,7 +4803,7 @@ async function lighthouseAudit(args) {
3947
4803
  const url = args.url.trim();
3948
4804
  const timeoutMs = args.timeoutMs ?? 120000;
3949
4805
  const port = cdpPort();
3950
- await mkdir8(ARTIFACT_DIR6, { recursive: true });
4806
+ await mkdir10(ARTIFACT_DIR6, { recursive: true });
3951
4807
  const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
3952
4808
  const formFactor = args.formFactor ?? "desktop";
3953
4809
  const lhArgs = [
@@ -3976,7 +4832,7 @@ async function lighthouseAudit(args) {
3976
4832
  }
3977
4833
  let raw;
3978
4834
  try {
3979
- raw = await readFile6(outPath, "utf8");
4835
+ raw = await readFile7(outPath, "utf8");
3980
4836
  } catch {
3981
4837
  raw = undefined;
3982
4838
  }
@@ -4063,14 +4919,14 @@ async function dispatchMouse(args) {
4063
4919
  }
4064
4920
 
4065
4921
  // src/tools/downloads.ts
4066
- import { readdir as readdir3, rename, stat as stat4 } from "node:fs/promises";
4067
- import { basename, extname, join as join8 } from "node:path";
4922
+ import { readdir as readdir3, rename as rename2, stat as stat4 } from "node:fs/promises";
4923
+ import { basename, extname, join as join9 } from "node:path";
4068
4924
 
4069
4925
  // src/tools/browser-session.ts
4070
- import { mkdir as mkdir9 } from "node:fs/promises";
4071
- import { join as join7 } from "node:path";
4926
+ import { mkdir as mkdir11 } from "node:fs/promises";
4927
+ import { join as join8 } from "node:path";
4072
4928
  var ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
4073
- var DOWNLOADS_DIR = join7(ARTIFACT_DIR7, "downloads");
4929
+ var DOWNLOADS_DIR = join8(ARTIFACT_DIR7, "downloads");
4074
4930
  var DOWNLOAD_BUFFER_LIMIT = 50;
4075
4931
  var session;
4076
4932
  function isDisconnect(e) {
@@ -4142,7 +4998,7 @@ async function browserSend(method, params = {}) {
4142
4998
  }
4143
4999
  }
4144
5000
  async function armDownloadsAt(path) {
4145
- await mkdir9(path, { recursive: true });
5001
+ await mkdir11(path, { recursive: true });
4146
5002
  const s = await getBrowserSession();
4147
5003
  await s.conn.send("Browser.setDownloadBehavior", { behavior: "allowAndName", downloadPath: path, eventsEnabled: true });
4148
5004
  s.armedPath = path;
@@ -4232,8 +5088,8 @@ async function waitForDownload(args = {}) {
4232
5088
  const taken = new Set(await readdir3(DOWNLOADS_DIR).catch(() => []));
4233
5089
  taken.delete(basename(rec.filePath));
4234
5090
  const finalName = nextFreeName(suggestedFilename, taken);
4235
- const path = join8(DOWNLOADS_DIR, finalName);
4236
- await rename(rec.filePath, path);
5091
+ const path = join9(DOWNLOADS_DIR, finalName);
5092
+ await rename2(rec.filePath, path);
4237
5093
  const bytes = (await stat4(path)).size;
4238
5094
  return { path, suggestedFilename, bytes, ...rec.url ? { url: rec.url } : {}, target };
4239
5095
  }
@@ -5234,7 +6090,7 @@ var MANIFEST = [
5234
6090
  },
5235
6091
  {
5236
6092
  name: "take_screenshot",
5237
- description: "Capture the viewport (default), the full scrollable page (fullPage), or a single element (uid or selector, exactly one and mutually exclusive) via raw CDP Page.captureScreenshot. Writes a PNG/JPEG under /tmp/cdp-toolkit (override with savePath) and returns {path,bytes,format,target}; raw base64 is only included when returnBase64 is set. quality applies to jpeg only.",
6093
+ description: "Capture the viewport (default), the full scrollable page (fullPage), or a single element (uid or selector, exactly one and mutually exclusive) via raw CDP Page.captureScreenshot — as one image when it fits, or as vertical bands stitched losslessly into one PNG when it does not (see tile). Writes a PNG/JPEG under /tmp/cdp-toolkit (override with savePath; its directory is created for you) and returns {path,bytes,format,target,scale} plus width/height MEASURED from the encoded bytes, which are omitted rather than guessed when the bytes cannot be decoded; renderSize/renderRestored (+renderRestoreError) accompany a renderWidth/renderHeight capture, tiled/bands a banded one. Raw base64 is only included when returnBase64 is set, and is refused on a banded capture. quality applies to jpeg only. Every capture costs one Page.getLayoutMetrics round trip, because Chrome does not refuse an oversized capture politely: past 16384 device px per side Page.captureScreenshot can simply never answer, and captureBeyondViewport leaves the tab resized to the clip, so the size is checked before the capture rather than after.",
5238
6094
  inputSchema: {
5239
6095
  type: "object",
5240
6096
  properties: {
@@ -5262,6 +6118,22 @@ var MANIFEST = [
5262
6118
  type: "boolean",
5263
6119
  description: "Capture the full scrollable content height computed from Page.getLayoutMetrics, not just the viewport."
5264
6120
  },
6121
+ scale: {
6122
+ type: "number",
6123
+ description: "Output-pixel multiplier for this one capture: greater than 0, at most 8, default 1. Output px = ceil(css px x scale x devicePixelRatio), so on a ratio-2 display scale 3 is 6x the CSS size; the page is never told anything changed (devicePixelRatio and innerWidth/innerHeight read identically before and after). Chrome only (capability screenshot.scale) — WebDriver BiDi's browsingContext.captureScreenshot has no scale parameter, so under Firefox call emulate with deviceScaleFactor first and capture at scale 1. A projection past Chrome's 16384 device px per side is refused before the browser is dialled, naming the largest scale that fits."
6124
+ },
6125
+ renderWidth: {
6126
+ type: "number",
6127
+ description: "Emulate this viewport width, in CSS px (integer, 1-16384), for the length of ONE capture, then put the previous viewport back. REQUIRED together with renderHeight. Not the same knob as scale: scale re-renders the same layout at more pixels, this changes what the page BELIEVES, so media queries flip and the document re-lays-out — which is how you capture a responsive page at 1920x1080 from a tab that is not that size. Declared by BOTH backends (capability screenshot.renderSize). The restore can only put back an override this toolkit itself applied: one set by the DevTools UI, another client, or a pre-1.9.2 build is invisible and gets reset to the real device. The result reports renderSize and renderRestored; renderRestored:false means the tab is STILL emulated and you have to fix it with emulate."
6128
+ },
6129
+ renderHeight: {
6130
+ type: "number",
6131
+ description: "Emulate this viewport height, in CSS px (integer, 1-16384), for the length of one capture. Required together with renderWidth — a viewport is two numbers, and honouring one while inventing the other would render at a size you never named and never see in the result. Note a narrow renderWidth reflows most documents TALLER, which is the usual way a render-size capture trips the 16384-device-px encode limit."
6132
+ },
6133
+ tile: {
6134
+ type: "boolean",
6135
+ description: "Whether this capture may be taken as vertical bands and stitched into one lossless PNG. Three-valued: omitted = AUTO (one shot when the projected output fits under Chrome's 16384 device px per side, bands when it does not — this is why a whole-page capture of a very long page now returns a file instead of hanging and wedging the tab); false = never band, refuse instead (the pre-1.9.2 behaviour); true = always band, even when one shot would have fitted. Only true costs a capability (screenshot.tile, Chrome only; Firefox was never driven for the band arithmetic). A banded capture is PNG-only and cannot also returnBase64, and only HEIGHT is tiled — a projected WIDTH past the limit is refused, not split, because bands are stitched by concatenating scanlines. Bands are a faithful crop of one document render: fixed and sticky elements do not repeat per band and seams are pixel-exact, but content the page only loads on real scroll (lazy images, virtualized lists) renders blank in bands the real viewport never reached, which is a property of the page and not of the stitch."
6136
+ },
5265
6137
  uid: {
5266
6138
  type: "number",
5267
6139
  description: "Element to clip to: a CDP backendDOMNodeId obtained from take_snapshot. Mutually exclusive with selector."
@@ -5272,11 +6144,11 @@ var MANIFEST = [
5272
6144
  },
5273
6145
  savePath: {
5274
6146
  type: "string",
5275
- description: "Override the output file path. Default: /tmp/cdp-toolkit/screenshot-<id>-<stamp>.<ext>."
6147
+ description: "Override the output file path. Default: /tmp/cdp-toolkit/screenshot-<id>-<stamp>.<ext>, where the stamp is the capture's START (a banded capture writes as it goes and has no finished image to name a file after). The destination directory is created from this path's own dirname, so a savePath outside /tmp/cdp-toolkit works."
5276
6148
  },
5277
6149
  returnBase64: {
5278
6150
  type: "boolean",
5279
- description: "Also return the raw base64 image bytes in the result, in addition to writing the file."
6151
+ description: "Also return the raw base64 image bytes in the result, in addition to writing the file. Refused on a banded capture (see tile): a stitched image runs to hundreds of megabytes and base64 inflates it by a third, into the caller's response — read the file at the returned path instead."
5280
6152
  }
5281
6153
  },
5282
6154
  required: [],
@@ -5285,7 +6157,7 @@ var MANIFEST = [
5285
6157
  },
5286
6158
  {
5287
6159
  name: "emulate",
5288
- description: "Apply any subset of Chrome emulation overrides in one call: device metrics (width/height together, plus deviceScaleFactor/mobile), userAgent, cpuThrottlingRate (>=1), emulated media type + mediaFeatures, and networkConditions. Pass clearOverrides:true to reset every override to the browser default (ignores all other fields). Non-metrics overrides are session-scoped and reset when the per-call connection closes; device-metrics overrides persist on the target until cleared or the renderer navigates/reloads.",
6160
+ description: "Apply any subset of Chrome emulation overrides in one call: device metrics (width/height together, plus deviceScaleFactor/mobile), userAgent, cpuThrottlingRate (>=1), emulated media type + mediaFeatures, and networkConditions. Pass clearOverrides:true to reset every override to the browser default (ignores all other fields); as of 1.9.2 that genuinely clears a device-metrics override set by a DIFFERENT process (Emulation.clearDeviceMetricsOverride is a no-op unless the clearing connection also set the override, so the driver re-asserts the tab's current size on its own connection first, then clears — and throws rather than answering cleared:true if it cannot verify that). Non-metrics overrides are session-scoped and reset when the per-call connection closes; device-metrics overrides persist on the target until cleared or the renderer navigates/reloads.",
5289
6161
  inputSchema: {
5290
6162
  type: "object",
5291
6163
  properties: {
@@ -6184,10 +7056,15 @@ class BidiConnection {
6184
7056
  } catch {}
6185
7057
  }
6186
7058
  }
6187
- async function connectBidiSession(port, opts = {}) {
6188
- const conn = new BidiConnection(`ws://127.0.0.1:${port}/session`, { timeoutMs: opts.timeoutMs });
7059
+ async function connectBidiSessionUrl(wsUrl, opts = {}) {
7060
+ const conn = new BidiConnection(wsUrl, { timeoutMs: opts.timeoutMs });
6189
7061
  await conn.connect();
6190
- await conn.newSession(opts.capabilities ?? {});
7062
+ try {
7063
+ await conn.newSession(opts.capabilities ?? {});
7064
+ } catch (e) {
7065
+ conn.dispose();
7066
+ throw e;
7067
+ }
6191
7068
  conn.retain();
6192
7069
  return conn;
6193
7070
  }
@@ -6763,21 +7640,27 @@ var BIDI_CAPABILITIES = new Set([
6763
7640
  "emulate.deviceMetrics",
6764
7641
  "screenshot.fullPage",
6765
7642
  "screenshot.element",
7643
+ "screenshot.renderSize",
6766
7644
  "network.intercept",
6767
7645
  "locate.xpath"
6768
7646
  ]);
6769
7647
  var connections = new Map;
6770
- async function getConnection(port, timeoutMs) {
6771
- const existing = connections.get(port);
7648
+ async function getConnection(endpoint, timeoutMs) {
7649
+ const existing = connections.get(endpoint);
6772
7650
  if (existing) {
6773
7651
  existing.retain();
6774
7652
  return existing;
6775
7653
  }
6776
- const conn = await connectBidiSession(port, {
7654
+ const conn = await connectBidiSessionUrl(endpoint, {
6777
7655
  ...timeoutMs !== undefined ? { timeoutMs } : {},
6778
7656
  capabilities: { alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } } }
7657
+ }).catch((e) => {
7658
+ if (e instanceof BidiError && /maximum number of active sessions/i.test(e.message)) {
7659
+ throw driverError2("disconnected", `Firefox already has an active WebDriver BiDi session on ${endpoint}; it allows only one. ` + `Close the other client, or restart Firefox, then retry. (A previously killed client can leave a ` + `session that Firefox does not reap.)`);
7660
+ }
7661
+ throw e;
6779
7662
  });
6780
- connections.set(port, conn);
7663
+ connections.set(endpoint, conn);
6781
7664
  return conn;
6782
7665
  }
6783
7666
  var bidiBeaconScripts = new Map;
@@ -7126,7 +8009,55 @@ class BidiPageDriver {
7126
8009
  throw mapBidiError(e);
7127
8010
  }
7128
8011
  }
7129
- async screenshot(opts) {
8012
+ async screenshot(opts, _consumeBands) {
8013
+ const renderSize = opts?.renderWidth !== undefined && opts?.renderHeight !== undefined ? { width: opts.renderWidth, height: opts.renderHeight } : undefined;
8014
+ if (!renderSize)
8015
+ return this.capture(opts);
8016
+ const previous = await readViewportRecord(this.browser.scheme, this.contextId);
8017
+ try {
8018
+ await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: { width: renderSize.width, height: renderSize.height } });
8019
+ } catch (e) {
8020
+ throw mapBidiError(e);
8021
+ }
8022
+ let shot;
8023
+ let captureError;
8024
+ try {
8025
+ shot = await this.capture(opts);
8026
+ } catch (e) {
8027
+ captureError = e;
8028
+ }
8029
+ let restoreError;
8030
+ try {
8031
+ if (previous) {
8032
+ await this.conn.send("browsingContext.setViewport", {
8033
+ context: this.contextId,
8034
+ viewport: { width: previous.width, height: previous.height },
8035
+ ...previous.deviceScaleFactor != null ? { devicePixelRatio: previous.deviceScaleFactor } : {}
8036
+ });
8037
+ } else {
8038
+ await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: null });
8039
+ }
8040
+ } catch (e) {
8041
+ restoreError = e instanceof Error ? e.message : String(e);
8042
+ }
8043
+ if (!shot) {
8044
+ if (restoreError !== undefined && captureError instanceof Error) {
8045
+ captureError.message = `${captureError.message} ${renderRestoreFailureNote(renderSize, restoreError)}`;
8046
+ }
8047
+ throw captureError;
8048
+ }
8049
+ const result = { ...shot, renderSize, renderRestored: restoreError === undefined };
8050
+ if (restoreError !== undefined)
8051
+ result.renderRestoreError = restoreError;
8052
+ return result;
8053
+ }
8054
+ async capture(opts) {
8055
+ if (opts?.scale !== undefined && opts.scale !== 1) {
8056
+ throw driverError2("unsupported", `screenshot scale ${opts.scale} is not supported by this backend (WebDriver BiDi's ` + "browsingContext.captureScreenshot has no scale parameter). Call emulate with " + `deviceScaleFactor:${opts.scale} first and capture at scale 1, or use --browser chrome.`);
8057
+ }
8058
+ if (opts?.tile === true) {
8059
+ throw driverError2("unsupported", "screenshot tile:true is not supported by this backend: band tiling is declared only where the band " + "arithmetic has been measured against a real browser, and Firefox was not driven for it " + `(browsingContext.captureScreenshot's clip:{type:"box"} is the primitive it would ride, untested). ` + "Capture a smaller region or a single element, or use --browser chrome for a tiled capture.");
8060
+ }
7130
8061
  const format = opts?.format ?? "png";
7131
8062
  const params = {
7132
8063
  context: this.contextId,
@@ -7151,12 +8082,18 @@ class BidiPageDriver {
7151
8082
  await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: null, devicePixelRatio: null }).catch(() => {
7152
8083
  return;
7153
8084
  });
8085
+ await clearViewportRecord(this.browser.scheme, this.contextId);
7154
8086
  return { applied: [] };
7155
8087
  }
7156
8088
  const applied = [];
7157
8089
  try {
7158
8090
  if (opts.width != null && opts.height != null) {
7159
8091
  await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: { width: opts.width, height: opts.height }, ...opts.deviceScaleFactor != null ? { devicePixelRatio: opts.deviceScaleFactor } : {} });
8092
+ await writeViewportRecord(this.browser.scheme, this.contextId, {
8093
+ width: opts.width,
8094
+ height: opts.height,
8095
+ ...opts.deviceScaleFactor != null ? { deviceScaleFactor: opts.deviceScaleFactor } : {}
8096
+ });
7160
8097
  applied.push("deviceMetrics");
7161
8098
  }
7162
8099
  if (opts.userAgent != null) {
@@ -7364,19 +8301,19 @@ class BidiPageDriver {
7364
8301
  }
7365
8302
 
7366
8303
  class BidiBrowserDriver {
7367
- port;
8304
+ endpoint;
7368
8305
  timeoutMs;
7369
8306
  scheme = "bidi";
7370
8307
  lifetime = "session";
7371
8308
  capabilities = BIDI_CAPABILITIES;
7372
8309
  uidStability = "document-stamp";
7373
8310
  snapshotFidelity = "dom-heuristic";
7374
- constructor(port, timeoutMs) {
7375
- this.port = port;
8311
+ constructor(endpoint, timeoutMs) {
8312
+ this.endpoint = endpoint;
7376
8313
  this.timeoutMs = timeoutMs;
7377
8314
  }
7378
8315
  async listPages() {
7379
- const conn = await getConnection(this.port, this.timeoutMs);
8316
+ const conn = await getConnection(this.endpoint, this.timeoutMs);
7380
8317
  try {
7381
8318
  const { contexts } = await conn.send("browsingContext.getTree", {}).catch((e) => {
7382
8319
  throw mapBidiError(e);
@@ -7387,7 +8324,7 @@ class BidiBrowserDriver {
7387
8324
  }
7388
8325
  }
7389
8326
  async newPage(url) {
7390
- const conn = await getConnection(this.port, this.timeoutMs);
8327
+ const conn = await getConnection(this.endpoint, this.timeoutMs);
7391
8328
  try {
7392
8329
  const { context } = await conn.send("browsingContext.create", { type: "tab" }).catch((e) => {
7393
8330
  throw mapBidiError(e);
@@ -7402,7 +8339,7 @@ class BidiBrowserDriver {
7402
8339
  }
7403
8340
  }
7404
8341
  async closePage(id) {
7405
- const conn = await getConnection(this.port, this.timeoutMs);
8342
+ const conn = await getConnection(this.endpoint, this.timeoutMs);
7406
8343
  try {
7407
8344
  await conn.send("browsingContext.close", { context: id }).catch((e) => {
7408
8345
  throw mapBidiError(e);
@@ -7413,7 +8350,7 @@ class BidiBrowserDriver {
7413
8350
  }
7414
8351
  }
7415
8352
  async activatePage(id) {
7416
- const conn = await getConnection(this.port, this.timeoutMs);
8353
+ const conn = await getConnection(this.endpoint, this.timeoutMs);
7417
8354
  try {
7418
8355
  await conn.send("browsingContext.activate", { context: id }).catch((e) => {
7419
8356
  throw mapBidiError(e);
@@ -7425,7 +8362,7 @@ class BidiBrowserDriver {
7425
8362
  }
7426
8363
  }
7427
8364
  async page(selector) {
7428
- const conn = await getConnection(this.port, this.timeoutMs);
8365
+ const conn = await getConnection(this.endpoint, this.timeoutMs);
7429
8366
  let ctx;
7430
8367
  try {
7431
8368
  ctx = await resolveContext(conn, selector);
@@ -7437,13 +8374,13 @@ class BidiBrowserDriver {
7437
8374
  return new BidiPageDriver(conn, ctx.context, info, this);
7438
8375
  }
7439
8376
  async installActivityBeacon(contextId) {
7440
- const conn = await getConnection(this.port, this.timeoutMs).catch(() => {
8377
+ const conn = await getConnection(this.endpoint, this.timeoutMs).catch(() => {
7441
8378
  return;
7442
8379
  });
7443
8380
  if (!conn)
7444
8381
  return false;
7445
8382
  try {
7446
- const key = `${this.port}:${contextId}`;
8383
+ const key = `${this.endpoint}:${contextId}`;
7447
8384
  if (!bidiBeaconScripts.has(key)) {
7448
8385
  const registered = await conn.send("script.addPreloadScript", {
7449
8386
  functionDeclaration: BEACON_FUNCTION_DECLARATION,
@@ -7464,7 +8401,7 @@ class BidiBrowserDriver {
7464
8401
  }
7465
8402
  }
7466
8403
  async readActivityBeacon(contextId) {
7467
- const conn = await getConnection(this.port, this.timeoutMs).catch(() => {
8404
+ const conn = await getConnection(this.endpoint, this.timeoutMs).catch(() => {
7468
8405
  return;
7469
8406
  });
7470
8407
  if (!conn)
@@ -7487,18 +8424,24 @@ class BidiBrowserDriver {
7487
8424
  }
7488
8425
  async dispose() {
7489
8426
  for (const key of [...bidiBeaconScripts.keys()]) {
7490
- if (key.startsWith(`${this.port}:`))
8427
+ if (key.startsWith(`${this.endpoint}:`))
7491
8428
  bidiBeaconScripts.delete(key);
7492
8429
  }
7493
- const conn = connections.get(this.port);
8430
+ const conn = connections.get(this.endpoint);
7494
8431
  if (!conn)
7495
8432
  return;
7496
- connections.delete(this.port);
8433
+ connections.delete(this.endpoint);
8434
+ await conn.send("session.end", {}).catch(() => {
8435
+ return;
8436
+ });
7497
8437
  conn.dispose();
7498
8438
  }
7499
8439
  }
7500
8440
  function createFirefoxDriver(port, opts) {
7501
- return new BidiBrowserDriver(port, opts?.timeoutMs);
8441
+ return new BidiBrowserDriver(`ws://127.0.0.1:${port}/session`, opts?.timeoutMs);
8442
+ }
8443
+ function createFirefoxDriverForEndpoint(wsUrl, opts) {
8444
+ return new BidiBrowserDriver(wsUrl, opts?.timeoutMs);
7502
8445
  }
7503
8446
 
7504
8447
  // src/bidi/launch.ts
@@ -7506,7 +8449,7 @@ import { spawn as spawn3 } from "node:child_process";
7506
8449
  import { mkdtempSync, rmSync } from "node:fs";
7507
8450
  import { createConnection, createServer } from "node:net";
7508
8451
  import { tmpdir } from "node:os";
7509
- import { join as join9 } from "node:path";
8452
+ import { join as join10 } from "node:path";
7510
8453
  function resolveBinary() {
7511
8454
  const candidates = [
7512
8455
  process.env.FIREFOX_BIN,
@@ -7580,7 +8523,7 @@ async function launchFirefox(opts = {}) {
7580
8523
  const binary = resolveBinary();
7581
8524
  const port = await pickPort(opts.port);
7582
8525
  const isThrowaway = opts.profilePath === undefined;
7583
- const profilePath = opts.profilePath ?? mkdtempSync(join9(tmpdir(), "cdp-toolkit-ff-profile-"));
8526
+ const profilePath = opts.profilePath ?? mkdtempSync(join10(tmpdir(), "cdp-toolkit-ff-profile-"));
7584
8527
  const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
7585
8528
  if (opts.headless)
7586
8529
  args.push("--headless");
@@ -7618,10 +8561,10 @@ async function launchFirefox(opts = {}) {
7618
8561
  }
7619
8562
 
7620
8563
  // src/backend.ts
7621
- function resolveBrowserKind(argv) {
8564
+ function resolveBrowserKind(argv, env = process.env) {
7622
8565
  const flagIdx = argv.indexOf("--browser");
7623
8566
  const flagVal = flagIdx !== -1 ? argv[flagIdx + 1] : undefined;
7624
- const raw = flagVal ?? process.env.CDP_BROWSER ?? "chrome";
8567
+ const raw = flagVal ?? env.CDP_BROWSER ?? "chrome";
7625
8568
  if (raw !== "chrome" && raw !== "firefox") {
7626
8569
  throw new Error(`unknown --browser '${raw}': expected 'chrome' or 'firefox' (or set CDP_BROWSER)`);
7627
8570
  }
@@ -7638,7 +8581,79 @@ function stripBrowserFlag(argv) {
7638
8581
  }
7639
8582
  return out;
7640
8583
  }
8584
+ function normalizeBidiEndpoint(raw) {
8585
+ const value = raw.trim();
8586
+ if (/^wss?:\/\//i.test(value)) {
8587
+ let url;
8588
+ try {
8589
+ url = new URL(value);
8590
+ } catch {
8591
+ throw new Error(`invalid Firefox endpoint '${raw}': not a parseable ws:// URL`);
8592
+ }
8593
+ if (url.pathname === "" || url.pathname === "/")
8594
+ url.pathname = "/session";
8595
+ return url.toString();
8596
+ }
8597
+ if (/^\d+$/.test(value))
8598
+ return `ws://127.0.0.1:${assertPort(value, raw)}/session`;
8599
+ const hostPort = /^([^:/]+):(\d+)$/.exec(value);
8600
+ if (hostPort)
8601
+ return `ws://${hostPort[1]}:${assertPort(hostPort[2], raw)}/session`;
8602
+ throw new Error(`invalid Firefox endpoint '${raw}': expected a port (9223), host:port, or a ws:// URL`);
8603
+ }
8604
+ function assertPort(digits, raw) {
8605
+ const port = Number(digits);
8606
+ if (!Number.isInteger(port) || port < 1 || port > 65535) {
8607
+ throw new Error(`invalid Firefox endpoint '${raw}': port must be 1-65535`);
8608
+ }
8609
+ return port;
8610
+ }
8611
+ function resolveFirefoxEndpoint(argv, env = process.env) {
8612
+ const flagIdx = argv.indexOf("--connect");
8613
+ const raw = (flagIdx !== -1 ? argv[flagIdx + 1] : undefined) ?? env.CDP_FIREFOX_ENDPOINT;
8614
+ if (raw === undefined || raw === "")
8615
+ return;
8616
+ return normalizeBidiEndpoint(raw);
8617
+ }
8618
+ function stripConnectFlag(argv) {
8619
+ const out = [];
8620
+ for (let i = 0;i < argv.length; i++) {
8621
+ if (argv[i] === "--connect") {
8622
+ i++;
8623
+ continue;
8624
+ }
8625
+ out.push(argv[i]);
8626
+ }
8627
+ return out;
8628
+ }
8629
+ function resolveBackend(argv, env = process.env) {
8630
+ const endpoint = resolveFirefoxEndpoint(argv, env);
8631
+ let kind = resolveBrowserKind(argv, env);
8632
+ const explicitBrowser = argv.includes("--browser") || env.CDP_BROWSER !== undefined;
8633
+ if (endpoint !== undefined) {
8634
+ if (explicitBrowser && kind === "chrome") {
8635
+ throw new Error("--connect / CDP_FIREFOX_ENDPOINT is a Firefox attach option and is not valid with the chrome backend");
8636
+ }
8637
+ kind = "firefox";
8638
+ }
8639
+ return endpoint !== undefined ? { kind, endpoint } : { kind };
8640
+ }
7641
8641
  async function startFirefoxSession(opts) {
8642
+ if (opts?.endpoint !== undefined) {
8643
+ const driver2 = createFirefoxDriverForEndpoint(opts.endpoint);
8644
+ let disposed2 = false;
8645
+ return {
8646
+ driver: driver2,
8647
+ async dispose() {
8648
+ if (disposed2)
8649
+ return;
8650
+ disposed2 = true;
8651
+ await driver2.dispose().catch(() => {
8652
+ return;
8653
+ });
8654
+ }
8655
+ };
8656
+ }
7642
8657
  const launched = await launchFirefox({ headless: opts?.headless });
7643
8658
  const driver = createFirefoxDriver(launched.port);
7644
8659
  let disposed = false;
@@ -7656,9 +8671,9 @@ async function startFirefoxSession(opts) {
7656
8671
  };
7657
8672
  }
7658
8673
  var sessionPromise;
7659
- function getOrCreateFirefoxSession() {
8674
+ function getOrCreateFirefoxSession(opts) {
7660
8675
  if (!sessionPromise)
7661
- sessionPromise = startFirefoxSession();
8676
+ sessionPromise = startFirefoxSession({ endpoint: opts?.endpoint });
7662
8677
  return sessionPromise;
7663
8678
  }
7664
8679
  async function disposeFirefoxSession() {
@@ -7859,8 +8874,8 @@ var FIREFOX_TOOLS = {
7859
8874
  };
7860
8875
 
7861
8876
  // src/mcp.ts
7862
- var VERSION = "1.9.1";
7863
- var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
8877
+ var VERSION = "1.9.5";
8878
+ var { kind: BROWSER, endpoint: FIREFOX_ENDPOINT } = resolveBackend(process.argv.slice(2));
7864
8879
  var AVAILABILITY = toolAvailability(BROWSER);
7865
8880
  var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
7866
8881
  var dispatch = TOOLS;
@@ -7906,7 +8921,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7906
8921
  if (!neutralFn)
7907
8922
  return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
7908
8923
  try {
7909
- const session2 = await getOrCreateFirefoxSession();
8924
+ const session2 = await getOrCreateFirefoxSession({ endpoint: FIREFOX_ENDPOINT });
7910
8925
  const result = await neutralFn(session2.driver, args);
7911
8926
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7912
8927
  } catch (err) {
@@ -7918,7 +8933,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7918
8933
  auditCoverage();
7919
8934
  markLongLivedProcess();
7920
8935
  await server.connect(new StdioServerTransport);
7921
- console.error(`[cdp-toolkit] MCP server v${VERSION} ready, browser=${BROWSER}, ${AVAILABILITY.available.length} tools, CDP_BASE=${BASE}`);
8936
+ console.error(`[cdp-toolkit] MCP server v${VERSION} ready, browser=${BROWSER}${FIREFOX_ENDPOINT ? ` (attach ${FIREFOX_ENDPOINT})` : ""}, ${AVAILABILITY.available.length} tools, CDP_BASE=${BASE}`);
7922
8937
  var shuttingDown = false;
7923
8938
  async function shutdown() {
7924
8939
  if (shuttingDown)