cdp-toolkit 1.9.1 → 1.9.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +434 -0
- package/CONTRACT.md +59 -2
- package/README.md +68 -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 +230 -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/cli.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
|
}
|
|
@@ -4522,10 +5378,15 @@ class BidiConnection {
|
|
|
4522
5378
|
} catch {}
|
|
4523
5379
|
}
|
|
4524
5380
|
}
|
|
4525
|
-
async function
|
|
4526
|
-
const conn = new BidiConnection(
|
|
5381
|
+
async function connectBidiSessionUrl(wsUrl, opts = {}) {
|
|
5382
|
+
const conn = new BidiConnection(wsUrl, { timeoutMs: opts.timeoutMs });
|
|
4527
5383
|
await conn.connect();
|
|
4528
|
-
|
|
5384
|
+
try {
|
|
5385
|
+
await conn.newSession(opts.capabilities ?? {});
|
|
5386
|
+
} catch (e) {
|
|
5387
|
+
conn.dispose();
|
|
5388
|
+
throw e;
|
|
5389
|
+
}
|
|
4529
5390
|
conn.retain();
|
|
4530
5391
|
return conn;
|
|
4531
5392
|
}
|
|
@@ -5101,21 +5962,27 @@ var BIDI_CAPABILITIES = new Set([
|
|
|
5101
5962
|
"emulate.deviceMetrics",
|
|
5102
5963
|
"screenshot.fullPage",
|
|
5103
5964
|
"screenshot.element",
|
|
5965
|
+
"screenshot.renderSize",
|
|
5104
5966
|
"network.intercept",
|
|
5105
5967
|
"locate.xpath"
|
|
5106
5968
|
]);
|
|
5107
5969
|
var connections = new Map;
|
|
5108
|
-
async function getConnection(
|
|
5109
|
-
const existing = connections.get(
|
|
5970
|
+
async function getConnection(endpoint, timeoutMs) {
|
|
5971
|
+
const existing = connections.get(endpoint);
|
|
5110
5972
|
if (existing) {
|
|
5111
5973
|
existing.retain();
|
|
5112
5974
|
return existing;
|
|
5113
5975
|
}
|
|
5114
|
-
const conn = await
|
|
5976
|
+
const conn = await connectBidiSessionUrl(endpoint, {
|
|
5115
5977
|
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
5116
5978
|
capabilities: { alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } } }
|
|
5979
|
+
}).catch((e) => {
|
|
5980
|
+
if (e instanceof BidiError && /maximum number of active sessions/i.test(e.message)) {
|
|
5981
|
+
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.)`);
|
|
5982
|
+
}
|
|
5983
|
+
throw e;
|
|
5117
5984
|
});
|
|
5118
|
-
connections.set(
|
|
5985
|
+
connections.set(endpoint, conn);
|
|
5119
5986
|
return conn;
|
|
5120
5987
|
}
|
|
5121
5988
|
var bidiBeaconScripts = new Map;
|
|
@@ -5464,7 +6331,55 @@ class BidiPageDriver {
|
|
|
5464
6331
|
throw mapBidiError(e);
|
|
5465
6332
|
}
|
|
5466
6333
|
}
|
|
5467
|
-
async screenshot(opts) {
|
|
6334
|
+
async screenshot(opts, _consumeBands) {
|
|
6335
|
+
const renderSize = opts?.renderWidth !== undefined && opts?.renderHeight !== undefined ? { width: opts.renderWidth, height: opts.renderHeight } : undefined;
|
|
6336
|
+
if (!renderSize)
|
|
6337
|
+
return this.capture(opts);
|
|
6338
|
+
const previous = await readViewportRecord(this.browser.scheme, this.contextId);
|
|
6339
|
+
try {
|
|
6340
|
+
await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: { width: renderSize.width, height: renderSize.height } });
|
|
6341
|
+
} catch (e) {
|
|
6342
|
+
throw mapBidiError(e);
|
|
6343
|
+
}
|
|
6344
|
+
let shot;
|
|
6345
|
+
let captureError;
|
|
6346
|
+
try {
|
|
6347
|
+
shot = await this.capture(opts);
|
|
6348
|
+
} catch (e) {
|
|
6349
|
+
captureError = e;
|
|
6350
|
+
}
|
|
6351
|
+
let restoreError;
|
|
6352
|
+
try {
|
|
6353
|
+
if (previous) {
|
|
6354
|
+
await this.conn.send("browsingContext.setViewport", {
|
|
6355
|
+
context: this.contextId,
|
|
6356
|
+
viewport: { width: previous.width, height: previous.height },
|
|
6357
|
+
...previous.deviceScaleFactor != null ? { devicePixelRatio: previous.deviceScaleFactor } : {}
|
|
6358
|
+
});
|
|
6359
|
+
} else {
|
|
6360
|
+
await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: null });
|
|
6361
|
+
}
|
|
6362
|
+
} catch (e) {
|
|
6363
|
+
restoreError = e instanceof Error ? e.message : String(e);
|
|
6364
|
+
}
|
|
6365
|
+
if (!shot) {
|
|
6366
|
+
if (restoreError !== undefined && captureError instanceof Error) {
|
|
6367
|
+
captureError.message = `${captureError.message} ${renderRestoreFailureNote(renderSize, restoreError)}`;
|
|
6368
|
+
}
|
|
6369
|
+
throw captureError;
|
|
6370
|
+
}
|
|
6371
|
+
const result = { ...shot, renderSize, renderRestored: restoreError === undefined };
|
|
6372
|
+
if (restoreError !== undefined)
|
|
6373
|
+
result.renderRestoreError = restoreError;
|
|
6374
|
+
return result;
|
|
6375
|
+
}
|
|
6376
|
+
async capture(opts) {
|
|
6377
|
+
if (opts?.scale !== undefined && opts.scale !== 1) {
|
|
6378
|
+
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.`);
|
|
6379
|
+
}
|
|
6380
|
+
if (opts?.tile === true) {
|
|
6381
|
+
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.");
|
|
6382
|
+
}
|
|
5468
6383
|
const format = opts?.format ?? "png";
|
|
5469
6384
|
const params = {
|
|
5470
6385
|
context: this.contextId,
|
|
@@ -5489,12 +6404,18 @@ class BidiPageDriver {
|
|
|
5489
6404
|
await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: null, devicePixelRatio: null }).catch(() => {
|
|
5490
6405
|
return;
|
|
5491
6406
|
});
|
|
6407
|
+
await clearViewportRecord(this.browser.scheme, this.contextId);
|
|
5492
6408
|
return { applied: [] };
|
|
5493
6409
|
}
|
|
5494
6410
|
const applied = [];
|
|
5495
6411
|
try {
|
|
5496
6412
|
if (opts.width != null && opts.height != null) {
|
|
5497
6413
|
await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: { width: opts.width, height: opts.height }, ...opts.deviceScaleFactor != null ? { devicePixelRatio: opts.deviceScaleFactor } : {} });
|
|
6414
|
+
await writeViewportRecord(this.browser.scheme, this.contextId, {
|
|
6415
|
+
width: opts.width,
|
|
6416
|
+
height: opts.height,
|
|
6417
|
+
...opts.deviceScaleFactor != null ? { deviceScaleFactor: opts.deviceScaleFactor } : {}
|
|
6418
|
+
});
|
|
5498
6419
|
applied.push("deviceMetrics");
|
|
5499
6420
|
}
|
|
5500
6421
|
if (opts.userAgent != null) {
|
|
@@ -5702,19 +6623,19 @@ class BidiPageDriver {
|
|
|
5702
6623
|
}
|
|
5703
6624
|
|
|
5704
6625
|
class BidiBrowserDriver {
|
|
5705
|
-
|
|
6626
|
+
endpoint;
|
|
5706
6627
|
timeoutMs;
|
|
5707
6628
|
scheme = "bidi";
|
|
5708
6629
|
lifetime = "session";
|
|
5709
6630
|
capabilities = BIDI_CAPABILITIES;
|
|
5710
6631
|
uidStability = "document-stamp";
|
|
5711
6632
|
snapshotFidelity = "dom-heuristic";
|
|
5712
|
-
constructor(
|
|
5713
|
-
this.
|
|
6633
|
+
constructor(endpoint, timeoutMs) {
|
|
6634
|
+
this.endpoint = endpoint;
|
|
5714
6635
|
this.timeoutMs = timeoutMs;
|
|
5715
6636
|
}
|
|
5716
6637
|
async listPages() {
|
|
5717
|
-
const conn = await getConnection(this.
|
|
6638
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs);
|
|
5718
6639
|
try {
|
|
5719
6640
|
const { contexts } = await conn.send("browsingContext.getTree", {}).catch((e) => {
|
|
5720
6641
|
throw mapBidiError(e);
|
|
@@ -5725,7 +6646,7 @@ class BidiBrowserDriver {
|
|
|
5725
6646
|
}
|
|
5726
6647
|
}
|
|
5727
6648
|
async newPage(url) {
|
|
5728
|
-
const conn = await getConnection(this.
|
|
6649
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs);
|
|
5729
6650
|
try {
|
|
5730
6651
|
const { context } = await conn.send("browsingContext.create", { type: "tab" }).catch((e) => {
|
|
5731
6652
|
throw mapBidiError(e);
|
|
@@ -5740,7 +6661,7 @@ class BidiBrowserDriver {
|
|
|
5740
6661
|
}
|
|
5741
6662
|
}
|
|
5742
6663
|
async closePage(id) {
|
|
5743
|
-
const conn = await getConnection(this.
|
|
6664
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs);
|
|
5744
6665
|
try {
|
|
5745
6666
|
await conn.send("browsingContext.close", { context: id }).catch((e) => {
|
|
5746
6667
|
throw mapBidiError(e);
|
|
@@ -5751,7 +6672,7 @@ class BidiBrowserDriver {
|
|
|
5751
6672
|
}
|
|
5752
6673
|
}
|
|
5753
6674
|
async activatePage(id) {
|
|
5754
|
-
const conn = await getConnection(this.
|
|
6675
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs);
|
|
5755
6676
|
try {
|
|
5756
6677
|
await conn.send("browsingContext.activate", { context: id }).catch((e) => {
|
|
5757
6678
|
throw mapBidiError(e);
|
|
@@ -5763,7 +6684,7 @@ class BidiBrowserDriver {
|
|
|
5763
6684
|
}
|
|
5764
6685
|
}
|
|
5765
6686
|
async page(selector) {
|
|
5766
|
-
const conn = await getConnection(this.
|
|
6687
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs);
|
|
5767
6688
|
let ctx;
|
|
5768
6689
|
try {
|
|
5769
6690
|
ctx = await resolveContext(conn, selector);
|
|
@@ -5775,13 +6696,13 @@ class BidiBrowserDriver {
|
|
|
5775
6696
|
return new BidiPageDriver(conn, ctx.context, info, this);
|
|
5776
6697
|
}
|
|
5777
6698
|
async installActivityBeacon(contextId) {
|
|
5778
|
-
const conn = await getConnection(this.
|
|
6699
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs).catch(() => {
|
|
5779
6700
|
return;
|
|
5780
6701
|
});
|
|
5781
6702
|
if (!conn)
|
|
5782
6703
|
return false;
|
|
5783
6704
|
try {
|
|
5784
|
-
const key = `${this.
|
|
6705
|
+
const key = `${this.endpoint}:${contextId}`;
|
|
5785
6706
|
if (!bidiBeaconScripts.has(key)) {
|
|
5786
6707
|
const registered = await conn.send("script.addPreloadScript", {
|
|
5787
6708
|
functionDeclaration: BEACON_FUNCTION_DECLARATION,
|
|
@@ -5802,7 +6723,7 @@ class BidiBrowserDriver {
|
|
|
5802
6723
|
}
|
|
5803
6724
|
}
|
|
5804
6725
|
async readActivityBeacon(contextId) {
|
|
5805
|
-
const conn = await getConnection(this.
|
|
6726
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs).catch(() => {
|
|
5806
6727
|
return;
|
|
5807
6728
|
});
|
|
5808
6729
|
if (!conn)
|
|
@@ -5825,18 +6746,24 @@ class BidiBrowserDriver {
|
|
|
5825
6746
|
}
|
|
5826
6747
|
async dispose() {
|
|
5827
6748
|
for (const key of [...bidiBeaconScripts.keys()]) {
|
|
5828
|
-
if (key.startsWith(`${this.
|
|
6749
|
+
if (key.startsWith(`${this.endpoint}:`))
|
|
5829
6750
|
bidiBeaconScripts.delete(key);
|
|
5830
6751
|
}
|
|
5831
|
-
const conn = connections.get(this.
|
|
6752
|
+
const conn = connections.get(this.endpoint);
|
|
5832
6753
|
if (!conn)
|
|
5833
6754
|
return;
|
|
5834
|
-
connections.delete(this.
|
|
6755
|
+
connections.delete(this.endpoint);
|
|
6756
|
+
await conn.send("session.end", {}).catch(() => {
|
|
6757
|
+
return;
|
|
6758
|
+
});
|
|
5835
6759
|
conn.dispose();
|
|
5836
6760
|
}
|
|
5837
6761
|
}
|
|
5838
6762
|
function createFirefoxDriver(port, opts) {
|
|
5839
|
-
return new BidiBrowserDriver(port
|
|
6763
|
+
return new BidiBrowserDriver(`ws://127.0.0.1:${port}/session`, opts?.timeoutMs);
|
|
6764
|
+
}
|
|
6765
|
+
function createFirefoxDriverForEndpoint(wsUrl, opts) {
|
|
6766
|
+
return new BidiBrowserDriver(wsUrl, opts?.timeoutMs);
|
|
5840
6767
|
}
|
|
5841
6768
|
|
|
5842
6769
|
// src/bidi/launch.ts
|
|
@@ -5844,7 +6771,7 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
5844
6771
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5845
6772
|
import { createConnection, createServer } from "node:net";
|
|
5846
6773
|
import { tmpdir } from "node:os";
|
|
5847
|
-
import { join as
|
|
6774
|
+
import { join as join10 } from "node:path";
|
|
5848
6775
|
function resolveBinary() {
|
|
5849
6776
|
const candidates = [
|
|
5850
6777
|
process.env.FIREFOX_BIN,
|
|
@@ -5918,7 +6845,7 @@ async function launchFirefox(opts = {}) {
|
|
|
5918
6845
|
const binary = resolveBinary();
|
|
5919
6846
|
const port = await pickPort(opts.port);
|
|
5920
6847
|
const isThrowaway = opts.profilePath === undefined;
|
|
5921
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
6848
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join10(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
5922
6849
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
5923
6850
|
if (opts.headless)
|
|
5924
6851
|
args.push("--headless");
|
|
@@ -5956,10 +6883,10 @@ async function launchFirefox(opts = {}) {
|
|
|
5956
6883
|
}
|
|
5957
6884
|
|
|
5958
6885
|
// src/backend.ts
|
|
5959
|
-
function resolveBrowserKind(argv) {
|
|
6886
|
+
function resolveBrowserKind(argv, env = process.env) {
|
|
5960
6887
|
const flagIdx = argv.indexOf("--browser");
|
|
5961
6888
|
const flagVal = flagIdx !== -1 ? argv[flagIdx + 1] : undefined;
|
|
5962
|
-
const raw = flagVal ??
|
|
6889
|
+
const raw = flagVal ?? env.CDP_BROWSER ?? "chrome";
|
|
5963
6890
|
if (raw !== "chrome" && raw !== "firefox") {
|
|
5964
6891
|
throw new Error(`unknown --browser '${raw}': expected 'chrome' or 'firefox' (or set CDP_BROWSER)`);
|
|
5965
6892
|
}
|
|
@@ -5976,7 +6903,79 @@ function stripBrowserFlag(argv) {
|
|
|
5976
6903
|
}
|
|
5977
6904
|
return out;
|
|
5978
6905
|
}
|
|
6906
|
+
function normalizeBidiEndpoint(raw) {
|
|
6907
|
+
const value = raw.trim();
|
|
6908
|
+
if (/^wss?:\/\//i.test(value)) {
|
|
6909
|
+
let url;
|
|
6910
|
+
try {
|
|
6911
|
+
url = new URL(value);
|
|
6912
|
+
} catch {
|
|
6913
|
+
throw new Error(`invalid Firefox endpoint '${raw}': not a parseable ws:// URL`);
|
|
6914
|
+
}
|
|
6915
|
+
if (url.pathname === "" || url.pathname === "/")
|
|
6916
|
+
url.pathname = "/session";
|
|
6917
|
+
return url.toString();
|
|
6918
|
+
}
|
|
6919
|
+
if (/^\d+$/.test(value))
|
|
6920
|
+
return `ws://127.0.0.1:${assertPort(value, raw)}/session`;
|
|
6921
|
+
const hostPort = /^([^:/]+):(\d+)$/.exec(value);
|
|
6922
|
+
if (hostPort)
|
|
6923
|
+
return `ws://${hostPort[1]}:${assertPort(hostPort[2], raw)}/session`;
|
|
6924
|
+
throw new Error(`invalid Firefox endpoint '${raw}': expected a port (9223), host:port, or a ws:// URL`);
|
|
6925
|
+
}
|
|
6926
|
+
function assertPort(digits, raw) {
|
|
6927
|
+
const port = Number(digits);
|
|
6928
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
6929
|
+
throw new Error(`invalid Firefox endpoint '${raw}': port must be 1-65535`);
|
|
6930
|
+
}
|
|
6931
|
+
return port;
|
|
6932
|
+
}
|
|
6933
|
+
function resolveFirefoxEndpoint(argv, env = process.env) {
|
|
6934
|
+
const flagIdx = argv.indexOf("--connect");
|
|
6935
|
+
const raw = (flagIdx !== -1 ? argv[flagIdx + 1] : undefined) ?? env.CDP_FIREFOX_ENDPOINT;
|
|
6936
|
+
if (raw === undefined || raw === "")
|
|
6937
|
+
return;
|
|
6938
|
+
return normalizeBidiEndpoint(raw);
|
|
6939
|
+
}
|
|
6940
|
+
function stripConnectFlag(argv) {
|
|
6941
|
+
const out = [];
|
|
6942
|
+
for (let i = 0;i < argv.length; i++) {
|
|
6943
|
+
if (argv[i] === "--connect") {
|
|
6944
|
+
i++;
|
|
6945
|
+
continue;
|
|
6946
|
+
}
|
|
6947
|
+
out.push(argv[i]);
|
|
6948
|
+
}
|
|
6949
|
+
return out;
|
|
6950
|
+
}
|
|
6951
|
+
function resolveBackend(argv, env = process.env) {
|
|
6952
|
+
const endpoint = resolveFirefoxEndpoint(argv, env);
|
|
6953
|
+
let kind = resolveBrowserKind(argv, env);
|
|
6954
|
+
const explicitBrowser = argv.includes("--browser") || env.CDP_BROWSER !== undefined;
|
|
6955
|
+
if (endpoint !== undefined) {
|
|
6956
|
+
if (explicitBrowser && kind === "chrome") {
|
|
6957
|
+
throw new Error("--connect / CDP_FIREFOX_ENDPOINT is a Firefox attach option and is not valid with the chrome backend");
|
|
6958
|
+
}
|
|
6959
|
+
kind = "firefox";
|
|
6960
|
+
}
|
|
6961
|
+
return endpoint !== undefined ? { kind, endpoint } : { kind };
|
|
6962
|
+
}
|
|
5979
6963
|
async function startFirefoxSession(opts) {
|
|
6964
|
+
if (opts?.endpoint !== undefined) {
|
|
6965
|
+
const driver2 = createFirefoxDriverForEndpoint(opts.endpoint);
|
|
6966
|
+
let disposed2 = false;
|
|
6967
|
+
return {
|
|
6968
|
+
driver: driver2,
|
|
6969
|
+
async dispose() {
|
|
6970
|
+
if (disposed2)
|
|
6971
|
+
return;
|
|
6972
|
+
disposed2 = true;
|
|
6973
|
+
await driver2.dispose().catch(() => {
|
|
6974
|
+
return;
|
|
6975
|
+
});
|
|
6976
|
+
}
|
|
6977
|
+
};
|
|
6978
|
+
}
|
|
5980
6979
|
const launched = await launchFirefox({ headless: opts?.headless });
|
|
5981
6980
|
const driver = createFirefoxDriver(launched.port);
|
|
5982
6981
|
let disposed = false;
|
|
@@ -5994,9 +6993,9 @@ async function startFirefoxSession(opts) {
|
|
|
5994
6993
|
};
|
|
5995
6994
|
}
|
|
5996
6995
|
var sessionPromise;
|
|
5997
|
-
function getOrCreateFirefoxSession() {
|
|
6996
|
+
function getOrCreateFirefoxSession(opts) {
|
|
5998
6997
|
if (!sessionPromise)
|
|
5999
|
-
sessionPromise = startFirefoxSession();
|
|
6998
|
+
sessionPromise = startFirefoxSession({ endpoint: opts?.endpoint });
|
|
6000
6999
|
return sessionPromise;
|
|
6001
7000
|
}
|
|
6002
7001
|
async function disposeFirefoxSession() {
|
|
@@ -6196,13 +7195,1688 @@ var FIREFOX_TOOLS = {
|
|
|
6196
7195
|
...LEASE_TOOLS
|
|
6197
7196
|
};
|
|
6198
7197
|
|
|
7198
|
+
// src/manifest.ts
|
|
7199
|
+
var MANIFEST = [
|
|
7200
|
+
{
|
|
7201
|
+
name: "list_pages",
|
|
7202
|
+
description: "Enumerate browser page targets via the CDP browser endpoint (GET /json/list). By default returns only page-type tabs; set 'all' to also include workers and background pages. Each entry carries the targetId used as a target selector elsewhere, plus an 'origin' field recording where the tab came from: 'agent' means this toolkit created it (the entry then also carries the creating 'label' and 'createdAt'), and this stays true after the creating agent releases its lease or dies, which is what makes a stray agent tab findable later. 'unknown' means there is no creation record. It never says 'human': the toolkit cannot prove a person opened a tab, so 'unknown' is the honest word for a tab it did not create, one opened before the toolkit ran, or one whose record could not be written. A tab whose record exists but could not be read reports origin 'unknown' plus 'originUnreadable', so a broken record is never mistaken for no record. Under CDP_REQUIRE_LEASE this call also reaps abandoned agent tabs, but reap-CLOSE and lease-RECLAIMABLE (list_leases' 'stale') now fire at different times: a lease reads reclaimable the moment its TTL elapses, cheap and reversible since another agent may simply take it, while this call only CLOSES the tab, destructively, after an ADDITIONAL grace period past that TTL (CDP_REAP_GRACE_MS, default 2700000ms/45 minutes, so 60 minutes total after last use) — a tab an agent is mid-build between calls on is not destroyed out from under it. A dead-pid lease is reaped immediately regardless of grace: that process is never coming back. Closed tabs are reported in an additive 'reaped' array ({targetId,label,reason}) present only when something was actually closed; a tab with no lease is never reaped. Every entry under an active, readable lease of this backend also carries 'lease': {label, pid, idleMs, expiresAt, stale} — idleMs and expiresAt are computed fresh on every call (now-lastUsedAt and lastUsedAt+ttlMs), never read off disk, and this is unconditional on 'probe'. Set 'probe' to true to also ping each page-type target's renderer: one bounded (500ms) check per target, never more, so a single wedged tab cannot stall the listing beyond its own budget. This adds 'responsive' (true iff the ping answered in time; false on a timeout, a page-side exception, or an unreachable target — never an error for the whole call) and, where that same round trip found human-attributed input, 'humanActiveMs' (see claim_page and list_leases for what that field means and cannot prove). 'responsive' and 'humanActiveMs' are both ABSENT — never false/null — when 'probe' is not set, and 'responsive' is likewise absent on a backend that cannot answer this ping at all.",
|
|
7203
|
+
inputSchema: {
|
|
7204
|
+
type: "object",
|
|
7205
|
+
properties: {
|
|
7206
|
+
lease: {
|
|
7207
|
+
type: "string",
|
|
7208
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7209
|
+
},
|
|
7210
|
+
all: {
|
|
7211
|
+
type: "boolean",
|
|
7212
|
+
description: "Include non-page targets (service/shared workers, background pages) when true; otherwise only page-type tabs are listed."
|
|
7213
|
+
},
|
|
7214
|
+
probe: {
|
|
7215
|
+
type: "boolean",
|
|
7216
|
+
description: "Ping each page-type target's renderer before returning: one bounded (500ms) Runtime.evaluate per target, never more. Adds 'responsive' to every page-type entry and, where the same round trip finds human-attributed input, 'humanActiveMs'. A wedged or unreachable tab reports responsive:false rather than failing the call. Defaults to false, which is byte-identical to the pre-1.8.0 shape."
|
|
7217
|
+
}
|
|
7218
|
+
},
|
|
7219
|
+
required: [],
|
|
7220
|
+
additionalProperties: false
|
|
7221
|
+
}
|
|
7222
|
+
},
|
|
7223
|
+
{
|
|
7224
|
+
name: "new_page",
|
|
7225
|
+
description: "Open a new browser tab via Target.createTarget and return {targetId,url}. Defaults to about:blank; navigation is not awaited here (use navigate_page to load and wait).",
|
|
7226
|
+
inputSchema: {
|
|
7227
|
+
type: "object",
|
|
7228
|
+
properties: {
|
|
7229
|
+
lease: {
|
|
7230
|
+
type: "string",
|
|
7231
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7232
|
+
},
|
|
7233
|
+
url: {
|
|
7234
|
+
type: "string",
|
|
7235
|
+
description: "URL to open in the new tab; defaults to about:blank."
|
|
7236
|
+
},
|
|
7237
|
+
claim: {
|
|
7238
|
+
type: "boolean",
|
|
7239
|
+
description: "Claim the new tab atomically as part of creating it and return a lease token alongside {targetId,url}. Default false, which is byte-identical to the pre-1.2 behavior. Under CDP_REQUIRE_LEASE the new tab is claimed and a lease returned even without claim:true, because a tab nobody holds is a tab nobody may drive; passing claim:true additionally makes the lease an explicit one, which requires its token on every later call even from this same process."
|
|
7240
|
+
},
|
|
7241
|
+
label: {
|
|
7242
|
+
type: "string",
|
|
7243
|
+
description: "Agent label recorded on the lease when claim:true, surfaced in conflict errors and list_leases. Defaults to pid-<pid>."
|
|
7244
|
+
},
|
|
7245
|
+
ttlMs: {
|
|
7246
|
+
type: "number",
|
|
7247
|
+
description: "How long the lease taken by claim:true survives without use before it is reclaimable. Defaults to CDP_LEASE_TTL_MS, else 900000 (15 minutes). Every checked call refreshes it, so an active agent never expires. Ignored without claim:true."
|
|
7248
|
+
}
|
|
7249
|
+
},
|
|
7250
|
+
required: [],
|
|
7251
|
+
additionalProperties: false
|
|
7252
|
+
}
|
|
7253
|
+
},
|
|
7254
|
+
{
|
|
7255
|
+
name: "close_page",
|
|
7256
|
+
description: "Close a page target via Target.closeTarget. Requires an explicit, resolvable target; refuses to guess and errors if the selector matches no target.",
|
|
7257
|
+
inputSchema: {
|
|
7258
|
+
type: "object",
|
|
7259
|
+
properties: {
|
|
7260
|
+
lease: {
|
|
7261
|
+
type: "string",
|
|
7262
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7263
|
+
},
|
|
7264
|
+
target: {
|
|
7265
|
+
type: "string",
|
|
7266
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7267
|
+
}
|
|
7268
|
+
},
|
|
7269
|
+
required: [
|
|
7270
|
+
"target"
|
|
7271
|
+
],
|
|
7272
|
+
additionalProperties: false
|
|
7273
|
+
}
|
|
7274
|
+
},
|
|
7275
|
+
{
|
|
7276
|
+
name: "select_page",
|
|
7277
|
+
description: "Activate/focus a page target via Target.activateTarget and persist its bare targetId to the selected-state file (CDP_STATE_DIR/selected). Requires an explicit, resolvable target.",
|
|
7278
|
+
inputSchema: {
|
|
7279
|
+
type: "object",
|
|
7280
|
+
properties: {
|
|
7281
|
+
lease: {
|
|
7282
|
+
type: "string",
|
|
7283
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7284
|
+
},
|
|
7285
|
+
target: {
|
|
7286
|
+
type: "string",
|
|
7287
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7288
|
+
}
|
|
7289
|
+
},
|
|
7290
|
+
required: [
|
|
7291
|
+
"target"
|
|
7292
|
+
],
|
|
7293
|
+
additionalProperties: false
|
|
7294
|
+
}
|
|
7295
|
+
},
|
|
7296
|
+
{
|
|
7297
|
+
name: "navigate_page",
|
|
7298
|
+
description: "Navigate a target page to a URL (Page.navigate), reload it (reload:true), OR go back/forward in its session history (history:'back'|'forward'), then wait for the load milestone with a bounded timeout so a wedged renderer can't hang. Exactly one of url / reload / history per call. Returns {url,frameId,waitedFor} (plus reloaded:true on a reload, traversed:'back'|'forward' on a history move); waitUntil supports 'load'|'domcontentloaded' only (no 'networkidle'), and there is no auto-snapshot of the new page. Pass reload:true with ignoreCache:true for a hard reload that refetches every subresource (e.g. to pick up a freshly-deployed, non-content-hashed bundle the HTTP cache would serve stale).",
|
|
7299
|
+
inputSchema: {
|
|
7300
|
+
type: "object",
|
|
7301
|
+
properties: {
|
|
7302
|
+
lease: {
|
|
7303
|
+
type: "string",
|
|
7304
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7305
|
+
},
|
|
7306
|
+
target: {
|
|
7307
|
+
type: "string",
|
|
7308
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7309
|
+
},
|
|
7310
|
+
url: {
|
|
7311
|
+
type: "string",
|
|
7312
|
+
description: "Destination URL to navigate to. Required unless reload:true."
|
|
7313
|
+
},
|
|
7314
|
+
reload: {
|
|
7315
|
+
type: "boolean",
|
|
7316
|
+
description: "Reload the current page (Page.reload) instead of navigating to url. Default false."
|
|
7317
|
+
},
|
|
7318
|
+
history: {
|
|
7319
|
+
type: "string",
|
|
7320
|
+
enum: [
|
|
7321
|
+
"back",
|
|
7322
|
+
"forward"
|
|
7323
|
+
],
|
|
7324
|
+
description: "Traverse this tab's session history instead of loading a url: 'back' and 'forward' are the browser's Back and Forward buttons. Mutually exclusive with 'url' and 'reload' — passing two of the three is refused by name rather than resolved by precedence. Works on BOTH backends (Chrome: Page.getNavigationHistory + Page.navigateToHistoryEntry; Firefox: browsingContext.traverseHistory) and waits for the same load milestone as an ordinary navigation. Going back from the first entry, or forward from the last, is an ERROR naming the direction — never a silent success that navigated nowhere. The result carries traversed:'back'|'forward' alongside the resulting url."
|
|
7325
|
+
},
|
|
7326
|
+
ignoreCache: {
|
|
7327
|
+
type: "boolean",
|
|
7328
|
+
description: "On reload, bypass the HTTP cache (hard reload) so subresources are refetched. Ignored unless reload:true. Default false."
|
|
7329
|
+
},
|
|
7330
|
+
waitUntil: {
|
|
7331
|
+
type: "string",
|
|
7332
|
+
enum: [
|
|
7333
|
+
"load",
|
|
7334
|
+
"domcontentloaded"
|
|
7335
|
+
],
|
|
7336
|
+
description: "Which load milestone to wait for. Defaults to 'load'. No 'networkidle' support."
|
|
7337
|
+
},
|
|
7338
|
+
timeoutMs: {
|
|
7339
|
+
type: "number",
|
|
7340
|
+
description: "Override the navigation timeout in milliseconds."
|
|
7341
|
+
}
|
|
7342
|
+
},
|
|
7343
|
+
additionalProperties: false
|
|
7344
|
+
}
|
|
7345
|
+
},
|
|
7346
|
+
{
|
|
7347
|
+
name: "wait_for",
|
|
7348
|
+
description: "Poll a target page until the given substring appears in document.body.innerText (Runtime.evaluate on a fixed interval), or throw on timeout. Text-substring waiting only: no aria/role/selector or event variants; throws rather than returning {found:false}.",
|
|
7349
|
+
inputSchema: {
|
|
7350
|
+
type: "object",
|
|
7351
|
+
properties: {
|
|
7352
|
+
lease: {
|
|
7353
|
+
type: "string",
|
|
7354
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7355
|
+
},
|
|
7356
|
+
target: {
|
|
7357
|
+
type: "string",
|
|
7358
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7359
|
+
},
|
|
7360
|
+
text: {
|
|
7361
|
+
type: "string",
|
|
7362
|
+
description: "Substring to wait for in document.body.innerText. Required."
|
|
7363
|
+
},
|
|
7364
|
+
timeoutMs: {
|
|
7365
|
+
type: "number",
|
|
7366
|
+
description: "Total time budget in milliseconds. Defaults to 15000."
|
|
7367
|
+
},
|
|
7368
|
+
pollMs: {
|
|
7369
|
+
type: "number",
|
|
7370
|
+
description: "Poll interval in milliseconds. Defaults to 250."
|
|
7371
|
+
}
|
|
7372
|
+
},
|
|
7373
|
+
required: [
|
|
7374
|
+
"text"
|
|
7375
|
+
],
|
|
7376
|
+
additionalProperties: false
|
|
7377
|
+
}
|
|
7378
|
+
},
|
|
7379
|
+
{
|
|
7380
|
+
name: "evaluate_script",
|
|
7381
|
+
description: "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string. Pass 'savePath' to write the evaluated value to a JSON file instead: the response then carries only {path,bytes,type,target} and the value itself never appears in it, in any form. That is the way to read a credential (a JWT or session token out of localStorage, for example) without putting the secret into the caller's transcript. This is the one tool that can EVALUATE inside an MV3 extension's background SERVICE WORKER rather than a page (list_network_requests/get_network_request/list_console_messages OBSERVE one): pass target 'worker:<extension-id>' (Chrome only) and read chrome.storage.local, chrome.runtime and the worker's own globals directly, with an idle-evicted worker started for you first (see 'wake').",
|
|
7382
|
+
inputSchema: {
|
|
7383
|
+
type: "object",
|
|
7384
|
+
properties: {
|
|
7385
|
+
lease: {
|
|
7386
|
+
type: "string",
|
|
7387
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7388
|
+
},
|
|
7389
|
+
target: {
|
|
7390
|
+
type: "string",
|
|
7391
|
+
description: "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring | 'label:<name>' -> the page-type target with exactly this label (checked against the origin ledger and live leases) | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js) and read chrome.storage.local from it. The worker: arm is CHROME-ONLY (Capability 'worker.targets') and is accepted by four tools only — evaluate_script, list_network_requests, get_network_request and list_console_messages — never as universal grammar; an MV3 worker that has been idle-evicted is started automatically first, see 'wake'."
|
|
7392
|
+
},
|
|
7393
|
+
wake: {
|
|
7394
|
+
type: "boolean",
|
|
7395
|
+
description: "Only valid with a 'worker:<substring>' target, and rejected on a page target rather than ignored. Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so a wake asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything; the error then says the worker may simply be asleep."
|
|
7396
|
+
},
|
|
7397
|
+
expression: {
|
|
7398
|
+
type: "string",
|
|
7399
|
+
description: "JavaScript to run. Evaluated as an expression when 'args' is omitted; must be a function literal (arrow or classic) whose parameters receive 'args' when 'args' is provided. REQUIRED — the code always goes in this key, never in 'function', 'code', 'js', 'script', 'fn' or 'body'; a call using one of those instead is rejected with an error naming the wrong key."
|
|
7400
|
+
},
|
|
7401
|
+
awaitPromise: {
|
|
7402
|
+
type: "boolean",
|
|
7403
|
+
description: "Await the result if it is a Promise (default true)."
|
|
7404
|
+
},
|
|
7405
|
+
args: {
|
|
7406
|
+
type: "array",
|
|
7407
|
+
description: "Positional JSON-serializable arguments to pass to the expression, treating it as a function. No live element/page handle is bound."
|
|
7408
|
+
},
|
|
7409
|
+
savePath: {
|
|
7410
|
+
type: "string",
|
|
7411
|
+
description: "Write the evaluated value to this file as JSON and KEEP IT OUT OF THE RESPONSE: with savePath set the result is {path,bytes,type,target} only, with no copy, preview or truncation of the value. Use it to read credentials without putting them in a transcript. An absolute path (starting with /) is used as-is; a relative path is resolved under the artifact dir (/tmp/cdp-toolkit). Missing parent directories are created. Omit it to get the value back inline, exactly as before."
|
|
7412
|
+
}
|
|
7413
|
+
},
|
|
7414
|
+
required: [
|
|
7415
|
+
"expression"
|
|
7416
|
+
],
|
|
7417
|
+
additionalProperties: false
|
|
7418
|
+
}
|
|
7419
|
+
},
|
|
7420
|
+
{
|
|
7421
|
+
name: "list_cookies",
|
|
7422
|
+
description: "Read the cookie store for the target page, INCLUDING httpOnly cookies, which document.cookie cannot see and an evaluate_script call therefore cannot reach. Each cookie carries name, value, domain, path, expires (Unix seconds, -1 for a session cookie), size, httpOnly, secure, sameSite ('strict'|'lax'|'none'|'default') and session. The read is page-scoped, not browser-wide: it returns the cookies of the resolved tab (Chrome: Network.getCookies; Firefox: storage.getCookies partitioned by that browsing context), so point at a page on the site whose cookies you want. Filter with 'domain' and/or 'name'. Pass 'savePath' to write the cookie array to a JSON file instead: the response is then {path,bytes,count,target} only, with no cookie value in it in any form, which is how to capture a session cookie without putting the credential in the caller's transcript.",
|
|
7423
|
+
inputSchema: {
|
|
7424
|
+
type: "object",
|
|
7425
|
+
properties: {
|
|
7426
|
+
lease: {
|
|
7427
|
+
type: "string",
|
|
7428
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7429
|
+
},
|
|
7430
|
+
target: {
|
|
7431
|
+
type: "string",
|
|
7432
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
7433
|
+
},
|
|
7434
|
+
domain: {
|
|
7435
|
+
type: "string",
|
|
7436
|
+
description: "Keep only cookies for this domain. A leading dot is ignored on both sides and subdomains of the given domain match too, so 'example.test' matches '.example.test' and 'app.example.test'. No wildcards."
|
|
7437
|
+
},
|
|
7438
|
+
name: {
|
|
7439
|
+
type: "string",
|
|
7440
|
+
description: "Keep only the cookie with exactly this name. Exact match, not a substring."
|
|
7441
|
+
},
|
|
7442
|
+
savePath: {
|
|
7443
|
+
type: "string",
|
|
7444
|
+
description: "Write the cookie array to this file as JSON and KEEP THE VALUES OUT OF THE RESPONSE: with savePath set the result is {path,bytes,count,target} only, with no copy, preview or truncation of any cookie value. An absolute path (starting with /) is used as-is; a relative path is resolved under the artifact dir (/tmp/cdp-toolkit). Missing parent directories are created. Omit it to get the cookies back inline."
|
|
7445
|
+
}
|
|
7446
|
+
},
|
|
7447
|
+
required: [],
|
|
7448
|
+
additionalProperties: false
|
|
7449
|
+
}
|
|
7450
|
+
},
|
|
7451
|
+
{
|
|
7452
|
+
name: "set_cookie",
|
|
7453
|
+
description: "Write one cookie into the target page's cookie store, INCLUDING an httpOnly or secure cookie, which document.cookie cannot create and an evaluate_script call therefore cannot either. Chrome uses Network.setCookie, Firefox uses storage.setCookie partitioned by the resolved browsing context. Either 'url' or 'domain' is REQUIRED: a cookie has to be attributed to a site and the call is refused with an error when neither is given, rather than guessing the current page's origin. The response is {set:true,target} and never echoes the value back, so a credential you just supplied does not land in the transcript twice. A 'set:true' is earned, not assumed: Chrome answers Network.setCookie with success:false when it declines a cookie (a domain the url does not belong to, a secure cookie on an insecure origin, an oversized value) and that refusal is raised as an error. On Firefox, 'domain' is derived from 'url' when only the url was given, because BiDi has no url parameter; a url with no host, such as about:blank or a data URL, is an error rather than a silent no-op. Read the result back with list_cookies when you need proof.",
|
|
7454
|
+
inputSchema: {
|
|
7455
|
+
type: "object",
|
|
7456
|
+
properties: {
|
|
7457
|
+
lease: {
|
|
7458
|
+
type: "string",
|
|
7459
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7460
|
+
},
|
|
7461
|
+
target: {
|
|
7462
|
+
type: "string",
|
|
7463
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
7464
|
+
},
|
|
7465
|
+
name: {
|
|
7466
|
+
type: "string",
|
|
7467
|
+
description: "Cookie name. Required and non-empty."
|
|
7468
|
+
},
|
|
7469
|
+
value: {
|
|
7470
|
+
type: "string",
|
|
7471
|
+
description: "Cookie value. Required. It is not echoed back in the response."
|
|
7472
|
+
},
|
|
7473
|
+
url: {
|
|
7474
|
+
type: "string",
|
|
7475
|
+
description: "The URL the cookie is set for, for example 'https://example.com/'. Chrome derives domain, path and secure from it. Firefox derives only the domain from its host. Give this or 'domain'."
|
|
7476
|
+
},
|
|
7477
|
+
domain: {
|
|
7478
|
+
type: "string",
|
|
7479
|
+
description: "Cookie domain, for example 'example.com' or '.example.com' for subdomains. Give this or 'url'. When both are given, this wins on Firefox and Chrome applies its own url plus domain consistency rule."
|
|
7480
|
+
},
|
|
7481
|
+
path: {
|
|
7482
|
+
type: "string",
|
|
7483
|
+
description: "Cookie path. Passed through exactly as given and NOT defaulted to '/': with a 'url' Chrome derives the path from it, so an invented default here would widen a cookie you meant to scope narrowly."
|
|
7484
|
+
},
|
|
7485
|
+
expires: {
|
|
7486
|
+
type: "number",
|
|
7487
|
+
description: "Expiry as Unix time in SECONDS, matching what list_cookies reports. Omit it for a session cookie, which is what both backends do when no expiry is supplied."
|
|
7488
|
+
},
|
|
7489
|
+
httpOnly: {
|
|
7490
|
+
type: "boolean",
|
|
7491
|
+
description: "Set the httpOnly flag, making the cookie invisible to document.cookie. Only a protocol write like this one can create such a cookie."
|
|
7492
|
+
},
|
|
7493
|
+
secure: {
|
|
7494
|
+
type: "boolean",
|
|
7495
|
+
description: "Set the secure flag. Chrome declines a secure cookie on an insecure origin, and that refusal surfaces as an error."
|
|
7496
|
+
},
|
|
7497
|
+
sameSite: {
|
|
7498
|
+
type: "string",
|
|
7499
|
+
enum: ["strict", "lax", "none", "default"],
|
|
7500
|
+
description: "SameSite attribute, lowercase, matching what list_cookies reports. 'default' means the attribute is not set at all."
|
|
7501
|
+
}
|
|
7502
|
+
},
|
|
7503
|
+
required: ["name", "value"],
|
|
7504
|
+
additionalProperties: false
|
|
7505
|
+
}
|
|
7506
|
+
},
|
|
7507
|
+
{
|
|
7508
|
+
name: "delete_cookies",
|
|
7509
|
+
description: "Delete the named cookie from the target page's cookie store, httpOnly cookies included. Chrome uses Network.deleteCookies, Firefox uses storage.deleteCookies with a filter, partitioned by the resolved browsing context. Both 'name' and one of 'url' or 'domain' are REQUIRED: without a site constraint the call would delete by name across the whole partition, so it is refused with an error instead. Narrow further with 'path'. The response is {deleted:true,target} and carries NO count, because neither protocol reports how many cookies it removed and a number here would be invented; call list_cookies before and after when you need a real count. 'deleted:true' means the backend accepted and performed the deletion, not that a matching cookie existed, since deleting an absent cookie is a success on both backends.",
|
|
7510
|
+
inputSchema: {
|
|
7511
|
+
type: "object",
|
|
7512
|
+
properties: {
|
|
7513
|
+
lease: {
|
|
7514
|
+
type: "string",
|
|
7515
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7516
|
+
},
|
|
7517
|
+
target: {
|
|
7518
|
+
type: "string",
|
|
7519
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
7520
|
+
},
|
|
7521
|
+
name: {
|
|
7522
|
+
type: "string",
|
|
7523
|
+
description: "Exact cookie name to delete. Required and non-empty. No wildcards: this removes a named cookie, not a swathe of them."
|
|
7524
|
+
},
|
|
7525
|
+
url: {
|
|
7526
|
+
type: "string",
|
|
7527
|
+
description: "Delete cookies matching this URL, for example 'https://example.com/'. Give this or 'domain'. On Firefox only the host is used, because BiDi's filter has no url field."
|
|
7528
|
+
},
|
|
7529
|
+
domain: {
|
|
7530
|
+
type: "string",
|
|
7531
|
+
description: "Delete cookies with this exact domain. Give this or 'url'."
|
|
7532
|
+
},
|
|
7533
|
+
path: {
|
|
7534
|
+
type: "string",
|
|
7535
|
+
description: "Narrow the deletion to cookies with this exact path. Omit to match any path allowed by the other constraints."
|
|
7536
|
+
}
|
|
7537
|
+
},
|
|
7538
|
+
required: ["name"],
|
|
7539
|
+
additionalProperties: false
|
|
7540
|
+
}
|
|
7541
|
+
},
|
|
7542
|
+
{
|
|
7543
|
+
name: "take_snapshot",
|
|
7544
|
+
description: "Capture the page's accessibility tree (Accessibility.getFullAXTree) as a compact indented text tree where each line is prefixed with [uid], the node's CDP backendDOMNodeId. These uids are the stateless element references that every interaction tool (click/hover/fill/etc.) feeds back to resolve a live DOM node (via DOM.resolveNode({ backendNodeId: uid })), so run this first to discover uids.",
|
|
7545
|
+
inputSchema: {
|
|
7546
|
+
type: "object",
|
|
7547
|
+
properties: {
|
|
7548
|
+
lease: {
|
|
7549
|
+
type: "string",
|
|
7550
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7551
|
+
},
|
|
7552
|
+
target: {
|
|
7553
|
+
type: "string",
|
|
7554
|
+
description: "Target page selector: 'active' (default, = first page-type target) | 'index:N' (Nth page-type target, 0-based) | 'url:<substring>' | 'title:<substring>' | 'label:<name>' (exact label match, origin ledger or live lease) | '<32-hex targetId>' (exact target by id)."
|
|
7555
|
+
},
|
|
7556
|
+
interactiveOnly: {
|
|
7557
|
+
type: "boolean",
|
|
7558
|
+
description: "When true, emit only interactive/meaningful nodes flattened into a readable list; default false returns the full hierarchical a11y tree."
|
|
7559
|
+
}
|
|
7560
|
+
},
|
|
7561
|
+
required: [],
|
|
7562
|
+
additionalProperties: false
|
|
7563
|
+
}
|
|
7564
|
+
},
|
|
7565
|
+
{
|
|
7566
|
+
name: "click",
|
|
7567
|
+
description: "Click an element via a synthetic mouse press/release at the element's scrolled-into-view bounding-rect center. Target the element with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector). clickCount:3 triple-clicks (selects a paragraph/line in most editors). 'modifiers' holds Alt/Control/Meta/Shift for the press and release, like a real modifier-click; on the Firefox backend a non-empty 'modifiers' throws (not yet supported over BiDi), so a modifier click there needs --browser chrome.",
|
|
7568
|
+
inputSchema: {
|
|
7569
|
+
type: "object",
|
|
7570
|
+
properties: {
|
|
7571
|
+
lease: {
|
|
7572
|
+
type: "string",
|
|
7573
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7574
|
+
},
|
|
7575
|
+
target: {
|
|
7576
|
+
type: "string",
|
|
7577
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7578
|
+
},
|
|
7579
|
+
uid: {
|
|
7580
|
+
type: "number",
|
|
7581
|
+
description: "CDP backendDOMNodeId of the element to click, obtained from take_snapshot. Provide exactly one of uid or selector."
|
|
7582
|
+
},
|
|
7583
|
+
selector: {
|
|
7584
|
+
type: "string",
|
|
7585
|
+
description: "CSS selector for the element to click (resolved via document.querySelector). Provide exactly one of uid or selector."
|
|
7586
|
+
},
|
|
7587
|
+
button: {
|
|
7588
|
+
type: "string",
|
|
7589
|
+
enum: [
|
|
7590
|
+
"left",
|
|
7591
|
+
"right",
|
|
7592
|
+
"middle"
|
|
7593
|
+
],
|
|
7594
|
+
description: "Mouse button: 'left' (default), 'right', or 'middle'."
|
|
7595
|
+
},
|
|
7596
|
+
clickCount: {
|
|
7597
|
+
type: "number",
|
|
7598
|
+
description: "Number of clicks: 1 = single (default), 2 = double-click, 3 = triple-click."
|
|
7599
|
+
},
|
|
7600
|
+
modifiers: {
|
|
7601
|
+
type: "array",
|
|
7602
|
+
items: {
|
|
7603
|
+
type: "string",
|
|
7604
|
+
enum: [
|
|
7605
|
+
"Alt",
|
|
7606
|
+
"Control",
|
|
7607
|
+
"Meta",
|
|
7608
|
+
"Shift"
|
|
7609
|
+
]
|
|
7610
|
+
},
|
|
7611
|
+
description: "Modifier keys held for the click's press and release, e.g. ['Shift'] for a shift-click. Not supported on the Firefox backend: passing a non-empty array there throws."
|
|
7612
|
+
}
|
|
7613
|
+
},
|
|
7614
|
+
required: [],
|
|
7615
|
+
additionalProperties: false
|
|
7616
|
+
}
|
|
7617
|
+
},
|
|
7618
|
+
{
|
|
7619
|
+
name: "hover",
|
|
7620
|
+
description: "Hover the mouse over an element by dispatching a mouseMoved event at its scrolled-into-view center, firing framework hover handlers. Target with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector).",
|
|
7621
|
+
inputSchema: {
|
|
7622
|
+
type: "object",
|
|
7623
|
+
properties: {
|
|
7624
|
+
lease: {
|
|
7625
|
+
type: "string",
|
|
7626
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7627
|
+
},
|
|
7628
|
+
target: {
|
|
7629
|
+
type: "string",
|
|
7630
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7631
|
+
},
|
|
7632
|
+
uid: {
|
|
7633
|
+
type: "number",
|
|
7634
|
+
description: "CDP backendDOMNodeId of the element to hover, obtained from take_snapshot. Provide exactly one of uid or selector."
|
|
7635
|
+
},
|
|
7636
|
+
selector: {
|
|
7637
|
+
type: "string",
|
|
7638
|
+
description: "CSS selector for the element to hover (resolved via document.querySelector). Provide exactly one of uid or selector."
|
|
7639
|
+
}
|
|
7640
|
+
},
|
|
7641
|
+
required: [],
|
|
7642
|
+
additionalProperties: false
|
|
7643
|
+
}
|
|
7644
|
+
},
|
|
7645
|
+
{
|
|
7646
|
+
name: "drag",
|
|
7647
|
+
description: `Drag from a source element to a destination. 'from' takes exactly one of uid (a CDP backendDOMNodeId from take_snapshot) or selector. The destination is exactly one of 'to' (an element via uid/selector, or an absolute viewport point via x+y) or 'by' ({dx,dy} offset from the source point — sliders, map panning, resize handles). mode:'mouse' (default) dispatches synthetic mouse press/move/release: right for widgets built on raw pointer events, and Chrome does turn it into a real drag, but WHICH drag events reach the page depends on where the interpolated pointer path happens to land — measured on Chrome 151, the default steps:2 delivers ZERO dragover events, so an HTML5 drop zone written the standard way (preventDefault inside dragover) refuses the drop entirely. mode:'html5' performs the drag deterministically instead: Chrome's drag interception hands back the DragData the page's own dragstart built, and the toolkit replays it as dragEnter/dragOver/drop exactly at the destination, so a draggable="true" / dataTransfer drop zone works regardless of the pointer path. mode:'html5' is CHROME-ONLY: it requires capability 'input.html5Drag' and is rejected with a clear error under the Firefox backend, where the tool itself remains available for mouse-mode drags. 'steps' (default 2) sets how many interpolated mouse-move events are dispatched between source and destination; raise it for DnD libraries with a movement threshold or per-frame sampling.`,
|
|
7648
|
+
inputSchema: {
|
|
7649
|
+
type: "object",
|
|
7650
|
+
properties: {
|
|
7651
|
+
lease: {
|
|
7652
|
+
type: "string",
|
|
7653
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7654
|
+
},
|
|
7655
|
+
target: {
|
|
7656
|
+
type: "string",
|
|
7657
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7658
|
+
},
|
|
7659
|
+
from: {
|
|
7660
|
+
type: "object",
|
|
7661
|
+
description: "Source element to drag from. Provide exactly one of uid or selector.",
|
|
7662
|
+
properties: {
|
|
7663
|
+
uid: {
|
|
7664
|
+
type: "number",
|
|
7665
|
+
description: "CDP backendDOMNodeId of the source element, obtained from take_snapshot."
|
|
7666
|
+
},
|
|
7667
|
+
selector: {
|
|
7668
|
+
type: "string",
|
|
7669
|
+
description: "CSS selector for the source element (resolved via document.querySelector)."
|
|
7670
|
+
}
|
|
7671
|
+
},
|
|
7672
|
+
additionalProperties: false
|
|
7673
|
+
},
|
|
7674
|
+
to: {
|
|
7675
|
+
type: "object",
|
|
7676
|
+
description: "Where the drag ends: exactly one of uid, selector, or x+y. Mutually exclusive with 'by'; exactly one of to/by is required.",
|
|
7677
|
+
properties: {
|
|
7678
|
+
uid: {
|
|
7679
|
+
type: "number",
|
|
7680
|
+
description: "CDP backendDOMNodeId of the destination element, obtained from take_snapshot."
|
|
7681
|
+
},
|
|
7682
|
+
selector: {
|
|
7683
|
+
type: "string",
|
|
7684
|
+
description: "CSS selector for the destination element (resolved via document.querySelector)."
|
|
7685
|
+
},
|
|
7686
|
+
x: {
|
|
7687
|
+
type: "number",
|
|
7688
|
+
description: "Absolute viewport x-coordinate to drop at. Must be given together with 'y'. Use this when there is no droppable element to name (a canvas, a slider track)."
|
|
7689
|
+
},
|
|
7690
|
+
y: {
|
|
7691
|
+
type: "number",
|
|
7692
|
+
description: "Absolute viewport y-coordinate to drop at. Must be given together with 'x'."
|
|
7693
|
+
}
|
|
7694
|
+
},
|
|
7695
|
+
additionalProperties: false
|
|
7696
|
+
},
|
|
7697
|
+
by: {
|
|
7698
|
+
type: "object",
|
|
7699
|
+
description: "Drag by an offset from the source element's center instead of to a destination: at least one of dx/dy, the other defaults to 0. Mutually exclusive with 'to'; exactly one of to/by is required. Use for sliders (by:{dx:40}), map panning, and resize handles.",
|
|
7700
|
+
properties: {
|
|
7701
|
+
dx: {
|
|
7702
|
+
type: "number",
|
|
7703
|
+
description: "Horizontal offset in CSS pixels; positive drags RIGHT."
|
|
7704
|
+
},
|
|
7705
|
+
dy: {
|
|
7706
|
+
type: "number",
|
|
7707
|
+
description: "Vertical offset in CSS pixels; positive drags DOWN."
|
|
7708
|
+
}
|
|
7709
|
+
},
|
|
7710
|
+
additionalProperties: false
|
|
7711
|
+
},
|
|
7712
|
+
mode: {
|
|
7713
|
+
type: "string",
|
|
7714
|
+
enum: [
|
|
7715
|
+
"mouse",
|
|
7716
|
+
"html5"
|
|
7717
|
+
],
|
|
7718
|
+
description: "'mouse' (default): synthetic mouse press/move/release, right for widgets built on raw pointer events. Chrome does turn this into a real HTML5 drag too, but WHICH drag events reach the page depends on the interpolated pointer path: at the default steps:2, an HTML5 drop zone written the standard way (preventDefault inside dragover) sees zero dragover events and refuses the drop. 'html5': real HTML5 drag-and-drop (dragstart/dragEnter/dragOver/drop with the page's own dataTransfer), deterministic regardless of pointer path. 'html5' is CHROME-ONLY and is rejected with a clear error under the Firefox backend."
|
|
7719
|
+
},
|
|
7720
|
+
steps: {
|
|
7721
|
+
type: "number",
|
|
7722
|
+
description: "Number of interpolated mouse-move events dispatched between the source and destination points, evenly spaced, the last landing exactly on the destination. Integer 1-500, default 2 (midpoint then destination). Raise it for DnD libraries with a movement threshold or per-frame sampling."
|
|
7723
|
+
}
|
|
7724
|
+
},
|
|
7725
|
+
required: [
|
|
7726
|
+
"from"
|
|
7727
|
+
],
|
|
7728
|
+
additionalProperties: false
|
|
7729
|
+
}
|
|
7730
|
+
},
|
|
7731
|
+
{
|
|
7732
|
+
name: "scroll",
|
|
7733
|
+
description: "Dispatch a wheel/scroll event at an anchor point: provide at most one of 'uid', 'selector', or 'x'+'y'; omit all three to scroll at the viewport center. An element anchor ('uid' or 'selector') is scrolled into view first, the same as click/hover. At least one of 'deltaX'/'deltaY' is required; positive 'deltaY' scrolls DOWN and positive 'deltaX' scrolls RIGHT (wheel-event convention). Chrome dispatches Input.dispatchMouseEvent{type:'mouseWheel'}; Firefox dispatches WebDriver BiDi's 'wheel' input source. Returns the resolved anchor point ({x,y}) plus the delta actually dispatched.",
|
|
7734
|
+
inputSchema: {
|
|
7735
|
+
type: "object",
|
|
7736
|
+
properties: {
|
|
7737
|
+
lease: {
|
|
7738
|
+
type: "string",
|
|
7739
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7740
|
+
},
|
|
7741
|
+
target: {
|
|
7742
|
+
type: "string",
|
|
7743
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7744
|
+
},
|
|
7745
|
+
uid: {
|
|
7746
|
+
type: "number",
|
|
7747
|
+
description: "CDP backendDOMNodeId of the element to scroll into view and anchor at, obtained from take_snapshot. Provide at most one of uid, selector, or x+y; omit all three to scroll at the viewport center."
|
|
7748
|
+
},
|
|
7749
|
+
selector: {
|
|
7750
|
+
type: "string",
|
|
7751
|
+
description: "CSS selector for the element to scroll into view and anchor at (resolved via document.querySelector). Provide at most one of uid, selector, or x+y; omit all three to scroll at the viewport center."
|
|
7752
|
+
},
|
|
7753
|
+
x: {
|
|
7754
|
+
type: "number",
|
|
7755
|
+
description: "Absolute viewport x-coordinate to anchor the scroll at. Must be given together with 'y'. Provide at most one of uid, selector, or x+y."
|
|
7756
|
+
},
|
|
7757
|
+
y: {
|
|
7758
|
+
type: "number",
|
|
7759
|
+
description: "Absolute viewport y-coordinate to anchor the scroll at. Must be given together with 'x'. Provide at most one of uid, selector, or x+y."
|
|
7760
|
+
},
|
|
7761
|
+
deltaX: {
|
|
7762
|
+
type: "number",
|
|
7763
|
+
description: "Horizontal scroll delta; positive scrolls RIGHT. At least one of deltaX/deltaY is required."
|
|
7764
|
+
},
|
|
7765
|
+
deltaY: {
|
|
7766
|
+
type: "number",
|
|
7767
|
+
description: "Vertical scroll delta; positive scrolls DOWN. At least one of deltaX/deltaY is required."
|
|
7768
|
+
}
|
|
7769
|
+
},
|
|
7770
|
+
required: [],
|
|
7771
|
+
additionalProperties: false
|
|
7772
|
+
}
|
|
7773
|
+
},
|
|
7774
|
+
{
|
|
7775
|
+
name: "dispatch_mouse",
|
|
7776
|
+
description: "Dispatch exactly one raw mouse event ('move', 'down', or 'up') at absolute viewport coordinates: the toolkit's lowest-level input primitive. Compose move/down/move/up calls yourself to reach anything a physical mouse can do that click/drag's fixed sequences cannot — canvas drag-painting, marquee/rubber-band selection, a custom widget with its own hit-testing. Chrome-only (capability 'input.raw'): absent from tools/list under the Firefox backend, never present-and-throwing.",
|
|
7777
|
+
inputSchema: {
|
|
7778
|
+
type: "object",
|
|
7779
|
+
properties: {
|
|
7780
|
+
lease: {
|
|
7781
|
+
type: "string",
|
|
7782
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7783
|
+
},
|
|
7784
|
+
target: {
|
|
7785
|
+
type: "string",
|
|
7786
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7787
|
+
},
|
|
7788
|
+
action: {
|
|
7789
|
+
type: "string",
|
|
7790
|
+
enum: [
|
|
7791
|
+
"move",
|
|
7792
|
+
"down",
|
|
7793
|
+
"up"
|
|
7794
|
+
],
|
|
7795
|
+
description: "Which raw event to dispatch: 'move' (mouseMoved), 'down' (mousePressed), or 'up' (mouseReleased)."
|
|
7796
|
+
},
|
|
7797
|
+
x: {
|
|
7798
|
+
type: "number",
|
|
7799
|
+
description: "Viewport x-coordinate. Required on every call: CDP has no notion of a 'current pointer position' to default from."
|
|
7800
|
+
},
|
|
7801
|
+
y: {
|
|
7802
|
+
type: "number",
|
|
7803
|
+
description: "Viewport y-coordinate. Required on every call."
|
|
7804
|
+
},
|
|
7805
|
+
button: {
|
|
7806
|
+
type: "string",
|
|
7807
|
+
enum: [
|
|
7808
|
+
"left",
|
|
7809
|
+
"right",
|
|
7810
|
+
"middle"
|
|
7811
|
+
],
|
|
7812
|
+
description: "Mouse button: 'left' (default), 'right', or 'middle'."
|
|
7813
|
+
},
|
|
7814
|
+
clickCount: {
|
|
7815
|
+
type: "number",
|
|
7816
|
+
description: "Click-run length for a 'down' or 'up' event (2 = the second half of a double-click, 3 = triple-click). Ignored for 'move'; defaults to 1."
|
|
7817
|
+
},
|
|
7818
|
+
modifiers: {
|
|
7819
|
+
type: "array",
|
|
7820
|
+
items: {
|
|
7821
|
+
type: "string",
|
|
7822
|
+
enum: [
|
|
7823
|
+
"Alt",
|
|
7824
|
+
"Control",
|
|
7825
|
+
"Meta",
|
|
7826
|
+
"Shift"
|
|
7827
|
+
]
|
|
7828
|
+
},
|
|
7829
|
+
description: "Modifier keys held for this one event, e.g. ['Shift']."
|
|
7830
|
+
}
|
|
7831
|
+
},
|
|
7832
|
+
required: [
|
|
7833
|
+
"action",
|
|
7834
|
+
"x",
|
|
7835
|
+
"y"
|
|
7836
|
+
],
|
|
7837
|
+
additionalProperties: false
|
|
7838
|
+
}
|
|
7839
|
+
},
|
|
7840
|
+
{
|
|
7841
|
+
name: "wait_for_download",
|
|
7842
|
+
description: "Wait for a file download to finish and return it as a real file on disk: {path,suggestedFilename,bytes,url,target}. The file is written under the artifact dir's downloads/ folder and renamed from Chrome's internal guid to the page's own filename, collision-suffixed (report.csv, report-1.csv, ...). ORDERING RULE, and it is not optional: download capture must be ARMED BEFORE the click that starts the download — call wait_for_download{arm:true} first (it arms and returns immediately, reporting {armed:true,downloadPath,pending}), then click, then call wait_for_download to collect the finished file. This is Chrome's behavior, not a preference: the download-behavior override is per-connection state that Chrome REVERTS the moment the arming client disconnects, and an unarmed headless Chrome denies the download outright, so a download triggered before anything armed is lost with no file anywhere. SIDE EFFECT, browser-global: arming redirects EVERY download in this browser (all tabs, all origins) into the toolkit's downloads directory for as long as this server runs, and downloads no longer land in the user's normal Downloads folder. Because the arm lives on a connection this server process holds open, this is an MCP-server capability: under the one-shot CLI the connection dies with the process and nothing is captured. Chrome-only (capability 'browser.downloads'): absent from tools/list under the Firefox backend, never present-and-throwing, because WebDriver BiDi has no command to redirect a download to a chosen directory.",
|
|
7843
|
+
inputSchema: {
|
|
7844
|
+
type: "object",
|
|
7845
|
+
properties: {
|
|
7846
|
+
lease: {
|
|
7847
|
+
type: "string",
|
|
7848
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7849
|
+
},
|
|
7850
|
+
target: {
|
|
7851
|
+
type: "string",
|
|
7852
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'. A download is browser-scoped, so this names the tab for the lease check and for the echoed target, not which download is returned."
|
|
7853
|
+
},
|
|
7854
|
+
arm: {
|
|
7855
|
+
type: "boolean",
|
|
7856
|
+
description: "Arm download capture and return immediately instead of waiting: {armed:true,downloadPath,pending}. Call this BEFORE the click that triggers the download. 'pending' counts downloads that already completed and have not been collected yet, so an arm call doubles as a peek."
|
|
7857
|
+
},
|
|
7858
|
+
timeoutMs: {
|
|
7859
|
+
type: "number",
|
|
7860
|
+
description: "How long to wait for a download to complete, in milliseconds. Default 30000. Ignored with arm:true. A download that already completed and was not yet collected is returned immediately, so this only bounds the wait for a download still in flight."
|
|
7861
|
+
}
|
|
7862
|
+
},
|
|
7863
|
+
required: [],
|
|
7864
|
+
additionalProperties: false
|
|
7865
|
+
}
|
|
7866
|
+
},
|
|
7867
|
+
{
|
|
7868
|
+
name: "grant_permissions",
|
|
7869
|
+
description: "Grant browser permissions for an origin up front, so a page asking for geolocation / notifications / clipboard gets an answer instead of showing a prompt no agent can click. 'permissions' takes CDP PermissionType values ('geolocation', 'notifications', 'clipboardReadWrite', 'camera', 'microphone', 'midi', ...); an unknown name is refused by Chrome with the bad value in the message. Returns {granted,origin,target}, or {reset:true,target} for a reset-only call. Grants are keyed by ORIGIN, not by tab: every tab on that origin is affected, including ones opened later. reset:true clears this server's previous grants first (or instead, when no permissions are given) — note CDP's reset is not origin-scoped, so it clears them for every origin at once. The grant lives on a connection this server process holds open and Chrome DISCARDS IT when that connection closes, so this is an MCP-server capability: under the one-shot CLI the grant dies with the process. Chrome-only (capability 'browser.permissions'): absent from tools/list under the Firefox backend, never present-and-throwing.",
|
|
7870
|
+
inputSchema: {
|
|
7871
|
+
type: "object",
|
|
7872
|
+
properties: {
|
|
7873
|
+
lease: {
|
|
7874
|
+
type: "string",
|
|
7875
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7876
|
+
},
|
|
7877
|
+
target: {
|
|
7878
|
+
type: "string",
|
|
7879
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'. Used for the lease check and, when 'origin' is omitted, as the source of the origin to grant for."
|
|
7880
|
+
},
|
|
7881
|
+
permissions: {
|
|
7882
|
+
type: "array",
|
|
7883
|
+
items: {
|
|
7884
|
+
type: "string"
|
|
7885
|
+
},
|
|
7886
|
+
description: "CDP PermissionType values to grant, e.g. ['geolocation'] or ['clipboardReadWrite','clipboardSanitizedWrite']. Required unless reset:true. An empty array is refused rather than treated as a successful no-op."
|
|
7887
|
+
},
|
|
7888
|
+
origin: {
|
|
7889
|
+
type: "string",
|
|
7890
|
+
description: "Origin the grant applies to, e.g. 'https://example.com'. Defaults to the target tab's own origin. Required explicitly when that tab has no grantable origin (data:, blob:, about:blank all serialize to the opaque origin 'null'), which is refused with a message naming the tab's url."
|
|
7891
|
+
},
|
|
7892
|
+
reset: {
|
|
7893
|
+
type: "boolean",
|
|
7894
|
+
description: "Clear this server's previous permission grants (Browser.resetPermissions). With 'permissions' it resets FIRST and then grants, so the result is exactly the listed permissions; on its own it is a reset-only call answering {reset:true}. Not scoped to 'origin': CDP resets every origin at once."
|
|
7895
|
+
}
|
|
7896
|
+
},
|
|
7897
|
+
required: [],
|
|
7898
|
+
additionalProperties: false
|
|
7899
|
+
}
|
|
7900
|
+
},
|
|
7901
|
+
{
|
|
7902
|
+
name: "fill",
|
|
7903
|
+
description: "Fill an element by focusing it, clearing existing content, then inserting 'value' via Input.insertText (atomic paste-like commit, not per-character keystrokes). Target with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector).",
|
|
7904
|
+
inputSchema: {
|
|
7905
|
+
type: "object",
|
|
7906
|
+
properties: {
|
|
7907
|
+
lease: {
|
|
7908
|
+
type: "string",
|
|
7909
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7910
|
+
},
|
|
7911
|
+
target: {
|
|
7912
|
+
type: "string",
|
|
7913
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7914
|
+
},
|
|
7915
|
+
uid: {
|
|
7916
|
+
type: "number",
|
|
7917
|
+
description: "CDP backendDOMNodeId of the field to fill, obtained from take_snapshot. Provide exactly one of uid or selector."
|
|
7918
|
+
},
|
|
7919
|
+
selector: {
|
|
7920
|
+
type: "string",
|
|
7921
|
+
description: "CSS selector for the field to fill (resolved via document.querySelector). Provide exactly one of uid or selector."
|
|
7922
|
+
},
|
|
7923
|
+
value: {
|
|
7924
|
+
type: "string",
|
|
7925
|
+
description: "The text value to set; the field is cleared first so this overwrites existing content."
|
|
7926
|
+
}
|
|
7927
|
+
},
|
|
7928
|
+
required: [
|
|
7929
|
+
"value"
|
|
7930
|
+
],
|
|
7931
|
+
additionalProperties: false
|
|
7932
|
+
}
|
|
7933
|
+
},
|
|
7934
|
+
{
|
|
7935
|
+
name: "fill_form",
|
|
7936
|
+
description: "Fill multiple form fields in one call; each field is focused, cleared, then set via Input.insertText. Each field in the non-empty 'fields' array takes exactly one of uid (a CDP backendDOMNodeId from take_snapshot) or selector, plus its string value.",
|
|
7937
|
+
inputSchema: {
|
|
7938
|
+
type: "object",
|
|
7939
|
+
properties: {
|
|
7940
|
+
lease: {
|
|
7941
|
+
type: "string",
|
|
7942
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7943
|
+
},
|
|
7944
|
+
target: {
|
|
7945
|
+
type: "string",
|
|
7946
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7947
|
+
},
|
|
7948
|
+
fields: {
|
|
7949
|
+
type: "array",
|
|
7950
|
+
description: "Non-empty array of fields to fill, each with exactly one of uid or selector plus a string value.",
|
|
7951
|
+
items: {
|
|
7952
|
+
type: "object",
|
|
7953
|
+
properties: {
|
|
7954
|
+
uid: {
|
|
7955
|
+
type: "number",
|
|
7956
|
+
description: "CDP backendDOMNodeId of the field, obtained from take_snapshot. Provide exactly one of uid or selector."
|
|
7957
|
+
},
|
|
7958
|
+
selector: {
|
|
7959
|
+
type: "string",
|
|
7960
|
+
description: "CSS selector for the field (resolved via document.querySelector). Provide exactly one of uid or selector."
|
|
7961
|
+
},
|
|
7962
|
+
value: {
|
|
7963
|
+
type: "string",
|
|
7964
|
+
description: "The text value to set; the field is cleared first so this overwrites existing content."
|
|
7965
|
+
}
|
|
7966
|
+
},
|
|
7967
|
+
required: [
|
|
7968
|
+
"value"
|
|
7969
|
+
],
|
|
7970
|
+
additionalProperties: false
|
|
7971
|
+
}
|
|
7972
|
+
}
|
|
7973
|
+
},
|
|
7974
|
+
required: [
|
|
7975
|
+
"fields"
|
|
7976
|
+
],
|
|
7977
|
+
additionalProperties: false
|
|
7978
|
+
}
|
|
7979
|
+
},
|
|
7980
|
+
{
|
|
7981
|
+
name: "type_text",
|
|
7982
|
+
description: "Focus an element and append 'text' via Input.insertText without clearing first (closest to typing). Target with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector).",
|
|
7983
|
+
inputSchema: {
|
|
7984
|
+
type: "object",
|
|
7985
|
+
properties: {
|
|
7986
|
+
lease: {
|
|
7987
|
+
type: "string",
|
|
7988
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
7989
|
+
},
|
|
7990
|
+
target: {
|
|
7991
|
+
type: "string",
|
|
7992
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
7993
|
+
},
|
|
7994
|
+
uid: {
|
|
7995
|
+
type: "number",
|
|
7996
|
+
description: "CDP backendDOMNodeId of the element to type into, obtained from take_snapshot. Provide exactly one of uid or selector."
|
|
7997
|
+
},
|
|
7998
|
+
selector: {
|
|
7999
|
+
type: "string",
|
|
8000
|
+
description: "CSS selector for the element to type into (resolved via document.querySelector). Provide exactly one of uid or selector."
|
|
8001
|
+
},
|
|
8002
|
+
text: {
|
|
8003
|
+
type: "string",
|
|
8004
|
+
description: "The text to insert; appended to existing content rather than overwriting it."
|
|
8005
|
+
}
|
|
8006
|
+
},
|
|
8007
|
+
required: [
|
|
8008
|
+
"text"
|
|
8009
|
+
],
|
|
8010
|
+
additionalProperties: false
|
|
8011
|
+
}
|
|
8012
|
+
},
|
|
8013
|
+
{
|
|
8014
|
+
name: "press_key",
|
|
8015
|
+
description: "Dispatch a keyDown/keyUp pair for a single key with optional modifiers. 'key' is a named key (Enter, Tab, Escape, ArrowDown, Backspace, etc.) or a single printable character; the named-key table is curated (no F-keys/numpad/IME).",
|
|
8016
|
+
inputSchema: {
|
|
8017
|
+
type: "object",
|
|
8018
|
+
properties: {
|
|
8019
|
+
lease: {
|
|
8020
|
+
type: "string",
|
|
8021
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8022
|
+
},
|
|
8023
|
+
target: {
|
|
8024
|
+
type: "string",
|
|
8025
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
8026
|
+
},
|
|
8027
|
+
key: {
|
|
8028
|
+
type: "string",
|
|
8029
|
+
description: "Key to press: a named key like 'Enter', 'Tab', 'Escape', 'ArrowDown', 'Backspace', or a single printable character. Required."
|
|
8030
|
+
},
|
|
8031
|
+
modifiers: {
|
|
8032
|
+
type: "array",
|
|
8033
|
+
description: "Optional modifier names held during the press: 'Control'/'Ctrl', 'Shift', 'Alt', 'Meta'/'Cmd'.",
|
|
8034
|
+
items: {
|
|
8035
|
+
type: "string"
|
|
8036
|
+
}
|
|
8037
|
+
}
|
|
8038
|
+
},
|
|
8039
|
+
required: [
|
|
8040
|
+
"key"
|
|
8041
|
+
],
|
|
8042
|
+
additionalProperties: false
|
|
8043
|
+
}
|
|
8044
|
+
},
|
|
8045
|
+
{
|
|
8046
|
+
name: "upload_file",
|
|
8047
|
+
description: "Attach one or more files to an <input type=file> element via DOM.setFileInputFiles. Target the input with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector'; 'files' is an absolute path or array of absolute paths.",
|
|
8048
|
+
inputSchema: {
|
|
8049
|
+
type: "object",
|
|
8050
|
+
properties: {
|
|
8051
|
+
lease: {
|
|
8052
|
+
type: "string",
|
|
8053
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8054
|
+
},
|
|
8055
|
+
target: {
|
|
8056
|
+
type: "string",
|
|
8057
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
8058
|
+
},
|
|
8059
|
+
uid: {
|
|
8060
|
+
type: "number",
|
|
8061
|
+
description: "CDP backendDOMNodeId of the file input element, obtained from take_snapshot. Provide exactly one of uid or selector."
|
|
8062
|
+
},
|
|
8063
|
+
selector: {
|
|
8064
|
+
type: "string",
|
|
8065
|
+
description: "CSS selector for the file input element (resolved via document.querySelector). Provide exactly one of uid or selector."
|
|
8066
|
+
},
|
|
8067
|
+
files: {
|
|
8068
|
+
type: [
|
|
8069
|
+
"string",
|
|
8070
|
+
"array"
|
|
8071
|
+
],
|
|
8072
|
+
items: {
|
|
8073
|
+
type: "string"
|
|
8074
|
+
},
|
|
8075
|
+
description: "Absolute path, or array of absolute paths, to the file(s) to attach to the <input type=file>. Accepts a single string or an array of strings."
|
|
8076
|
+
}
|
|
8077
|
+
},
|
|
8078
|
+
required: [
|
|
8079
|
+
"files"
|
|
8080
|
+
],
|
|
8081
|
+
additionalProperties: false
|
|
8082
|
+
}
|
|
8083
|
+
},
|
|
8084
|
+
{
|
|
8085
|
+
name: "take_screenshot",
|
|
8086
|
+
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.",
|
|
8087
|
+
inputSchema: {
|
|
8088
|
+
type: "object",
|
|
8089
|
+
properties: {
|
|
8090
|
+
lease: {
|
|
8091
|
+
type: "string",
|
|
8092
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8093
|
+
},
|
|
8094
|
+
target: {
|
|
8095
|
+
type: "string",
|
|
8096
|
+
description: "Target page selector. undefined or 'active' -> first page target; '<32-hex targetId>' -> exact target by id; 'index:N' -> Nth page target (0-based); 'url:<substring>' -> first page whose url contains substring; 'title:<substring>' -> first page whose title contains substring; 'label:<name>' -> the page with exactly this label (origin ledger or live lease)."
|
|
8097
|
+
},
|
|
8098
|
+
format: {
|
|
8099
|
+
type: "string",
|
|
8100
|
+
enum: [
|
|
8101
|
+
"png",
|
|
8102
|
+
"jpeg"
|
|
8103
|
+
],
|
|
8104
|
+
description: "Image format. Defaults to png. quality applies only to jpeg."
|
|
8105
|
+
},
|
|
8106
|
+
quality: {
|
|
8107
|
+
type: "number",
|
|
8108
|
+
description: "JPEG quality 0-100 (default 80). Ignored for png."
|
|
8109
|
+
},
|
|
8110
|
+
fullPage: {
|
|
8111
|
+
type: "boolean",
|
|
8112
|
+
description: "Capture the full scrollable content height computed from Page.getLayoutMetrics, not just the viewport."
|
|
8113
|
+
},
|
|
8114
|
+
scale: {
|
|
8115
|
+
type: "number",
|
|
8116
|
+
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."
|
|
8117
|
+
},
|
|
8118
|
+
renderWidth: {
|
|
8119
|
+
type: "number",
|
|
8120
|
+
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."
|
|
8121
|
+
},
|
|
8122
|
+
renderHeight: {
|
|
8123
|
+
type: "number",
|
|
8124
|
+
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."
|
|
8125
|
+
},
|
|
8126
|
+
tile: {
|
|
8127
|
+
type: "boolean",
|
|
8128
|
+
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."
|
|
8129
|
+
},
|
|
8130
|
+
uid: {
|
|
8131
|
+
type: "number",
|
|
8132
|
+
description: "Element to clip to: a CDP backendDOMNodeId obtained from take_snapshot. Mutually exclusive with selector."
|
|
8133
|
+
},
|
|
8134
|
+
selector: {
|
|
8135
|
+
type: "string",
|
|
8136
|
+
description: "CSS selector to clip the capture to. Mutually exclusive with uid."
|
|
8137
|
+
},
|
|
8138
|
+
savePath: {
|
|
8139
|
+
type: "string",
|
|
8140
|
+
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."
|
|
8141
|
+
},
|
|
8142
|
+
returnBase64: {
|
|
8143
|
+
type: "boolean",
|
|
8144
|
+
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."
|
|
8145
|
+
}
|
|
8146
|
+
},
|
|
8147
|
+
required: [],
|
|
8148
|
+
additionalProperties: false
|
|
8149
|
+
}
|
|
8150
|
+
},
|
|
8151
|
+
{
|
|
8152
|
+
name: "emulate",
|
|
8153
|
+
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.",
|
|
8154
|
+
inputSchema: {
|
|
8155
|
+
type: "object",
|
|
8156
|
+
properties: {
|
|
8157
|
+
lease: {
|
|
8158
|
+
type: "string",
|
|
8159
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8160
|
+
},
|
|
8161
|
+
target: {
|
|
8162
|
+
type: "string",
|
|
8163
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
8164
|
+
},
|
|
8165
|
+
width: {
|
|
8166
|
+
type: "number",
|
|
8167
|
+
description: "Device-metrics viewport width in CSS pixels. Must be supplied together with height."
|
|
8168
|
+
},
|
|
8169
|
+
height: {
|
|
8170
|
+
type: "number",
|
|
8171
|
+
description: "Device-metrics viewport height in CSS pixels. Must be supplied together with width."
|
|
8172
|
+
},
|
|
8173
|
+
deviceScaleFactor: {
|
|
8174
|
+
type: "number",
|
|
8175
|
+
description: "Device pixel ratio (DPR) for the device-metrics override; 0 uses the platform default."
|
|
8176
|
+
},
|
|
8177
|
+
mobile: {
|
|
8178
|
+
type: "boolean",
|
|
8179
|
+
description: "Whether to emulate a mobile device (affects viewport meta handling, scrollbars, etc.) for the device-metrics override."
|
|
8180
|
+
},
|
|
8181
|
+
userAgent: {
|
|
8182
|
+
type: "string",
|
|
8183
|
+
description: "User-Agent string to override via Emulation.setUserAgentOverride."
|
|
8184
|
+
},
|
|
8185
|
+
cpuThrottlingRate: {
|
|
8186
|
+
type: "number",
|
|
8187
|
+
description: "CPU throttling multiplier: 1 = no throttle, 2 = 2x slower, etc. Must be >= 1."
|
|
8188
|
+
},
|
|
8189
|
+
media: {
|
|
8190
|
+
type: "string",
|
|
8191
|
+
description: "Emulated CSS media type: 'screen' | 'print' | '' (clear)."
|
|
8192
|
+
},
|
|
8193
|
+
mediaFeatures: {
|
|
8194
|
+
type: "array",
|
|
8195
|
+
description: "Emulated media features, e.g. [{ name: 'prefers-color-scheme', value: 'dark' }].",
|
|
8196
|
+
items: {
|
|
8197
|
+
type: "object",
|
|
8198
|
+
properties: {
|
|
8199
|
+
name: {
|
|
8200
|
+
type: "string",
|
|
8201
|
+
description: "Media feature name, e.g. 'prefers-color-scheme'."
|
|
8202
|
+
},
|
|
8203
|
+
value: {
|
|
8204
|
+
type: "string",
|
|
8205
|
+
description: "Media feature value, e.g. 'dark'."
|
|
8206
|
+
}
|
|
8207
|
+
},
|
|
8208
|
+
required: [
|
|
8209
|
+
"name",
|
|
8210
|
+
"value"
|
|
8211
|
+
],
|
|
8212
|
+
additionalProperties: false
|
|
8213
|
+
}
|
|
8214
|
+
},
|
|
8215
|
+
networkConditions: {
|
|
8216
|
+
type: "object",
|
|
8217
|
+
description: "Network condition overrides applied via Network.emulateNetworkConditions.",
|
|
8218
|
+
properties: {
|
|
8219
|
+
offline: {
|
|
8220
|
+
type: "boolean",
|
|
8221
|
+
description: "True to simulate offline."
|
|
8222
|
+
},
|
|
8223
|
+
latency: {
|
|
8224
|
+
type: "number",
|
|
8225
|
+
description: "Additional round-trip latency in milliseconds."
|
|
8226
|
+
},
|
|
8227
|
+
downloadThroughput: {
|
|
8228
|
+
type: "number",
|
|
8229
|
+
description: "Max download throughput in bytes/sec (-1 = no limit)."
|
|
8230
|
+
},
|
|
8231
|
+
uploadThroughput: {
|
|
8232
|
+
type: "number",
|
|
8233
|
+
description: "Max upload throughput in bytes/sec (-1 = no limit)."
|
|
8234
|
+
},
|
|
8235
|
+
connectionType: {
|
|
8236
|
+
type: "string",
|
|
8237
|
+
description: "Connection type: 'none' | 'cellular2g' | 'cellular3g' | 'cellular4g' | 'bluetooth' | 'ethernet' | 'wifi' | 'wimax' | 'other'."
|
|
8238
|
+
}
|
|
8239
|
+
},
|
|
8240
|
+
additionalProperties: false
|
|
8241
|
+
},
|
|
8242
|
+
clearOverrides: {
|
|
8243
|
+
type: "boolean",
|
|
8244
|
+
description: "Reset every override (device metrics, UA, CPU, media, network) to the browser default; ignores all other fields when true."
|
|
8245
|
+
}
|
|
8246
|
+
},
|
|
8247
|
+
required: [],
|
|
8248
|
+
additionalProperties: false
|
|
8249
|
+
}
|
|
8250
|
+
},
|
|
8251
|
+
{
|
|
8252
|
+
name: "resize_page",
|
|
8253
|
+
description: "Set the page's device-metrics width/height (the narrow case of emulate), optionally with deviceScaleFactor and mobile, then verify by reading back window.innerWidth/innerHeight after the override is applied. Requires positive numeric width and height. The device-metrics override persists on the target until cleared or the renderer navigates/reloads.",
|
|
8254
|
+
inputSchema: {
|
|
8255
|
+
type: "object",
|
|
8256
|
+
properties: {
|
|
8257
|
+
lease: {
|
|
8258
|
+
type: "string",
|
|
8259
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8260
|
+
},
|
|
8261
|
+
target: {
|
|
8262
|
+
type: "string",
|
|
8263
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
8264
|
+
},
|
|
8265
|
+
width: {
|
|
8266
|
+
type: "number",
|
|
8267
|
+
description: "Viewport width in CSS pixels (must be positive)."
|
|
8268
|
+
},
|
|
8269
|
+
height: {
|
|
8270
|
+
type: "number",
|
|
8271
|
+
description: "Viewport height in CSS pixels (must be positive)."
|
|
8272
|
+
},
|
|
8273
|
+
deviceScaleFactor: {
|
|
8274
|
+
type: "number",
|
|
8275
|
+
description: "Device pixel ratio (DPR); 0 uses the platform default."
|
|
8276
|
+
},
|
|
8277
|
+
mobile: {
|
|
8278
|
+
type: "boolean",
|
|
8279
|
+
description: "Whether to emulate a mobile device for the device-metrics override."
|
|
8280
|
+
}
|
|
8281
|
+
},
|
|
8282
|
+
required: [
|
|
8283
|
+
"width",
|
|
8284
|
+
"height"
|
|
8285
|
+
],
|
|
8286
|
+
additionalProperties: false
|
|
8287
|
+
}
|
|
8288
|
+
},
|
|
8289
|
+
{
|
|
8290
|
+
name: "handle_dialog",
|
|
8291
|
+
description: "Wait for the next JavaScript dialog (alert/confirm/prompt/beforeunload) on a page and respond via Page.handleJavaScriptDialog, accepting or dismissing it. Default mode resolves with the first handled dialog or throws on timeout; set autoMs to handle every dialog opening during a fixed window and resolve with the list. The dialog must be triggered out-of-band (e.g. by clicking a button), since a blocking dialog freezes the renderer.",
|
|
8292
|
+
inputSchema: {
|
|
8293
|
+
type: "object",
|
|
8294
|
+
properties: {
|
|
8295
|
+
lease: {
|
|
8296
|
+
type: "string",
|
|
8297
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8298
|
+
},
|
|
8299
|
+
target: {
|
|
8300
|
+
type: "string",
|
|
8301
|
+
description: "Target page selector: 'active' (or omitted) for the first page-type target | 'index:N' (0-based) | 'url:<substring>' | 'title:<substring>' | 'label:<name>' (exact label match, origin ledger or live lease) | a 32-hex <targetId>."
|
|
8302
|
+
},
|
|
8303
|
+
accept: {
|
|
8304
|
+
type: "boolean",
|
|
8305
|
+
description: "Whether to accept (OK) or dismiss (Cancel) the dialog."
|
|
8306
|
+
},
|
|
8307
|
+
promptText: {
|
|
8308
|
+
type: "string",
|
|
8309
|
+
description: "Text to enter for a prompt() dialog when accepting."
|
|
8310
|
+
},
|
|
8311
|
+
timeoutMs: {
|
|
8312
|
+
type: "number",
|
|
8313
|
+
description: "How long to wait for the next dialog in milliseconds (default 15000)."
|
|
8314
|
+
},
|
|
8315
|
+
autoMs: {
|
|
8316
|
+
type: "number",
|
|
8317
|
+
description: "Auto-handle mode: keep handling every dialog that opens for this many milliseconds, then resolve with the list of handled dialogs (an empty list is valid; never throws on 'no dialog')."
|
|
8318
|
+
}
|
|
8319
|
+
},
|
|
8320
|
+
required: [
|
|
8321
|
+
"accept"
|
|
8322
|
+
],
|
|
8323
|
+
additionalProperties: false
|
|
8324
|
+
}
|
|
8325
|
+
},
|
|
8326
|
+
{
|
|
8327
|
+
name: "list_console_messages",
|
|
8328
|
+
description: "Read console output (logs, warnings, exceptions) captured for the target page. By default reads the target's existing shared buffer and returns parsed console entries (empty if no capture has run); with reload:true it reloads the page and records a fresh capture window (both console+network) so a network reload never wipes console history. SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker's console.log arrives as Runtime.consoleAPICalled on its own session. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
8329
|
+
inputSchema: {
|
|
8330
|
+
type: "object",
|
|
8331
|
+
properties: {
|
|
8332
|
+
lease: {
|
|
8333
|
+
type: "string",
|
|
8334
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8335
|
+
},
|
|
8336
|
+
target: {
|
|
8337
|
+
type: "string",
|
|
8338
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
8339
|
+
},
|
|
8340
|
+
reload: {
|
|
8341
|
+
type: "boolean",
|
|
8342
|
+
description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer). With a worker: target there is nothing to reload: the window records for durationMs while the worker runs, and the session it holds keeps that worker from being idle-evicted until the capture stops."
|
|
8343
|
+
},
|
|
8344
|
+
durationMs: {
|
|
8345
|
+
type: "number",
|
|
8346
|
+
description: "Capture window for reload mode, in milliseconds. Default 2500."
|
|
8347
|
+
},
|
|
8348
|
+
wake: {
|
|
8349
|
+
type: "boolean",
|
|
8350
|
+
description: "Worker targets only, and only when a capture is being started (reload:true). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
8351
|
+
}
|
|
8352
|
+
},
|
|
8353
|
+
required: [],
|
|
8354
|
+
additionalProperties: false
|
|
8355
|
+
}
|
|
8356
|
+
},
|
|
8357
|
+
{
|
|
8358
|
+
name: "get_console_message",
|
|
8359
|
+
description: "Return a single console entry by zero-based index from the target's existing console buffer. Throws if the index is out of range; run list_console_messages (optionally with reload:true) first to populate the buffer.",
|
|
8360
|
+
inputSchema: {
|
|
8361
|
+
type: "object",
|
|
8362
|
+
properties: {
|
|
8363
|
+
lease: {
|
|
8364
|
+
type: "string",
|
|
8365
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8366
|
+
},
|
|
8367
|
+
target: {
|
|
8368
|
+
type: "string",
|
|
8369
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
8370
|
+
},
|
|
8371
|
+
index: {
|
|
8372
|
+
type: "number",
|
|
8373
|
+
description: "Zero-based index into the parsed console entries. Default 0."
|
|
8374
|
+
}
|
|
8375
|
+
},
|
|
8376
|
+
required: [],
|
|
8377
|
+
additionalProperties: false
|
|
8378
|
+
}
|
|
8379
|
+
},
|
|
8380
|
+
{
|
|
8381
|
+
name: "list_network_requests",
|
|
8382
|
+
description: "Return correlated network request rows (one per requestId, with status/headers/state) for the target page. By default reads the target's existing buffer; with reload:true it reloads and records a fresh both-domains capture window. Use filterUrl to keep only requests whose URL contains a substring. SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
8383
|
+
inputSchema: {
|
|
8384
|
+
type: "object",
|
|
8385
|
+
properties: {
|
|
8386
|
+
lease: {
|
|
8387
|
+
type: "string",
|
|
8388
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8389
|
+
},
|
|
8390
|
+
target: {
|
|
8391
|
+
type: "string",
|
|
8392
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
8393
|
+
},
|
|
8394
|
+
reload: {
|
|
8395
|
+
type: "boolean",
|
|
8396
|
+
description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer). With a worker: target there is nothing to reload: the window records for durationMs while the worker runs, and the session it holds keeps that worker from being idle-evicted until the capture stops."
|
|
8397
|
+
},
|
|
8398
|
+
durationMs: {
|
|
8399
|
+
type: "number",
|
|
8400
|
+
description: "Capture window for reload mode, in milliseconds. Default 2500."
|
|
8401
|
+
},
|
|
8402
|
+
filterUrl: {
|
|
8403
|
+
type: "string",
|
|
8404
|
+
description: "Only return requests whose URL contains this substring."
|
|
8405
|
+
},
|
|
8406
|
+
wake: {
|
|
8407
|
+
type: "boolean",
|
|
8408
|
+
description: "Worker targets only, and only when a capture is being started (reload:true). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
8409
|
+
}
|
|
8410
|
+
},
|
|
8411
|
+
required: [],
|
|
8412
|
+
additionalProperties: false
|
|
8413
|
+
}
|
|
8414
|
+
},
|
|
8415
|
+
{
|
|
8416
|
+
name: "get_network_request",
|
|
8417
|
+
description: "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh capture (a reload for a page, a listen window for a worker) and is matched by url ONLY (a fresh capture re-mints requestIds, so a carried-over requestId cannot fetch a body, so it returns metadata plus bodyUnavailableReason). SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
8418
|
+
inputSchema: {
|
|
8419
|
+
type: "object",
|
|
8420
|
+
properties: {
|
|
8421
|
+
lease: {
|
|
8422
|
+
type: "string",
|
|
8423
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8424
|
+
},
|
|
8425
|
+
target: {
|
|
8426
|
+
type: "string",
|
|
8427
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
8428
|
+
},
|
|
8429
|
+
requestId: {
|
|
8430
|
+
type: "string",
|
|
8431
|
+
description: "Match by exact requestId (metadata only, cannot fetch a body, since reload re-mints requestIds)."
|
|
8432
|
+
},
|
|
8433
|
+
url: {
|
|
8434
|
+
type: "string",
|
|
8435
|
+
description: "Match by URL substring (first match). Required for body fetch (includeBody), since url is stable across reload."
|
|
8436
|
+
},
|
|
8437
|
+
includeBody: {
|
|
8438
|
+
type: "boolean",
|
|
8439
|
+
description: "Also fetch the response body; drives a fresh reload capture and must be used with the `url` selector."
|
|
8440
|
+
},
|
|
8441
|
+
durationMs: {
|
|
8442
|
+
type: "number",
|
|
8443
|
+
description: "Capture window for the reload-driven body fetch, in milliseconds. Default 2500."
|
|
8444
|
+
},
|
|
8445
|
+
wake: {
|
|
8446
|
+
type: "boolean",
|
|
8447
|
+
description: "Worker targets only, and only when a capture is being started (includeBody together with url). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
8448
|
+
}
|
|
8449
|
+
},
|
|
8450
|
+
required: [],
|
|
8451
|
+
additionalProperties: false
|
|
8452
|
+
}
|
|
8453
|
+
},
|
|
8454
|
+
{
|
|
8455
|
+
name: "performance_start_trace",
|
|
8456
|
+
description: "Start a performance trace on the target page over raw CDP (Tracing.start) and park the recording connection in-process. Must be paired with performance_stop_trace WITHIN THE SAME PROCESS; for a robust cross-call trace use performance_trace instead. Throws if a trace is already in progress for the target.",
|
|
8457
|
+
inputSchema: {
|
|
8458
|
+
type: "object",
|
|
8459
|
+
properties: {
|
|
8460
|
+
lease: {
|
|
8461
|
+
type: "string",
|
|
8462
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8463
|
+
},
|
|
8464
|
+
target: {
|
|
8465
|
+
type: "string",
|
|
8466
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
8467
|
+
},
|
|
8468
|
+
categories: {
|
|
8469
|
+
type: "array",
|
|
8470
|
+
items: {
|
|
8471
|
+
type: "string"
|
|
8472
|
+
},
|
|
8473
|
+
description: "Trace categories to include. Defaults to the timeline + user-timing + loading + disabled-by-default timeline tracks that carry LCP/LayoutShift/RunTask."
|
|
8474
|
+
}
|
|
8475
|
+
},
|
|
8476
|
+
required: [],
|
|
8477
|
+
additionalProperties: false
|
|
8478
|
+
}
|
|
8479
|
+
},
|
|
8480
|
+
{
|
|
8481
|
+
name: "performance_stop_trace",
|
|
8482
|
+
description: "Stop the in-process trace started by performance_start_trace, drain buffered Tracing.dataCollected events, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics}. Throws if no live trace exists in this process (e.g. start ran in a different process); use performance_trace instead. The 'target' arg is accepted only for API symmetry; at most one trace is ever live per process.",
|
|
8483
|
+
inputSchema: {
|
|
8484
|
+
type: "object",
|
|
8485
|
+
properties: {
|
|
8486
|
+
lease: {
|
|
8487
|
+
type: "string",
|
|
8488
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8489
|
+
},
|
|
8490
|
+
target: {
|
|
8491
|
+
type: "string",
|
|
8492
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
8493
|
+
}
|
|
8494
|
+
},
|
|
8495
|
+
required: [],
|
|
8496
|
+
additionalProperties: false
|
|
8497
|
+
}
|
|
8498
|
+
},
|
|
8499
|
+
{
|
|
8500
|
+
name: "performance_analyze_insight",
|
|
8501
|
+
description: "CDP-native approximation of the DevTools insight analyzer: read a trace JSON file (bare array or {traceEvents:[...]}) at the given tracePath and return headline metrics (FCP/LCP/CLS/TBT, long tasks, layout shifts). Requires an explicit tracePath returned by performance_trace/performance_stop_trace; there is no implicit 'latest trace'. Numbers approximate DevTools (no main-thread attribution or frame-scoped LCP).",
|
|
8502
|
+
inputSchema: {
|
|
8503
|
+
type: "object",
|
|
8504
|
+
properties: {
|
|
8505
|
+
lease: {
|
|
8506
|
+
type: "string",
|
|
8507
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8508
|
+
},
|
|
8509
|
+
tracePath: {
|
|
8510
|
+
type: "string",
|
|
8511
|
+
description: "Filesystem path to a trace JSON file written by performance_trace or performance_stop_trace (bare array OR {traceEvents:[...]}). Required."
|
|
8512
|
+
}
|
|
8513
|
+
},
|
|
8514
|
+
required: [],
|
|
8515
|
+
additionalProperties: false
|
|
8516
|
+
}
|
|
8517
|
+
},
|
|
8518
|
+
{
|
|
8519
|
+
name: "performance_trace",
|
|
8520
|
+
description: "PRIMARY one-shot trace: start tracing, optionally reload or navigate the page, wait durationMs (default 3000), end the trace, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics,target}. Holds one connection open for the whole window, so it is immune to the cross-process limitation of start/stop, and it is the recommended entry point.",
|
|
8521
|
+
inputSchema: {
|
|
8522
|
+
type: "object",
|
|
8523
|
+
properties: {
|
|
8524
|
+
lease: {
|
|
8525
|
+
type: "string",
|
|
8526
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8527
|
+
},
|
|
8528
|
+
target: {
|
|
8529
|
+
type: "string",
|
|
8530
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
8531
|
+
},
|
|
8532
|
+
durationMs: {
|
|
8533
|
+
type: "number",
|
|
8534
|
+
description: "How long to record after the optional reload/navigate, in milliseconds. Default 3000."
|
|
8535
|
+
},
|
|
8536
|
+
reload: {
|
|
8537
|
+
type: "boolean",
|
|
8538
|
+
description: "Reload the page after starting the trace to capture full navigation timing."
|
|
8539
|
+
},
|
|
8540
|
+
navigateTo: {
|
|
8541
|
+
type: "string",
|
|
8542
|
+
description: "Navigate to this URL after starting the trace (alternative to reload)."
|
|
8543
|
+
},
|
|
8544
|
+
categories: {
|
|
8545
|
+
type: "array",
|
|
8546
|
+
items: {
|
|
8547
|
+
type: "string"
|
|
8548
|
+
},
|
|
8549
|
+
description: "Trace categories to include. Defaults to the timeline + user-timing + loading + disabled-by-default timeline tracks that carry LCP/LayoutShift/RunTask."
|
|
8550
|
+
}
|
|
8551
|
+
},
|
|
8552
|
+
required: [],
|
|
8553
|
+
additionalProperties: false
|
|
8554
|
+
}
|
|
8555
|
+
},
|
|
8556
|
+
{
|
|
8557
|
+
name: "start_screen_recording",
|
|
8558
|
+
description: "Start recording the target tab to video over raw CDP (Page.startScreencast), holding one persistent connection open and spooling every Page.screencastFrame to disk. Chrome emits a frame ON REPAINT, not on a clock, so the stream is variable-rate: each frame is timestamped into a ledger and stop_screen_recording turns those timestamps into per-frame display durations, which is what makes a still page hold one long frame instead of the video racing or freezing. Every frame is acked immediately (an unacked frame stalls the stream). ffmpeg is probed here, not at stop, so a missing encoder fails before a recording is captured and thrown away. Must be paired with stop_screen_recording WITHIN THE SAME PROCESS (the frames are events on this connection and cannot be re-attached from another process, so the pair works under the MCP server, not the one-process-per-call CLI). Throws if a recording is already in progress for the target; recordings on DIFFERENT targets run concurrently. Chrome only: WebDriver BiDi has no screencast primitive, so both tools are absent under --browser firefox.",
|
|
8559
|
+
inputSchema: {
|
|
8560
|
+
type: "object",
|
|
8561
|
+
properties: {
|
|
8562
|
+
lease: {
|
|
8563
|
+
type: "string",
|
|
8564
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8565
|
+
},
|
|
8566
|
+
target: {
|
|
8567
|
+
type: "string",
|
|
8568
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
8569
|
+
},
|
|
8570
|
+
format: {
|
|
8571
|
+
type: "string",
|
|
8572
|
+
enum: [
|
|
8573
|
+
"jpeg",
|
|
8574
|
+
"png"
|
|
8575
|
+
],
|
|
8576
|
+
description: "Frame image format. Defaults to jpeg (far smaller per repaint); png is lossless. quality applies to jpeg only."
|
|
8577
|
+
},
|
|
8578
|
+
quality: {
|
|
8579
|
+
type: "number",
|
|
8580
|
+
description: "JPEG frame quality 0-100. Ignored for png. Omitted by default, which leaves Chrome's own default in place."
|
|
8581
|
+
},
|
|
8582
|
+
maxWidth: {
|
|
8583
|
+
type: "number",
|
|
8584
|
+
description: "Cap the streamed frame width in pixels. Also the way to pin frame size if the viewport might change mid-recording."
|
|
8585
|
+
},
|
|
8586
|
+
maxHeight: {
|
|
8587
|
+
type: "number",
|
|
8588
|
+
description: "Cap the streamed frame height in pixels."
|
|
8589
|
+
},
|
|
8590
|
+
everyNthFrame: {
|
|
8591
|
+
type: "number",
|
|
8592
|
+
description: "Capture only every Nth repaint (1 = every frame). Reduces spool size on a busy page; per-frame durations keep playback at wallclock speed either way."
|
|
8593
|
+
},
|
|
8594
|
+
bringToFront: {
|
|
8595
|
+
type: "boolean",
|
|
8596
|
+
description: "Activate the tab (Page.bringToFront) before recording. Default false. A fully backgrounded or occluded tab may never repaint, and a recording of one captures 0 frames."
|
|
8597
|
+
}
|
|
8598
|
+
},
|
|
8599
|
+
required: [],
|
|
8600
|
+
additionalProperties: false
|
|
8601
|
+
}
|
|
8602
|
+
},
|
|
8603
|
+
{
|
|
8604
|
+
name: "stop_screen_recording",
|
|
8605
|
+
description: "Stop the recording started by start_screen_recording, assemble the spooled frames into an MP4 with ffmpeg using PER-FRAME durations from the ledger (frame N is held until frame N+1 painted; the last frame is held until this call), and return {path,bytes,durationMs,frameCount,encodedFrames,codec,encoder,width,height,droppedFrames,target}. Encoder ladder, probed once at start: hevc_videotoolbox then h264_videotoolbox then libx265 then libx264, with -tag:v hvc1 on HEVC so QuickTime plays it, +faststart, yuv420p, and even dimensions forced. With one recording in flight 'target' may be omitted; with several it is required, because guessing which to stop can lose another agent's recording. Throws if nothing is recording in this process, and throws rather than writing a silent empty video when 0 frames arrived. On an ffmpeg failure the spooled frames are KEPT and the spool path plus the exact command are named in the error. Encoded video tops out at 25 fps: ffmpeg's concat demuxer represents image timestamps on a 1/25s grid, so frames captured less than 40ms apart are coalesced deliberately (frameCount reports what was captured, encodedFrames what reached the video) instead of being dropped silently by ffmpeg. durationMs is the sum of the encoded durations; the file itself runs one 40ms step longer, because the concat demuxer needs the final frame repeated for its hold to count.",
|
|
8606
|
+
inputSchema: {
|
|
8607
|
+
type: "object",
|
|
8608
|
+
properties: {
|
|
8609
|
+
lease: {
|
|
8610
|
+
type: "string",
|
|
8611
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8612
|
+
},
|
|
8613
|
+
target: {
|
|
8614
|
+
type: "string",
|
|
8615
|
+
description: "Which recording to stop: 'active' (default, valid only when exactly one recording is in flight) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
8616
|
+
},
|
|
8617
|
+
savePath: {
|
|
8618
|
+
type: "string",
|
|
8619
|
+
description: "Override the output file path. Default: /tmp/cdp-toolkit/screen-recording-<targetIdShort>-<stamp>.mp4."
|
|
8620
|
+
}
|
|
8621
|
+
},
|
|
8622
|
+
required: [],
|
|
8623
|
+
additionalProperties: false
|
|
8624
|
+
}
|
|
8625
|
+
},
|
|
8626
|
+
{
|
|
8627
|
+
name: "take_heapsnapshot",
|
|
8628
|
+
description: "Capture a V8 heap snapshot of the selected page target over raw CDP (HeapProfiler.takeHeapSnapshot, accumulating addHeapSnapshotChunk events) and write it as a .heapsnapshot JSON file loadable by the DevTools Memory panel; returns {path,bytes,chunks,target} only and does not parse/summarize the snapshot. Writes under /tmp/cdp-toolkit (CDP_ARTIFACT_DIR) unless savePath is given.",
|
|
8629
|
+
inputSchema: {
|
|
8630
|
+
type: "object",
|
|
8631
|
+
properties: {
|
|
8632
|
+
lease: {
|
|
8633
|
+
type: "string",
|
|
8634
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8635
|
+
},
|
|
8636
|
+
target: {
|
|
8637
|
+
type: "string",
|
|
8638
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
8639
|
+
},
|
|
8640
|
+
savePath: {
|
|
8641
|
+
type: "string",
|
|
8642
|
+
description: "Override the output path. An absolute path (starting with /) is used as-is; a relative path is resolved under the artifact dir (/tmp/cdp-toolkit). Defaults to an auto-named take_heapsnapshot-<targetId>-<timestamp>.heapsnapshot file."
|
|
8643
|
+
}
|
|
8644
|
+
},
|
|
8645
|
+
required: [],
|
|
8646
|
+
additionalProperties: false
|
|
8647
|
+
}
|
|
8648
|
+
},
|
|
8649
|
+
{
|
|
8650
|
+
name: "lighthouse_audit",
|
|
8651
|
+
description: "Run a Lighthouse audit against a URL by shelling out to `npx --yes lighthouse` (the toolkit's sole non-CDP tool); Lighthouse attaches to the already-running Chrome on the remote-debugging port and audits its own about:blank tab rather than any live user tab. Writes a JSON report under /tmp/cdp-toolkit and returns {path, bytes} plus per-category scores (0..1 or null).",
|
|
8652
|
+
inputSchema: {
|
|
8653
|
+
type: "object",
|
|
8654
|
+
properties: {
|
|
8655
|
+
lease: {
|
|
8656
|
+
type: "string",
|
|
8657
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8658
|
+
},
|
|
8659
|
+
url: {
|
|
8660
|
+
type: "string",
|
|
8661
|
+
description: "The URL to audit. Required, and never points at a user tab implicitly."
|
|
8662
|
+
},
|
|
8663
|
+
categories: {
|
|
8664
|
+
type: "array",
|
|
8665
|
+
items: {
|
|
8666
|
+
type: "string"
|
|
8667
|
+
},
|
|
8668
|
+
description: 'Lighthouse categories to run (passed as --only-categories). Defaults to the full set, e.g. ["performance"] or ["performance","accessibility","seo"].'
|
|
8669
|
+
},
|
|
8670
|
+
savePath: {
|
|
8671
|
+
type: "string",
|
|
8672
|
+
description: "Override the report output path; defaults to a timestamped file under /tmp/cdp-toolkit."
|
|
8673
|
+
},
|
|
8674
|
+
formFactor: {
|
|
8675
|
+
type: "string",
|
|
8676
|
+
enum: [
|
|
8677
|
+
"desktop",
|
|
8678
|
+
"mobile"
|
|
8679
|
+
],
|
|
8680
|
+
description: 'Form factor: "desktop" (default, uses --preset=desktop to avoid heavy mobile throttling) or "mobile".'
|
|
8681
|
+
},
|
|
8682
|
+
timeoutMs: {
|
|
8683
|
+
type: "number",
|
|
8684
|
+
description: "Overall budget for the lighthouse process in milliseconds. Default 120000."
|
|
8685
|
+
}
|
|
8686
|
+
},
|
|
8687
|
+
required: [
|
|
8688
|
+
"url"
|
|
8689
|
+
],
|
|
8690
|
+
additionalProperties: false
|
|
8691
|
+
}
|
|
8692
|
+
},
|
|
8693
|
+
{
|
|
8694
|
+
name: "mock_request",
|
|
8695
|
+
description: "Register a mock rule on a target's persistent fake-backend session (CDP Fetch domain): intercept requests whose URL matches urlPattern and fulfill them with a canned response, fail (abort) them, or continue them, optionally with fault injection (delayMs/failRate). The session survives reloads and navigations and lives until clear_mocks. Call repeatedly to mock several endpoints on the same target. Pass reload:true to apply immediately. Persistent across calls only via the MCP server (not the one-shot CLI).",
|
|
8696
|
+
inputSchema: {
|
|
8697
|
+
type: "object",
|
|
8698
|
+
properties: {
|
|
8699
|
+
lease: {
|
|
8700
|
+
type: "string",
|
|
8701
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8702
|
+
},
|
|
8703
|
+
target: {
|
|
8704
|
+
type: "string",
|
|
8705
|
+
description: "Target selector: active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>. Defaults to the active page."
|
|
8706
|
+
},
|
|
8707
|
+
urlPattern: {
|
|
8708
|
+
type: "string",
|
|
8709
|
+
description: `CDP Fetch urlPattern glob: '*' matches any run of characters, '?' exactly one, '\\' escapes. Only matching request URLs are paused. e.g. "*/api/users*".`
|
|
8710
|
+
},
|
|
8711
|
+
action: {
|
|
8712
|
+
type: "string",
|
|
8713
|
+
enum: [
|
|
8714
|
+
"fulfill",
|
|
8715
|
+
"fail",
|
|
8716
|
+
"continue"
|
|
8717
|
+
],
|
|
8718
|
+
description: 'What to do with a matched request. Default "fulfill".'
|
|
8719
|
+
},
|
|
8720
|
+
status: {
|
|
8721
|
+
type: "number",
|
|
8722
|
+
description: "fulfill: HTTP status code for the canned response. Default 200."
|
|
8723
|
+
},
|
|
8724
|
+
body: {
|
|
8725
|
+
type: "string",
|
|
8726
|
+
description: "fulfill: response body string (base64-encoded for CDP internally)."
|
|
8727
|
+
},
|
|
8728
|
+
contentType: {
|
|
8729
|
+
type: "string",
|
|
8730
|
+
description: 'fulfill: Content-Type header. Default "application/json".'
|
|
8731
|
+
},
|
|
8732
|
+
headers: {
|
|
8733
|
+
type: "object",
|
|
8734
|
+
description: 'fulfill: extra response headers (name->value); override Content-Type case-insensitively. Add "Access-Control-Allow-Origin" for cross-origin fetches.'
|
|
8735
|
+
},
|
|
8736
|
+
errorReason: {
|
|
8737
|
+
type: "string",
|
|
8738
|
+
description: 'fail: CDP Network.ErrorReason, e.g. "Failed" (default), "BlockedByClient", "ConnectionRefused", "TimedOut".'
|
|
8739
|
+
},
|
|
8740
|
+
method: {
|
|
8741
|
+
type: "string",
|
|
8742
|
+
description: 'Only mock requests with this HTTP method (e.g. "POST"); other methods pass through.'
|
|
8743
|
+
},
|
|
8744
|
+
delayMs: {
|
|
8745
|
+
type: "number",
|
|
8746
|
+
description: "Fault injection: artificial latency in ms before responding."
|
|
8747
|
+
},
|
|
8748
|
+
failRate: {
|
|
8749
|
+
type: "number",
|
|
8750
|
+
description: "Fault injection: probability 0..1 of failing a matched request regardless of action (resilience testing)."
|
|
8751
|
+
},
|
|
8752
|
+
reload: {
|
|
8753
|
+
type: "boolean",
|
|
8754
|
+
description: "Reload the target after arming so the mock immediately catches traffic."
|
|
8755
|
+
}
|
|
8756
|
+
},
|
|
8757
|
+
required: [
|
|
8758
|
+
"urlPattern"
|
|
8759
|
+
],
|
|
8760
|
+
additionalProperties: false
|
|
8761
|
+
}
|
|
8762
|
+
},
|
|
8763
|
+
{
|
|
8764
|
+
name: "list_mocks",
|
|
8765
|
+
description: "List active fake-backend sessions (one per target) with their rules and hit counts. Prunes sessions whose tab has closed. Use to see what is currently being mocked.",
|
|
8766
|
+
inputSchema: {
|
|
8767
|
+
type: "object",
|
|
8768
|
+
properties: {
|
|
8769
|
+
lease: {
|
|
8770
|
+
type: "string",
|
|
8771
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8772
|
+
},
|
|
8773
|
+
target: {
|
|
8774
|
+
type: "string",
|
|
8775
|
+
description: "Unused filter placeholder; list_mocks returns all active sessions."
|
|
8776
|
+
}
|
|
8777
|
+
},
|
|
8778
|
+
required: [],
|
|
8779
|
+
additionalProperties: false
|
|
8780
|
+
}
|
|
8781
|
+
},
|
|
8782
|
+
{
|
|
8783
|
+
name: "clear_mocks",
|
|
8784
|
+
description: "Tear down fake-backend sessions (Fetch.disable + close the connection). Clears the resolved target's session by default, or every active session with all:true.",
|
|
8785
|
+
inputSchema: {
|
|
8786
|
+
type: "object",
|
|
8787
|
+
properties: {
|
|
8788
|
+
lease: {
|
|
8789
|
+
type: "string",
|
|
8790
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
8791
|
+
},
|
|
8792
|
+
target: {
|
|
8793
|
+
type: "string",
|
|
8794
|
+
description: "Target selector whose mock session to clear. Defaults to the active page. Ignored when all:true."
|
|
8795
|
+
},
|
|
8796
|
+
all: {
|
|
8797
|
+
type: "boolean",
|
|
8798
|
+
description: "Clear every active mock session instead of just the resolved target's."
|
|
8799
|
+
}
|
|
8800
|
+
},
|
|
8801
|
+
required: [],
|
|
8802
|
+
additionalProperties: false
|
|
8803
|
+
}
|
|
8804
|
+
},
|
|
8805
|
+
{
|
|
8806
|
+
name: "claim_page",
|
|
8807
|
+
description: "Take exclusive ownership of a browser tab and return an opaque lease token. Two modes. FRESH TAB: with no target and no targetId, opens a new tab (optionally at url) and claims it, so 'give me my own tab' is one call. TAKEOVER: with target, claims a tab that is already open — this is how you work in a tab the human already has open when they ask you to, instead of opening your own — resolving any target selector against the live page list and never creating anything. The answer's 'opened' flag says which happened (true only when this call created the tab), and that same creation record is what release_page consults, so a tab you took over is left open when you release it. Any later tool call against a leased tab must carry the matching token in its 'lease' argument or it is refused, naming the holder. The lease is reclaimable once its owning process dies, its ttlMs elapses without use, or its tab is closed. Refused from the CLI: a CLI invocation is one process per call, so its lease would be reclaimable immediately. IS SOMEBODY ELSE IN THIS TAB: the answer also carries 'humanActiveMs', milliseconds since input that this server did not dispatch — i.e. since a person last clicked, typed, or scrolled here. null means NO DATA (the tab carries no activity beacon, which is normal the first time anyone claims a person's tab, or every input on it was this server's own); it never means 'no human'. The field is absent entirely on a backend that cannot answer. On a takeover of a tab a human used within the last 30 seconds the answer additionally carries 'contention', a warning string. THE CLAIM IS NEVER REFUSED FOR THIS: taking over a person's tab is what takeover mode is for, so by the time you read the warning you already hold the lease and the tab is yours to drive. It tells you that driving it now means fighting a live person for the keyboard and mouse, so you can open your own tab instead or ask first. Known limits: input inside a cross-origin iframe is invisible to it, and a second MCP server's clicks read as human to this one.",
|
|
8808
|
+
inputSchema: {
|
|
8809
|
+
type: "object",
|
|
8810
|
+
properties: {
|
|
8811
|
+
target: {
|
|
8812
|
+
type: "string",
|
|
8813
|
+
description: "Take over an already-open tab, e.g. one the human has open, when asked to: pass any target selector — active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>. Resolved against the live page list only, so it NEVER opens a tab: a selector that matches nothing is an error, not a new blank tab. The tab is left open on release because the toolkit did not create it. Refused if another live process holds it, including a lease the gate acquired for that process automatically under CDP_REQUIRE_LEASE: this takes over unleased (human) tabs and never steals one from a live agent. Mutually exclusive with targetId."
|
|
8814
|
+
},
|
|
8815
|
+
targetId: {
|
|
8816
|
+
type: "string",
|
|
8817
|
+
description: "Claim this already-open page target instead of opening a new tab. Must be an exact target id, not a selector. Kept for back-compat: prefer 'target', which accepts an exact id too plus every other selector form. Mutually exclusive with target."
|
|
8818
|
+
},
|
|
8819
|
+
url: {
|
|
8820
|
+
type: "string",
|
|
8821
|
+
description: "When opening a fresh tab, navigate it here. Ignored when target or targetId is given, since both of those claim a tab that is already open."
|
|
8822
|
+
},
|
|
8823
|
+
label: {
|
|
8824
|
+
type: "string",
|
|
8825
|
+
description: "Agent label recorded on the lease and surfaced in conflict errors and list_leases. Defaults to pid-<pid>."
|
|
8826
|
+
},
|
|
8827
|
+
ttlMs: {
|
|
8828
|
+
type: "number",
|
|
8829
|
+
description: "How long the lease survives without use before it is reclaimable. Defaults to CDP_LEASE_TTL_MS, else 900000 (15 minutes). Every checked call refreshes it, so an active agent never expires."
|
|
8830
|
+
}
|
|
8831
|
+
},
|
|
8832
|
+
required: [],
|
|
8833
|
+
additionalProperties: false
|
|
8834
|
+
}
|
|
8835
|
+
},
|
|
8836
|
+
{
|
|
8837
|
+
name: "release_page",
|
|
8838
|
+
description: "Give a lease back, and close the tab if this toolkit opened it. Takes exactly one of 'lease' (the token claim_page returned) or 'target' (a selector for a tab this process holds, which is how you release a lease the gate acquired for you automatically, since that path never hands you a token). A tab with a creation record from this toolkit is closed; a tab that was already open and merely claimed is released and left alone. Override either way with 'close'. Idempotent: an already-released, reclaimed, or expired lease reports released:false, and a release that did not happen never closes anything, because by then the tab may belong to another agent. Answers {released, closed, targetId}.",
|
|
8839
|
+
inputSchema: {
|
|
8840
|
+
type: "object",
|
|
8841
|
+
properties: {
|
|
8842
|
+
lease: {
|
|
8843
|
+
type: "string",
|
|
8844
|
+
description: "The opaque token claim_page (or new_page with claim:true) returned. Mutually exclusive with 'target'. Under CDP_REQUIRE_LEASE a lease the gate acquired for you automatically never produced a token in the first place, so give 'target' instead."
|
|
8845
|
+
},
|
|
8846
|
+
target: {
|
|
8847
|
+
type: "string",
|
|
8848
|
+
description: "Target selector for a tab this process holds: active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>. Mutually exclusive with 'lease'. Refused if another process holds the tab."
|
|
8849
|
+
},
|
|
8850
|
+
close: {
|
|
8851
|
+
type: "boolean",
|
|
8852
|
+
description: "Force the close decision instead of letting the creation ledger decide. true closes even a tab this toolkit did not open; false keeps an agent-created tab open. Omit for the default, which closes only tabs this toolkit created."
|
|
8853
|
+
}
|
|
8854
|
+
},
|
|
8855
|
+
required: [],
|
|
8856
|
+
additionalProperties: false
|
|
8857
|
+
}
|
|
8858
|
+
},
|
|
8859
|
+
{
|
|
8860
|
+
name: "list_leases",
|
|
8861
|
+
description: "Enumerate every active lease for diagnosis: backend, target id, agent label, pid, createdAt, lastUsedAt, ttlMs, whether the owning process is still alive, and whether the lease is reclaimable and why. Requires no token. Never returns the lease nonce, so this cannot be used to forge a token. A row for a lease file that could not be read or parsed instead carries `unreadable` (the errno, or \"unparseable\"), with label, pid, and the timestamp fields as zero placeholders and stale always false, since stale is what marks a lease free to take and an unreadable one must never read that way. Under CDP_REQUIRE_LEASE this call reaps abandoned agent tabs exactly as list_pages does (see that tool's description for the grace period between a lease reading reclaimable and its tab actually being closed), reporting them in an additive 'reaped' array and omitting their rows from the listing. A row also carries 'humanActiveMs' — milliseconds since input this server did not dispatch, i.e. since a person last used that tab — WHERE THE TAB CAN ANSWER. The field is ABSENT rather than null when it cannot: no activity beacon on that tab, every input on it accounted for by this server, a row belonging to the other backend, a tab no longer open, or a page that would not evaluate. Absence therefore means 'no answer', never 'nobody is there'. Note that lastUsedAt measures TOOLKIT calls only, so a tab a person has been typing in for ten minutes looks perfectly idle by that field and is exactly the case humanActiveMs exists to expose. Two further fields are computed fresh on every call, never read off disk: 'idleMs' (now minus lastUsedAt) and 'expiresAt' (lastUsedAt plus ttlMs — the same value claim_page already returns under that name). Both are omitted on an 'unreadable' row, whose lastUsedAt and ttlMs are the zeroed placeholders described above.",
|
|
8862
|
+
inputSchema: {
|
|
8863
|
+
type: "object",
|
|
8864
|
+
properties: {},
|
|
8865
|
+
required: [],
|
|
8866
|
+
additionalProperties: false
|
|
8867
|
+
}
|
|
8868
|
+
}
|
|
8869
|
+
];
|
|
8870
|
+
|
|
6199
8871
|
// src/cli.ts
|
|
6200
8872
|
var USAGE = `cdp-toolkit: raw single-target CDP, 29-tool chrome-devtools-mcp parity, plus a Firefox backend over WebDriver BiDi.
|
|
6201
8873
|
|
|
6202
8874
|
Usage:
|
|
6203
|
-
bun run src/cli.ts <tool> [--browser chrome|firefox] [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
|
|
8875
|
+
bun run src/cli.ts <tool> [--browser chrome|firefox] [--connect <port|host:port|ws-url>] [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
|
|
6204
8876
|
bun run src/cli.ts --list [--browser chrome|firefox]
|
|
6205
8877
|
bun run src/cli.ts --capabilities [--browser chrome|firefox]
|
|
8878
|
+
bun run src/cli.ts --help | -h # this message
|
|
8879
|
+
bun run src/cli.ts <tool> --help | -h # that tool's arguments, sourced from its schema
|
|
6206
8880
|
|
|
6207
8881
|
Examples:
|
|
6208
8882
|
bun run src/cli.ts list_pages
|
|
@@ -6212,10 +8886,24 @@ Examples:
|
|
|
6212
8886
|
bun run src/cli.ts take_screenshot --target url:example --fullPage true
|
|
6213
8887
|
bun run src/cli.ts --browser firefox take_snapshot
|
|
6214
8888
|
bun run src/cli.ts --capabilities --browser firefox
|
|
8889
|
+
bun run src/cli.ts take_screenshot --help
|
|
8890
|
+
bun run src/cli.ts --connect 9223 take_snapshot # attach to your own logged-in Firefox (implies --browser firefox)
|
|
8891
|
+
|
|
8892
|
+
--help/-h is recognised ANYWHERE in argv, before any tool dispatch: it never touches the
|
|
8893
|
+
browser (no CDP connection, no lease, no file written) and is never passed through as a tool
|
|
8894
|
+
argument, even when it appears after a tool name.
|
|
6215
8895
|
|
|
6216
8896
|
Backend selection: --browser chrome|firefox, else the CDP_BROWSER env var, else "chrome" (default,
|
|
6217
|
-
zero behavior change for existing users). Firefox
|
|
6218
|
-
|
|
8897
|
+
zero behavior change for existing users). Firefox runs in one of two modes:
|
|
8898
|
+
- LAUNCH (default): a fresh throwaway-profile Firefox is spawned for this one invocation, used
|
|
8899
|
+
for exactly one tool call, then disposed (session ended, process killed) before the process
|
|
8900
|
+
exits. This is a login wall for anything that needs your real session.
|
|
8901
|
+
- ATTACH (--connect <port|host:port|ws-url>, or the CDP_FIREFOX_ENDPOINT env var): connects to a
|
|
8902
|
+
Firefox YOU already started with --remote-debugging-port, so tools see your real logged-in
|
|
8903
|
+
profile. This process never launches or kills that Firefox; dispose only ends the BiDi session.
|
|
8904
|
+
--connect implies --browser firefox and conflicts with an explicit --browser chrome. Firefox
|
|
8905
|
+
allows only one active BiDi session at a time, so a client that dies without disposing cleanly
|
|
8906
|
+
(e.g. SIGKILL) can leave the slot occupied until Firefox restarts.
|
|
6219
8907
|
|
|
6220
8908
|
Target selector grammar: active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>
|
|
6221
8909
|
plus worker:<substr> (Chrome only) for evaluate_script, list_network_requests, get_network_request
|
|
@@ -6238,7 +8926,7 @@ function coerce(raw) {
|
|
|
6238
8926
|
return raw;
|
|
6239
8927
|
}
|
|
6240
8928
|
function parseArgv(argv) {
|
|
6241
|
-
const out = { list: false, capabilities: false, args: {} };
|
|
8929
|
+
const out = { list: false, capabilities: false, help: false, args: {} };
|
|
6242
8930
|
let jsonObj;
|
|
6243
8931
|
const flagPairs = [];
|
|
6244
8932
|
for (let i = 0;i < argv.length; i++) {
|
|
@@ -6251,6 +8939,10 @@ function parseArgv(argv) {
|
|
|
6251
8939
|
out.capabilities = true;
|
|
6252
8940
|
continue;
|
|
6253
8941
|
}
|
|
8942
|
+
if (token === "--help" || token === "-h") {
|
|
8943
|
+
out.help = true;
|
|
8944
|
+
continue;
|
|
8945
|
+
}
|
|
6254
8946
|
if (token.startsWith("--")) {
|
|
6255
8947
|
const key = token.slice(2);
|
|
6256
8948
|
const next = argv[i + 1];
|
|
@@ -6285,10 +8977,61 @@ function parseArgv(argv) {
|
|
|
6285
8977
|
function isToolName(name) {
|
|
6286
8978
|
return Object.prototype.hasOwnProperty.call(TOOLS, name);
|
|
6287
8979
|
}
|
|
8980
|
+
function formatType(prop) {
|
|
8981
|
+
if (!prop)
|
|
8982
|
+
return "any";
|
|
8983
|
+
if (Array.isArray(prop.enum) && prop.enum.length > 0)
|
|
8984
|
+
return prop.enum.map(String).join("|");
|
|
8985
|
+
if (Array.isArray(prop.type))
|
|
8986
|
+
return prop.type.join("|");
|
|
8987
|
+
return prop.type ?? "any";
|
|
8988
|
+
}
|
|
8989
|
+
function printToolHelp(name) {
|
|
8990
|
+
const spec = MANIFEST.find((t) => t.name === name);
|
|
8991
|
+
if (!spec) {
|
|
8992
|
+
process.stdout.write(`${name}
|
|
8993
|
+
`);
|
|
8994
|
+
return;
|
|
8995
|
+
}
|
|
8996
|
+
const required = new Set(spec.inputSchema.required ?? []);
|
|
8997
|
+
const props = spec.inputSchema.properties ?? {};
|
|
8998
|
+
const keys = Object.keys(props);
|
|
8999
|
+
const lines = [spec.name, "", spec.description, ""];
|
|
9000
|
+
if (keys.length === 0) {
|
|
9001
|
+
lines.push("Arguments: none.");
|
|
9002
|
+
} else {
|
|
9003
|
+
lines.push("Arguments:");
|
|
9004
|
+
for (const key of keys) {
|
|
9005
|
+
const prop = props[key];
|
|
9006
|
+
const reqTag = required.has(key) ? "required" : "optional";
|
|
9007
|
+
lines.push(` --${key} <${formatType(prop)}> (${reqTag})`);
|
|
9008
|
+
if (prop?.description)
|
|
9009
|
+
lines.push(` ${prop.description}`);
|
|
9010
|
+
}
|
|
9011
|
+
}
|
|
9012
|
+
lines.push("", `Usage: cdp ${spec.name} [--target <sel>] [--json '<obj>'] [--<key> <value> ...]`);
|
|
9013
|
+
process.stdout.write(`${lines.join(`
|
|
9014
|
+
`)}
|
|
9015
|
+
`);
|
|
9016
|
+
}
|
|
6288
9017
|
async function main() {
|
|
6289
9018
|
const rawArgv = process.argv.slice(2);
|
|
6290
|
-
const browser =
|
|
6291
|
-
const parsed = parseArgv(stripBrowserFlag(rawArgv));
|
|
9019
|
+
const { kind: browser, endpoint } = resolveBackend(rawArgv);
|
|
9020
|
+
const parsed = parseArgv(stripConnectFlag(stripBrowserFlag(rawArgv)));
|
|
9021
|
+
if (parsed.help) {
|
|
9022
|
+
if (!parsed.tool) {
|
|
9023
|
+
process.stdout.write(`${USAGE}
|
|
9024
|
+
`);
|
|
9025
|
+
return 0;
|
|
9026
|
+
}
|
|
9027
|
+
if (!isToolName(parsed.tool)) {
|
|
9028
|
+
process.stderr.write(`${JSON.stringify({ error: `unknown tool '${parsed.tool}'. Run --list to see all ${TOOL_NAMES.length} tools.` })}
|
|
9029
|
+
`);
|
|
9030
|
+
return 1;
|
|
9031
|
+
}
|
|
9032
|
+
printToolHelp(parsed.tool);
|
|
9033
|
+
return 0;
|
|
9034
|
+
}
|
|
6292
9035
|
const availability = toolAvailability(browser);
|
|
6293
9036
|
const availableSet = new Set(availability.available);
|
|
6294
9037
|
if (parsed.capabilities) {
|
|
@@ -6349,7 +9092,7 @@ async function main() {
|
|
|
6349
9092
|
`);
|
|
6350
9093
|
return 1;
|
|
6351
9094
|
}
|
|
6352
|
-
const session2 = await startFirefoxSession();
|
|
9095
|
+
const session2 = await startFirefoxSession({ endpoint });
|
|
6353
9096
|
try {
|
|
6354
9097
|
const result = await neutralFn(session2.driver, parsed.args);
|
|
6355
9098
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|