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