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/mcp.js CHANGED
@@ -406,6 +406,12 @@ async function resolveLiveLabel(backend, label, liveIds) {
406
406
 
407
407
  // src/workers.ts
408
408
  var WORKER_SELECTOR_PREFIX = "worker:";
409
+ var WORKER_CAPABLE_TOOLS = [
410
+ "evaluate_script",
411
+ "list_network_requests",
412
+ "get_network_request",
413
+ "list_console_messages"
414
+ ];
409
415
  var WORKER_TARGET_TYPES = new Set(["service_worker", "shared_worker"]);
410
416
  function isWorkerSelector(selector) {
411
417
  return typeof selector === "string" && selector.startsWith(WORKER_SELECTOR_PREFIX);
@@ -424,8 +430,18 @@ function resolveWorkerTargets(targets, needle) {
424
430
  };
425
431
  }
426
432
  var WORKER_EMPTY_NEEDLE_MESSAGE = "'worker:' needs a substring of the worker's url, e.g. worker:<extension-id> or worker:background.js";
427
- 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.";
428
- 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.";
433
+ 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.";
434
+ 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(", ")}.`;
435
+ function workerWakeMisuseMessage(kind) {
436
+ if (kind === "not-a-worker-selector") {
437
+ 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.";
438
+ }
439
+ 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.";
440
+ }
441
+ function workerBufferReadMissMessage(needle, liveWorkers) {
442
+ const running = liveWorkers.length ? ` (running workers: ${urlList(liveWorkers)})` : " (no workers are running)";
443
+ 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.";
444
+ }
429
445
  function urlList(workers) {
430
446
  return workers.map((w) => w.url).join(", ");
431
447
  }
@@ -1105,6 +1121,8 @@ async function clearMocks(args = {}) {
1105
1121
  }
1106
1122
 
1107
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";
1108
1126
  var UID_STAMP_ATTR = "data-cdp-uid";
1109
1127
  var LEGACY_NUMERIC_UID = /^[0-9]+$/;
1110
1128
  var REQUIRED_CAPABILITIES = {
@@ -1134,6 +1152,41 @@ function interpolatePoints(from, to, steps) {
1134
1152
  out.push({ x: to.x, y: to.y });
1135
1153
  return out;
1136
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
+ }
1137
1190
 
1138
1191
  // src/cdp/driver.ts
1139
1192
  function driverError(code, message, data) {
@@ -1365,14 +1418,73 @@ async function backendNodeIdOf(conn, loc) {
1365
1418
  }
1366
1419
  return searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
1367
1420
  }
1368
- async function fullPageClip(conn) {
1369
- const m = await conn.send("Page.getLayoutMetrics");
1421
+ async function layoutMetrics(conn) {
1422
+ return conn.send("Page.getLayoutMetrics");
1423
+ }
1424
+ function contentClip(m) {
1370
1425
  const size = m.cssContentSize ?? m.contentSize;
1371
1426
  if (!size)
1372
1427
  throw driverError("page-error", "Page.getLayoutMetrics returned no content size");
1373
1428
  return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height) };
1374
1429
  }
1375
- 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) {
1376
1488
  const backendNodeId = await backendNodeIdOf(conn, loc);
1377
1489
  await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => {
1378
1490
  return;
@@ -1389,6 +1501,10 @@ async function elementClip(conn, loc) {
1389
1501
  throw driverError("page-error", "resolved element has zero area");
1390
1502
  return { x: minX, y: minY, width, height };
1391
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
+ }
1392
1508
  var CDP_CAPABILITIES = new Set([
1393
1509
  "trace.performance",
1394
1510
  "heap.snapshot",
@@ -1399,6 +1515,9 @@ var CDP_CAPABILITIES = new Set([
1399
1515
  "emulate.networkConditions",
1400
1516
  "screenshot.fullPage",
1401
1517
  "screenshot.element",
1518
+ "screenshot.scale",
1519
+ "screenshot.renderSize",
1520
+ "screenshot.tile",
1402
1521
  "network.intercept",
1403
1522
  "snapshot.accessibilityTree",
1404
1523
  "input.insertTextAtomic",
@@ -1689,31 +1808,128 @@ class CdpPageDriver {
1689
1808
  const { objectId } = await resolveElementLocator(this.conn, loc);
1690
1809
  await this.conn.send("DOM.setFileInputFiles", { files: [...files], objectId });
1691
1810
  }
1692
- 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) {
1693
1856
  await this.ensureDomain("Page");
1694
1857
  const format = opts?.format ?? "png";
1695
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);
1696
1873
  const params = { format, captureBeyondViewport: true };
1697
1874
  if (quality != null)
1698
1875
  params.quality = quality;
1699
- if (opts?.clip)
1700
- params.clip = { ...await elementClip(this.conn, opts.clip), scale: 1 };
1701
- else if (opts?.fullPage)
1702
- params.clip = { ...await fullPageClip(this.conn), scale: 1 };
1876
+ if (sendsClip)
1877
+ params.clip = { ...clip, scale };
1703
1878
  const { data } = await this.conn.send("Page.captureScreenshot", params);
1704
1879
  return { data: new Uint8Array(Buffer.from(data, "base64")), format };
1705
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
+ }
1706
1908
  async emulate(opts) {
1707
1909
  if (opts.clearOverrides) {
1708
1910
  const c = () => {
1709
1911
  return;
1710
1912
  };
1711
- 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
+ }
1712
1924
  await this.conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(c);
1713
1925
  await this.conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(c);
1714
1926
  await this.conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(c);
1715
1927
  await this.ensureDomainSoft("Network");
1716
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);
1717
1933
  return { applied: [] };
1718
1934
  }
1719
1935
  const applied = [];
@@ -1727,6 +1943,12 @@ class CdpPageDriver {
1727
1943
  deviceScaleFactor: opts.deviceScaleFactor ?? 0,
1728
1944
  mobile: opts.mobile ?? false
1729
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
+ });
1730
1952
  applied.push("deviceMetrics");
1731
1953
  }
1732
1954
  if (opts.userAgent != null) {
@@ -2114,8 +2336,522 @@ async function reapStaleAgentTabs(driver, backend) {
2114
2336
  }
2115
2337
 
2116
2338
  // src/shared-tools.ts
2117
- import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
2118
- 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
2119
2855
  var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2120
2856
  var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
2121
2857
 
@@ -2260,8 +2996,8 @@ async function closePage(driver, args) {
2260
2996
  return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
2261
2997
  }
2262
2998
  async function writeSelectedFile(id) {
2263
- await mkdir3(STATE_DIR, { recursive: true });
2264
- await writeFile3(join3(STATE_DIR, "selected"), id, "utf8");
2999
+ await mkdir5(STATE_DIR, { recursive: true });
3000
+ await writeFile4(join4(STATE_DIR, "selected"), id, "utf8");
2265
3001
  }
2266
3002
  async function selectPage(driver, args) {
2267
3003
  if (args.target === undefined || args.target === "")
@@ -2325,10 +3061,10 @@ function serializeForSink(value) {
2325
3061
  return json ?? "null";
2326
3062
  }
2327
3063
  async function writeJsonSink(savePath, value) {
2328
- const path = savePath.startsWith("/") ? savePath : join3(ARTIFACT_DIR, savePath);
2329
- await mkdir3(dirname(path), { recursive: true });
3064
+ const path = savePath.startsWith("/") ? savePath : join4(ARTIFACT_DIR, savePath);
3065
+ await mkdir5(dirname2(path), { recursive: true });
2330
3066
  const json = serializeForSink(value);
2331
- await writeFile3(path, json, "utf8");
3067
+ await writeFile4(path, json, "utf8");
2332
3068
  return { path, bytes: Buffer.byteLength(json, "utf8") };
2333
3069
  }
2334
3070
  function sinkRequested(savePath) {
@@ -2632,30 +3368,166 @@ async function uploadFile(driver, args) {
2632
3368
  function stamp() {
2633
3369
  return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
2634
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
+ }
2635
3469
  async function takeScreenshot(driver, args = {}) {
2636
3470
  if (args.uid !== undefined && args.uid !== "" && args.selector)
2637
3471
  throw new SharedToolError("provide exactly one of uid or selector, not both");
2638
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);
2639
3476
  return withPage2(driver, args.target, async (page) => {
2640
3477
  const clip = args.uid !== undefined || args.selector ? locatorOf(args) : undefined;
2641
- const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
2642
- if (data.byteLength === 0)
2643
- throw new SharedToolError("captureScreenshot returned empty data");
2644
- await mkdir3(ARTIFACT_DIR, { recursive: true });
2645
- const ext = outFormat === "jpeg" ? "jpg" : "png";
2646
- const path = args.savePath ?? join3(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
2647
- await writeFile3(path, Buffer.from(data));
2648
- const result = {
2649
- path,
2650
- bytes: data.byteLength,
2651
- format: outFormat,
2652
- 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);
2653
3486
  };
2654
- if (args.returnBase64)
2655
- 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
+ }
2656
3513
  return result;
2657
3514
  });
2658
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
+ }
2659
3531
  async function emulate(driver, args = {}) {
2660
3532
  return withPage2(driver, args.target, async (page) => {
2661
3533
  if (args.clearOverrides) {
@@ -2826,10 +3698,10 @@ var LEASE_TOOLS = {
2826
3698
  };
2827
3699
 
2828
3700
  // src/tools/console.ts
2829
- import { readFile as readFile3 } from "node:fs/promises";
3701
+ import { readFile as readFile4 } from "node:fs/promises";
2830
3702
 
2831
3703
  // src/tools/recorder.ts
2832
- 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";
2833
3705
  var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
2834
3706
  function recFile(targetId) {
2835
3707
  return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
@@ -2840,6 +3712,31 @@ function captureFile(targetId, captureId) {
2840
3712
  function delay2(ms) {
2841
3713
  return new Promise((r) => setTimeout(r, ms));
2842
3714
  }
3715
+ async function resolveRecorderTarget(target, opts) {
3716
+ if (!isWorkerSelector(target))
3717
+ return resolveTarget(target);
3718
+ if (opts.capture)
3719
+ return resolveWorkerSelector(target, { wake: opts.wake ?? true });
3720
+ const needle = workerNeedle(target);
3721
+ if (needle === "")
3722
+ throw new CdpError(WORKER_EMPTY_NEEDLE_MESSAGE);
3723
+ const { matches, liveWorkers } = resolveWorkerTargets(await listTargets(), needle);
3724
+ if (matches.length > 1)
3725
+ throw new CdpError(workerAmbiguityMessage(needle, matches));
3726
+ if (matches.length === 1)
3727
+ return matches[0];
3728
+ throw new CdpError(workerBufferReadMissMessage(needle, liveWorkers));
3729
+ }
3730
+ function assertWakeApplies(target, wake, capture) {
3731
+ if (wake === undefined)
3732
+ return;
3733
+ if (typeof wake !== "boolean")
3734
+ throw new CdpError("'wake' must be a boolean");
3735
+ if (!isWorkerSelector(target))
3736
+ throw new CdpError(workerWakeMisuseMessage("not-a-worker-selector"));
3737
+ if (!capture)
3738
+ throw new CdpError(workerWakeMisuseMessage("read-only-call"));
3739
+ }
2843
3740
  var NETWORK_EVENTS = [
2844
3741
  "Network.requestWillBeSent",
2845
3742
  "Network.responseReceived",
@@ -2850,11 +3747,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
2850
3747
  async function startRecorder(target, opts = {}) {
2851
3748
  const network = opts.network ?? true;
2852
3749
  const consoleCap = opts.console ?? true;
2853
- await mkdir4(ARTIFACT_DIR2, { recursive: true });
3750
+ await mkdir6(ARTIFACT_DIR2, { recursive: true });
2854
3751
  const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
2855
3752
  const file = opts.file ?? recFile(resolved.id);
2856
3753
  if (opts.truncate) {
2857
- await writeFile4(file, "");
3754
+ await writeFile5(file, "");
2858
3755
  }
2859
3756
  let chain = Promise.resolve();
2860
3757
  let dropped = 0;
@@ -2905,7 +3802,8 @@ async function startRecorder(target, opts = {}) {
2905
3802
  };
2906
3803
  }
2907
3804
  async function captureWindow(target, durationMs, opts = {}) {
2908
- const resolved = await resolveTarget(target);
3805
+ const resolved = await resolveRecorderTarget(target, { capture: true, wake: opts.wake });
3806
+ const isWorker = isWorkerTargetType(resolved.type);
2909
3807
  const captureId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
2910
3808
  const file = captureFile(resolved.id, captureId);
2911
3809
  const rec = await startRecorder(resolved.id, {
@@ -2915,13 +3813,16 @@ async function captureWindow(target, durationMs, opts = {}) {
2915
3813
  file,
2916
3814
  timeoutMs: opts.timeoutMs
2917
3815
  });
2918
- await rec.conn.send("Page.enable");
2919
- await rec.conn.send("Page.reload", { ignoreCache: false });
3816
+ if (!isWorker) {
3817
+ await rec.conn.send("Page.enable");
3818
+ await rec.conn.send("Page.reload", { ignoreCache: false });
3819
+ }
2920
3820
  await delay2(durationMs);
2921
3821
  return {
2922
3822
  file: rec.file,
2923
3823
  conn: rec.conn,
2924
3824
  resolved: { id: rec.target.id, url: rec.target.url, title: rec.target.title },
3825
+ mode: isWorker ? "listen" : "reload",
2925
3826
  droppedWrites: rec.droppedWrites,
2926
3827
  flush: () => rec.flush(),
2927
3828
  async stop() {
@@ -2996,7 +3897,7 @@ function lineToEntry(line, index) {
2996
3897
  async function readConsoleEntries(file) {
2997
3898
  let raw;
2998
3899
  try {
2999
- raw = await readFile3(file, "utf8");
3900
+ raw = await readFile4(file, "utf8");
3000
3901
  } catch {
3001
3902
  return [];
3002
3903
  }
@@ -3021,8 +3922,9 @@ async function readConsoleEntries(file) {
3021
3922
  return out;
3022
3923
  }
3023
3924
  async function listConsoleMessages(args = {}) {
3925
+ assertWakeApplies(args.target, args.wake, args.reload === true);
3024
3926
  if (args.reload) {
3025
- const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS);
3927
+ const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS, { wake: args.wake });
3026
3928
  await cap.stop();
3027
3929
  const messages2 = await readConsoleEntries(cap.file);
3028
3930
  return {
@@ -3032,7 +3934,7 @@ async function listConsoleMessages(args = {}) {
3032
3934
  droppedWrites: cap.droppedWrites()
3033
3935
  };
3034
3936
  }
3035
- const target = await resolveTarget(args.target);
3937
+ const target = await resolveRecorderTarget(args.target, { capture: false });
3036
3938
  const messages = await readConsoleEntries(recFile(target.id));
3037
3939
  return {
3038
3940
  target: { id: target.id, url: target.url, title: target.title },
@@ -3041,7 +3943,7 @@ async function listConsoleMessages(args = {}) {
3041
3943
  };
3042
3944
  }
3043
3945
  async function getConsoleMessage(args = {}) {
3044
- const target = await resolveTarget(args.target);
3946
+ const target = await resolveRecorderTarget(args.target, { capture: false });
3045
3947
  const messages = await readConsoleEntries(recFile(target.id));
3046
3948
  const index = args.index ?? 0;
3047
3949
  const entry = messages[index];
@@ -3052,7 +3954,7 @@ async function getConsoleMessage(args = {}) {
3052
3954
  }
3053
3955
 
3054
3956
  // src/tools/network.ts
3055
- import { readFile as readFile4 } from "node:fs/promises";
3957
+ import { readFile as readFile5 } from "node:fs/promises";
3056
3958
  var DEFAULT_CAPTURE_MS2 = 2500;
3057
3959
  function asHeaders(v) {
3058
3960
  if (!v || typeof v !== "object")
@@ -3119,7 +4021,7 @@ function correlate(lines) {
3119
4021
  async function readNetworkRequests(file) {
3120
4022
  let raw;
3121
4023
  try {
3122
- raw = await readFile4(file, "utf8");
4024
+ raw = await readFile5(file, "utf8");
3123
4025
  } catch {
3124
4026
  return [];
3125
4027
  }
@@ -3139,14 +4041,15 @@ async function listNetworkRequests(args = {}) {
3139
4041
  let file;
3140
4042
  let resolved;
3141
4043
  let droppedWrites;
4044
+ assertWakeApplies(args.target, args.wake, args.reload === true);
3142
4045
  if (args.reload) {
3143
- const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
4046
+ const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
3144
4047
  await cap.stop();
3145
4048
  file = cap.file;
3146
4049
  resolved = cap.resolved;
3147
4050
  droppedWrites = cap.droppedWrites();
3148
4051
  } else {
3149
- const target = await resolveTarget(args.target);
4052
+ const target = await resolveRecorderTarget(args.target, { capture: false });
3150
4053
  file = recFile(target.id);
3151
4054
  resolved = { id: target.id, url: target.url, title: target.title };
3152
4055
  }
@@ -3167,8 +4070,9 @@ async function getNetworkRequest(args = {}) {
3167
4070
  return requests.find((r) => r.url.includes(args.url));
3168
4071
  };
3169
4072
  const describeSelector = () => args.requestId ? `requestId ${args.requestId}` : `url ~ '${args.url}'`;
4073
+ assertWakeApplies(args.target, args.wake, Boolean(args.includeBody && args.url));
3170
4074
  if (args.includeBody && args.url) {
3171
- const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
4075
+ const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
3172
4076
  try {
3173
4077
  await cap.flush();
3174
4078
  const match2 = (await readNetworkRequests(cap.file)).find((r) => r.url.includes(args.url));
@@ -3185,7 +4089,7 @@ async function getNetworkRequest(args = {}) {
3185
4089
  await cap.stop();
3186
4090
  }
3187
4091
  }
3188
- const target = await resolveTarget(args.target);
4092
+ const target = await resolveRecorderTarget(args.target, { capture: false });
3189
4093
  const match = pick(await readNetworkRequests(recFile(target.id)));
3190
4094
  if (!match) {
3191
4095
  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)` : ``));
@@ -3201,8 +4105,8 @@ async function getNetworkRequest(args = {}) {
3201
4105
  }
3202
4106
 
3203
4107
  // src/tools/performance.ts
3204
- import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, stat, unlink as unlink3 } from "node:fs/promises";
3205
- 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";
3206
4110
  var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3207
4111
  var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
3208
4112
  var DEFAULT_CATEGORIES = [
@@ -3220,7 +4124,7 @@ function shortId(id) {
3220
4124
  return id.slice(0, 8);
3221
4125
  }
3222
4126
  function stateFilePath() {
3223
- return join4(STATE_DIR2, "trace-state.json");
4127
+ return join5(STATE_DIR2, "trace-state.json");
3224
4128
  }
3225
4129
  function bufferTraceData(conn, sink) {
3226
4130
  return conn.on("Tracing.dataCollected", (params) => {
@@ -3242,10 +4146,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
3242
4146
  return sink;
3243
4147
  }
3244
4148
  async function writeTrace(events, targetId) {
3245
- await mkdir5(ARTIFACT_DIR3, { recursive: true });
3246
- 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`);
3247
4151
  const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
3248
- await writeFile5(path, body, "utf8");
4152
+ await writeFile6(path, body, "utf8");
3249
4153
  const { size } = await stat(path);
3250
4154
  return { path, bytes: size };
3251
4155
  }
@@ -3265,7 +4169,7 @@ async function performanceStartTrace(args = {}) {
3265
4169
  throw err;
3266
4170
  }
3267
4171
  liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
3268
- await mkdir5(STATE_DIR2, { recursive: true });
4172
+ await mkdir7(STATE_DIR2, { recursive: true });
3269
4173
  const state = {
3270
4174
  targetId: target.id,
3271
4175
  url: target.url,
@@ -3273,7 +4177,7 @@ async function performanceStartTrace(args = {}) {
3273
4177
  pid: process.pid,
3274
4178
  categories
3275
4179
  };
3276
- await writeFile5(stateFilePath(), JSON.stringify(state), "utf8");
4180
+ await writeFile6(stateFilePath(), JSON.stringify(state), "utf8");
3277
4181
  return {
3278
4182
  target: { id: target.id, url: target.url, title: target.title },
3279
4183
  categories,
@@ -3286,7 +4190,7 @@ async function performanceStopTrace(args = {}) {
3286
4190
  if (!entry) {
3287
4191
  let hint = "";
3288
4192
  try {
3289
- const raw = await readFile5(stateFilePath(), "utf8");
4193
+ const raw = await readFile6(stateFilePath(), "utf8");
3290
4194
  const state = JSON.parse(raw);
3291
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)`;
3292
4196
  } catch {}
@@ -3298,7 +4202,7 @@ async function performanceStopTrace(args = {}) {
3298
4202
  } finally {
3299
4203
  conn.close();
3300
4204
  liveTraces.delete(target.id);
3301
- await unlink3(stateFilePath()).catch(() => {});
4205
+ await unlink4(stateFilePath()).catch(() => {});
3302
4206
  }
3303
4207
  const written = await writeTrace(events, target.id);
3304
4208
  const metrics = analyzeEvents(events);
@@ -3416,7 +4320,7 @@ async function performanceAnalyzeInsight(args = {}) {
3416
4320
  }
3417
4321
  let raw;
3418
4322
  try {
3419
- raw = await readFile5(args.tracePath, "utf8");
4323
+ raw = await readFile6(args.tracePath, "utf8");
3420
4324
  } catch (err) {
3421
4325
  throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
3422
4326
  }
@@ -3434,8 +4338,8 @@ async function performanceAnalyzeInsight(args = {}) {
3434
4338
 
3435
4339
  // src/tools/screencast.ts
3436
4340
  import { spawn } from "node:child_process";
3437
- import { mkdir as mkdir6, rm, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
3438
- 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";
3439
4343
  var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3440
4344
  var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
3441
4345
  function frameDurationsMs(ledger, stopWallclockMs) {
@@ -3491,7 +4395,7 @@ function pngSize(buf) {
3491
4395
  return;
3492
4396
  return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
3493
4397
  }
3494
- 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]);
3495
4399
  function jpegSize(buf) {
3496
4400
  if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
3497
4401
  return;
@@ -3513,7 +4417,7 @@ function jpegSize(buf) {
3513
4417
  if (i + 3 >= buf.length)
3514
4418
  return;
3515
4419
  const segLen = buf.readUInt16BE(i + 2);
3516
- if (JPEG_SOF.has(marker)) {
4420
+ if (JPEG_SOF2.has(marker)) {
3517
4421
  if (i + 9 > buf.length)
3518
4422
  return;
3519
4423
  return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
@@ -3656,9 +4560,9 @@ async function startScreenRecording(args = {}) {
3656
4560
  conn.close();
3657
4561
  throw err;
3658
4562
  }
3659
- const spoolDir = join5(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
4563
+ const spoolDir = join6(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
3660
4564
  try {
3661
- await mkdir6(spoolDir, { recursive: true });
4565
+ await mkdir8(spoolDir, { recursive: true });
3662
4566
  } catch (err) {
3663
4567
  conn.close();
3664
4568
  throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
@@ -3699,7 +4603,7 @@ async function startScreenRecording(args = {}) {
3699
4603
  seq += 1;
3700
4604
  const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
3701
4605
  rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
3702
- rec.writes = rec.writes.then(() => writeFile6(join5(spoolDir, file), buf)).catch(() => {
4606
+ rec.writes = rec.writes.then(() => writeFile7(join6(spoolDir, file), buf)).catch(() => {
3703
4607
  rec.dropped += 1;
3704
4608
  rec.unwritten.add(file);
3705
4609
  });
@@ -3721,7 +4625,7 @@ async function startScreenRecording(args = {}) {
3721
4625
  } catch (err) {
3722
4626
  rec.off();
3723
4627
  conn.close();
3724
- await rm(spoolDir, { recursive: true, force: true }).catch(() => {});
4628
+ await rm2(spoolDir, { recursive: true, force: true }).catch(() => {});
3725
4629
  throw err;
3726
4630
  }
3727
4631
  liveRecordings.set(target.id, rec);
@@ -3759,14 +4663,14 @@ async function stopScreenRecording(args = {}) {
3759
4663
  liveRecordings.delete(rec.target.id);
3760
4664
  const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
3761
4665
  if (!ledger.length) {
3762
- await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
4666
+ await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
3763
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.`);
3764
4668
  }
3765
- await mkdir6(ARTIFACT_DIR4, { recursive: true });
3766
- 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`);
3767
4671
  const frames = coalesceToConcatGrid(ledger, stoppedAt);
3768
- const manifestPath = join5(rec.spoolDir, "frames.ffconcat");
3769
- await writeFile6(manifestPath, buildConcatManifest(frames), "utf8");
4672
+ const manifestPath = join6(rec.spoolDir, "frames.ffconcat");
4673
+ await writeFile7(manifestPath, buildConcatManifest(frames), "utf8");
3770
4674
  const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
3771
4675
  const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
3772
4676
  let res;
@@ -3780,7 +4684,7 @@ async function stopScreenRecording(args = {}) {
3780
4684
  ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
3781
4685
  }
3782
4686
  const { size } = await stat2(outPath);
3783
- await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
4687
+ await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
3784
4688
  return {
3785
4689
  path: outPath,
3786
4690
  bytes: size,
@@ -3797,8 +4701,8 @@ ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
3797
4701
  }
3798
4702
 
3799
4703
  // src/tools/heap.ts
3800
- import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
3801
- 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";
3802
4706
  var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3803
4707
  function stamp4(targetId) {
3804
4708
  const short = targetId.slice(0, 8);
@@ -3823,9 +4727,9 @@ async function takeHeapsnapshot(args = {}) {
3823
4727
  if (snapshot.length === 0) {
3824
4728
  throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
3825
4729
  }
3826
- await mkdir7(ARTIFACT_DIR5, { recursive: true });
3827
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join6(ARTIFACT_DIR5, args.savePath) : join6(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
3828
- 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");
3829
4733
  const bytes = Buffer.byteLength(snapshot, "utf8");
3830
4734
  return {
3831
4735
  path,
@@ -3838,7 +4742,7 @@ async function takeHeapsnapshot(args = {}) {
3838
4742
 
3839
4743
  // src/tools/lighthouse.ts
3840
4744
  import { spawn as spawn2 } from "node:child_process";
3841
- 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";
3842
4746
  var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
3843
4747
  function cdpPort() {
3844
4748
  const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
@@ -3899,7 +4803,7 @@ async function lighthouseAudit(args) {
3899
4803
  const url = args.url.trim();
3900
4804
  const timeoutMs = args.timeoutMs ?? 120000;
3901
4805
  const port = cdpPort();
3902
- await mkdir8(ARTIFACT_DIR6, { recursive: true });
4806
+ await mkdir10(ARTIFACT_DIR6, { recursive: true });
3903
4807
  const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
3904
4808
  const formFactor = args.formFactor ?? "desktop";
3905
4809
  const lhArgs = [
@@ -3928,7 +4832,7 @@ async function lighthouseAudit(args) {
3928
4832
  }
3929
4833
  let raw;
3930
4834
  try {
3931
- raw = await readFile6(outPath, "utf8");
4835
+ raw = await readFile7(outPath, "utf8");
3932
4836
  } catch {
3933
4837
  raw = undefined;
3934
4838
  }
@@ -4015,14 +4919,14 @@ async function dispatchMouse(args) {
4015
4919
  }
4016
4920
 
4017
4921
  // src/tools/downloads.ts
4018
- import { readdir as readdir3, rename, stat as stat4 } from "node:fs/promises";
4019
- 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";
4020
4924
 
4021
4925
  // src/tools/browser-session.ts
4022
- import { mkdir as mkdir9 } from "node:fs/promises";
4023
- import { join as join7 } from "node:path";
4926
+ import { mkdir as mkdir11 } from "node:fs/promises";
4927
+ import { join as join8 } from "node:path";
4024
4928
  var ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
4025
- var DOWNLOADS_DIR = join7(ARTIFACT_DIR7, "downloads");
4929
+ var DOWNLOADS_DIR = join8(ARTIFACT_DIR7, "downloads");
4026
4930
  var DOWNLOAD_BUFFER_LIMIT = 50;
4027
4931
  var session;
4028
4932
  function isDisconnect(e) {
@@ -4094,7 +4998,7 @@ async function browserSend(method, params = {}) {
4094
4998
  }
4095
4999
  }
4096
5000
  async function armDownloadsAt(path) {
4097
- await mkdir9(path, { recursive: true });
5001
+ await mkdir11(path, { recursive: true });
4098
5002
  const s = await getBrowserSession();
4099
5003
  await s.conn.send("Browser.setDownloadBehavior", { behavior: "allowAndName", downloadPath: path, eventsEnabled: true });
4100
5004
  s.armedPath = path;
@@ -4184,8 +5088,8 @@ async function waitForDownload(args = {}) {
4184
5088
  const taken = new Set(await readdir3(DOWNLOADS_DIR).catch(() => []));
4185
5089
  taken.delete(basename(rec.filePath));
4186
5090
  const finalName = nextFreeName(suggestedFilename, taken);
4187
- const path = join8(DOWNLOADS_DIR, finalName);
4188
- await rename(rec.filePath, path);
5091
+ const path = join9(DOWNLOADS_DIR, finalName);
5092
+ await rename2(rec.filePath, path);
4189
5093
  const bytes = (await stat4(path)).size;
4190
5094
  return { path, suggestedFilename, bytes, ...rec.url ? { url: rec.url } : {}, target };
4191
5095
  }
@@ -4481,7 +5385,7 @@ var MANIFEST = [
4481
5385
  },
4482
5386
  {
4483
5387
  name: "evaluate_script",
4484
- description: "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string. Pass 'savePath' to write the evaluated value to a JSON file instead: the response then carries only {path,bytes,type,target} and the value itself never appears in it, in any form. That is the way to read a credential (a JWT or session token out of localStorage, for example) without putting the secret into the caller's transcript. This is also the one tool that can evaluate inside an MV3 extension's background SERVICE WORKER rather than a page: pass target 'worker:<extension-id>' (Chrome only) and read chrome.storage.local, chrome.runtime and the worker's own globals directly, with an idle-evicted worker started for you first (see 'wake').",
5388
+ description: "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string. Pass 'savePath' to write the evaluated value to a JSON file instead: the response then carries only {path,bytes,type,target} and the value itself never appears in it, in any form. That is the way to read a credential (a JWT or session token out of localStorage, for example) without putting the secret into the caller's transcript. This is the one tool that can EVALUATE inside an MV3 extension's background SERVICE WORKER rather than a page (list_network_requests/get_network_request/list_console_messages OBSERVE one): pass target 'worker:<extension-id>' (Chrome only) and read chrome.storage.local, chrome.runtime and the worker's own globals directly, with an idle-evicted worker started for you first (see 'wake').",
4485
5389
  inputSchema: {
4486
5390
  type: "object",
4487
5391
  properties: {
@@ -4491,7 +5395,7 @@ var MANIFEST = [
4491
5395
  },
4492
5396
  target: {
4493
5397
  type: "string",
4494
- description: "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring | 'label:<name>' -> the page-type target with exactly this label (checked against the origin ledger and live leases) | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js) and read chrome.storage.local from it. The worker: arm is CHROME-ONLY (Capability 'worker.targets') and is accepted by this tool only; an MV3 worker that has been idle-evicted is started automatically first, see 'wake'."
5398
+ description: "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring | 'label:<name>' -> the page-type target with exactly this label (checked against the origin ledger and live leases) | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js) and read chrome.storage.local from it. The worker: arm is CHROME-ONLY (Capability 'worker.targets') and is accepted by four tools only — evaluate_script, list_network_requests, get_network_request and list_console_messages — never as universal grammar; an MV3 worker that has been idle-evicted is started automatically first, see 'wake'."
4495
5399
  },
4496
5400
  wake: {
4497
5401
  type: "boolean",
@@ -5186,7 +6090,7 @@ var MANIFEST = [
5186
6090
  },
5187
6091
  {
5188
6092
  name: "take_screenshot",
5189
- 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.",
5190
6094
  inputSchema: {
5191
6095
  type: "object",
5192
6096
  properties: {
@@ -5214,6 +6118,22 @@ var MANIFEST = [
5214
6118
  type: "boolean",
5215
6119
  description: "Capture the full scrollable content height computed from Page.getLayoutMetrics, not just the viewport."
5216
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
+ },
5217
6137
  uid: {
5218
6138
  type: "number",
5219
6139
  description: "Element to clip to: a CDP backendDOMNodeId obtained from take_snapshot. Mutually exclusive with selector."
@@ -5224,11 +6144,11 @@ var MANIFEST = [
5224
6144
  },
5225
6145
  savePath: {
5226
6146
  type: "string",
5227
- 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."
5228
6148
  },
5229
6149
  returnBase64: {
5230
6150
  type: "boolean",
5231
- 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."
5232
6152
  }
5233
6153
  },
5234
6154
  required: [],
@@ -5237,7 +6157,7 @@ var MANIFEST = [
5237
6157
  },
5238
6158
  {
5239
6159
  name: "emulate",
5240
- 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.",
5241
6161
  inputSchema: {
5242
6162
  type: "object",
5243
6163
  properties: {
@@ -5412,7 +6332,7 @@ var MANIFEST = [
5412
6332
  },
5413
6333
  {
5414
6334
  name: "list_console_messages",
5415
- description: "Read console output (logs, warnings, exceptions) captured for the target page. By default reads the target's existing shared buffer and returns parsed console entries (empty if no capture has run); with reload:true it reloads the page and records a fresh capture window (both console+network) so a network reload never wipes console history.",
6335
+ description: "Read console output (logs, warnings, exceptions) captured for the target page. By default reads the target's existing shared buffer and returns parsed console entries (empty if no capture has run); with reload:true it reloads the page and records a fresh capture window (both console+network) so a network reload never wipes console history. SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker's console.log arrives as Runtime.consoleAPICalled on its own session. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
5416
6336
  inputSchema: {
5417
6337
  type: "object",
5418
6338
  properties: {
@@ -5422,15 +6342,19 @@ var MANIFEST = [
5422
6342
  },
5423
6343
  target: {
5424
6344
  type: "string",
5425
- description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
6345
+ description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
5426
6346
  },
5427
6347
  reload: {
5428
6348
  type: "boolean",
5429
- description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer)."
6349
+ description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer). With a worker: target there is nothing to reload: the window records for durationMs while the worker runs, and the session it holds keeps that worker from being idle-evicted until the capture stops."
5430
6350
  },
5431
6351
  durationMs: {
5432
6352
  type: "number",
5433
6353
  description: "Capture window for reload mode, in milliseconds. Default 2500."
6354
+ },
6355
+ wake: {
6356
+ type: "boolean",
6357
+ description: "Worker targets only, and only when a capture is being started (reload:true). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
5434
6358
  }
5435
6359
  },
5436
6360
  required: [],
@@ -5462,7 +6386,7 @@ var MANIFEST = [
5462
6386
  },
5463
6387
  {
5464
6388
  name: "list_network_requests",
5465
- description: "Return correlated network request rows (one per requestId, with status/headers/state) for the target page. By default reads the target's existing buffer; with reload:true it reloads and records a fresh both-domains capture window. Use filterUrl to keep only requests whose URL contains a substring.",
6389
+ description: "Return correlated network request rows (one per requestId, with status/headers/state) for the target page. By default reads the target's existing buffer; with reload:true it reloads and records a fresh both-domains capture window. Use filterUrl to keep only requests whose URL contains a substring. SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
5466
6390
  inputSchema: {
5467
6391
  type: "object",
5468
6392
  properties: {
@@ -5472,11 +6396,11 @@ var MANIFEST = [
5472
6396
  },
5473
6397
  target: {
5474
6398
  type: "string",
5475
- description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
6399
+ description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
5476
6400
  },
5477
6401
  reload: {
5478
6402
  type: "boolean",
5479
- description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer)."
6403
+ description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer). With a worker: target there is nothing to reload: the window records for durationMs while the worker runs, and the session it holds keeps that worker from being idle-evicted until the capture stops."
5480
6404
  },
5481
6405
  durationMs: {
5482
6406
  type: "number",
@@ -5485,6 +6409,10 @@ var MANIFEST = [
5485
6409
  filterUrl: {
5486
6410
  type: "string",
5487
6411
  description: "Only return requests whose URL contains this substring."
6412
+ },
6413
+ wake: {
6414
+ type: "boolean",
6415
+ description: "Worker targets only, and only when a capture is being started (reload:true). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
5488
6416
  }
5489
6417
  },
5490
6418
  required: [],
@@ -5493,7 +6421,7 @@ var MANIFEST = [
5493
6421
  },
5494
6422
  {
5495
6423
  name: "get_network_request",
5496
- description: "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh reload capture and is matched by url ONLY (reload re-mints requestIds, so a carried-over requestId cannot fetch a body, so it returns metadata plus bodyUnavailableReason).",
6424
+ description: "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh capture (a reload for a page, a listen window for a worker) and is matched by url ONLY (a fresh capture re-mints requestIds, so a carried-over requestId cannot fetch a body, so it returns metadata plus bodyUnavailableReason). SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
5497
6425
  inputSchema: {
5498
6426
  type: "object",
5499
6427
  properties: {
@@ -5503,7 +6431,7 @@ var MANIFEST = [
5503
6431
  },
5504
6432
  target: {
5505
6433
  type: "string",
5506
- description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
6434
+ description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
5507
6435
  },
5508
6436
  requestId: {
5509
6437
  type: "string",
@@ -5520,6 +6448,10 @@ var MANIFEST = [
5520
6448
  durationMs: {
5521
6449
  type: "number",
5522
6450
  description: "Capture window for the reload-driven body fetch, in milliseconds. Default 2500."
6451
+ },
6452
+ wake: {
6453
+ type: "boolean",
6454
+ description: "Worker targets only, and only when a capture is being started (includeBody together with url). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
5523
6455
  }
5524
6456
  },
5525
6457
  required: [],
@@ -6124,10 +7056,15 @@ class BidiConnection {
6124
7056
  } catch {}
6125
7057
  }
6126
7058
  }
6127
- async function connectBidiSession(port, opts = {}) {
6128
- 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 });
6129
7061
  await conn.connect();
6130
- await conn.newSession(opts.capabilities ?? {});
7062
+ try {
7063
+ await conn.newSession(opts.capabilities ?? {});
7064
+ } catch (e) {
7065
+ conn.dispose();
7066
+ throw e;
7067
+ }
6131
7068
  conn.retain();
6132
7069
  return conn;
6133
7070
  }
@@ -6703,21 +7640,27 @@ var BIDI_CAPABILITIES = new Set([
6703
7640
  "emulate.deviceMetrics",
6704
7641
  "screenshot.fullPage",
6705
7642
  "screenshot.element",
7643
+ "screenshot.renderSize",
6706
7644
  "network.intercept",
6707
7645
  "locate.xpath"
6708
7646
  ]);
6709
7647
  var connections = new Map;
6710
- async function getConnection(port, timeoutMs) {
6711
- const existing = connections.get(port);
7648
+ async function getConnection(endpoint, timeoutMs) {
7649
+ const existing = connections.get(endpoint);
6712
7650
  if (existing) {
6713
7651
  existing.retain();
6714
7652
  return existing;
6715
7653
  }
6716
- const conn = await connectBidiSession(port, {
7654
+ const conn = await connectBidiSessionUrl(endpoint, {
6717
7655
  ...timeoutMs !== undefined ? { timeoutMs } : {},
6718
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;
6719
7662
  });
6720
- connections.set(port, conn);
7663
+ connections.set(endpoint, conn);
6721
7664
  return conn;
6722
7665
  }
6723
7666
  var bidiBeaconScripts = new Map;
@@ -7066,7 +8009,55 @@ class BidiPageDriver {
7066
8009
  throw mapBidiError(e);
7067
8010
  }
7068
8011
  }
7069
- 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
+ }
7070
8061
  const format = opts?.format ?? "png";
7071
8062
  const params = {
7072
8063
  context: this.contextId,
@@ -7091,12 +8082,18 @@ class BidiPageDriver {
7091
8082
  await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: null, devicePixelRatio: null }).catch(() => {
7092
8083
  return;
7093
8084
  });
8085
+ await clearViewportRecord(this.browser.scheme, this.contextId);
7094
8086
  return { applied: [] };
7095
8087
  }
7096
8088
  const applied = [];
7097
8089
  try {
7098
8090
  if (opts.width != null && opts.height != null) {
7099
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
+ });
7100
8097
  applied.push("deviceMetrics");
7101
8098
  }
7102
8099
  if (opts.userAgent != null) {
@@ -7304,19 +8301,19 @@ class BidiPageDriver {
7304
8301
  }
7305
8302
 
7306
8303
  class BidiBrowserDriver {
7307
- port;
8304
+ endpoint;
7308
8305
  timeoutMs;
7309
8306
  scheme = "bidi";
7310
8307
  lifetime = "session";
7311
8308
  capabilities = BIDI_CAPABILITIES;
7312
8309
  uidStability = "document-stamp";
7313
8310
  snapshotFidelity = "dom-heuristic";
7314
- constructor(port, timeoutMs) {
7315
- this.port = port;
8311
+ constructor(endpoint, timeoutMs) {
8312
+ this.endpoint = endpoint;
7316
8313
  this.timeoutMs = timeoutMs;
7317
8314
  }
7318
8315
  async listPages() {
7319
- const conn = await getConnection(this.port, this.timeoutMs);
8316
+ const conn = await getConnection(this.endpoint, this.timeoutMs);
7320
8317
  try {
7321
8318
  const { contexts } = await conn.send("browsingContext.getTree", {}).catch((e) => {
7322
8319
  throw mapBidiError(e);
@@ -7327,7 +8324,7 @@ class BidiBrowserDriver {
7327
8324
  }
7328
8325
  }
7329
8326
  async newPage(url) {
7330
- const conn = await getConnection(this.port, this.timeoutMs);
8327
+ const conn = await getConnection(this.endpoint, this.timeoutMs);
7331
8328
  try {
7332
8329
  const { context } = await conn.send("browsingContext.create", { type: "tab" }).catch((e) => {
7333
8330
  throw mapBidiError(e);
@@ -7342,7 +8339,7 @@ class BidiBrowserDriver {
7342
8339
  }
7343
8340
  }
7344
8341
  async closePage(id) {
7345
- const conn = await getConnection(this.port, this.timeoutMs);
8342
+ const conn = await getConnection(this.endpoint, this.timeoutMs);
7346
8343
  try {
7347
8344
  await conn.send("browsingContext.close", { context: id }).catch((e) => {
7348
8345
  throw mapBidiError(e);
@@ -7353,7 +8350,7 @@ class BidiBrowserDriver {
7353
8350
  }
7354
8351
  }
7355
8352
  async activatePage(id) {
7356
- const conn = await getConnection(this.port, this.timeoutMs);
8353
+ const conn = await getConnection(this.endpoint, this.timeoutMs);
7357
8354
  try {
7358
8355
  await conn.send("browsingContext.activate", { context: id }).catch((e) => {
7359
8356
  throw mapBidiError(e);
@@ -7365,7 +8362,7 @@ class BidiBrowserDriver {
7365
8362
  }
7366
8363
  }
7367
8364
  async page(selector) {
7368
- const conn = await getConnection(this.port, this.timeoutMs);
8365
+ const conn = await getConnection(this.endpoint, this.timeoutMs);
7369
8366
  let ctx;
7370
8367
  try {
7371
8368
  ctx = await resolveContext(conn, selector);
@@ -7377,13 +8374,13 @@ class BidiBrowserDriver {
7377
8374
  return new BidiPageDriver(conn, ctx.context, info, this);
7378
8375
  }
7379
8376
  async installActivityBeacon(contextId) {
7380
- const conn = await getConnection(this.port, this.timeoutMs).catch(() => {
8377
+ const conn = await getConnection(this.endpoint, this.timeoutMs).catch(() => {
7381
8378
  return;
7382
8379
  });
7383
8380
  if (!conn)
7384
8381
  return false;
7385
8382
  try {
7386
- const key = `${this.port}:${contextId}`;
8383
+ const key = `${this.endpoint}:${contextId}`;
7387
8384
  if (!bidiBeaconScripts.has(key)) {
7388
8385
  const registered = await conn.send("script.addPreloadScript", {
7389
8386
  functionDeclaration: BEACON_FUNCTION_DECLARATION,
@@ -7404,7 +8401,7 @@ class BidiBrowserDriver {
7404
8401
  }
7405
8402
  }
7406
8403
  async readActivityBeacon(contextId) {
7407
- const conn = await getConnection(this.port, this.timeoutMs).catch(() => {
8404
+ const conn = await getConnection(this.endpoint, this.timeoutMs).catch(() => {
7408
8405
  return;
7409
8406
  });
7410
8407
  if (!conn)
@@ -7427,18 +8424,24 @@ class BidiBrowserDriver {
7427
8424
  }
7428
8425
  async dispose() {
7429
8426
  for (const key of [...bidiBeaconScripts.keys()]) {
7430
- if (key.startsWith(`${this.port}:`))
8427
+ if (key.startsWith(`${this.endpoint}:`))
7431
8428
  bidiBeaconScripts.delete(key);
7432
8429
  }
7433
- const conn = connections.get(this.port);
8430
+ const conn = connections.get(this.endpoint);
7434
8431
  if (!conn)
7435
8432
  return;
7436
- connections.delete(this.port);
8433
+ connections.delete(this.endpoint);
8434
+ await conn.send("session.end", {}).catch(() => {
8435
+ return;
8436
+ });
7437
8437
  conn.dispose();
7438
8438
  }
7439
8439
  }
7440
8440
  function createFirefoxDriver(port, opts) {
7441
- 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);
7442
8445
  }
7443
8446
 
7444
8447
  // src/bidi/launch.ts
@@ -7446,7 +8449,7 @@ import { spawn as spawn3 } from "node:child_process";
7446
8449
  import { mkdtempSync, rmSync } from "node:fs";
7447
8450
  import { createConnection, createServer } from "node:net";
7448
8451
  import { tmpdir } from "node:os";
7449
- import { join as join9 } from "node:path";
8452
+ import { join as join10 } from "node:path";
7450
8453
  function resolveBinary() {
7451
8454
  const candidates = [
7452
8455
  process.env.FIREFOX_BIN,
@@ -7520,7 +8523,7 @@ async function launchFirefox(opts = {}) {
7520
8523
  const binary = resolveBinary();
7521
8524
  const port = await pickPort(opts.port);
7522
8525
  const isThrowaway = opts.profilePath === undefined;
7523
- const profilePath = opts.profilePath ?? mkdtempSync(join9(tmpdir(), "cdp-toolkit-ff-profile-"));
8526
+ const profilePath = opts.profilePath ?? mkdtempSync(join10(tmpdir(), "cdp-toolkit-ff-profile-"));
7524
8527
  const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
7525
8528
  if (opts.headless)
7526
8529
  args.push("--headless");
@@ -7558,10 +8561,10 @@ async function launchFirefox(opts = {}) {
7558
8561
  }
7559
8562
 
7560
8563
  // src/backend.ts
7561
- function resolveBrowserKind(argv) {
8564
+ function resolveBrowserKind(argv, env = process.env) {
7562
8565
  const flagIdx = argv.indexOf("--browser");
7563
8566
  const flagVal = flagIdx !== -1 ? argv[flagIdx + 1] : undefined;
7564
- const raw = flagVal ?? process.env.CDP_BROWSER ?? "chrome";
8567
+ const raw = flagVal ?? env.CDP_BROWSER ?? "chrome";
7565
8568
  if (raw !== "chrome" && raw !== "firefox") {
7566
8569
  throw new Error(`unknown --browser '${raw}': expected 'chrome' or 'firefox' (or set CDP_BROWSER)`);
7567
8570
  }
@@ -7578,7 +8581,79 @@ function stripBrowserFlag(argv) {
7578
8581
  }
7579
8582
  return out;
7580
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
+ }
7581
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
+ }
7582
8657
  const launched = await launchFirefox({ headless: opts?.headless });
7583
8658
  const driver = createFirefoxDriver(launched.port);
7584
8659
  let disposed = false;
@@ -7596,9 +8671,9 @@ async function startFirefoxSession(opts) {
7596
8671
  };
7597
8672
  }
7598
8673
  var sessionPromise;
7599
- function getOrCreateFirefoxSession() {
8674
+ function getOrCreateFirefoxSession(opts) {
7600
8675
  if (!sessionPromise)
7601
- sessionPromise = startFirefoxSession();
8676
+ sessionPromise = startFirefoxSession({ endpoint: opts?.endpoint });
7602
8677
  return sessionPromise;
7603
8678
  }
7604
8679
  async function disposeFirefoxSession() {
@@ -7643,6 +8718,11 @@ async function withPage3(driver, target, fn) {
7643
8718
  }
7644
8719
  var consoleBuffers = new Map;
7645
8720
  var networkBuffers = new Map;
8721
+ function refuseWorkerSelector(driver, target) {
8722
+ if (isWorkerSelector(target) && !driver.capabilities.has("worker.targets")) {
8723
+ throw new BidiToolError(WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
8724
+ }
8725
+ }
7646
8726
  function bufferFor(map, id) {
7647
8727
  let b = map.get(id);
7648
8728
  if (!b) {
@@ -7671,6 +8751,7 @@ async function captureFresh(page, durationMs) {
7671
8751
  }
7672
8752
  }
7673
8753
  async function listConsoleMessages2(driver, args = {}) {
8754
+ refuseWorkerSelector(driver, args.target);
7674
8755
  return withPage3(driver, args.target, async (page) => {
7675
8756
  if (args.reload)
7676
8757
  await captureFresh(page, args.durationMs ?? 2500);
@@ -7679,6 +8760,7 @@ async function listConsoleMessages2(driver, args = {}) {
7679
8760
  });
7680
8761
  }
7681
8762
  async function getConsoleMessage2(driver, args = {}) {
8763
+ refuseWorkerSelector(driver, args.target);
7682
8764
  return withPage3(driver, args.target, async (page) => {
7683
8765
  const buf = bufferFor(consoleBuffers, page.info.id);
7684
8766
  const idx = args.index ?? 0;
@@ -7689,6 +8771,7 @@ async function getConsoleMessage2(driver, args = {}) {
7689
8771
  });
7690
8772
  }
7691
8773
  async function listNetworkRequests2(driver, args = {}) {
8774
+ refuseWorkerSelector(driver, args.target);
7692
8775
  return withPage3(driver, args.target, async (page) => {
7693
8776
  if (args.reload)
7694
8777
  await captureFresh(page, args.durationMs ?? 2500);
@@ -7699,6 +8782,7 @@ async function listNetworkRequests2(driver, args = {}) {
7699
8782
  });
7700
8783
  }
7701
8784
  async function getNetworkRequest2(driver, args = {}) {
8785
+ refuseWorkerSelector(driver, args.target);
7702
8786
  if (!args.requestId && !args.url)
7703
8787
  throw new BidiToolError("get_network_request requires requestId or url");
7704
8788
  return withPage3(driver, args.target, async (page) => {
@@ -7790,8 +8874,8 @@ var FIREFOX_TOOLS = {
7790
8874
  };
7791
8875
 
7792
8876
  // src/mcp.ts
7793
- var VERSION = "1.9.0";
7794
- var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
8877
+ var VERSION = "1.9.4";
8878
+ var { kind: BROWSER, endpoint: FIREFOX_ENDPOINT } = resolveBackend(process.argv.slice(2));
7795
8879
  var AVAILABILITY = toolAvailability(BROWSER);
7796
8880
  var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
7797
8881
  var dispatch = TOOLS;
@@ -7837,7 +8921,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7837
8921
  if (!neutralFn)
7838
8922
  return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
7839
8923
  try {
7840
- const session2 = await getOrCreateFirefoxSession();
8924
+ const session2 = await getOrCreateFirefoxSession({ endpoint: FIREFOX_ENDPOINT });
7841
8925
  const result = await neutralFn(session2.driver, args);
7842
8926
  return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
7843
8927
  } catch (err) {
@@ -7849,7 +8933,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
7849
8933
  auditCoverage();
7850
8934
  markLongLivedProcess();
7851
8935
  await server.connect(new StdioServerTransport);
7852
- 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}`);
7853
8937
  var shuttingDown = false;
7854
8938
  async function shutdown() {
7855
8939
  if (shuttingDown)