cdp-toolkit 1.9.0 → 1.9.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -404,6 +404,12 @@ async function resolveLiveLabel(backend, label, liveIds) {
404
404
 
405
405
  // src/workers.ts
406
406
  var WORKER_SELECTOR_PREFIX = "worker:";
407
+ var WORKER_CAPABLE_TOOLS = [
408
+ "evaluate_script",
409
+ "list_network_requests",
410
+ "get_network_request",
411
+ "list_console_messages"
412
+ ];
407
413
  var WORKER_TARGET_TYPES = new Set(["service_worker", "shared_worker"]);
408
414
  function isWorkerSelector(selector) {
409
415
  return typeof selector === "string" && selector.startsWith(WORKER_SELECTOR_PREFIX);
@@ -422,8 +428,18 @@ function resolveWorkerTargets(targets, needle) {
422
428
  };
423
429
  }
424
430
  var WORKER_EMPTY_NEEDLE_MESSAGE = "'worker:' needs a substring of the worker's url, e.g. worker:<extension-id> or worker:background.js";
425
- var WORKER_SELECTOR_UNSUPPORTED_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, which this backend cannot address " + "(WebDriver BiDi has no extension service-worker target; Capability 'worker.targets' is Chrome-only). " + "Use --browser chrome to evaluate in an extension's background worker.";
426
- var WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless). evaluate_script is the one tool that " + "accepts a worker: target.";
431
+ var WORKER_SELECTOR_UNSUPPORTED_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, which this backend cannot address " + "(WebDriver BiDi has no extension service-worker target; Capability 'worker.targets' is Chrome-only). " + "Use --browser chrome to evaluate in, or record console/network from, an extension's background worker.";
432
+ var WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless). The tools that accept a worker: target are " + `${WORKER_CAPABLE_TOOLS.join(", ")}.`;
433
+ function workerWakeMisuseMessage(kind) {
434
+ if (kind === "not-a-worker-selector") {
435
+ return "'wake' only applies to a target of the form 'worker:<substring>'. A page is never asleep, and a bare " + "worker target id cannot be woken either: Chrome destroys the target when an MV3 worker is evicted and " + "mints a NEW id when it restarts, so only the url-substring form can name a worker that is not running.";
436
+ }
437
+ return "'wake' only applies when a capture is being started (reload:true, or includeBody with url). Reading an " + "existing buffer cannot use a woken worker: a restarted MV3 worker gets a NEW target id, and the buffer is " + "keyed by id, so the wake would hand back an empty capture. Re-run with reload:true to record a fresh one.";
438
+ }
439
+ function workerBufferReadMissMessage(needle, liveWorkers) {
440
+ const running = liveWorkers.length ? ` (running workers: ${urlList(liveWorkers)})` : " (no workers are running)";
441
+ return `no running worker whose url contains '${needle}'${running}. ` + "An MV3 extension service worker is idle-evicted after seconds of inactivity and then appears in NO target " + "listing, so this is usually a sleeping worker rather than a wrong substring. This call only READS a buffer " + "recorded earlier, and that buffer is keyed by the worker's target id, which Chrome re-mints on restart — so " + "re-run with reload:true, which starts the worker and records a fresh capture window.";
442
+ }
427
443
  function urlList(workers) {
428
444
  return workers.map((w) => w.url).join(", ");
429
445
  }
@@ -1103,6 +1119,8 @@ async function clearMocks(args = {}) {
1103
1119
  }
1104
1120
 
1105
1121
  // src/driver.ts
1122
+ import { mkdir as mkdir3, readFile as readFile3, unlink as unlink3, writeFile as writeFile3 } from "node:fs/promises";
1123
+ import { join as join3 } from "node:path";
1106
1124
  var UID_STAMP_ATTR = "data-cdp-uid";
1107
1125
  var LEGACY_NUMERIC_UID = /^[0-9]+$/;
1108
1126
  var REQUIRED_CAPABILITIES = {
@@ -1132,6 +1150,41 @@ function interpolatePoints(from, to, steps) {
1132
1150
  out.push({ x: to.x, y: to.y });
1133
1151
  return out;
1134
1152
  }
1153
+ function isTiledCapture(delivery) {
1154
+ return delivery.tiled === true;
1155
+ }
1156
+ function viewportRecordFile(scheme, targetId) {
1157
+ return join3(leaseDir(), `viewport-${scheme}-${targetId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
1158
+ }
1159
+ async function readViewportRecord(scheme, targetId) {
1160
+ try {
1161
+ const raw = JSON.parse(await readFile3(viewportRecordFile(scheme, targetId), "utf8"));
1162
+ if (typeof raw?.width !== "number" || typeof raw?.height !== "number")
1163
+ return;
1164
+ return {
1165
+ width: raw.width,
1166
+ height: raw.height,
1167
+ ...typeof raw.deviceScaleFactor === "number" ? { deviceScaleFactor: raw.deviceScaleFactor } : {},
1168
+ ...typeof raw.mobile === "boolean" ? { mobile: raw.mobile } : {}
1169
+ };
1170
+ } catch {
1171
+ return;
1172
+ }
1173
+ }
1174
+ async function writeViewportRecord(scheme, targetId, rec) {
1175
+ try {
1176
+ await mkdir3(leaseDir(), { recursive: true });
1177
+ await writeFile3(viewportRecordFile(scheme, targetId), JSON.stringify({ scheme, targetId, ...rec }), "utf8");
1178
+ } catch {}
1179
+ }
1180
+ function renderRestoreFailureNote(size, why) {
1181
+ 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}.`;
1182
+ }
1183
+ async function clearViewportRecord(scheme, targetId) {
1184
+ try {
1185
+ await unlink3(viewportRecordFile(scheme, targetId));
1186
+ } catch {}
1187
+ }
1135
1188
 
1136
1189
  // src/cdp/driver.ts
1137
1190
  function driverError(code, message, data) {
@@ -1363,14 +1416,73 @@ async function backendNodeIdOf(conn, loc) {
1363
1416
  }
1364
1417
  return searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
1365
1418
  }
1366
- async function fullPageClip(conn) {
1367
- const m = await conn.send("Page.getLayoutMetrics");
1419
+ async function layoutMetrics(conn) {
1420
+ return conn.send("Page.getLayoutMetrics");
1421
+ }
1422
+ function contentClip(m) {
1368
1423
  const size = m.cssContentSize ?? m.contentSize;
1369
1424
  if (!size)
1370
1425
  throw driverError("page-error", "Page.getLayoutMetrics returned no content size");
1371
1426
  return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height) };
1372
1427
  }
1373
- async function elementClip(conn, loc) {
1428
+ function devicePixelsPerCssPx(m) {
1429
+ const css = m.cssVisualViewport?.clientWidth ?? m.cssLayoutViewport?.clientWidth;
1430
+ const dev = m.visualViewport?.clientWidth ?? m.layoutViewport?.clientWidth;
1431
+ if (!css || !dev)
1432
+ return 1;
1433
+ const ratio = dev / css;
1434
+ return Number.isFinite(ratio) && ratio > 0 ? ratio : 1;
1435
+ }
1436
+ function scrollOffsetCssPx(m, devicePx) {
1437
+ const css = m.cssVisualViewport ?? m.cssLayoutViewport;
1438
+ if (css)
1439
+ return { x: css.pageX ?? 0, y: css.pageY ?? 0 };
1440
+ const legacy = m.layoutViewport;
1441
+ if (legacy)
1442
+ return { x: (legacy.pageX ?? 0) / devicePx, y: (legacy.pageY ?? 0) / devicePx };
1443
+ return { x: 0, y: 0 };
1444
+ }
1445
+ var MAX_ENCODED_SIDE_PX = 16384;
1446
+ function assertClipEncodable(clip, scale, devicePx, renderSize) {
1447
+ const outWidth = Math.ceil(clip.width * scale * devicePx);
1448
+ const outHeight = Math.ceil(clip.height * scale * devicePx);
1449
+ if (outWidth <= MAX_ENCODED_SIDE_PX && outHeight <= MAX_ENCODED_SIDE_PX)
1450
+ return;
1451
+ const at = renderSize ? ` at render size ${renderSize.width}x${renderSize.height}` : "";
1452
+ 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; `;
1453
+ if (scale !== 1) {
1454
+ const fits = Math.floor(MAX_ENCODED_SIDE_PX / (Math.max(clip.width, clip.height) * devicePx) * 100) / 100;
1455
+ 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.`));
1456
+ }
1457
+ const maxCssSide = Math.floor(MAX_ENCODED_SIDE_PX / devicePx);
1458
+ 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.`));
1459
+ }
1460
+ function projectedSide(cssLength, scale, devicePx) {
1461
+ return Math.ceil(cssLength * scale * devicePx);
1462
+ }
1463
+ function planTiledCapture(clip, scale, devicePx, tile) {
1464
+ const untiled = { tiled: false, bandCount: 1, bandHeightCss: clip.height };
1465
+ if (tile === false)
1466
+ return untiled;
1467
+ const fits = projectedSide(clip.width, scale, devicePx) <= MAX_ENCODED_SIDE_PX && projectedSide(clip.height, scale, devicePx) <= MAX_ENCODED_SIDE_PX;
1468
+ if (fits && tile !== true)
1469
+ return untiled;
1470
+ const outWidth = projectedSide(clip.width, scale, devicePx);
1471
+ if (outWidth > MAX_ENCODED_SIDE_PX) {
1472
+ const maxCssWidth = Math.floor(MAX_ENCODED_SIDE_PX / (scale * devicePx));
1473
+ 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.`);
1474
+ }
1475
+ let bandHeightCss = Math.floor(MAX_ENCODED_SIDE_PX / (scale * devicePx));
1476
+ while (bandHeightCss > 1 && projectedSide(bandHeightCss, scale, devicePx) > MAX_ENCODED_SIDE_PX)
1477
+ bandHeightCss -= 1;
1478
+ if (bandHeightCss < 1) {
1479
+ 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.`);
1480
+ }
1481
+ if (tile === true && clip.height <= bandHeightCss)
1482
+ bandHeightCss = Math.max(1, Math.ceil(clip.height / 2));
1483
+ return { tiled: true, bandCount: Math.max(1, Math.ceil(clip.height / bandHeightCss)), bandHeightCss };
1484
+ }
1485
+ async function elementBox(conn, loc) {
1374
1486
  const backendNodeId = await backendNodeIdOf(conn, loc);
1375
1487
  await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => {
1376
1488
  return;
@@ -1387,6 +1499,10 @@ async function elementClip(conn, loc) {
1387
1499
  throw driverError("page-error", "resolved element has zero area");
1388
1500
  return { x: minX, y: minY, width, height };
1389
1501
  }
1502
+ function documentClip(box, m, devicePx) {
1503
+ const scroll = scrollOffsetCssPx(m, devicePx);
1504
+ return { x: box.x + scroll.x, y: box.y + scroll.y, width: box.width, height: box.height };
1505
+ }
1390
1506
  var CDP_CAPABILITIES = new Set([
1391
1507
  "trace.performance",
1392
1508
  "heap.snapshot",
@@ -1397,6 +1513,9 @@ var CDP_CAPABILITIES = new Set([
1397
1513
  "emulate.networkConditions",
1398
1514
  "screenshot.fullPage",
1399
1515
  "screenshot.element",
1516
+ "screenshot.scale",
1517
+ "screenshot.renderSize",
1518
+ "screenshot.tile",
1400
1519
  "network.intercept",
1401
1520
  "snapshot.accessibilityTree",
1402
1521
  "input.insertTextAtomic",
@@ -1687,31 +1806,128 @@ class CdpPageDriver {
1687
1806
  const { objectId } = await resolveElementLocator(this.conn, loc);
1688
1807
  await this.conn.send("DOM.setFileInputFiles", { files: [...files], objectId });
1689
1808
  }
1690
- async screenshot(opts) {
1809
+ async screenshot(opts, consumeBands) {
1810
+ const renderSize = opts?.renderWidth !== undefined && opts?.renderHeight !== undefined ? { width: opts.renderWidth, height: opts.renderHeight } : undefined;
1811
+ if (!renderSize)
1812
+ return this.capture(opts, consumeBands);
1813
+ const previous = await readViewportRecord(this.browser.scheme, this.info.id);
1814
+ await this.conn.send("Emulation.setDeviceMetricsOverride", {
1815
+ width: renderSize.width,
1816
+ height: renderSize.height,
1817
+ deviceScaleFactor: previous?.deviceScaleFactor ?? 0,
1818
+ mobile: previous?.mobile ?? false
1819
+ });
1820
+ let shot;
1821
+ let captureError;
1822
+ try {
1823
+ shot = await this.capture(opts, consumeBands);
1824
+ } catch (e) {
1825
+ captureError = e;
1826
+ }
1827
+ let restoreError;
1828
+ try {
1829
+ if (previous) {
1830
+ await this.conn.send("Emulation.setDeviceMetricsOverride", {
1831
+ width: previous.width,
1832
+ height: previous.height,
1833
+ deviceScaleFactor: previous.deviceScaleFactor ?? 0,
1834
+ mobile: previous.mobile ?? false
1835
+ });
1836
+ } else {
1837
+ await this.conn.send("Emulation.clearDeviceMetricsOverride");
1838
+ }
1839
+ } catch (e) {
1840
+ restoreError = e instanceof Error ? e.message : String(e);
1841
+ }
1842
+ if (!shot) {
1843
+ if (restoreError !== undefined && captureError instanceof Error) {
1844
+ captureError.message = `${captureError.message} ${renderRestoreFailureNote(renderSize, restoreError)}`;
1845
+ }
1846
+ throw captureError;
1847
+ }
1848
+ const result = Object.assign(shot, { renderSize, renderRestored: restoreError === undefined });
1849
+ if (restoreError !== undefined)
1850
+ result.renderRestoreError = restoreError;
1851
+ return result;
1852
+ }
1853
+ async capture(opts, consumeBands) {
1691
1854
  await this.ensureDomain("Page");
1692
1855
  const format = opts?.format ?? "png";
1693
1856
  const quality = format === "jpeg" ? opts?.quality ?? 80 : undefined;
1857
+ const scale = opts?.scale ?? 1;
1858
+ const renderSize = opts?.renderWidth !== undefined && opts?.renderHeight !== undefined ? { width: opts.renderWidth, height: opts.renderHeight } : undefined;
1859
+ const box = opts?.clip ? await elementBox(this.conn, opts.clip) : undefined;
1860
+ const metrics = await layoutMetrics(this.conn);
1861
+ const devicePx = devicePixelsPerCssPx(metrics);
1862
+ const clip = box ? documentClip(box, metrics, devicePx) : contentClip(metrics);
1863
+ const sendsClip = opts?.clip !== undefined || opts?.fullPage === true || scale !== 1 || renderSize !== undefined;
1864
+ if (opts?.tile === true && !consumeBands) {
1865
+ throw driverError("unsupported", "screenshot tile:true needs a caller that can consume bands; this one passed no band consumer");
1866
+ }
1867
+ const plan = planTiledCapture(clip, scale, devicePx, consumeBands ? opts?.tile : false);
1868
+ if (plan.tiled)
1869
+ return this.captureTiled(clip, plan, scale, format, consumeBands);
1870
+ assertClipEncodable(clip, scale, devicePx, renderSize);
1694
1871
  const params = { format, captureBeyondViewport: true };
1695
1872
  if (quality != null)
1696
1873
  params.quality = quality;
1697
- if (opts?.clip)
1698
- params.clip = { ...await elementClip(this.conn, opts.clip), scale: 1 };
1699
- else if (opts?.fullPage)
1700
- params.clip = { ...await fullPageClip(this.conn), scale: 1 };
1874
+ if (sendsClip)
1875
+ params.clip = { ...clip, scale };
1701
1876
  const { data } = await this.conn.send("Page.captureScreenshot", params);
1702
1877
  return { data: new Uint8Array(Buffer.from(data, "base64")), format };
1703
1878
  }
1879
+ async captureTiled(clip, plan, scale, format, consumeBands) {
1880
+ const capture = {
1881
+ bands: this.bands(clip, plan, scale, format),
1882
+ bandCount: plan.bandCount,
1883
+ bandHeightCss: plan.bandHeightCss,
1884
+ clip,
1885
+ format: "png"
1886
+ };
1887
+ const consumed = await consumeBands(capture);
1888
+ return { tiled: true, format: "png", bandCount: plan.bandCount, bandHeightCss: plan.bandHeightCss, consumed };
1889
+ }
1890
+ async* bands(clip, plan, scale, format) {
1891
+ if (format !== "png") {
1892
+ throw driverError("unsupported", `a tiled capture is PNG-only (bands are stitched as PNG scanlines); got format "${format}"`);
1893
+ }
1894
+ const bottom = clip.y + clip.height;
1895
+ for (let i = 0;i < plan.bandCount; i++) {
1896
+ const y = clip.y + i * plan.bandHeightCss;
1897
+ const height = Math.min(plan.bandHeightCss, bottom - y);
1898
+ const { data } = await this.conn.send("Page.captureScreenshot", {
1899
+ format: "png",
1900
+ captureBeyondViewport: true,
1901
+ clip: { x: clip.x, y, width: clip.width, height, scale }
1902
+ });
1903
+ yield new Uint8Array(Buffer.from(data, "base64"));
1904
+ }
1905
+ }
1704
1906
  async emulate(opts) {
1705
1907
  if (opts.clearOverrides) {
1706
1908
  const c = () => {
1707
1909
  return;
1708
1910
  };
1709
- await this.conn.send("Emulation.clearDeviceMetricsOverride").catch(c);
1911
+ let metricsClearError;
1912
+ try {
1913
+ await this.ensureDomain("Page");
1914
+ const metrics = await layoutMetrics(this.conn);
1915
+ const width = metrics.cssVisualViewport?.clientWidth ?? metrics.cssLayoutViewport?.clientWidth ?? metrics.visualViewport?.clientWidth ?? metrics.layoutViewport?.clientWidth ?? 1;
1916
+ const height = metrics.cssVisualViewport?.clientHeight ?? metrics.cssLayoutViewport?.clientHeight ?? metrics.visualViewport?.clientHeight ?? metrics.layoutViewport?.clientHeight ?? 1;
1917
+ await this.conn.send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: 0, mobile: false });
1918
+ await this.conn.send("Emulation.clearDeviceMetricsOverride");
1919
+ } catch (e) {
1920
+ metricsClearError = e instanceof Error ? e.message : String(e);
1921
+ }
1710
1922
  await this.conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(c);
1711
1923
  await this.conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(c);
1712
1924
  await this.conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(c);
1713
1925
  await this.ensureDomainSoft("Network");
1714
1926
  await this.conn.send("Network.emulateNetworkConditions", { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }).catch(c);
1927
+ if (metricsClearError !== undefined) {
1928
+ throw driverError("page-error", `emulate: could not verify the device-metrics override was cleared (${metricsClearError})`);
1929
+ }
1930
+ await clearViewportRecord(this.browser.scheme, this.info.id);
1715
1931
  return { applied: [] };
1716
1932
  }
1717
1933
  const applied = [];
@@ -1725,6 +1941,12 @@ class CdpPageDriver {
1725
1941
  deviceScaleFactor: opts.deviceScaleFactor ?? 0,
1726
1942
  mobile: opts.mobile ?? false
1727
1943
  });
1944
+ await writeViewportRecord(this.browser.scheme, this.info.id, {
1945
+ width: opts.width,
1946
+ height: opts.height,
1947
+ deviceScaleFactor: opts.deviceScaleFactor ?? 0,
1948
+ mobile: opts.mobile ?? false
1949
+ });
1728
1950
  applied.push("deviceMetrics");
1729
1951
  }
1730
1952
  if (opts.userAgent != null) {
@@ -2112,8 +2334,522 @@ async function reapStaleAgentTabs(driver, backend) {
2112
2334
  }
2113
2335
 
2114
2336
  // src/shared-tools.ts
2115
- import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
2116
- import { dirname, join as join3 } from "node:path";
2337
+ import { mkdir as mkdir5, writeFile as writeFile4 } from "node:fs/promises";
2338
+ import { dirname as dirname2, join as join4 } from "node:path";
2339
+
2340
+ // src/png.ts
2341
+ import { once } from "node:events";
2342
+ import { mkdir as mkdir4, open, rename, rm } from "node:fs/promises";
2343
+ import { dirname } from "node:path";
2344
+ import { createDeflate, createInflate, deflateSync } from "node:zlib";
2345
+ var PNG_SIGNATURE = Uint8Array.of(137, 80, 78, 71, 13, 10, 26, 10);
2346
+ var ZLIB_CHUNK = 1 << 18;
2347
+ var BLOCK_RAW_BYTES = 1 << 22;
2348
+ var MAX_BLOCK_ROWS = 4096;
2349
+ var DECIDE_LEVEL = 6;
2350
+ var CARRIED_CHUNKS = ["gAMA", "cHRM", "sRGB", "iCCP", "pHYs"];
2351
+ var CHANNELS = { 0: 1, 2: 3, 4: 2, 6: 4 };
2352
+ var LEGAL_DEPTHS = {
2353
+ 0: [1, 2, 4, 8, 16],
2354
+ 2: [8, 16],
2355
+ 4: [8, 16],
2356
+ 6: [8, 16]
2357
+ };
2358
+ var COLOUR_TYPE_NAMES = {
2359
+ 0: "greyscale",
2360
+ 2: "truecolour RGB",
2361
+ 3: "palette",
2362
+ 4: "greyscale+alpha",
2363
+ 6: "RGBA"
2364
+ };
2365
+
2366
+ class PngStitchError extends Error {
2367
+ constructor(message) {
2368
+ super(message);
2369
+ this.name = "PngStitchError";
2370
+ }
2371
+ }
2372
+ var CRC_TABLE = (() => {
2373
+ const table = new Uint32Array(256);
2374
+ for (let n = 0;n < 256; n++) {
2375
+ let c = n;
2376
+ for (let k = 0;k < 8; k++)
2377
+ c = (c & 1) !== 0 ? 3988292384 ^ c >>> 1 : c >>> 1;
2378
+ table[n] = c >>> 0;
2379
+ }
2380
+ return table;
2381
+ })();
2382
+ function crcUpdate(crc, bytes) {
2383
+ let c = crc;
2384
+ for (let i = 0;i < bytes.length; i++)
2385
+ c = CRC_TABLE[(c ^ bytes[i]) & 255] ^ c >>> 8;
2386
+ return c >>> 0;
2387
+ }
2388
+ function crc32(bytes) {
2389
+ return (crcUpdate(4294967295, bytes) ^ 4294967295) >>> 0;
2390
+ }
2391
+ function readU32(b, off) {
2392
+ return (b[off] << 24 | b[off + 1] << 16 | b[off + 2] << 8 | b[off + 3]) >>> 0;
2393
+ }
2394
+ function chunkTypeAt(b, off) {
2395
+ return String.fromCharCode(b[off], b[off + 1], b[off + 2], b[off + 3]);
2396
+ }
2397
+ function sameBytes(a, b) {
2398
+ if (a.length !== b.length)
2399
+ return false;
2400
+ for (let i = 0;i < a.length; i++)
2401
+ if (a[i] !== b[i])
2402
+ return false;
2403
+ return true;
2404
+ }
2405
+ function paeth(a, b, c) {
2406
+ const p = a + b - c;
2407
+ const pa = p > a ? p - a : a - p;
2408
+ const pb = p > b ? p - b : b - p;
2409
+ const pc = p > c ? p - c : c - p;
2410
+ if (pa <= pb && pa <= pc)
2411
+ return a;
2412
+ return pb <= pc ? b : c;
2413
+ }
2414
+ function unfilterRow(type, row, prev, bpp, at) {
2415
+ const n = row.length;
2416
+ switch (type) {
2417
+ case 0:
2418
+ return;
2419
+ case 1:
2420
+ for (let i = bpp;i < n; i++)
2421
+ row[i] = row[i] + row[i - bpp] & 255;
2422
+ return;
2423
+ case 2:
2424
+ for (let i = 0;i < n; i++)
2425
+ row[i] = row[i] + prev[i] & 255;
2426
+ return;
2427
+ case 3:
2428
+ for (let i = 0;i < n; i++) {
2429
+ const a = i >= bpp ? row[i - bpp] : 0;
2430
+ row[i] = row[i] + (a + prev[i] >> 1) & 255;
2431
+ }
2432
+ return;
2433
+ case 4:
2434
+ for (let i = 0;i < n; i++) {
2435
+ const a = i >= bpp ? row[i - bpp] : 0;
2436
+ const c = i >= bpp ? prev[i - bpp] : 0;
2437
+ row[i] = row[i] + paeth(a, prev[i], c) & 255;
2438
+ }
2439
+ return;
2440
+ default:
2441
+ throw new PngStitchError(`${at}: unknown row filter type ${type} (PNG defines 0-4)`);
2442
+ }
2443
+ }
2444
+ function absSigned(v) {
2445
+ const u = v & 255;
2446
+ return u < 128 ? u : 256 - u;
2447
+ }
2448
+ function chooseFilter(row, prev, bpp) {
2449
+ const n = row.length;
2450
+ let s0 = 0, s1 = 0, s2 = 0, s3 = 0, s4 = 0;
2451
+ for (let i = 0;i < n; i++) {
2452
+ const x = row[i];
2453
+ const b = prev[i];
2454
+ const a = i >= bpp ? row[i - bpp] : 0;
2455
+ const c = i >= bpp ? prev[i - bpp] : 0;
2456
+ s0 += absSigned(x);
2457
+ s1 += absSigned(x - a);
2458
+ s2 += absSigned(x - b);
2459
+ s3 += absSigned(x - (a + b >> 1));
2460
+ s4 += absSigned(x - paeth(a, b, c));
2461
+ }
2462
+ let best = 0, bestSum = s0;
2463
+ if (s1 < bestSum) {
2464
+ best = 1;
2465
+ bestSum = s1;
2466
+ }
2467
+ if (s2 < bestSum) {
2468
+ best = 2;
2469
+ bestSum = s2;
2470
+ }
2471
+ if (s3 < bestSum) {
2472
+ best = 3;
2473
+ bestSum = s3;
2474
+ }
2475
+ if (s4 < bestSum) {
2476
+ best = 4;
2477
+ }
2478
+ return best;
2479
+ }
2480
+ function writeFilteredRow(type, row, prev, bpp, out, off) {
2481
+ const n = row.length;
2482
+ switch (type) {
2483
+ case 0:
2484
+ out.set(row, off);
2485
+ return;
2486
+ case 1:
2487
+ for (let i = 0;i < n; i++)
2488
+ out[off + i] = row[i] - (i >= bpp ? row[i - bpp] : 0) & 255;
2489
+ return;
2490
+ case 2:
2491
+ for (let i = 0;i < n; i++)
2492
+ out[off + i] = row[i] - prev[i] & 255;
2493
+ return;
2494
+ case 3:
2495
+ for (let i = 0;i < n; i++) {
2496
+ const a = i >= bpp ? row[i - bpp] : 0;
2497
+ out[off + i] = row[i] - (a + prev[i] >> 1) & 255;
2498
+ }
2499
+ return;
2500
+ default:
2501
+ for (let i = 0;i < n; i++) {
2502
+ const a = i >= bpp ? row[i - bpp] : 0;
2503
+ const c = i >= bpp ? prev[i - bpp] : 0;
2504
+ out[off + i] = row[i] - paeth(a, prev[i], c) & 255;
2505
+ }
2506
+ return;
2507
+ }
2508
+ }
2509
+ function filterUnitBytes(h) {
2510
+ return Math.max(1, Math.ceil(CHANNELS[h.colourType] * h.bitDepth / 8));
2511
+ }
2512
+ function bytesPerRow(h) {
2513
+ return Math.ceil(h.width * CHANNELS[h.colourType] * h.bitDepth / 8);
2514
+ }
2515
+ function readIhdr(band, dataStart, at) {
2516
+ const width = readU32(band, dataStart);
2517
+ const height = readU32(band, dataStart + 4);
2518
+ const bitDepth = band[dataStart + 8];
2519
+ const colourType = band[dataStart + 9];
2520
+ const compression = band[dataStart + 10];
2521
+ const filterMethod = band[dataStart + 11];
2522
+ const interlace = band[dataStart + 12];
2523
+ if (width < 1 || height < 1)
2524
+ throw new PngStitchError(`${at}: IHDR declares ${width}x${height}; both dimensions must be >= 1`);
2525
+ if (compression !== 0)
2526
+ throw new PngStitchError(`${at}: IHDR compression method ${compression}; PNG defines only 0 (deflate)`);
2527
+ if (filterMethod !== 0)
2528
+ throw new PngStitchError(`${at}: IHDR filter method ${filterMethod}; PNG defines only 0`);
2529
+ if (interlace === 1) {
2530
+ 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).`);
2531
+ }
2532
+ if (interlace !== 0)
2533
+ throw new PngStitchError(`${at}: IHDR interlace method ${interlace}; PNG defines only 0 (none) and 1 (Adam7)`);
2534
+ if (colourType === 3) {
2535
+ 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.`);
2536
+ }
2537
+ if (CHANNELS[colourType] === undefined)
2538
+ throw new PngStitchError(`${at}: IHDR colour type ${colourType}; PNG defines 0, 2, 3, 4 and 6`);
2539
+ if (!LEGAL_DEPTHS[colourType].includes(bitDepth)) {
2540
+ throw new PngStitchError(`${at}: bit depth ${bitDepth} is not legal for colour type ${colourType} (${COLOUR_TYPE_NAMES[colourType]}); ` + `allowed: ${LEGAL_DEPTHS[colourType].join(", ")}`);
2541
+ }
2542
+ return { width, height, bitDepth, colourType };
2543
+ }
2544
+ function parseBand(band, index) {
2545
+ const at = `band ${index}`;
2546
+ if (band.length < 8)
2547
+ throw new PngStitchError(`${at}: ${band.length} bytes is too short to be a PNG`);
2548
+ for (let i = 0;i < 8; i++) {
2549
+ if (band[i] !== PNG_SIGNATURE[i])
2550
+ throw new PngStitchError(`${at}: not a PNG (bad 8-byte signature)`);
2551
+ }
2552
+ let header;
2553
+ const idat = [];
2554
+ const carried = [];
2555
+ let sawIend = false;
2556
+ let off = 8;
2557
+ while (off + 8 <= band.length) {
2558
+ const len = readU32(band, off);
2559
+ const type = chunkTypeAt(band, off + 4);
2560
+ const dataStart = off + 8;
2561
+ const dataEnd = dataStart + len;
2562
+ if (dataEnd + 4 > band.length) {
2563
+ throw new PngStitchError(`${at}: chunk "${type}" at byte ${off} claims ${len} payload bytes but only ${band.length - dataStart} remain (truncated PNG)`);
2564
+ }
2565
+ if (type === "IHDR") {
2566
+ if (off !== 8)
2567
+ throw new PngStitchError(`${at}: IHDR must be the first chunk, found it at byte ${off}`);
2568
+ if (len !== 13)
2569
+ throw new PngStitchError(`${at}: IHDR payload is ${len} bytes, must be 13`);
2570
+ const want = readU32(band, dataEnd);
2571
+ const got = crc32(band.subarray(off + 4, dataEnd));
2572
+ if (want !== got)
2573
+ throw new PngStitchError(`${at}: IHDR CRC mismatch (file says 0x${want.toString(16)}, computed 0x${got.toString(16)})`);
2574
+ header = readIhdr(band, dataStart, at);
2575
+ } else if (!header) {
2576
+ throw new PngStitchError(`${at}: first chunk is "${type}", expected IHDR`);
2577
+ } else if (type === "IDAT") {
2578
+ idat.push([dataStart, dataEnd]);
2579
+ } else if (type === "IEND") {
2580
+ sawIend = true;
2581
+ break;
2582
+ } else if (CARRIED_CHUNKS.includes(type)) {
2583
+ carried.push({ type, data: band.subarray(dataStart, dataEnd) });
2584
+ }
2585
+ off = dataEnd + 4;
2586
+ }
2587
+ if (!header)
2588
+ throw new PngStitchError(`${at}: no IHDR chunk`);
2589
+ if (idat.length === 0)
2590
+ throw new PngStitchError(`${at}: no IDAT chunk`);
2591
+ if (!sawIend)
2592
+ throw new PngStitchError(`${at}: no IEND chunk (truncated PNG)`);
2593
+ return { header, idat, carried };
2594
+ }
2595
+ function assertCompatible(first, next, firstCarried, nextCarried, index) {
2596
+ const at = `band ${index}`;
2597
+ if (next.width !== first.width)
2598
+ throw new PngStitchError(`${at}: width ${next.width} does not match band 1's width ${first.width}`);
2599
+ if (next.bitDepth !== first.bitDepth)
2600
+ throw new PngStitchError(`${at}: bit depth ${next.bitDepth} does not match band 1's bit depth ${first.bitDepth}`);
2601
+ if (next.colourType !== first.colourType) {
2602
+ 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] ?? "?"})`);
2603
+ }
2604
+ for (const type of CARRIED_CHUNKS) {
2605
+ const a = firstCarried.find((c) => c.type === type);
2606
+ const b = nextCarried.find((c) => c.type === type);
2607
+ if (!a && !b)
2608
+ continue;
2609
+ if (!a || !b)
2610
+ 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`);
2611
+ if (!sameBytes(a.data, b.data))
2612
+ throw new PngStitchError(`${at}: its "${type}" chunk differs from band 1's; the bands disagree about how their samples are interpreted`);
2613
+ }
2614
+ }
2615
+
2616
+ class ChunkWriter {
2617
+ fh;
2618
+ pos = 0;
2619
+ constructor(fh) {
2620
+ this.fh = fh;
2621
+ }
2622
+ get bytes() {
2623
+ return this.pos;
2624
+ }
2625
+ async writeRaw(b) {
2626
+ await this.fh.write(b, 0, b.length, this.pos);
2627
+ this.pos += b.length;
2628
+ }
2629
+ async writeChunk(type, payload) {
2630
+ const head = Buffer.allocUnsafe(8);
2631
+ head.writeUInt32BE(payload.length, 0);
2632
+ head.write(type, 4, "latin1");
2633
+ const crc = (crcUpdate(crcUpdate(4294967295, head.subarray(4)), payload) ^ 4294967295) >>> 0;
2634
+ const tail = Buffer.allocUnsafe(4);
2635
+ tail.writeUInt32BE(crc, 0);
2636
+ await this.fh.writev([head, payload, tail], this.pos);
2637
+ this.pos += 8 + payload.length + 4;
2638
+ }
2639
+ async patchAt(position, b) {
2640
+ await this.fh.write(b, 0, b.length, position);
2641
+ }
2642
+ }
2643
+ function ihdrChunk(h, height) {
2644
+ const buf = Buffer.allocUnsafe(25);
2645
+ buf.writeUInt32BE(13, 0);
2646
+ buf.write("IHDR", 4, "latin1");
2647
+ buf.writeUInt32BE(h.width, 8);
2648
+ buf.writeUInt32BE(height, 12);
2649
+ buf[16] = h.bitDepth;
2650
+ buf[17] = h.colourType;
2651
+ buf[18] = 0;
2652
+ buf[19] = 0;
2653
+ buf[20] = 0;
2654
+ buf.writeUInt32BE(crc32(buf.subarray(4, 21)), 21);
2655
+ return buf;
2656
+ }
2657
+
2658
+ class RowEncoder {
2659
+ writer;
2660
+ bpr;
2661
+ bpp;
2662
+ deflate = createDeflate({ chunkSize: ZLIB_CHUNK });
2663
+ pump;
2664
+ rowsPerBlock;
2665
+ block;
2666
+ blockRows = 0;
2667
+ candNone;
2668
+ candHeuristic;
2669
+ prevRaw;
2670
+ constructor(writer, bpr, bpp) {
2671
+ this.writer = writer;
2672
+ this.bpr = bpr;
2673
+ this.bpp = bpp;
2674
+ this.rowsPerBlock = Math.min(MAX_BLOCK_ROWS, Math.max(1, Math.floor(BLOCK_RAW_BYTES / bpr)));
2675
+ this.block = Buffer.allocUnsafe(this.rowsPerBlock * bpr);
2676
+ this.candNone = Buffer.allocUnsafe(this.rowsPerBlock * (bpr + 1));
2677
+ this.candHeuristic = Buffer.allocUnsafe(this.rowsPerBlock * (bpr + 1));
2678
+ this.prevRaw = new Uint8Array(bpr);
2679
+ this.pump = (async () => {
2680
+ for await (const chunk of this.deflate)
2681
+ await this.writer.writeChunk("IDAT", chunk);
2682
+ })();
2683
+ this.pump.catch(() => {});
2684
+ }
2685
+ async addRow(raw) {
2686
+ this.block.set(raw, this.blockRows * this.bpr);
2687
+ this.blockRows += 1;
2688
+ if (this.blockRows === this.rowsPerBlock)
2689
+ await this.emitBlock();
2690
+ }
2691
+ async emitBlock() {
2692
+ const n = this.blockRows;
2693
+ if (n === 0)
2694
+ return;
2695
+ const { bpr, bpp } = this;
2696
+ const stride = bpr + 1;
2697
+ let prev = this.prevRaw;
2698
+ for (let r = 0;r < n; r++) {
2699
+ const row = this.block.subarray(r * bpr, (r + 1) * bpr);
2700
+ const at = r * stride;
2701
+ this.candNone[at] = 0;
2702
+ this.candNone.set(row, at + 1);
2703
+ const filter = chooseFilter(row, prev, bpp);
2704
+ this.candHeuristic[at] = filter;
2705
+ writeFilteredRow(filter, row, prev, bpp, this.candHeuristic, at + 1);
2706
+ prev = row;
2707
+ }
2708
+ const used = n * stride;
2709
+ const a = this.candNone.subarray(0, used);
2710
+ const b = this.candHeuristic.subarray(0, used);
2711
+ const heuristicWins = deflateSync(b, { level: DECIDE_LEVEL }).length < deflateSync(a, { level: DECIDE_LEVEL }).length;
2712
+ this.prevRaw.set(prev);
2713
+ this.blockRows = 0;
2714
+ const out = Buffer.from(heuristicWins ? b : a);
2715
+ if (!this.deflate.write(out))
2716
+ await once(this.deflate, "drain");
2717
+ }
2718
+ async finish() {
2719
+ await this.emitBlock();
2720
+ this.deflate.end();
2721
+ await this.pump;
2722
+ }
2723
+ async abort() {
2724
+ this.deflate.destroy();
2725
+ await this.pump.catch(() => {});
2726
+ }
2727
+ }
2728
+ async function* inflateBandIdat(band, slices, at) {
2729
+ const inf = createInflate({ chunkSize: ZLIB_CHUNK });
2730
+ const feed = (async () => {
2731
+ for (const [start, end] of slices) {
2732
+ const view = Buffer.from(band.buffer, band.byteOffset + start, end - start);
2733
+ if (!inf.write(view))
2734
+ await once(inf, "drain");
2735
+ }
2736
+ inf.end();
2737
+ })();
2738
+ feed.catch(() => {});
2739
+ try {
2740
+ for await (const chunk of inf)
2741
+ yield chunk;
2742
+ } catch (err) {
2743
+ throw new PngStitchError(`${at}: IDAT stream would not inflate (${err.message})`);
2744
+ } finally {
2745
+ inf.destroy();
2746
+ }
2747
+ }
2748
+ function isAsyncIterable(x) {
2749
+ return typeof x === "object" && x !== null && Symbol.asyncIterator in x;
2750
+ }
2751
+ async function* eachBand(bands) {
2752
+ if (isAsyncIterable(bands)) {
2753
+ for await (const b of bands)
2754
+ yield b;
2755
+ } else {
2756
+ for (const b of bands)
2757
+ yield b;
2758
+ }
2759
+ }
2760
+ async function stitchPngBandsToFile(bands, outPath) {
2761
+ await mkdir4(dirname(outPath), { recursive: true });
2762
+ const partPath = `${outPath}.stitch-partial`;
2763
+ const fh = await open(partPath, "w");
2764
+ let done = false;
2765
+ try {
2766
+ const result = await stitchInto(fh, bands);
2767
+ await fh.close();
2768
+ await rename(partPath, outPath);
2769
+ done = true;
2770
+ return result;
2771
+ } finally {
2772
+ if (!done) {
2773
+ await fh.close().catch(() => {});
2774
+ await rm(partPath, { force: true }).catch(() => {});
2775
+ }
2776
+ }
2777
+ }
2778
+ async function stitchInto(fh, bands) {
2779
+ const writer = new ChunkWriter(fh);
2780
+ let first;
2781
+ let firstCarried = [];
2782
+ let encoder;
2783
+ let prevRow = new Uint8Array(0);
2784
+ let rowBuf = new Uint8Array(0);
2785
+ let bpp = 0;
2786
+ let bpr = 0;
2787
+ let totalRows = 0;
2788
+ let index = 0;
2789
+ try {
2790
+ for await (const band of eachBand(bands)) {
2791
+ index += 1;
2792
+ const parsed = parseBand(band, index);
2793
+ const at = `band ${index}`;
2794
+ if (!first) {
2795
+ first = parsed.header;
2796
+ firstCarried = parsed.carried;
2797
+ bpp = filterUnitBytes(first);
2798
+ bpr = bytesPerRow(first);
2799
+ prevRow = new Uint8Array(bpr);
2800
+ rowBuf = new Uint8Array(1 + bpr);
2801
+ await writer.writeRaw(PNG_SIGNATURE);
2802
+ await writer.writeRaw(ihdrChunk(first, 1));
2803
+ for (const c of parsed.carried)
2804
+ await writer.writeChunk(c.type, c.data);
2805
+ encoder = new RowEncoder(writer, bpr, bpp);
2806
+ } else {
2807
+ assertCompatible(first, parsed.header, firstCarried, parsed.carried, index);
2808
+ }
2809
+ const enc = encoder;
2810
+ const raw = rowBuf.subarray(1);
2811
+ prevRow.fill(0);
2812
+ let rows = 0;
2813
+ let filled = 0;
2814
+ for await (const chunk of inflateBandIdat(band, parsed.idat, at)) {
2815
+ let off = 0;
2816
+ while (off < chunk.length) {
2817
+ const take = Math.min(rowBuf.length - filled, chunk.length - off);
2818
+ rowBuf.set(chunk.subarray(off, off + take), filled);
2819
+ filled += take;
2820
+ off += take;
2821
+ if (filled !== rowBuf.length)
2822
+ continue;
2823
+ if (rows >= parsed.header.height) {
2824
+ throw new PngStitchError(`${at}: IDAT holds more scanlines than its IHDR height of ${parsed.header.height}`);
2825
+ }
2826
+ unfilterRow(rowBuf[0], raw, prevRow, bpp, at);
2827
+ await enc.addRow(raw);
2828
+ prevRow.set(raw);
2829
+ rows += 1;
2830
+ filled = 0;
2831
+ }
2832
+ }
2833
+ if (filled !== 0 || rows !== parsed.header.height) {
2834
+ throw new PngStitchError(`${at}: IDAT decoded to ${rows} complete scanlines${filled > 0 ? ` plus ${filled} trailing bytes` : ""}, but its IHDR declares ${parsed.header.height}`);
2835
+ }
2836
+ totalRows += parsed.header.height;
2837
+ if (totalRows > 2147483647)
2838
+ throw new PngStitchError(`stitched height would be ${totalRows}, past PNG's 2^31-1 limit`);
2839
+ }
2840
+ if (!first || !encoder)
2841
+ throw new PngStitchError("no bands to stitch");
2842
+ await encoder.finish();
2843
+ await writer.writeChunk("IEND", new Uint8Array(0));
2844
+ await writer.patchAt(8, ihdrChunk(first, totalRows));
2845
+ return { width: first.width, height: totalRows, bytes: writer.bytes, bands: index };
2846
+ } catch (err) {
2847
+ await encoder?.abort();
2848
+ throw err;
2849
+ }
2850
+ }
2851
+
2852
+ // src/shared-tools.ts
2117
2853
  var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2118
2854
  var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
2119
2855
 
@@ -2258,8 +2994,8 @@ async function closePage(driver, args) {
2258
2994
  return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
2259
2995
  }
2260
2996
  async function writeSelectedFile(id) {
2261
- await mkdir3(STATE_DIR, { recursive: true });
2262
- await writeFile3(join3(STATE_DIR, "selected"), id, "utf8");
2997
+ await mkdir5(STATE_DIR, { recursive: true });
2998
+ await writeFile4(join4(STATE_DIR, "selected"), id, "utf8");
2263
2999
  }
2264
3000
  async function selectPage(driver, args) {
2265
3001
  if (args.target === undefined || args.target === "")
@@ -2323,10 +3059,10 @@ function serializeForSink(value) {
2323
3059
  return json ?? "null";
2324
3060
  }
2325
3061
  async function writeJsonSink(savePath, value) {
2326
- const path = savePath.startsWith("/") ? savePath : join3(ARTIFACT_DIR, savePath);
2327
- await mkdir3(dirname(path), { recursive: true });
3062
+ const path = savePath.startsWith("/") ? savePath : join4(ARTIFACT_DIR, savePath);
3063
+ await mkdir5(dirname2(path), { recursive: true });
2328
3064
  const json = serializeForSink(value);
2329
- await writeFile3(path, json, "utf8");
3065
+ await writeFile4(path, json, "utf8");
2330
3066
  return { path, bytes: Buffer.byteLength(json, "utf8") };
2331
3067
  }
2332
3068
  function sinkRequested(savePath) {
@@ -2630,30 +3366,166 @@ async function uploadFile(driver, args) {
2630
3366
  function stamp() {
2631
3367
  return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
2632
3368
  }
3369
+ var MAX_SCREENSHOT_SCALE = 8;
3370
+ function resolveScreenshotScale(scale, capabilities) {
3371
+ if (scale === undefined)
3372
+ return 1;
3373
+ if (typeof scale !== "number" || !Number.isFinite(scale) || scale <= 0 || scale > MAX_SCREENSHOT_SCALE) {
3374
+ throw new SharedToolError(`take_screenshot: 'scale' must be a finite number greater than 0 and at most ${MAX_SCREENSHOT_SCALE} (default 1)`);
3375
+ }
3376
+ if (scale !== 1 && !capabilities.has("screenshot.scale")) {
3377
+ 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.`);
3378
+ }
3379
+ return scale;
3380
+ }
3381
+ var MAX_RENDER_SIZE_PX = 16384;
3382
+ function resolveRenderSize(renderWidth, renderHeight, capabilities) {
3383
+ if (renderWidth === undefined && renderHeight === undefined)
3384
+ return;
3385
+ if (renderWidth === undefined || renderHeight === undefined) {
3386
+ throw new SharedToolError("take_screenshot: 'renderWidth' and 'renderHeight' are required together (a viewport is two numbers; " + `got ${renderWidth === undefined ? "only renderHeight" : "only renderWidth"})`);
3387
+ }
3388
+ for (const [name, value] of [["renderWidth", renderWidth], ["renderHeight", renderHeight]]) {
3389
+ if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > MAX_RENDER_SIZE_PX) {
3390
+ throw new SharedToolError(`take_screenshot: '${name}' must be an integer between 1 and ${MAX_RENDER_SIZE_PX} css px (got ${JSON.stringify(value)})`);
3391
+ }
3392
+ }
3393
+ if (!capabilities.has("screenshot.renderSize")) {
3394
+ 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.`);
3395
+ }
3396
+ return { width: renderWidth, height: renderHeight };
3397
+ }
3398
+ function resolveTileMode(tile, capabilities, format, returnBase64) {
3399
+ if (tile === undefined)
3400
+ return;
3401
+ if (typeof tile !== "boolean")
3402
+ throw new SharedToolError(`take_screenshot: 'tile' must be true, false, or omitted for automatic (got ${JSON.stringify(tile)})`);
3403
+ if (!tile)
3404
+ return false;
3405
+ if (!capabilities.has("screenshot.tile")) {
3406
+ 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.");
3407
+ }
3408
+ if (format !== "png")
3409
+ throw tiledCaptureRefusal("format");
3410
+ if (returnBase64)
3411
+ throw tiledCaptureRefusal("base64");
3412
+ return true;
3413
+ }
3414
+ function tiledCaptureRefusal(reason, banded) {
3415
+ 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).` : "";
3416
+ if (reason === "format") {
3417
+ 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.`);
3418
+ }
3419
+ 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.`);
3420
+ }
3421
+ function pngPixelSize(b, view) {
3422
+ if (b.length < 24)
3423
+ return;
3424
+ if (view.getUint32(0) !== 2303741511 || view.getUint32(4) !== 218765834)
3425
+ return;
3426
+ if (String.fromCharCode(b[12], b[13], b[14], b[15]) !== "IHDR")
3427
+ return;
3428
+ return { width: view.getUint32(16), height: view.getUint32(20) };
3429
+ }
3430
+ var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
3431
+ function jpegPixelSize(b, view) {
3432
+ if (b.length < 4 || b[0] !== 255 || b[1] !== 216)
3433
+ return;
3434
+ let i = 2;
3435
+ while (i + 3 < b.length) {
3436
+ if (b[i] !== 255) {
3437
+ i += 1;
3438
+ continue;
3439
+ }
3440
+ const marker = b[i + 1];
3441
+ if (marker === 255) {
3442
+ i += 1;
3443
+ continue;
3444
+ }
3445
+ if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
3446
+ i += 2;
3447
+ continue;
3448
+ }
3449
+ const segLen = view.getUint16(i + 2);
3450
+ if (JPEG_SOF.has(marker)) {
3451
+ if (i + 9 > b.length)
3452
+ return;
3453
+ return { width: view.getUint16(i + 7), height: view.getUint16(i + 5) };
3454
+ }
3455
+ if (segLen < 2)
3456
+ return;
3457
+ i += 2 + segLen;
3458
+ }
3459
+ return;
3460
+ }
3461
+ function imagePixelSize(bytes) {
3462
+ if (bytes.byteLength < 4)
3463
+ return;
3464
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
3465
+ return pngPixelSize(bytes, view) ?? jpegPixelSize(bytes, view);
3466
+ }
2633
3467
  async function takeScreenshot(driver, args = {}) {
2634
3468
  if (args.uid !== undefined && args.uid !== "" && args.selector)
2635
3469
  throw new SharedToolError("provide exactly one of uid or selector, not both");
2636
3470
  const format = args.format ?? "png";
3471
+ const scale = resolveScreenshotScale(args.scale, driver.capabilities);
3472
+ const renderSize = resolveRenderSize(args.renderWidth, args.renderHeight, driver.capabilities);
3473
+ const tile = resolveTileMode(args.tile, driver.capabilities, format, args.returnBase64);
2637
3474
  return withPage2(driver, args.target, async (page) => {
2638
3475
  const clip = args.uid !== undefined || args.selector ? locatorOf(args) : undefined;
2639
- const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
2640
- if (data.byteLength === 0)
2641
- throw new SharedToolError("captureScreenshot returned empty data");
2642
- await mkdir3(ARTIFACT_DIR, { recursive: true });
2643
- const ext = outFormat === "jpeg" ? "jpg" : "png";
2644
- const path = args.savePath ?? join3(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
2645
- await writeFile3(path, Buffer.from(data));
2646
- const result = {
2647
- path,
2648
- bytes: data.byteLength,
2649
- format: outFormat,
2650
- target: target3(page.info)
3476
+ const ext = format === "jpeg" ? "jpg" : "png";
3477
+ const path = args.savePath ?? join4(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
3478
+ const stitch = async (banded) => {
3479
+ if (format !== "png")
3480
+ throw tiledCaptureRefusal("format", banded);
3481
+ if (args.returnBase64)
3482
+ throw tiledCaptureRefusal("base64", banded);
3483
+ return stitchPngBandsToFile(banded.bands, path);
2651
3484
  };
2652
- if (args.returnBase64)
2653
- result.base64 = Buffer.from(data).toString("base64");
3485
+ const shot = await page.screenshot({
3486
+ format,
3487
+ quality: args.quality,
3488
+ fullPage: args.fullPage,
3489
+ scale,
3490
+ ...clip ? { clip } : {},
3491
+ ...renderSize ? { renderWidth: renderSize.width, renderHeight: renderSize.height } : {},
3492
+ ...tile !== undefined ? { tile } : {}
3493
+ }, stitch);
3494
+ const result = isTiledCapture(shot) ? {
3495
+ path,
3496
+ bytes: shot.consumed.bytes,
3497
+ format: "png",
3498
+ target: target3(page.info),
3499
+ scale,
3500
+ width: shot.consumed.width,
3501
+ height: shot.consumed.height,
3502
+ tiled: true,
3503
+ bands: shot.consumed.bands
3504
+ } : await writeOneShot(shot.data, shot.format, path, target3(page.info), scale, args.returnBase64);
3505
+ if (renderSize && shot.renderSize) {
3506
+ result.renderSize = shot.renderSize;
3507
+ result.renderRestored = shot.renderRestored ?? false;
3508
+ if (shot.renderRestoreError !== undefined)
3509
+ result.renderRestoreError = shot.renderRestoreError;
3510
+ }
2654
3511
  return result;
2655
3512
  });
2656
3513
  }
3514
+ async function writeOneShot(data, outFormat, path, target, scale, returnBase64) {
3515
+ if (data.byteLength === 0)
3516
+ throw new SharedToolError("captureScreenshot returned empty data");
3517
+ await mkdir5(dirname2(path), { recursive: true });
3518
+ await writeFile4(path, Buffer.from(data));
3519
+ const size = imagePixelSize(data);
3520
+ const result = { path, bytes: data.byteLength, format: outFormat, target, scale };
3521
+ if (size) {
3522
+ result.width = size.width;
3523
+ result.height = size.height;
3524
+ }
3525
+ if (returnBase64)
3526
+ result.base64 = Buffer.from(data).toString("base64");
3527
+ return result;
3528
+ }
2657
3529
  async function emulate(driver, args = {}) {
2658
3530
  return withPage2(driver, args.target, async (page) => {
2659
3531
  if (args.clearOverrides) {
@@ -2824,10 +3696,10 @@ var LEASE_TOOLS = {
2824
3696
  };
2825
3697
 
2826
3698
  // src/tools/console.ts
2827
- import { readFile as readFile3 } from "node:fs/promises";
3699
+ import { readFile as readFile4 } from "node:fs/promises";
2828
3700
 
2829
3701
  // src/tools/recorder.ts
2830
- import { mkdir as mkdir4, appendFile, writeFile as writeFile4, copyFile } from "node:fs/promises";
3702
+ import { mkdir as mkdir6, appendFile, writeFile as writeFile5, copyFile } from "node:fs/promises";
2831
3703
  var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2832
3704
  function recFile(targetId) {
2833
3705
  return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
@@ -2838,6 +3710,31 @@ function captureFile(targetId, captureId) {
2838
3710
  function delay2(ms) {
2839
3711
  return new Promise((r) => setTimeout(r, ms));
2840
3712
  }
3713
+ async function resolveRecorderTarget(target, opts) {
3714
+ if (!isWorkerSelector(target))
3715
+ return resolveTarget(target);
3716
+ if (opts.capture)
3717
+ return resolveWorkerSelector(target, { wake: opts.wake ?? true });
3718
+ const needle = workerNeedle(target);
3719
+ if (needle === "")
3720
+ throw new CdpError(WORKER_EMPTY_NEEDLE_MESSAGE);
3721
+ const { matches, liveWorkers } = resolveWorkerTargets(await listTargets(), needle);
3722
+ if (matches.length > 1)
3723
+ throw new CdpError(workerAmbiguityMessage(needle, matches));
3724
+ if (matches.length === 1)
3725
+ return matches[0];
3726
+ throw new CdpError(workerBufferReadMissMessage(needle, liveWorkers));
3727
+ }
3728
+ function assertWakeApplies(target, wake, capture) {
3729
+ if (wake === undefined)
3730
+ return;
3731
+ if (typeof wake !== "boolean")
3732
+ throw new CdpError("'wake' must be a boolean");
3733
+ if (!isWorkerSelector(target))
3734
+ throw new CdpError(workerWakeMisuseMessage("not-a-worker-selector"));
3735
+ if (!capture)
3736
+ throw new CdpError(workerWakeMisuseMessage("read-only-call"));
3737
+ }
2841
3738
  var NETWORK_EVENTS = [
2842
3739
  "Network.requestWillBeSent",
2843
3740
  "Network.responseReceived",
@@ -2848,11 +3745,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
2848
3745
  async function startRecorder(target, opts = {}) {
2849
3746
  const network = opts.network ?? true;
2850
3747
  const consoleCap = opts.console ?? true;
2851
- await mkdir4(ARTIFACT_DIR2, { recursive: true });
3748
+ await mkdir6(ARTIFACT_DIR2, { recursive: true });
2852
3749
  const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
2853
3750
  const file = opts.file ?? recFile(resolved.id);
2854
3751
  if (opts.truncate) {
2855
- await writeFile4(file, "");
3752
+ await writeFile5(file, "");
2856
3753
  }
2857
3754
  let chain = Promise.resolve();
2858
3755
  let dropped = 0;
@@ -2903,7 +3800,8 @@ async function startRecorder(target, opts = {}) {
2903
3800
  };
2904
3801
  }
2905
3802
  async function captureWindow(target, durationMs, opts = {}) {
2906
- const resolved = await resolveTarget(target);
3803
+ const resolved = await resolveRecorderTarget(target, { capture: true, wake: opts.wake });
3804
+ const isWorker = isWorkerTargetType(resolved.type);
2907
3805
  const captureId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
2908
3806
  const file = captureFile(resolved.id, captureId);
2909
3807
  const rec = await startRecorder(resolved.id, {
@@ -2913,13 +3811,16 @@ async function captureWindow(target, durationMs, opts = {}) {
2913
3811
  file,
2914
3812
  timeoutMs: opts.timeoutMs
2915
3813
  });
2916
- await rec.conn.send("Page.enable");
2917
- await rec.conn.send("Page.reload", { ignoreCache: false });
3814
+ if (!isWorker) {
3815
+ await rec.conn.send("Page.enable");
3816
+ await rec.conn.send("Page.reload", { ignoreCache: false });
3817
+ }
2918
3818
  await delay2(durationMs);
2919
3819
  return {
2920
3820
  file: rec.file,
2921
3821
  conn: rec.conn,
2922
3822
  resolved: { id: rec.target.id, url: rec.target.url, title: rec.target.title },
3823
+ mode: isWorker ? "listen" : "reload",
2923
3824
  droppedWrites: rec.droppedWrites,
2924
3825
  flush: () => rec.flush(),
2925
3826
  async stop() {
@@ -2994,7 +3895,7 @@ function lineToEntry(line, index) {
2994
3895
  async function readConsoleEntries(file) {
2995
3896
  let raw;
2996
3897
  try {
2997
- raw = await readFile3(file, "utf8");
3898
+ raw = await readFile4(file, "utf8");
2998
3899
  } catch {
2999
3900
  return [];
3000
3901
  }
@@ -3019,8 +3920,9 @@ async function readConsoleEntries(file) {
3019
3920
  return out;
3020
3921
  }
3021
3922
  async function listConsoleMessages(args = {}) {
3923
+ assertWakeApplies(args.target, args.wake, args.reload === true);
3022
3924
  if (args.reload) {
3023
- const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS);
3925
+ const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS, { wake: args.wake });
3024
3926
  await cap.stop();
3025
3927
  const messages2 = await readConsoleEntries(cap.file);
3026
3928
  return {
@@ -3030,7 +3932,7 @@ async function listConsoleMessages(args = {}) {
3030
3932
  droppedWrites: cap.droppedWrites()
3031
3933
  };
3032
3934
  }
3033
- const target = await resolveTarget(args.target);
3935
+ const target = await resolveRecorderTarget(args.target, { capture: false });
3034
3936
  const messages = await readConsoleEntries(recFile(target.id));
3035
3937
  return {
3036
3938
  target: { id: target.id, url: target.url, title: target.title },
@@ -3039,7 +3941,7 @@ async function listConsoleMessages(args = {}) {
3039
3941
  };
3040
3942
  }
3041
3943
  async function getConsoleMessage(args = {}) {
3042
- const target = await resolveTarget(args.target);
3944
+ const target = await resolveRecorderTarget(args.target, { capture: false });
3043
3945
  const messages = await readConsoleEntries(recFile(target.id));
3044
3946
  const index = args.index ?? 0;
3045
3947
  const entry = messages[index];
@@ -3050,7 +3952,7 @@ async function getConsoleMessage(args = {}) {
3050
3952
  }
3051
3953
 
3052
3954
  // src/tools/network.ts
3053
- import { readFile as readFile4 } from "node:fs/promises";
3955
+ import { readFile as readFile5 } from "node:fs/promises";
3054
3956
  var DEFAULT_CAPTURE_MS2 = 2500;
3055
3957
  function asHeaders(v) {
3056
3958
  if (!v || typeof v !== "object")
@@ -3117,7 +4019,7 @@ function correlate(lines) {
3117
4019
  async function readNetworkRequests(file) {
3118
4020
  let raw;
3119
4021
  try {
3120
- raw = await readFile4(file, "utf8");
4022
+ raw = await readFile5(file, "utf8");
3121
4023
  } catch {
3122
4024
  return [];
3123
4025
  }
@@ -3137,14 +4039,15 @@ async function listNetworkRequests(args = {}) {
3137
4039
  let file;
3138
4040
  let resolved;
3139
4041
  let droppedWrites;
4042
+ assertWakeApplies(args.target, args.wake, args.reload === true);
3140
4043
  if (args.reload) {
3141
- const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
4044
+ const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
3142
4045
  await cap.stop();
3143
4046
  file = cap.file;
3144
4047
  resolved = cap.resolved;
3145
4048
  droppedWrites = cap.droppedWrites();
3146
4049
  } else {
3147
- const target = await resolveTarget(args.target);
4050
+ const target = await resolveRecorderTarget(args.target, { capture: false });
3148
4051
  file = recFile(target.id);
3149
4052
  resolved = { id: target.id, url: target.url, title: target.title };
3150
4053
  }
@@ -3165,8 +4068,9 @@ async function getNetworkRequest(args = {}) {
3165
4068
  return requests.find((r) => r.url.includes(args.url));
3166
4069
  };
3167
4070
  const describeSelector = () => args.requestId ? `requestId ${args.requestId}` : `url ~ '${args.url}'`;
4071
+ assertWakeApplies(args.target, args.wake, Boolean(args.includeBody && args.url));
3168
4072
  if (args.includeBody && args.url) {
3169
- const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
4073
+ const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
3170
4074
  try {
3171
4075
  await cap.flush();
3172
4076
  const match2 = (await readNetworkRequests(cap.file)).find((r) => r.url.includes(args.url));
@@ -3183,7 +4087,7 @@ async function getNetworkRequest(args = {}) {
3183
4087
  await cap.stop();
3184
4088
  }
3185
4089
  }
3186
- const target = await resolveTarget(args.target);
4090
+ const target = await resolveRecorderTarget(args.target, { capture: false });
3187
4091
  const match = pick(await readNetworkRequests(recFile(target.id)));
3188
4092
  if (!match) {
3189
4093
  throw new CdpError(`no network request matched ${describeSelector()}; run list_network_requests with reload:true first` + (args.includeBody ? `, then fetch the body with the url selector (reload re-mints requestIds)` : ``));
@@ -3199,8 +4103,8 @@ async function getNetworkRequest(args = {}) {
3199
4103
  }
3200
4104
 
3201
4105
  // src/tools/performance.ts
3202
- import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, stat, unlink as unlink3 } from "node:fs/promises";
3203
- import { join as join4 } from "node:path";
4106
+ import { mkdir as mkdir7, readFile as readFile6, writeFile as writeFile6, stat, unlink as unlink4 } from "node:fs/promises";
4107
+ import { join as join5 } from "node:path";
3204
4108
  var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3205
4109
  var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
3206
4110
  var DEFAULT_CATEGORIES = [
@@ -3218,7 +4122,7 @@ function shortId(id) {
3218
4122
  return id.slice(0, 8);
3219
4123
  }
3220
4124
  function stateFilePath() {
3221
- return join4(STATE_DIR2, "trace-state.json");
4125
+ return join5(STATE_DIR2, "trace-state.json");
3222
4126
  }
3223
4127
  function bufferTraceData(conn, sink) {
3224
4128
  return conn.on("Tracing.dataCollected", (params) => {
@@ -3240,10 +4144,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
3240
4144
  return sink;
3241
4145
  }
3242
4146
  async function writeTrace(events, targetId) {
3243
- await mkdir5(ARTIFACT_DIR3, { recursive: true });
3244
- const path = join4(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
4147
+ await mkdir7(ARTIFACT_DIR3, { recursive: true });
4148
+ const path = join5(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
3245
4149
  const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
3246
- await writeFile5(path, body, "utf8");
4150
+ await writeFile6(path, body, "utf8");
3247
4151
  const { size } = await stat(path);
3248
4152
  return { path, bytes: size };
3249
4153
  }
@@ -3263,7 +4167,7 @@ async function performanceStartTrace(args = {}) {
3263
4167
  throw err;
3264
4168
  }
3265
4169
  liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
3266
- await mkdir5(STATE_DIR2, { recursive: true });
4170
+ await mkdir7(STATE_DIR2, { recursive: true });
3267
4171
  const state = {
3268
4172
  targetId: target.id,
3269
4173
  url: target.url,
@@ -3271,7 +4175,7 @@ async function performanceStartTrace(args = {}) {
3271
4175
  pid: process.pid,
3272
4176
  categories
3273
4177
  };
3274
- await writeFile5(stateFilePath(), JSON.stringify(state), "utf8");
4178
+ await writeFile6(stateFilePath(), JSON.stringify(state), "utf8");
3275
4179
  return {
3276
4180
  target: { id: target.id, url: target.url, title: target.title },
3277
4181
  categories,
@@ -3284,7 +4188,7 @@ async function performanceStopTrace(args = {}) {
3284
4188
  if (!entry) {
3285
4189
  let hint = "";
3286
4190
  try {
3287
- const raw = await readFile5(stateFilePath(), "utf8");
4191
+ const raw = await readFile6(stateFilePath(), "utf8");
3288
4192
  const state = JSON.parse(raw);
3289
4193
  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)`;
3290
4194
  } catch {}
@@ -3296,7 +4200,7 @@ async function performanceStopTrace(args = {}) {
3296
4200
  } finally {
3297
4201
  conn.close();
3298
4202
  liveTraces.delete(target.id);
3299
- await unlink3(stateFilePath()).catch(() => {});
4203
+ await unlink4(stateFilePath()).catch(() => {});
3300
4204
  }
3301
4205
  const written = await writeTrace(events, target.id);
3302
4206
  const metrics = analyzeEvents(events);
@@ -3414,7 +4318,7 @@ async function performanceAnalyzeInsight(args = {}) {
3414
4318
  }
3415
4319
  let raw;
3416
4320
  try {
3417
- raw = await readFile5(args.tracePath, "utf8");
4321
+ raw = await readFile6(args.tracePath, "utf8");
3418
4322
  } catch (err) {
3419
4323
  throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
3420
4324
  }
@@ -3432,8 +4336,8 @@ async function performanceAnalyzeInsight(args = {}) {
3432
4336
 
3433
4337
  // src/tools/screencast.ts
3434
4338
  import { spawn } from "node:child_process";
3435
- import { mkdir as mkdir6, rm, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3436
- import { join as join5 } from "node:path";
4339
+ import { mkdir as mkdir8, rm as rm2, stat as stat2, writeFile as writeFile7 } from "node:fs/promises";
4340
+ import { join as join6 } from "node:path";
3437
4341
  var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3438
4342
  var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
3439
4343
  function frameDurationsMs(ledger, stopWallclockMs) {
@@ -3489,7 +4393,7 @@ function pngSize(buf) {
3489
4393
  return;
3490
4394
  return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
3491
4395
  }
3492
- var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
4396
+ var JPEG_SOF2 = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
3493
4397
  function jpegSize(buf) {
3494
4398
  if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
3495
4399
  return;
@@ -3511,7 +4415,7 @@ function jpegSize(buf) {
3511
4415
  if (i + 3 >= buf.length)
3512
4416
  return;
3513
4417
  const segLen = buf.readUInt16BE(i + 2);
3514
- if (JPEG_SOF.has(marker)) {
4418
+ if (JPEG_SOF2.has(marker)) {
3515
4419
  if (i + 9 > buf.length)
3516
4420
  return;
3517
4421
  return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
@@ -3654,9 +4558,9 @@ async function startScreenRecording(args = {}) {
3654
4558
  conn.close();
3655
4559
  throw err;
3656
4560
  }
3657
- const spoolDir = join5(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
4561
+ const spoolDir = join6(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
3658
4562
  try {
3659
- await mkdir6(spoolDir, { recursive: true });
4563
+ await mkdir8(spoolDir, { recursive: true });
3660
4564
  } catch (err) {
3661
4565
  conn.close();
3662
4566
  throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
@@ -3697,7 +4601,7 @@ async function startScreenRecording(args = {}) {
3697
4601
  seq += 1;
3698
4602
  const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
3699
4603
  rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
3700
- rec.writes = rec.writes.then(() => writeFile6(join5(spoolDir, file), buf)).catch(() => {
4604
+ rec.writes = rec.writes.then(() => writeFile7(join6(spoolDir, file), buf)).catch(() => {
3701
4605
  rec.dropped += 1;
3702
4606
  rec.unwritten.add(file);
3703
4607
  });
@@ -3719,7 +4623,7 @@ async function startScreenRecording(args = {}) {
3719
4623
  } catch (err) {
3720
4624
  rec.off();
3721
4625
  conn.close();
3722
- await rm(spoolDir, { recursive: true, force: true }).catch(() => {});
4626
+ await rm2(spoolDir, { recursive: true, force: true }).catch(() => {});
3723
4627
  throw err;
3724
4628
  }
3725
4629
  liveRecordings.set(target.id, rec);
@@ -3757,14 +4661,14 @@ async function stopScreenRecording(args = {}) {
3757
4661
  liveRecordings.delete(rec.target.id);
3758
4662
  const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
3759
4663
  if (!ledger.length) {
3760
- await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
4664
+ await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
3761
4665
  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.`);
3762
4666
  }
3763
- await mkdir6(ARTIFACT_DIR4, { recursive: true });
3764
- const outPath = args.savePath ?? join5(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
4667
+ await mkdir8(ARTIFACT_DIR4, { recursive: true });
4668
+ const outPath = args.savePath ?? join6(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
3765
4669
  const frames = coalesceToConcatGrid(ledger, stoppedAt);
3766
- const manifestPath = join5(rec.spoolDir, "frames.ffconcat");
3767
- await writeFile6(manifestPath, buildConcatManifest(frames), "utf8");
4670
+ const manifestPath = join6(rec.spoolDir, "frames.ffconcat");
4671
+ await writeFile7(manifestPath, buildConcatManifest(frames), "utf8");
3768
4672
  const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
3769
4673
  const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
3770
4674
  let res;
@@ -3778,7 +4682,7 @@ async function stopScreenRecording(args = {}) {
3778
4682
  ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
3779
4683
  }
3780
4684
  const { size } = await stat2(outPath);
3781
- await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
4685
+ await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
3782
4686
  return {
3783
4687
  path: outPath,
3784
4688
  bytes: size,
@@ -3795,8 +4699,8 @@ ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
3795
4699
  }
3796
4700
 
3797
4701
  // src/tools/heap.ts
3798
- import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
3799
- import { join as join6 } from "node:path";
4702
+ import { mkdir as mkdir9, writeFile as writeFile8 } from "node:fs/promises";
4703
+ import { join as join7 } from "node:path";
3800
4704
  var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3801
4705
  function stamp4(targetId) {
3802
4706
  const short = targetId.slice(0, 8);
@@ -3821,9 +4725,9 @@ async function takeHeapsnapshot(args = {}) {
3821
4725
  if (snapshot.length === 0) {
3822
4726
  throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
3823
4727
  }
3824
- await mkdir7(ARTIFACT_DIR5, { recursive: true });
3825
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join6(ARTIFACT_DIR5, args.savePath) : join6(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
3826
- await writeFile7(path, snapshot, "utf8");
4728
+ await mkdir9(ARTIFACT_DIR5, { recursive: true });
4729
+ const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join7(ARTIFACT_DIR5, args.savePath) : join7(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
4730
+ await writeFile8(path, snapshot, "utf8");
3827
4731
  const bytes = Buffer.byteLength(snapshot, "utf8");
3828
4732
  return {
3829
4733
  path,
@@ -3836,7 +4740,7 @@ async function takeHeapsnapshot(args = {}) {
3836
4740
 
3837
4741
  // src/tools/lighthouse.ts
3838
4742
  import { spawn as spawn2 } from "node:child_process";
3839
- import { mkdir as mkdir8, readFile as readFile6, stat as stat3 } from "node:fs/promises";
4743
+ import { mkdir as mkdir10, readFile as readFile7, stat as stat3 } from "node:fs/promises";
3840
4744
  var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3841
4745
  function cdpPort() {
3842
4746
  const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
@@ -3897,7 +4801,7 @@ async function lighthouseAudit(args) {
3897
4801
  const url = args.url.trim();
3898
4802
  const timeoutMs = args.timeoutMs ?? 120000;
3899
4803
  const port = cdpPort();
3900
- await mkdir8(ARTIFACT_DIR6, { recursive: true });
4804
+ await mkdir10(ARTIFACT_DIR6, { recursive: true });
3901
4805
  const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
3902
4806
  const formFactor = args.formFactor ?? "desktop";
3903
4807
  const lhArgs = [
@@ -3926,7 +4830,7 @@ async function lighthouseAudit(args) {
3926
4830
  }
3927
4831
  let raw;
3928
4832
  try {
3929
- raw = await readFile6(outPath, "utf8");
4833
+ raw = await readFile7(outPath, "utf8");
3930
4834
  } catch {
3931
4835
  raw = undefined;
3932
4836
  }
@@ -4013,14 +4917,14 @@ async function dispatchMouse(args) {
4013
4917
  }
4014
4918
 
4015
4919
  // src/tools/downloads.ts
4016
- import { readdir as readdir3, rename, stat as stat4 } from "node:fs/promises";
4017
- import { basename, extname, join as join8 } from "node:path";
4920
+ import { readdir as readdir3, rename as rename2, stat as stat4 } from "node:fs/promises";
4921
+ import { basename, extname, join as join9 } from "node:path";
4018
4922
 
4019
4923
  // src/tools/browser-session.ts
4020
- import { mkdir as mkdir9 } from "node:fs/promises";
4021
- import { join as join7 } from "node:path";
4924
+ import { mkdir as mkdir11 } from "node:fs/promises";
4925
+ import { join as join8 } from "node:path";
4022
4926
  var ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
4023
- var DOWNLOADS_DIR = join7(ARTIFACT_DIR7, "downloads");
4927
+ var DOWNLOADS_DIR = join8(ARTIFACT_DIR7, "downloads");
4024
4928
  var DOWNLOAD_BUFFER_LIMIT = 50;
4025
4929
  var session;
4026
4930
  function isDisconnect(e) {
@@ -4092,7 +4996,7 @@ async function browserSend(method, params = {}) {
4092
4996
  }
4093
4997
  }
4094
4998
  async function armDownloadsAt(path) {
4095
- await mkdir9(path, { recursive: true });
4999
+ await mkdir11(path, { recursive: true });
4096
5000
  const s = await getBrowserSession();
4097
5001
  await s.conn.send("Browser.setDownloadBehavior", { behavior: "allowAndName", downloadPath: path, eventsEnabled: true });
4098
5002
  s.armedPath = path;
@@ -4182,8 +5086,8 @@ async function waitForDownload(args = {}) {
4182
5086
  const taken = new Set(await readdir3(DOWNLOADS_DIR).catch(() => []));
4183
5087
  taken.delete(basename(rec.filePath));
4184
5088
  const finalName = nextFreeName(suggestedFilename, taken);
4185
- const path = join8(DOWNLOADS_DIR, finalName);
4186
- await rename(rec.filePath, path);
5089
+ const path = join9(DOWNLOADS_DIR, finalName);
5090
+ await rename2(rec.filePath, path);
4187
5091
  const bytes = (await stat4(path)).size;
4188
5092
  return { path, suggestedFilename, bytes, ...rec.url ? { url: rec.url } : {}, target };
4189
5093
  }