cdp-toolkit 1.9.0 → 1.9.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +507 -0
- package/CONTRACT.md +62 -5
- package/README.md +74 -10
- package/dist/cli.js +2940 -138
- package/dist/index.js +994 -90
- package/dist/mcp.js +1221 -137
- package/package.json +2 -1
- package/src/backend.ts +156 -21
- package/src/bidi/client.ts +48 -10
- package/src/bidi/driver.ts +228 -33
- package/src/bidi/launch.ts +23 -13
- package/src/bidi-tools.ts +21 -0
- package/src/cdp/driver.ts +594 -9
- package/src/cli.ts +111 -12
- package/src/driver.ts +254 -3
- package/src/manifest.ts +42 -14
- package/src/mcp.ts +21 -12
- package/src/png.ts +809 -0
- package/src/shared-tools.ts +313 -11
- package/src/tools/console.ts +24 -7
- package/src/tools/network.ts +21 -7
- package/src/tools/recorder.ts +110 -10
- package/src/types.ts +8 -6
- package/src/workers.ts +69 -7
package/dist/cli.js
CHANGED
|
@@ -406,6 +406,12 @@ async function resolveLiveLabel(backend, label, liveIds) {
|
|
|
406
406
|
|
|
407
407
|
// src/workers.ts
|
|
408
408
|
var WORKER_SELECTOR_PREFIX = "worker:";
|
|
409
|
+
var WORKER_CAPABLE_TOOLS = [
|
|
410
|
+
"evaluate_script",
|
|
411
|
+
"list_network_requests",
|
|
412
|
+
"get_network_request",
|
|
413
|
+
"list_console_messages"
|
|
414
|
+
];
|
|
409
415
|
var WORKER_TARGET_TYPES = new Set(["service_worker", "shared_worker"]);
|
|
410
416
|
function isWorkerSelector(selector) {
|
|
411
417
|
return typeof selector === "string" && selector.startsWith(WORKER_SELECTOR_PREFIX);
|
|
@@ -424,8 +430,18 @@ function resolveWorkerTargets(targets, needle) {
|
|
|
424
430
|
};
|
|
425
431
|
}
|
|
426
432
|
var WORKER_EMPTY_NEEDLE_MESSAGE = "'worker:' needs a substring of the worker's url, e.g. worker:<extension-id> or worker:background.js";
|
|
427
|
-
var WORKER_SELECTOR_UNSUPPORTED_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, which this backend cannot address " + "(WebDriver BiDi has no extension service-worker target; Capability 'worker.targets' is Chrome-only). " + "Use --browser chrome to evaluate in an extension's background worker.";
|
|
428
|
-
var WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless).
|
|
433
|
+
var WORKER_SELECTOR_UNSUPPORTED_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, which this backend cannot address " + "(WebDriver BiDi has no extension service-worker target; Capability 'worker.targets' is Chrome-only). " + "Use --browser chrome to evaluate in, or record console/network from, an extension's background worker.";
|
|
434
|
+
var WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless). The tools that accept a worker: target are " + `${WORKER_CAPABLE_TOOLS.join(", ")}.`;
|
|
435
|
+
function workerWakeMisuseMessage(kind) {
|
|
436
|
+
if (kind === "not-a-worker-selector") {
|
|
437
|
+
return "'wake' only applies to a target of the form 'worker:<substring>'. A page is never asleep, and a bare " + "worker target id cannot be woken either: Chrome destroys the target when an MV3 worker is evicted and " + "mints a NEW id when it restarts, so only the url-substring form can name a worker that is not running.";
|
|
438
|
+
}
|
|
439
|
+
return "'wake' only applies when a capture is being started (reload:true, or includeBody with url). Reading an " + "existing buffer cannot use a woken worker: a restarted MV3 worker gets a NEW target id, and the buffer is " + "keyed by id, so the wake would hand back an empty capture. Re-run with reload:true to record a fresh one.";
|
|
440
|
+
}
|
|
441
|
+
function workerBufferReadMissMessage(needle, liveWorkers) {
|
|
442
|
+
const running = liveWorkers.length ? ` (running workers: ${urlList(liveWorkers)})` : " (no workers are running)";
|
|
443
|
+
return `no running worker whose url contains '${needle}'${running}. ` + "An MV3 extension service worker is idle-evicted after seconds of inactivity and then appears in NO target " + "listing, so this is usually a sleeping worker rather than a wrong substring. This call only READS a buffer " + "recorded earlier, and that buffer is keyed by the worker's target id, which Chrome re-mints on restart — so " + "re-run with reload:true, which starts the worker and records a fresh capture window.";
|
|
444
|
+
}
|
|
429
445
|
function urlList(workers) {
|
|
430
446
|
return workers.map((w) => w.url).join(", ");
|
|
431
447
|
}
|
|
@@ -1105,6 +1121,8 @@ async function clearMocks(args = {}) {
|
|
|
1105
1121
|
}
|
|
1106
1122
|
|
|
1107
1123
|
// src/driver.ts
|
|
1124
|
+
import { mkdir as mkdir3, readFile as readFile3, unlink as unlink3, writeFile as writeFile3 } from "node:fs/promises";
|
|
1125
|
+
import { join as join3 } from "node:path";
|
|
1108
1126
|
var UID_STAMP_ATTR = "data-cdp-uid";
|
|
1109
1127
|
var LEGACY_NUMERIC_UID = /^[0-9]+$/;
|
|
1110
1128
|
var REQUIRED_CAPABILITIES = {
|
|
@@ -1134,6 +1152,41 @@ function interpolatePoints(from, to, steps) {
|
|
|
1134
1152
|
out.push({ x: to.x, y: to.y });
|
|
1135
1153
|
return out;
|
|
1136
1154
|
}
|
|
1155
|
+
function isTiledCapture(delivery) {
|
|
1156
|
+
return delivery.tiled === true;
|
|
1157
|
+
}
|
|
1158
|
+
function viewportRecordFile(scheme, targetId) {
|
|
1159
|
+
return join3(leaseDir(), `viewport-${scheme}-${targetId.replace(/[^A-Za-z0-9._-]/g, "_")}.json`);
|
|
1160
|
+
}
|
|
1161
|
+
async function readViewportRecord(scheme, targetId) {
|
|
1162
|
+
try {
|
|
1163
|
+
const raw = JSON.parse(await readFile3(viewportRecordFile(scheme, targetId), "utf8"));
|
|
1164
|
+
if (typeof raw?.width !== "number" || typeof raw?.height !== "number")
|
|
1165
|
+
return;
|
|
1166
|
+
return {
|
|
1167
|
+
width: raw.width,
|
|
1168
|
+
height: raw.height,
|
|
1169
|
+
...typeof raw.deviceScaleFactor === "number" ? { deviceScaleFactor: raw.deviceScaleFactor } : {},
|
|
1170
|
+
...typeof raw.mobile === "boolean" ? { mobile: raw.mobile } : {}
|
|
1171
|
+
};
|
|
1172
|
+
} catch {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
}
|
|
1176
|
+
async function writeViewportRecord(scheme, targetId, rec) {
|
|
1177
|
+
try {
|
|
1178
|
+
await mkdir3(leaseDir(), { recursive: true });
|
|
1179
|
+
await writeFile3(viewportRecordFile(scheme, targetId), JSON.stringify({ scheme, targetId, ...rec }), "utf8");
|
|
1180
|
+
} catch {}
|
|
1181
|
+
}
|
|
1182
|
+
function renderRestoreFailureNote(size, why) {
|
|
1183
|
+
return `— AND the ${size.width}x${size.height} render-size override could NOT be restored (${why}), so this page is STILL ` + `emulated at ${size.width}x${size.height}: put it back with emulate{width,height} or emulate{clearOverrides:true}.`;
|
|
1184
|
+
}
|
|
1185
|
+
async function clearViewportRecord(scheme, targetId) {
|
|
1186
|
+
try {
|
|
1187
|
+
await unlink3(viewportRecordFile(scheme, targetId));
|
|
1188
|
+
} catch {}
|
|
1189
|
+
}
|
|
1137
1190
|
|
|
1138
1191
|
// src/cdp/driver.ts
|
|
1139
1192
|
function driverError(code, message, data) {
|
|
@@ -1365,14 +1418,73 @@ async function backendNodeIdOf(conn, loc) {
|
|
|
1365
1418
|
}
|
|
1366
1419
|
return searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
|
|
1367
1420
|
}
|
|
1368
|
-
async function
|
|
1369
|
-
|
|
1421
|
+
async function layoutMetrics(conn) {
|
|
1422
|
+
return conn.send("Page.getLayoutMetrics");
|
|
1423
|
+
}
|
|
1424
|
+
function contentClip(m) {
|
|
1370
1425
|
const size = m.cssContentSize ?? m.contentSize;
|
|
1371
1426
|
if (!size)
|
|
1372
1427
|
throw driverError("page-error", "Page.getLayoutMetrics returned no content size");
|
|
1373
1428
|
return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height) };
|
|
1374
1429
|
}
|
|
1375
|
-
|
|
1430
|
+
function devicePixelsPerCssPx(m) {
|
|
1431
|
+
const css = m.cssVisualViewport?.clientWidth ?? m.cssLayoutViewport?.clientWidth;
|
|
1432
|
+
const dev = m.visualViewport?.clientWidth ?? m.layoutViewport?.clientWidth;
|
|
1433
|
+
if (!css || !dev)
|
|
1434
|
+
return 1;
|
|
1435
|
+
const ratio = dev / css;
|
|
1436
|
+
return Number.isFinite(ratio) && ratio > 0 ? ratio : 1;
|
|
1437
|
+
}
|
|
1438
|
+
function scrollOffsetCssPx(m, devicePx) {
|
|
1439
|
+
const css = m.cssVisualViewport ?? m.cssLayoutViewport;
|
|
1440
|
+
if (css)
|
|
1441
|
+
return { x: css.pageX ?? 0, y: css.pageY ?? 0 };
|
|
1442
|
+
const legacy = m.layoutViewport;
|
|
1443
|
+
if (legacy)
|
|
1444
|
+
return { x: (legacy.pageX ?? 0) / devicePx, y: (legacy.pageY ?? 0) / devicePx };
|
|
1445
|
+
return { x: 0, y: 0 };
|
|
1446
|
+
}
|
|
1447
|
+
var MAX_ENCODED_SIDE_PX = 16384;
|
|
1448
|
+
function assertClipEncodable(clip, scale, devicePx, renderSize) {
|
|
1449
|
+
const outWidth = Math.ceil(clip.width * scale * devicePx);
|
|
1450
|
+
const outHeight = Math.ceil(clip.height * scale * devicePx);
|
|
1451
|
+
if (outWidth <= MAX_ENCODED_SIDE_PX && outHeight <= MAX_ENCODED_SIDE_PX)
|
|
1452
|
+
return;
|
|
1453
|
+
const at = renderSize ? ` at render size ${renderSize.width}x${renderSize.height}` : "";
|
|
1454
|
+
const projection = `(clip ${clip.width}x${clip.height} css px${at} x scale ${scale} x device pixel ratio ${devicePx}), ` + `past Chrome's ${MAX_ENCODED_SIDE_PX} px per-side limit; `;
|
|
1455
|
+
if (scale !== 1) {
|
|
1456
|
+
const fits = Math.floor(MAX_ENCODED_SIDE_PX / (Math.max(clip.width, clip.height) * devicePx) * 100) / 100;
|
|
1457
|
+
throw driverError("page-error", `screenshot scale ${scale} would encode ${outWidth}x${outHeight} px ${projection}` + (fits >= 0.01 ? `the largest scale that fits this clip is ${fits}.` : `this clip is already past the limit at scale 1 — capture a smaller region.`));
|
|
1458
|
+
}
|
|
1459
|
+
const maxCssSide = Math.floor(MAX_ENCODED_SIDE_PX / devicePx);
|
|
1460
|
+
throw driverError("page-error", `this capture would encode ${outWidth}x${outHeight} px ${projection}` + `at device pixel ratio ${devicePx} the clip may be at most ${maxCssSide} css px per side` + (renderSize ? ` — a narrow renderWidth reflows the document TALLER, so widen it, shorten the page, or drop fullPage.` : ` — capture a smaller region or drop fullPage.`));
|
|
1461
|
+
}
|
|
1462
|
+
function projectedSide(cssLength, scale, devicePx) {
|
|
1463
|
+
return Math.ceil(cssLength * scale * devicePx);
|
|
1464
|
+
}
|
|
1465
|
+
function planTiledCapture(clip, scale, devicePx, tile) {
|
|
1466
|
+
const untiled = { tiled: false, bandCount: 1, bandHeightCss: clip.height };
|
|
1467
|
+
if (tile === false)
|
|
1468
|
+
return untiled;
|
|
1469
|
+
const fits = projectedSide(clip.width, scale, devicePx) <= MAX_ENCODED_SIDE_PX && projectedSide(clip.height, scale, devicePx) <= MAX_ENCODED_SIDE_PX;
|
|
1470
|
+
if (fits && tile !== true)
|
|
1471
|
+
return untiled;
|
|
1472
|
+
const outWidth = projectedSide(clip.width, scale, devicePx);
|
|
1473
|
+
if (outWidth > MAX_ENCODED_SIDE_PX) {
|
|
1474
|
+
const maxCssWidth = Math.floor(MAX_ENCODED_SIDE_PX / (scale * devicePx));
|
|
1475
|
+
throw driverError("page-error", `this capture is ${outWidth} px WIDE (clip ${clip.width} css px x scale ${scale} x device pixel ratio ${devicePx}), ` + `past Chrome's ${MAX_ENCODED_SIDE_PX} px per-side limit, and width cannot be tiled: bands are stitched ` + `vertically only. Lower scale (at most ${Math.floor(MAX_ENCODED_SIDE_PX / (clip.width * devicePx) * 100) / 100}), ` + `or set renderWidth at most ${maxCssWidth} css px, or clip to a narrower element.`);
|
|
1476
|
+
}
|
|
1477
|
+
let bandHeightCss = Math.floor(MAX_ENCODED_SIDE_PX / (scale * devicePx));
|
|
1478
|
+
while (bandHeightCss > 1 && projectedSide(bandHeightCss, scale, devicePx) > MAX_ENCODED_SIDE_PX)
|
|
1479
|
+
bandHeightCss -= 1;
|
|
1480
|
+
if (bandHeightCss < 1) {
|
|
1481
|
+
throw driverError("page-error", `scale ${scale} at device pixel ratio ${devicePx} projects a single css pixel to ${scale * devicePx} device px, ` + `past the ${MAX_ENCODED_SIDE_PX} px limit — no band height can fit. Lower scale.`);
|
|
1482
|
+
}
|
|
1483
|
+
if (tile === true && clip.height <= bandHeightCss)
|
|
1484
|
+
bandHeightCss = Math.max(1, Math.ceil(clip.height / 2));
|
|
1485
|
+
return { tiled: true, bandCount: Math.max(1, Math.ceil(clip.height / bandHeightCss)), bandHeightCss };
|
|
1486
|
+
}
|
|
1487
|
+
async function elementBox(conn, loc) {
|
|
1376
1488
|
const backendNodeId = await backendNodeIdOf(conn, loc);
|
|
1377
1489
|
await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => {
|
|
1378
1490
|
return;
|
|
@@ -1389,6 +1501,10 @@ async function elementClip(conn, loc) {
|
|
|
1389
1501
|
throw driverError("page-error", "resolved element has zero area");
|
|
1390
1502
|
return { x: minX, y: minY, width, height };
|
|
1391
1503
|
}
|
|
1504
|
+
function documentClip(box, m, devicePx) {
|
|
1505
|
+
const scroll = scrollOffsetCssPx(m, devicePx);
|
|
1506
|
+
return { x: box.x + scroll.x, y: box.y + scroll.y, width: box.width, height: box.height };
|
|
1507
|
+
}
|
|
1392
1508
|
var CDP_CAPABILITIES = new Set([
|
|
1393
1509
|
"trace.performance",
|
|
1394
1510
|
"heap.snapshot",
|
|
@@ -1399,6 +1515,9 @@ var CDP_CAPABILITIES = new Set([
|
|
|
1399
1515
|
"emulate.networkConditions",
|
|
1400
1516
|
"screenshot.fullPage",
|
|
1401
1517
|
"screenshot.element",
|
|
1518
|
+
"screenshot.scale",
|
|
1519
|
+
"screenshot.renderSize",
|
|
1520
|
+
"screenshot.tile",
|
|
1402
1521
|
"network.intercept",
|
|
1403
1522
|
"snapshot.accessibilityTree",
|
|
1404
1523
|
"input.insertTextAtomic",
|
|
@@ -1689,31 +1808,128 @@ class CdpPageDriver {
|
|
|
1689
1808
|
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1690
1809
|
await this.conn.send("DOM.setFileInputFiles", { files: [...files], objectId });
|
|
1691
1810
|
}
|
|
1692
|
-
async screenshot(opts) {
|
|
1811
|
+
async screenshot(opts, consumeBands) {
|
|
1812
|
+
const renderSize = opts?.renderWidth !== undefined && opts?.renderHeight !== undefined ? { width: opts.renderWidth, height: opts.renderHeight } : undefined;
|
|
1813
|
+
if (!renderSize)
|
|
1814
|
+
return this.capture(opts, consumeBands);
|
|
1815
|
+
const previous = await readViewportRecord(this.browser.scheme, this.info.id);
|
|
1816
|
+
await this.conn.send("Emulation.setDeviceMetricsOverride", {
|
|
1817
|
+
width: renderSize.width,
|
|
1818
|
+
height: renderSize.height,
|
|
1819
|
+
deviceScaleFactor: previous?.deviceScaleFactor ?? 0,
|
|
1820
|
+
mobile: previous?.mobile ?? false
|
|
1821
|
+
});
|
|
1822
|
+
let shot;
|
|
1823
|
+
let captureError;
|
|
1824
|
+
try {
|
|
1825
|
+
shot = await this.capture(opts, consumeBands);
|
|
1826
|
+
} catch (e) {
|
|
1827
|
+
captureError = e;
|
|
1828
|
+
}
|
|
1829
|
+
let restoreError;
|
|
1830
|
+
try {
|
|
1831
|
+
if (previous) {
|
|
1832
|
+
await this.conn.send("Emulation.setDeviceMetricsOverride", {
|
|
1833
|
+
width: previous.width,
|
|
1834
|
+
height: previous.height,
|
|
1835
|
+
deviceScaleFactor: previous.deviceScaleFactor ?? 0,
|
|
1836
|
+
mobile: previous.mobile ?? false
|
|
1837
|
+
});
|
|
1838
|
+
} else {
|
|
1839
|
+
await this.conn.send("Emulation.clearDeviceMetricsOverride");
|
|
1840
|
+
}
|
|
1841
|
+
} catch (e) {
|
|
1842
|
+
restoreError = e instanceof Error ? e.message : String(e);
|
|
1843
|
+
}
|
|
1844
|
+
if (!shot) {
|
|
1845
|
+
if (restoreError !== undefined && captureError instanceof Error) {
|
|
1846
|
+
captureError.message = `${captureError.message} ${renderRestoreFailureNote(renderSize, restoreError)}`;
|
|
1847
|
+
}
|
|
1848
|
+
throw captureError;
|
|
1849
|
+
}
|
|
1850
|
+
const result = Object.assign(shot, { renderSize, renderRestored: restoreError === undefined });
|
|
1851
|
+
if (restoreError !== undefined)
|
|
1852
|
+
result.renderRestoreError = restoreError;
|
|
1853
|
+
return result;
|
|
1854
|
+
}
|
|
1855
|
+
async capture(opts, consumeBands) {
|
|
1693
1856
|
await this.ensureDomain("Page");
|
|
1694
1857
|
const format = opts?.format ?? "png";
|
|
1695
1858
|
const quality = format === "jpeg" ? opts?.quality ?? 80 : undefined;
|
|
1859
|
+
const scale = opts?.scale ?? 1;
|
|
1860
|
+
const renderSize = opts?.renderWidth !== undefined && opts?.renderHeight !== undefined ? { width: opts.renderWidth, height: opts.renderHeight } : undefined;
|
|
1861
|
+
const box = opts?.clip ? await elementBox(this.conn, opts.clip) : undefined;
|
|
1862
|
+
const metrics = await layoutMetrics(this.conn);
|
|
1863
|
+
const devicePx = devicePixelsPerCssPx(metrics);
|
|
1864
|
+
const clip = box ? documentClip(box, metrics, devicePx) : contentClip(metrics);
|
|
1865
|
+
const sendsClip = opts?.clip !== undefined || opts?.fullPage === true || scale !== 1 || renderSize !== undefined;
|
|
1866
|
+
if (opts?.tile === true && !consumeBands) {
|
|
1867
|
+
throw driverError("unsupported", "screenshot tile:true needs a caller that can consume bands; this one passed no band consumer");
|
|
1868
|
+
}
|
|
1869
|
+
const plan = planTiledCapture(clip, scale, devicePx, consumeBands ? opts?.tile : false);
|
|
1870
|
+
if (plan.tiled)
|
|
1871
|
+
return this.captureTiled(clip, plan, scale, format, consumeBands);
|
|
1872
|
+
assertClipEncodable(clip, scale, devicePx, renderSize);
|
|
1696
1873
|
const params = { format, captureBeyondViewport: true };
|
|
1697
1874
|
if (quality != null)
|
|
1698
1875
|
params.quality = quality;
|
|
1699
|
-
if (
|
|
1700
|
-
params.clip = { ...
|
|
1701
|
-
else if (opts?.fullPage)
|
|
1702
|
-
params.clip = { ...await fullPageClip(this.conn), scale: 1 };
|
|
1876
|
+
if (sendsClip)
|
|
1877
|
+
params.clip = { ...clip, scale };
|
|
1703
1878
|
const { data } = await this.conn.send("Page.captureScreenshot", params);
|
|
1704
1879
|
return { data: new Uint8Array(Buffer.from(data, "base64")), format };
|
|
1705
1880
|
}
|
|
1881
|
+
async captureTiled(clip, plan, scale, format, consumeBands) {
|
|
1882
|
+
const capture = {
|
|
1883
|
+
bands: this.bands(clip, plan, scale, format),
|
|
1884
|
+
bandCount: plan.bandCount,
|
|
1885
|
+
bandHeightCss: plan.bandHeightCss,
|
|
1886
|
+
clip,
|
|
1887
|
+
format: "png"
|
|
1888
|
+
};
|
|
1889
|
+
const consumed = await consumeBands(capture);
|
|
1890
|
+
return { tiled: true, format: "png", bandCount: plan.bandCount, bandHeightCss: plan.bandHeightCss, consumed };
|
|
1891
|
+
}
|
|
1892
|
+
async* bands(clip, plan, scale, format) {
|
|
1893
|
+
if (format !== "png") {
|
|
1894
|
+
throw driverError("unsupported", `a tiled capture is PNG-only (bands are stitched as PNG scanlines); got format "${format}"`);
|
|
1895
|
+
}
|
|
1896
|
+
const bottom = clip.y + clip.height;
|
|
1897
|
+
for (let i = 0;i < plan.bandCount; i++) {
|
|
1898
|
+
const y = clip.y + i * plan.bandHeightCss;
|
|
1899
|
+
const height = Math.min(plan.bandHeightCss, bottom - y);
|
|
1900
|
+
const { data } = await this.conn.send("Page.captureScreenshot", {
|
|
1901
|
+
format: "png",
|
|
1902
|
+
captureBeyondViewport: true,
|
|
1903
|
+
clip: { x: clip.x, y, width: clip.width, height, scale }
|
|
1904
|
+
});
|
|
1905
|
+
yield new Uint8Array(Buffer.from(data, "base64"));
|
|
1906
|
+
}
|
|
1907
|
+
}
|
|
1706
1908
|
async emulate(opts) {
|
|
1707
1909
|
if (opts.clearOverrides) {
|
|
1708
1910
|
const c = () => {
|
|
1709
1911
|
return;
|
|
1710
1912
|
};
|
|
1711
|
-
|
|
1913
|
+
let metricsClearError;
|
|
1914
|
+
try {
|
|
1915
|
+
await this.ensureDomain("Page");
|
|
1916
|
+
const metrics = await layoutMetrics(this.conn);
|
|
1917
|
+
const width = metrics.cssVisualViewport?.clientWidth ?? metrics.cssLayoutViewport?.clientWidth ?? metrics.visualViewport?.clientWidth ?? metrics.layoutViewport?.clientWidth ?? 1;
|
|
1918
|
+
const height = metrics.cssVisualViewport?.clientHeight ?? metrics.cssLayoutViewport?.clientHeight ?? metrics.visualViewport?.clientHeight ?? metrics.layoutViewport?.clientHeight ?? 1;
|
|
1919
|
+
await this.conn.send("Emulation.setDeviceMetricsOverride", { width, height, deviceScaleFactor: 0, mobile: false });
|
|
1920
|
+
await this.conn.send("Emulation.clearDeviceMetricsOverride");
|
|
1921
|
+
} catch (e) {
|
|
1922
|
+
metricsClearError = e instanceof Error ? e.message : String(e);
|
|
1923
|
+
}
|
|
1712
1924
|
await this.conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(c);
|
|
1713
1925
|
await this.conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(c);
|
|
1714
1926
|
await this.conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(c);
|
|
1715
1927
|
await this.ensureDomainSoft("Network");
|
|
1716
1928
|
await this.conn.send("Network.emulateNetworkConditions", { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }).catch(c);
|
|
1929
|
+
if (metricsClearError !== undefined) {
|
|
1930
|
+
throw driverError("page-error", `emulate: could not verify the device-metrics override was cleared (${metricsClearError})`);
|
|
1931
|
+
}
|
|
1932
|
+
await clearViewportRecord(this.browser.scheme, this.info.id);
|
|
1717
1933
|
return { applied: [] };
|
|
1718
1934
|
}
|
|
1719
1935
|
const applied = [];
|
|
@@ -1727,6 +1943,12 @@ class CdpPageDriver {
|
|
|
1727
1943
|
deviceScaleFactor: opts.deviceScaleFactor ?? 0,
|
|
1728
1944
|
mobile: opts.mobile ?? false
|
|
1729
1945
|
});
|
|
1946
|
+
await writeViewportRecord(this.browser.scheme, this.info.id, {
|
|
1947
|
+
width: opts.width,
|
|
1948
|
+
height: opts.height,
|
|
1949
|
+
deviceScaleFactor: opts.deviceScaleFactor ?? 0,
|
|
1950
|
+
mobile: opts.mobile ?? false
|
|
1951
|
+
});
|
|
1730
1952
|
applied.push("deviceMetrics");
|
|
1731
1953
|
}
|
|
1732
1954
|
if (opts.userAgent != null) {
|
|
@@ -2114,8 +2336,522 @@ async function reapStaleAgentTabs(driver, backend) {
|
|
|
2114
2336
|
}
|
|
2115
2337
|
|
|
2116
2338
|
// src/shared-tools.ts
|
|
2117
|
-
import { mkdir as
|
|
2118
|
-
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
|
|
2119
2855
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2120
2856
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
2121
2857
|
|
|
@@ -2260,8 +2996,8 @@ async function closePage(driver, args) {
|
|
|
2260
2996
|
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
2261
2997
|
}
|
|
2262
2998
|
async function writeSelectedFile(id) {
|
|
2263
|
-
await
|
|
2264
|
-
await
|
|
2999
|
+
await mkdir5(STATE_DIR, { recursive: true });
|
|
3000
|
+
await writeFile4(join4(STATE_DIR, "selected"), id, "utf8");
|
|
2265
3001
|
}
|
|
2266
3002
|
async function selectPage(driver, args) {
|
|
2267
3003
|
if (args.target === undefined || args.target === "")
|
|
@@ -2325,10 +3061,10 @@ function serializeForSink(value) {
|
|
|
2325
3061
|
return json ?? "null";
|
|
2326
3062
|
}
|
|
2327
3063
|
async function writeJsonSink(savePath, value) {
|
|
2328
|
-
const path = savePath.startsWith("/") ? savePath :
|
|
2329
|
-
await
|
|
3064
|
+
const path = savePath.startsWith("/") ? savePath : join4(ARTIFACT_DIR, savePath);
|
|
3065
|
+
await mkdir5(dirname2(path), { recursive: true });
|
|
2330
3066
|
const json = serializeForSink(value);
|
|
2331
|
-
await
|
|
3067
|
+
await writeFile4(path, json, "utf8");
|
|
2332
3068
|
return { path, bytes: Buffer.byteLength(json, "utf8") };
|
|
2333
3069
|
}
|
|
2334
3070
|
function sinkRequested(savePath) {
|
|
@@ -2632,30 +3368,166 @@ async function uploadFile(driver, args) {
|
|
|
2632
3368
|
function stamp() {
|
|
2633
3369
|
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
2634
3370
|
}
|
|
3371
|
+
var MAX_SCREENSHOT_SCALE = 8;
|
|
3372
|
+
function resolveScreenshotScale(scale, capabilities) {
|
|
3373
|
+
if (scale === undefined)
|
|
3374
|
+
return 1;
|
|
3375
|
+
if (typeof scale !== "number" || !Number.isFinite(scale) || scale <= 0 || scale > MAX_SCREENSHOT_SCALE) {
|
|
3376
|
+
throw new SharedToolError(`take_screenshot: 'scale' must be a finite number greater than 0 and at most ${MAX_SCREENSHOT_SCALE} (default 1)`);
|
|
3377
|
+
}
|
|
3378
|
+
if (scale !== 1 && !capabilities.has("screenshot.scale")) {
|
|
3379
|
+
throw new SharedToolError("take_screenshot: 'scale' is not supported by this backend (WebDriver BiDi's browsingContext.captureScreenshot has no scale parameter). " + `Call emulate with deviceScaleFactor:${scale} first and capture at scale 1, or use --browser chrome for a per-capture scale.`);
|
|
3380
|
+
}
|
|
3381
|
+
return scale;
|
|
3382
|
+
}
|
|
3383
|
+
var MAX_RENDER_SIZE_PX = 16384;
|
|
3384
|
+
function resolveRenderSize(renderWidth, renderHeight, capabilities) {
|
|
3385
|
+
if (renderWidth === undefined && renderHeight === undefined)
|
|
3386
|
+
return;
|
|
3387
|
+
if (renderWidth === undefined || renderHeight === undefined) {
|
|
3388
|
+
throw new SharedToolError("take_screenshot: 'renderWidth' and 'renderHeight' are required together (a viewport is two numbers; " + `got ${renderWidth === undefined ? "only renderHeight" : "only renderWidth"})`);
|
|
3389
|
+
}
|
|
3390
|
+
for (const [name, value] of [["renderWidth", renderWidth], ["renderHeight", renderHeight]]) {
|
|
3391
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1 || value > MAX_RENDER_SIZE_PX) {
|
|
3392
|
+
throw new SharedToolError(`take_screenshot: '${name}' must be an integer between 1 and ${MAX_RENDER_SIZE_PX} css px (got ${JSON.stringify(value)})`);
|
|
3393
|
+
}
|
|
3394
|
+
}
|
|
3395
|
+
if (!capabilities.has("screenshot.renderSize")) {
|
|
3396
|
+
throw new SharedToolError("take_screenshot: 'renderWidth'/'renderHeight' are not supported by this backend (it cannot emulate a viewport for a capture). " + `Call emulate with width:${renderWidth}, height:${renderHeight} first, capture, then restore the viewport yourself.`);
|
|
3397
|
+
}
|
|
3398
|
+
return { width: renderWidth, height: renderHeight };
|
|
3399
|
+
}
|
|
3400
|
+
function resolveTileMode(tile, capabilities, format, returnBase64) {
|
|
3401
|
+
if (tile === undefined)
|
|
3402
|
+
return;
|
|
3403
|
+
if (typeof tile !== "boolean")
|
|
3404
|
+
throw new SharedToolError(`take_screenshot: 'tile' must be true, false, or omitted for automatic (got ${JSON.stringify(tile)})`);
|
|
3405
|
+
if (!tile)
|
|
3406
|
+
return false;
|
|
3407
|
+
if (!capabilities.has("screenshot.tile")) {
|
|
3408
|
+
throw new SharedToolError("take_screenshot: 'tile' is not supported by this backend (band tiling is declared only where the band arithmetic was measured against a real browser; " + "Firefox was not driven for it). Capture a smaller region or a single element, or use --browser chrome for a tiled capture.");
|
|
3409
|
+
}
|
|
3410
|
+
if (format !== "png")
|
|
3411
|
+
throw tiledCaptureRefusal("format");
|
|
3412
|
+
if (returnBase64)
|
|
3413
|
+
throw tiledCaptureRefusal("base64");
|
|
3414
|
+
return true;
|
|
3415
|
+
}
|
|
3416
|
+
function tiledCaptureRefusal(reason, banded) {
|
|
3417
|
+
const because = banded ? ` This capture needs ${banded.bandCount} bands (${banded.clip.width}x${banded.clip.height} css px is past what one image can encode).` : "";
|
|
3418
|
+
if (reason === "format") {
|
|
3419
|
+
return new SharedToolError("take_screenshot: a tiled capture is PNG-only — bands are stitched by concatenating PNG scanlines, and there is no lossless " + `JPEG equivalent to concatenate.${because} Pass format:"png", or lower scale / narrow the region until one JPEG fits.`);
|
|
3420
|
+
}
|
|
3421
|
+
return new SharedToolError("take_screenshot: 'returnBase64' cannot be combined with a tiled capture — a tiled image runs to hundreds of megabytes and base64 " + `inflates it by a third, into the caller's response.${because} Read the file at the returned path instead, or capture a region small enough for one shot.`);
|
|
3422
|
+
}
|
|
3423
|
+
function pngPixelSize(b, view) {
|
|
3424
|
+
if (b.length < 24)
|
|
3425
|
+
return;
|
|
3426
|
+
if (view.getUint32(0) !== 2303741511 || view.getUint32(4) !== 218765834)
|
|
3427
|
+
return;
|
|
3428
|
+
if (String.fromCharCode(b[12], b[13], b[14], b[15]) !== "IHDR")
|
|
3429
|
+
return;
|
|
3430
|
+
return { width: view.getUint32(16), height: view.getUint32(20) };
|
|
3431
|
+
}
|
|
3432
|
+
var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
|
|
3433
|
+
function jpegPixelSize(b, view) {
|
|
3434
|
+
if (b.length < 4 || b[0] !== 255 || b[1] !== 216)
|
|
3435
|
+
return;
|
|
3436
|
+
let i = 2;
|
|
3437
|
+
while (i + 3 < b.length) {
|
|
3438
|
+
if (b[i] !== 255) {
|
|
3439
|
+
i += 1;
|
|
3440
|
+
continue;
|
|
3441
|
+
}
|
|
3442
|
+
const marker = b[i + 1];
|
|
3443
|
+
if (marker === 255) {
|
|
3444
|
+
i += 1;
|
|
3445
|
+
continue;
|
|
3446
|
+
}
|
|
3447
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
3448
|
+
i += 2;
|
|
3449
|
+
continue;
|
|
3450
|
+
}
|
|
3451
|
+
const segLen = view.getUint16(i + 2);
|
|
3452
|
+
if (JPEG_SOF.has(marker)) {
|
|
3453
|
+
if (i + 9 > b.length)
|
|
3454
|
+
return;
|
|
3455
|
+
return { width: view.getUint16(i + 7), height: view.getUint16(i + 5) };
|
|
3456
|
+
}
|
|
3457
|
+
if (segLen < 2)
|
|
3458
|
+
return;
|
|
3459
|
+
i += 2 + segLen;
|
|
3460
|
+
}
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
function imagePixelSize(bytes) {
|
|
3464
|
+
if (bytes.byteLength < 4)
|
|
3465
|
+
return;
|
|
3466
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
|
|
3467
|
+
return pngPixelSize(bytes, view) ?? jpegPixelSize(bytes, view);
|
|
3468
|
+
}
|
|
2635
3469
|
async function takeScreenshot(driver, args = {}) {
|
|
2636
3470
|
if (args.uid !== undefined && args.uid !== "" && args.selector)
|
|
2637
3471
|
throw new SharedToolError("provide exactly one of uid or selector, not both");
|
|
2638
3472
|
const format = args.format ?? "png";
|
|
3473
|
+
const scale = resolveScreenshotScale(args.scale, driver.capabilities);
|
|
3474
|
+
const renderSize = resolveRenderSize(args.renderWidth, args.renderHeight, driver.capabilities);
|
|
3475
|
+
const tile = resolveTileMode(args.tile, driver.capabilities, format, args.returnBase64);
|
|
2639
3476
|
return withPage2(driver, args.target, async (page) => {
|
|
2640
3477
|
const clip = args.uid !== undefined || args.selector ? locatorOf(args) : undefined;
|
|
2641
|
-
const
|
|
2642
|
-
|
|
2643
|
-
|
|
2644
|
-
|
|
2645
|
-
|
|
2646
|
-
|
|
2647
|
-
|
|
2648
|
-
|
|
2649
|
-
path,
|
|
2650
|
-
bytes: data.byteLength,
|
|
2651
|
-
format: outFormat,
|
|
2652
|
-
target: target3(page.info)
|
|
3478
|
+
const ext = format === "jpeg" ? "jpg" : "png";
|
|
3479
|
+
const path = args.savePath ?? join4(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
3480
|
+
const stitch = async (banded) => {
|
|
3481
|
+
if (format !== "png")
|
|
3482
|
+
throw tiledCaptureRefusal("format", banded);
|
|
3483
|
+
if (args.returnBase64)
|
|
3484
|
+
throw tiledCaptureRefusal("base64", banded);
|
|
3485
|
+
return stitchPngBandsToFile(banded.bands, path);
|
|
2653
3486
|
};
|
|
2654
|
-
|
|
2655
|
-
|
|
3487
|
+
const shot = await page.screenshot({
|
|
3488
|
+
format,
|
|
3489
|
+
quality: args.quality,
|
|
3490
|
+
fullPage: args.fullPage,
|
|
3491
|
+
scale,
|
|
3492
|
+
...clip ? { clip } : {},
|
|
3493
|
+
...renderSize ? { renderWidth: renderSize.width, renderHeight: renderSize.height } : {},
|
|
3494
|
+
...tile !== undefined ? { tile } : {}
|
|
3495
|
+
}, stitch);
|
|
3496
|
+
const result = isTiledCapture(shot) ? {
|
|
3497
|
+
path,
|
|
3498
|
+
bytes: shot.consumed.bytes,
|
|
3499
|
+
format: "png",
|
|
3500
|
+
target: target3(page.info),
|
|
3501
|
+
scale,
|
|
3502
|
+
width: shot.consumed.width,
|
|
3503
|
+
height: shot.consumed.height,
|
|
3504
|
+
tiled: true,
|
|
3505
|
+
bands: shot.consumed.bands
|
|
3506
|
+
} : await writeOneShot(shot.data, shot.format, path, target3(page.info), scale, args.returnBase64);
|
|
3507
|
+
if (renderSize && shot.renderSize) {
|
|
3508
|
+
result.renderSize = shot.renderSize;
|
|
3509
|
+
result.renderRestored = shot.renderRestored ?? false;
|
|
3510
|
+
if (shot.renderRestoreError !== undefined)
|
|
3511
|
+
result.renderRestoreError = shot.renderRestoreError;
|
|
3512
|
+
}
|
|
2656
3513
|
return result;
|
|
2657
3514
|
});
|
|
2658
3515
|
}
|
|
3516
|
+
async function writeOneShot(data, outFormat, path, target, scale, returnBase64) {
|
|
3517
|
+
if (data.byteLength === 0)
|
|
3518
|
+
throw new SharedToolError("captureScreenshot returned empty data");
|
|
3519
|
+
await mkdir5(dirname2(path), { recursive: true });
|
|
3520
|
+
await writeFile4(path, Buffer.from(data));
|
|
3521
|
+
const size = imagePixelSize(data);
|
|
3522
|
+
const result = { path, bytes: data.byteLength, format: outFormat, target, scale };
|
|
3523
|
+
if (size) {
|
|
3524
|
+
result.width = size.width;
|
|
3525
|
+
result.height = size.height;
|
|
3526
|
+
}
|
|
3527
|
+
if (returnBase64)
|
|
3528
|
+
result.base64 = Buffer.from(data).toString("base64");
|
|
3529
|
+
return result;
|
|
3530
|
+
}
|
|
2659
3531
|
async function emulate(driver, args = {}) {
|
|
2660
3532
|
return withPage2(driver, args.target, async (page) => {
|
|
2661
3533
|
if (args.clearOverrides) {
|
|
@@ -2826,10 +3698,10 @@ var LEASE_TOOLS = {
|
|
|
2826
3698
|
};
|
|
2827
3699
|
|
|
2828
3700
|
// src/tools/console.ts
|
|
2829
|
-
import { readFile as
|
|
3701
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
2830
3702
|
|
|
2831
3703
|
// src/tools/recorder.ts
|
|
2832
|
-
import { mkdir as
|
|
3704
|
+
import { mkdir as mkdir6, appendFile, writeFile as writeFile5, copyFile } from "node:fs/promises";
|
|
2833
3705
|
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2834
3706
|
function recFile(targetId) {
|
|
2835
3707
|
return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
|
|
@@ -2840,6 +3712,31 @@ function captureFile(targetId, captureId) {
|
|
|
2840
3712
|
function delay2(ms) {
|
|
2841
3713
|
return new Promise((r) => setTimeout(r, ms));
|
|
2842
3714
|
}
|
|
3715
|
+
async function resolveRecorderTarget(target, opts) {
|
|
3716
|
+
if (!isWorkerSelector(target))
|
|
3717
|
+
return resolveTarget(target);
|
|
3718
|
+
if (opts.capture)
|
|
3719
|
+
return resolveWorkerSelector(target, { wake: opts.wake ?? true });
|
|
3720
|
+
const needle = workerNeedle(target);
|
|
3721
|
+
if (needle === "")
|
|
3722
|
+
throw new CdpError(WORKER_EMPTY_NEEDLE_MESSAGE);
|
|
3723
|
+
const { matches, liveWorkers } = resolveWorkerTargets(await listTargets(), needle);
|
|
3724
|
+
if (matches.length > 1)
|
|
3725
|
+
throw new CdpError(workerAmbiguityMessage(needle, matches));
|
|
3726
|
+
if (matches.length === 1)
|
|
3727
|
+
return matches[0];
|
|
3728
|
+
throw new CdpError(workerBufferReadMissMessage(needle, liveWorkers));
|
|
3729
|
+
}
|
|
3730
|
+
function assertWakeApplies(target, wake, capture) {
|
|
3731
|
+
if (wake === undefined)
|
|
3732
|
+
return;
|
|
3733
|
+
if (typeof wake !== "boolean")
|
|
3734
|
+
throw new CdpError("'wake' must be a boolean");
|
|
3735
|
+
if (!isWorkerSelector(target))
|
|
3736
|
+
throw new CdpError(workerWakeMisuseMessage("not-a-worker-selector"));
|
|
3737
|
+
if (!capture)
|
|
3738
|
+
throw new CdpError(workerWakeMisuseMessage("read-only-call"));
|
|
3739
|
+
}
|
|
2843
3740
|
var NETWORK_EVENTS = [
|
|
2844
3741
|
"Network.requestWillBeSent",
|
|
2845
3742
|
"Network.responseReceived",
|
|
@@ -2850,11 +3747,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
2850
3747
|
async function startRecorder(target, opts = {}) {
|
|
2851
3748
|
const network = opts.network ?? true;
|
|
2852
3749
|
const consoleCap = opts.console ?? true;
|
|
2853
|
-
await
|
|
3750
|
+
await mkdir6(ARTIFACT_DIR2, { recursive: true });
|
|
2854
3751
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
2855
3752
|
const file = opts.file ?? recFile(resolved.id);
|
|
2856
3753
|
if (opts.truncate) {
|
|
2857
|
-
await
|
|
3754
|
+
await writeFile5(file, "");
|
|
2858
3755
|
}
|
|
2859
3756
|
let chain = Promise.resolve();
|
|
2860
3757
|
let dropped = 0;
|
|
@@ -2905,7 +3802,8 @@ async function startRecorder(target, opts = {}) {
|
|
|
2905
3802
|
};
|
|
2906
3803
|
}
|
|
2907
3804
|
async function captureWindow(target, durationMs, opts = {}) {
|
|
2908
|
-
const resolved = await
|
|
3805
|
+
const resolved = await resolveRecorderTarget(target, { capture: true, wake: opts.wake });
|
|
3806
|
+
const isWorker = isWorkerTargetType(resolved.type);
|
|
2909
3807
|
const captureId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2910
3808
|
const file = captureFile(resolved.id, captureId);
|
|
2911
3809
|
const rec = await startRecorder(resolved.id, {
|
|
@@ -2915,13 +3813,16 @@ async function captureWindow(target, durationMs, opts = {}) {
|
|
|
2915
3813
|
file,
|
|
2916
3814
|
timeoutMs: opts.timeoutMs
|
|
2917
3815
|
});
|
|
2918
|
-
|
|
2919
|
-
|
|
3816
|
+
if (!isWorker) {
|
|
3817
|
+
await rec.conn.send("Page.enable");
|
|
3818
|
+
await rec.conn.send("Page.reload", { ignoreCache: false });
|
|
3819
|
+
}
|
|
2920
3820
|
await delay2(durationMs);
|
|
2921
3821
|
return {
|
|
2922
3822
|
file: rec.file,
|
|
2923
3823
|
conn: rec.conn,
|
|
2924
3824
|
resolved: { id: rec.target.id, url: rec.target.url, title: rec.target.title },
|
|
3825
|
+
mode: isWorker ? "listen" : "reload",
|
|
2925
3826
|
droppedWrites: rec.droppedWrites,
|
|
2926
3827
|
flush: () => rec.flush(),
|
|
2927
3828
|
async stop() {
|
|
@@ -2996,7 +3897,7 @@ function lineToEntry(line, index) {
|
|
|
2996
3897
|
async function readConsoleEntries(file) {
|
|
2997
3898
|
let raw;
|
|
2998
3899
|
try {
|
|
2999
|
-
raw = await
|
|
3900
|
+
raw = await readFile4(file, "utf8");
|
|
3000
3901
|
} catch {
|
|
3001
3902
|
return [];
|
|
3002
3903
|
}
|
|
@@ -3021,8 +3922,9 @@ async function readConsoleEntries(file) {
|
|
|
3021
3922
|
return out;
|
|
3022
3923
|
}
|
|
3023
3924
|
async function listConsoleMessages(args = {}) {
|
|
3925
|
+
assertWakeApplies(args.target, args.wake, args.reload === true);
|
|
3024
3926
|
if (args.reload) {
|
|
3025
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS);
|
|
3927
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS, { wake: args.wake });
|
|
3026
3928
|
await cap.stop();
|
|
3027
3929
|
const messages2 = await readConsoleEntries(cap.file);
|
|
3028
3930
|
return {
|
|
@@ -3032,7 +3934,7 @@ async function listConsoleMessages(args = {}) {
|
|
|
3032
3934
|
droppedWrites: cap.droppedWrites()
|
|
3033
3935
|
};
|
|
3034
3936
|
}
|
|
3035
|
-
const target = await
|
|
3937
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3036
3938
|
const messages = await readConsoleEntries(recFile(target.id));
|
|
3037
3939
|
return {
|
|
3038
3940
|
target: { id: target.id, url: target.url, title: target.title },
|
|
@@ -3041,7 +3943,7 @@ async function listConsoleMessages(args = {}) {
|
|
|
3041
3943
|
};
|
|
3042
3944
|
}
|
|
3043
3945
|
async function getConsoleMessage(args = {}) {
|
|
3044
|
-
const target = await
|
|
3946
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3045
3947
|
const messages = await readConsoleEntries(recFile(target.id));
|
|
3046
3948
|
const index = args.index ?? 0;
|
|
3047
3949
|
const entry = messages[index];
|
|
@@ -3052,7 +3954,7 @@ async function getConsoleMessage(args = {}) {
|
|
|
3052
3954
|
}
|
|
3053
3955
|
|
|
3054
3956
|
// src/tools/network.ts
|
|
3055
|
-
import { readFile as
|
|
3957
|
+
import { readFile as readFile5 } from "node:fs/promises";
|
|
3056
3958
|
var DEFAULT_CAPTURE_MS2 = 2500;
|
|
3057
3959
|
function asHeaders(v) {
|
|
3058
3960
|
if (!v || typeof v !== "object")
|
|
@@ -3119,7 +4021,7 @@ function correlate(lines) {
|
|
|
3119
4021
|
async function readNetworkRequests(file) {
|
|
3120
4022
|
let raw;
|
|
3121
4023
|
try {
|
|
3122
|
-
raw = await
|
|
4024
|
+
raw = await readFile5(file, "utf8");
|
|
3123
4025
|
} catch {
|
|
3124
4026
|
return [];
|
|
3125
4027
|
}
|
|
@@ -3139,14 +4041,15 @@ async function listNetworkRequests(args = {}) {
|
|
|
3139
4041
|
let file;
|
|
3140
4042
|
let resolved;
|
|
3141
4043
|
let droppedWrites;
|
|
4044
|
+
assertWakeApplies(args.target, args.wake, args.reload === true);
|
|
3142
4045
|
if (args.reload) {
|
|
3143
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
|
|
4046
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
|
|
3144
4047
|
await cap.stop();
|
|
3145
4048
|
file = cap.file;
|
|
3146
4049
|
resolved = cap.resolved;
|
|
3147
4050
|
droppedWrites = cap.droppedWrites();
|
|
3148
4051
|
} else {
|
|
3149
|
-
const target = await
|
|
4052
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3150
4053
|
file = recFile(target.id);
|
|
3151
4054
|
resolved = { id: target.id, url: target.url, title: target.title };
|
|
3152
4055
|
}
|
|
@@ -3167,8 +4070,9 @@ async function getNetworkRequest(args = {}) {
|
|
|
3167
4070
|
return requests.find((r) => r.url.includes(args.url));
|
|
3168
4071
|
};
|
|
3169
4072
|
const describeSelector = () => args.requestId ? `requestId ${args.requestId}` : `url ~ '${args.url}'`;
|
|
4073
|
+
assertWakeApplies(args.target, args.wake, Boolean(args.includeBody && args.url));
|
|
3170
4074
|
if (args.includeBody && args.url) {
|
|
3171
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
|
|
4075
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
|
|
3172
4076
|
try {
|
|
3173
4077
|
await cap.flush();
|
|
3174
4078
|
const match2 = (await readNetworkRequests(cap.file)).find((r) => r.url.includes(args.url));
|
|
@@ -3185,7 +4089,7 @@ async function getNetworkRequest(args = {}) {
|
|
|
3185
4089
|
await cap.stop();
|
|
3186
4090
|
}
|
|
3187
4091
|
}
|
|
3188
|
-
const target = await
|
|
4092
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
3189
4093
|
const match = pick(await readNetworkRequests(recFile(target.id)));
|
|
3190
4094
|
if (!match) {
|
|
3191
4095
|
throw new CdpError(`no network request matched ${describeSelector()}; run list_network_requests with reload:true first` + (args.includeBody ? `, then fetch the body with the url selector (reload re-mints requestIds)` : ``));
|
|
@@ -3201,8 +4105,8 @@ async function getNetworkRequest(args = {}) {
|
|
|
3201
4105
|
}
|
|
3202
4106
|
|
|
3203
4107
|
// src/tools/performance.ts
|
|
3204
|
-
import { mkdir as
|
|
3205
|
-
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";
|
|
3206
4110
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3207
4111
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
3208
4112
|
var DEFAULT_CATEGORIES = [
|
|
@@ -3220,7 +4124,7 @@ function shortId(id) {
|
|
|
3220
4124
|
return id.slice(0, 8);
|
|
3221
4125
|
}
|
|
3222
4126
|
function stateFilePath() {
|
|
3223
|
-
return
|
|
4127
|
+
return join5(STATE_DIR2, "trace-state.json");
|
|
3224
4128
|
}
|
|
3225
4129
|
function bufferTraceData(conn, sink) {
|
|
3226
4130
|
return conn.on("Tracing.dataCollected", (params) => {
|
|
@@ -3242,10 +4146,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
3242
4146
|
return sink;
|
|
3243
4147
|
}
|
|
3244
4148
|
async function writeTrace(events, targetId) {
|
|
3245
|
-
await
|
|
3246
|
-
const path =
|
|
4149
|
+
await mkdir7(ARTIFACT_DIR3, { recursive: true });
|
|
4150
|
+
const path = join5(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
3247
4151
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
3248
|
-
await
|
|
4152
|
+
await writeFile6(path, body, "utf8");
|
|
3249
4153
|
const { size } = await stat(path);
|
|
3250
4154
|
return { path, bytes: size };
|
|
3251
4155
|
}
|
|
@@ -3265,7 +4169,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
3265
4169
|
throw err;
|
|
3266
4170
|
}
|
|
3267
4171
|
liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
|
|
3268
|
-
await
|
|
4172
|
+
await mkdir7(STATE_DIR2, { recursive: true });
|
|
3269
4173
|
const state = {
|
|
3270
4174
|
targetId: target.id,
|
|
3271
4175
|
url: target.url,
|
|
@@ -3273,7 +4177,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
3273
4177
|
pid: process.pid,
|
|
3274
4178
|
categories
|
|
3275
4179
|
};
|
|
3276
|
-
await
|
|
4180
|
+
await writeFile6(stateFilePath(), JSON.stringify(state), "utf8");
|
|
3277
4181
|
return {
|
|
3278
4182
|
target: { id: target.id, url: target.url, title: target.title },
|
|
3279
4183
|
categories,
|
|
@@ -3286,7 +4190,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
3286
4190
|
if (!entry) {
|
|
3287
4191
|
let hint = "";
|
|
3288
4192
|
try {
|
|
3289
|
-
const raw = await
|
|
4193
|
+
const raw = await readFile6(stateFilePath(), "utf8");
|
|
3290
4194
|
const state = JSON.parse(raw);
|
|
3291
4195
|
hint = state.pid === process.pid ? ` (state file lists target ${shortId(state.targetId)} but its live connection is gone)` : ` (trace was started by a DIFFERENT process pid=${state.pid}; live recordings cannot cross process boundaries)`;
|
|
3292
4196
|
} catch {}
|
|
@@ -3298,7 +4202,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
3298
4202
|
} finally {
|
|
3299
4203
|
conn.close();
|
|
3300
4204
|
liveTraces.delete(target.id);
|
|
3301
|
-
await
|
|
4205
|
+
await unlink4(stateFilePath()).catch(() => {});
|
|
3302
4206
|
}
|
|
3303
4207
|
const written = await writeTrace(events, target.id);
|
|
3304
4208
|
const metrics = analyzeEvents(events);
|
|
@@ -3416,7 +4320,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
3416
4320
|
}
|
|
3417
4321
|
let raw;
|
|
3418
4322
|
try {
|
|
3419
|
-
raw = await
|
|
4323
|
+
raw = await readFile6(args.tracePath, "utf8");
|
|
3420
4324
|
} catch (err) {
|
|
3421
4325
|
throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
|
|
3422
4326
|
}
|
|
@@ -3434,8 +4338,8 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
3434
4338
|
|
|
3435
4339
|
// src/tools/screencast.ts
|
|
3436
4340
|
import { spawn } from "node:child_process";
|
|
3437
|
-
import { mkdir as
|
|
3438
|
-
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";
|
|
3439
4343
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3440
4344
|
var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
|
|
3441
4345
|
function frameDurationsMs(ledger, stopWallclockMs) {
|
|
@@ -3491,7 +4395,7 @@ function pngSize(buf) {
|
|
|
3491
4395
|
return;
|
|
3492
4396
|
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
3493
4397
|
}
|
|
3494
|
-
var
|
|
4398
|
+
var JPEG_SOF2 = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
|
|
3495
4399
|
function jpegSize(buf) {
|
|
3496
4400
|
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
|
|
3497
4401
|
return;
|
|
@@ -3513,7 +4417,7 @@ function jpegSize(buf) {
|
|
|
3513
4417
|
if (i + 3 >= buf.length)
|
|
3514
4418
|
return;
|
|
3515
4419
|
const segLen = buf.readUInt16BE(i + 2);
|
|
3516
|
-
if (
|
|
4420
|
+
if (JPEG_SOF2.has(marker)) {
|
|
3517
4421
|
if (i + 9 > buf.length)
|
|
3518
4422
|
return;
|
|
3519
4423
|
return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
|
|
@@ -3656,9 +4560,9 @@ async function startScreenRecording(args = {}) {
|
|
|
3656
4560
|
conn.close();
|
|
3657
4561
|
throw err;
|
|
3658
4562
|
}
|
|
3659
|
-
const spoolDir =
|
|
4563
|
+
const spoolDir = join6(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
|
|
3660
4564
|
try {
|
|
3661
|
-
await
|
|
4565
|
+
await mkdir8(spoolDir, { recursive: true });
|
|
3662
4566
|
} catch (err) {
|
|
3663
4567
|
conn.close();
|
|
3664
4568
|
throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
|
|
@@ -3699,7 +4603,7 @@ async function startScreenRecording(args = {}) {
|
|
|
3699
4603
|
seq += 1;
|
|
3700
4604
|
const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
|
|
3701
4605
|
rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
|
|
3702
|
-
rec.writes = rec.writes.then(() =>
|
|
4606
|
+
rec.writes = rec.writes.then(() => writeFile7(join6(spoolDir, file), buf)).catch(() => {
|
|
3703
4607
|
rec.dropped += 1;
|
|
3704
4608
|
rec.unwritten.add(file);
|
|
3705
4609
|
});
|
|
@@ -3721,7 +4625,7 @@ async function startScreenRecording(args = {}) {
|
|
|
3721
4625
|
} catch (err) {
|
|
3722
4626
|
rec.off();
|
|
3723
4627
|
conn.close();
|
|
3724
|
-
await
|
|
4628
|
+
await rm2(spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
3725
4629
|
throw err;
|
|
3726
4630
|
}
|
|
3727
4631
|
liveRecordings.set(target.id, rec);
|
|
@@ -3759,14 +4663,14 @@ async function stopScreenRecording(args = {}) {
|
|
|
3759
4663
|
liveRecordings.delete(rec.target.id);
|
|
3760
4664
|
const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
|
|
3761
4665
|
if (!ledger.length) {
|
|
3762
|
-
await
|
|
4666
|
+
await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
3763
4667
|
throw new CdpError(`screen recording captured 0 frames for target ${shortId2(rec.target.id)}. Chrome emits a screencast frame only on repaint: a fully backgrounded or occluded tab may never paint. Retry with bringToFront:true, or drive the page while recording.`);
|
|
3764
4668
|
}
|
|
3765
|
-
await
|
|
3766
|
-
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`);
|
|
3767
4671
|
const frames = coalesceToConcatGrid(ledger, stoppedAt);
|
|
3768
|
-
const manifestPath =
|
|
3769
|
-
await
|
|
4672
|
+
const manifestPath = join6(rec.spoolDir, "frames.ffconcat");
|
|
4673
|
+
await writeFile7(manifestPath, buildConcatManifest(frames), "utf8");
|
|
3770
4674
|
const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
|
|
3771
4675
|
const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
|
|
3772
4676
|
let res;
|
|
@@ -3780,7 +4684,7 @@ async function stopScreenRecording(args = {}) {
|
|
|
3780
4684
|
ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
3781
4685
|
}
|
|
3782
4686
|
const { size } = await stat2(outPath);
|
|
3783
|
-
await
|
|
4687
|
+
await rm2(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
3784
4688
|
return {
|
|
3785
4689
|
path: outPath,
|
|
3786
4690
|
bytes: size,
|
|
@@ -3797,8 +4701,8 @@ ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
|
3797
4701
|
}
|
|
3798
4702
|
|
|
3799
4703
|
// src/tools/heap.ts
|
|
3800
|
-
import { mkdir as
|
|
3801
|
-
import { join as
|
|
4704
|
+
import { mkdir as mkdir9, writeFile as writeFile8 } from "node:fs/promises";
|
|
4705
|
+
import { join as join7 } from "node:path";
|
|
3802
4706
|
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3803
4707
|
function stamp4(targetId) {
|
|
3804
4708
|
const short = targetId.slice(0, 8);
|
|
@@ -3823,9 +4727,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
3823
4727
|
if (snapshot.length === 0) {
|
|
3824
4728
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
3825
4729
|
}
|
|
3826
|
-
await
|
|
3827
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
3828
|
-
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");
|
|
3829
4733
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
3830
4734
|
return {
|
|
3831
4735
|
path,
|
|
@@ -3838,7 +4742,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
3838
4742
|
|
|
3839
4743
|
// src/tools/lighthouse.ts
|
|
3840
4744
|
import { spawn as spawn2 } from "node:child_process";
|
|
3841
|
-
import { mkdir as
|
|
4745
|
+
import { mkdir as mkdir10, readFile as readFile7, stat as stat3 } from "node:fs/promises";
|
|
3842
4746
|
var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3843
4747
|
function cdpPort() {
|
|
3844
4748
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
@@ -3899,7 +4803,7 @@ async function lighthouseAudit(args) {
|
|
|
3899
4803
|
const url = args.url.trim();
|
|
3900
4804
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
3901
4805
|
const port = cdpPort();
|
|
3902
|
-
await
|
|
4806
|
+
await mkdir10(ARTIFACT_DIR6, { recursive: true });
|
|
3903
4807
|
const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
|
|
3904
4808
|
const formFactor = args.formFactor ?? "desktop";
|
|
3905
4809
|
const lhArgs = [
|
|
@@ -3928,7 +4832,7 @@ async function lighthouseAudit(args) {
|
|
|
3928
4832
|
}
|
|
3929
4833
|
let raw;
|
|
3930
4834
|
try {
|
|
3931
|
-
raw = await
|
|
4835
|
+
raw = await readFile7(outPath, "utf8");
|
|
3932
4836
|
} catch {
|
|
3933
4837
|
raw = undefined;
|
|
3934
4838
|
}
|
|
@@ -4015,14 +4919,14 @@ async function dispatchMouse(args) {
|
|
|
4015
4919
|
}
|
|
4016
4920
|
|
|
4017
4921
|
// src/tools/downloads.ts
|
|
4018
|
-
import { readdir as readdir3, rename, stat as stat4 } from "node:fs/promises";
|
|
4019
|
-
import { basename, extname, join as
|
|
4922
|
+
import { readdir as readdir3, rename as rename2, stat as stat4 } from "node:fs/promises";
|
|
4923
|
+
import { basename, extname, join as join9 } from "node:path";
|
|
4020
4924
|
|
|
4021
4925
|
// src/tools/browser-session.ts
|
|
4022
|
-
import { mkdir as
|
|
4023
|
-
import { join as
|
|
4926
|
+
import { mkdir as mkdir11 } from "node:fs/promises";
|
|
4927
|
+
import { join as join8 } from "node:path";
|
|
4024
4928
|
var ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
4025
|
-
var DOWNLOADS_DIR =
|
|
4929
|
+
var DOWNLOADS_DIR = join8(ARTIFACT_DIR7, "downloads");
|
|
4026
4930
|
var DOWNLOAD_BUFFER_LIMIT = 50;
|
|
4027
4931
|
var session;
|
|
4028
4932
|
function isDisconnect(e) {
|
|
@@ -4094,7 +4998,7 @@ async function browserSend(method, params = {}) {
|
|
|
4094
4998
|
}
|
|
4095
4999
|
}
|
|
4096
5000
|
async function armDownloadsAt(path) {
|
|
4097
|
-
await
|
|
5001
|
+
await mkdir11(path, { recursive: true });
|
|
4098
5002
|
const s = await getBrowserSession();
|
|
4099
5003
|
await s.conn.send("Browser.setDownloadBehavior", { behavior: "allowAndName", downloadPath: path, eventsEnabled: true });
|
|
4100
5004
|
s.armedPath = path;
|
|
@@ -4184,8 +5088,8 @@ async function waitForDownload(args = {}) {
|
|
|
4184
5088
|
const taken = new Set(await readdir3(DOWNLOADS_DIR).catch(() => []));
|
|
4185
5089
|
taken.delete(basename(rec.filePath));
|
|
4186
5090
|
const finalName = nextFreeName(suggestedFilename, taken);
|
|
4187
|
-
const path =
|
|
4188
|
-
await
|
|
5091
|
+
const path = join9(DOWNLOADS_DIR, finalName);
|
|
5092
|
+
await rename2(rec.filePath, path);
|
|
4189
5093
|
const bytes = (await stat4(path)).size;
|
|
4190
5094
|
return { path, suggestedFilename, bytes, ...rec.url ? { url: rec.url } : {}, target };
|
|
4191
5095
|
}
|
|
@@ -4474,10 +5378,15 @@ class BidiConnection {
|
|
|
4474
5378
|
} catch {}
|
|
4475
5379
|
}
|
|
4476
5380
|
}
|
|
4477
|
-
async function
|
|
4478
|
-
const conn = new BidiConnection(
|
|
5381
|
+
async function connectBidiSessionUrl(wsUrl, opts = {}) {
|
|
5382
|
+
const conn = new BidiConnection(wsUrl, { timeoutMs: opts.timeoutMs });
|
|
4479
5383
|
await conn.connect();
|
|
4480
|
-
|
|
5384
|
+
try {
|
|
5385
|
+
await conn.newSession(opts.capabilities ?? {});
|
|
5386
|
+
} catch (e) {
|
|
5387
|
+
conn.dispose();
|
|
5388
|
+
throw e;
|
|
5389
|
+
}
|
|
4481
5390
|
conn.retain();
|
|
4482
5391
|
return conn;
|
|
4483
5392
|
}
|
|
@@ -5053,21 +5962,27 @@ var BIDI_CAPABILITIES = new Set([
|
|
|
5053
5962
|
"emulate.deviceMetrics",
|
|
5054
5963
|
"screenshot.fullPage",
|
|
5055
5964
|
"screenshot.element",
|
|
5965
|
+
"screenshot.renderSize",
|
|
5056
5966
|
"network.intercept",
|
|
5057
5967
|
"locate.xpath"
|
|
5058
5968
|
]);
|
|
5059
5969
|
var connections = new Map;
|
|
5060
|
-
async function getConnection(
|
|
5061
|
-
const existing = connections.get(
|
|
5970
|
+
async function getConnection(endpoint, timeoutMs) {
|
|
5971
|
+
const existing = connections.get(endpoint);
|
|
5062
5972
|
if (existing) {
|
|
5063
5973
|
existing.retain();
|
|
5064
5974
|
return existing;
|
|
5065
5975
|
}
|
|
5066
|
-
const conn = await
|
|
5976
|
+
const conn = await connectBidiSessionUrl(endpoint, {
|
|
5067
5977
|
...timeoutMs !== undefined ? { timeoutMs } : {},
|
|
5068
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;
|
|
5069
5984
|
});
|
|
5070
|
-
connections.set(
|
|
5985
|
+
connections.set(endpoint, conn);
|
|
5071
5986
|
return conn;
|
|
5072
5987
|
}
|
|
5073
5988
|
var bidiBeaconScripts = new Map;
|
|
@@ -5416,7 +6331,55 @@ class BidiPageDriver {
|
|
|
5416
6331
|
throw mapBidiError(e);
|
|
5417
6332
|
}
|
|
5418
6333
|
}
|
|
5419
|
-
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
|
+
}
|
|
5420
6383
|
const format = opts?.format ?? "png";
|
|
5421
6384
|
const params = {
|
|
5422
6385
|
context: this.contextId,
|
|
@@ -5441,12 +6404,18 @@ class BidiPageDriver {
|
|
|
5441
6404
|
await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: null, devicePixelRatio: null }).catch(() => {
|
|
5442
6405
|
return;
|
|
5443
6406
|
});
|
|
6407
|
+
await clearViewportRecord(this.browser.scheme, this.contextId);
|
|
5444
6408
|
return { applied: [] };
|
|
5445
6409
|
}
|
|
5446
6410
|
const applied = [];
|
|
5447
6411
|
try {
|
|
5448
6412
|
if (opts.width != null && opts.height != null) {
|
|
5449
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
|
+
});
|
|
5450
6419
|
applied.push("deviceMetrics");
|
|
5451
6420
|
}
|
|
5452
6421
|
if (opts.userAgent != null) {
|
|
@@ -5654,19 +6623,19 @@ class BidiPageDriver {
|
|
|
5654
6623
|
}
|
|
5655
6624
|
|
|
5656
6625
|
class BidiBrowserDriver {
|
|
5657
|
-
|
|
6626
|
+
endpoint;
|
|
5658
6627
|
timeoutMs;
|
|
5659
6628
|
scheme = "bidi";
|
|
5660
6629
|
lifetime = "session";
|
|
5661
6630
|
capabilities = BIDI_CAPABILITIES;
|
|
5662
6631
|
uidStability = "document-stamp";
|
|
5663
6632
|
snapshotFidelity = "dom-heuristic";
|
|
5664
|
-
constructor(
|
|
5665
|
-
this.
|
|
6633
|
+
constructor(endpoint, timeoutMs) {
|
|
6634
|
+
this.endpoint = endpoint;
|
|
5666
6635
|
this.timeoutMs = timeoutMs;
|
|
5667
6636
|
}
|
|
5668
6637
|
async listPages() {
|
|
5669
|
-
const conn = await getConnection(this.
|
|
6638
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs);
|
|
5670
6639
|
try {
|
|
5671
6640
|
const { contexts } = await conn.send("browsingContext.getTree", {}).catch((e) => {
|
|
5672
6641
|
throw mapBidiError(e);
|
|
@@ -5677,7 +6646,7 @@ class BidiBrowserDriver {
|
|
|
5677
6646
|
}
|
|
5678
6647
|
}
|
|
5679
6648
|
async newPage(url) {
|
|
5680
|
-
const conn = await getConnection(this.
|
|
6649
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs);
|
|
5681
6650
|
try {
|
|
5682
6651
|
const { context } = await conn.send("browsingContext.create", { type: "tab" }).catch((e) => {
|
|
5683
6652
|
throw mapBidiError(e);
|
|
@@ -5692,7 +6661,7 @@ class BidiBrowserDriver {
|
|
|
5692
6661
|
}
|
|
5693
6662
|
}
|
|
5694
6663
|
async closePage(id) {
|
|
5695
|
-
const conn = await getConnection(this.
|
|
6664
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs);
|
|
5696
6665
|
try {
|
|
5697
6666
|
await conn.send("browsingContext.close", { context: id }).catch((e) => {
|
|
5698
6667
|
throw mapBidiError(e);
|
|
@@ -5703,7 +6672,7 @@ class BidiBrowserDriver {
|
|
|
5703
6672
|
}
|
|
5704
6673
|
}
|
|
5705
6674
|
async activatePage(id) {
|
|
5706
|
-
const conn = await getConnection(this.
|
|
6675
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs);
|
|
5707
6676
|
try {
|
|
5708
6677
|
await conn.send("browsingContext.activate", { context: id }).catch((e) => {
|
|
5709
6678
|
throw mapBidiError(e);
|
|
@@ -5715,7 +6684,7 @@ class BidiBrowserDriver {
|
|
|
5715
6684
|
}
|
|
5716
6685
|
}
|
|
5717
6686
|
async page(selector) {
|
|
5718
|
-
const conn = await getConnection(this.
|
|
6687
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs);
|
|
5719
6688
|
let ctx;
|
|
5720
6689
|
try {
|
|
5721
6690
|
ctx = await resolveContext(conn, selector);
|
|
@@ -5727,13 +6696,13 @@ class BidiBrowserDriver {
|
|
|
5727
6696
|
return new BidiPageDriver(conn, ctx.context, info, this);
|
|
5728
6697
|
}
|
|
5729
6698
|
async installActivityBeacon(contextId) {
|
|
5730
|
-
const conn = await getConnection(this.
|
|
6699
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs).catch(() => {
|
|
5731
6700
|
return;
|
|
5732
6701
|
});
|
|
5733
6702
|
if (!conn)
|
|
5734
6703
|
return false;
|
|
5735
6704
|
try {
|
|
5736
|
-
const key = `${this.
|
|
6705
|
+
const key = `${this.endpoint}:${contextId}`;
|
|
5737
6706
|
if (!bidiBeaconScripts.has(key)) {
|
|
5738
6707
|
const registered = await conn.send("script.addPreloadScript", {
|
|
5739
6708
|
functionDeclaration: BEACON_FUNCTION_DECLARATION,
|
|
@@ -5754,7 +6723,7 @@ class BidiBrowserDriver {
|
|
|
5754
6723
|
}
|
|
5755
6724
|
}
|
|
5756
6725
|
async readActivityBeacon(contextId) {
|
|
5757
|
-
const conn = await getConnection(this.
|
|
6726
|
+
const conn = await getConnection(this.endpoint, this.timeoutMs).catch(() => {
|
|
5758
6727
|
return;
|
|
5759
6728
|
});
|
|
5760
6729
|
if (!conn)
|
|
@@ -5777,18 +6746,24 @@ class BidiBrowserDriver {
|
|
|
5777
6746
|
}
|
|
5778
6747
|
async dispose() {
|
|
5779
6748
|
for (const key of [...bidiBeaconScripts.keys()]) {
|
|
5780
|
-
if (key.startsWith(`${this.
|
|
6749
|
+
if (key.startsWith(`${this.endpoint}:`))
|
|
5781
6750
|
bidiBeaconScripts.delete(key);
|
|
5782
6751
|
}
|
|
5783
|
-
const conn = connections.get(this.
|
|
6752
|
+
const conn = connections.get(this.endpoint);
|
|
5784
6753
|
if (!conn)
|
|
5785
6754
|
return;
|
|
5786
|
-
connections.delete(this.
|
|
6755
|
+
connections.delete(this.endpoint);
|
|
6756
|
+
await conn.send("session.end", {}).catch(() => {
|
|
6757
|
+
return;
|
|
6758
|
+
});
|
|
5787
6759
|
conn.dispose();
|
|
5788
6760
|
}
|
|
5789
6761
|
}
|
|
5790
6762
|
function createFirefoxDriver(port, opts) {
|
|
5791
|
-
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);
|
|
5792
6767
|
}
|
|
5793
6768
|
|
|
5794
6769
|
// src/bidi/launch.ts
|
|
@@ -5796,7 +6771,7 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
5796
6771
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5797
6772
|
import { createConnection, createServer } from "node:net";
|
|
5798
6773
|
import { tmpdir } from "node:os";
|
|
5799
|
-
import { join as
|
|
6774
|
+
import { join as join10 } from "node:path";
|
|
5800
6775
|
function resolveBinary() {
|
|
5801
6776
|
const candidates = [
|
|
5802
6777
|
process.env.FIREFOX_BIN,
|
|
@@ -5870,7 +6845,7 @@ async function launchFirefox(opts = {}) {
|
|
|
5870
6845
|
const binary = resolveBinary();
|
|
5871
6846
|
const port = await pickPort(opts.port);
|
|
5872
6847
|
const isThrowaway = opts.profilePath === undefined;
|
|
5873
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
6848
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join10(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
5874
6849
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
5875
6850
|
if (opts.headless)
|
|
5876
6851
|
args.push("--headless");
|
|
@@ -5908,10 +6883,10 @@ async function launchFirefox(opts = {}) {
|
|
|
5908
6883
|
}
|
|
5909
6884
|
|
|
5910
6885
|
// src/backend.ts
|
|
5911
|
-
function resolveBrowserKind(argv) {
|
|
6886
|
+
function resolveBrowserKind(argv, env = process.env) {
|
|
5912
6887
|
const flagIdx = argv.indexOf("--browser");
|
|
5913
6888
|
const flagVal = flagIdx !== -1 ? argv[flagIdx + 1] : undefined;
|
|
5914
|
-
const raw = flagVal ??
|
|
6889
|
+
const raw = flagVal ?? env.CDP_BROWSER ?? "chrome";
|
|
5915
6890
|
if (raw !== "chrome" && raw !== "firefox") {
|
|
5916
6891
|
throw new Error(`unknown --browser '${raw}': expected 'chrome' or 'firefox' (or set CDP_BROWSER)`);
|
|
5917
6892
|
}
|
|
@@ -5928,7 +6903,79 @@ function stripBrowserFlag(argv) {
|
|
|
5928
6903
|
}
|
|
5929
6904
|
return out;
|
|
5930
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
|
+
}
|
|
5931
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
|
+
}
|
|
5932
6979
|
const launched = await launchFirefox({ headless: opts?.headless });
|
|
5933
6980
|
const driver = createFirefoxDriver(launched.port);
|
|
5934
6981
|
let disposed = false;
|
|
@@ -5946,9 +6993,9 @@ async function startFirefoxSession(opts) {
|
|
|
5946
6993
|
};
|
|
5947
6994
|
}
|
|
5948
6995
|
var sessionPromise;
|
|
5949
|
-
function getOrCreateFirefoxSession() {
|
|
6996
|
+
function getOrCreateFirefoxSession(opts) {
|
|
5950
6997
|
if (!sessionPromise)
|
|
5951
|
-
sessionPromise = startFirefoxSession();
|
|
6998
|
+
sessionPromise = startFirefoxSession({ endpoint: opts?.endpoint });
|
|
5952
6999
|
return sessionPromise;
|
|
5953
7000
|
}
|
|
5954
7001
|
async function disposeFirefoxSession() {
|
|
@@ -5993,6 +7040,11 @@ async function withPage3(driver, target, fn) {
|
|
|
5993
7040
|
}
|
|
5994
7041
|
var consoleBuffers = new Map;
|
|
5995
7042
|
var networkBuffers = new Map;
|
|
7043
|
+
function refuseWorkerSelector(driver, target) {
|
|
7044
|
+
if (isWorkerSelector(target) && !driver.capabilities.has("worker.targets")) {
|
|
7045
|
+
throw new BidiToolError(WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
7046
|
+
}
|
|
7047
|
+
}
|
|
5996
7048
|
function bufferFor(map, id) {
|
|
5997
7049
|
let b = map.get(id);
|
|
5998
7050
|
if (!b) {
|
|
@@ -6021,6 +7073,7 @@ async function captureFresh(page, durationMs) {
|
|
|
6021
7073
|
}
|
|
6022
7074
|
}
|
|
6023
7075
|
async function listConsoleMessages2(driver, args = {}) {
|
|
7076
|
+
refuseWorkerSelector(driver, args.target);
|
|
6024
7077
|
return withPage3(driver, args.target, async (page) => {
|
|
6025
7078
|
if (args.reload)
|
|
6026
7079
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
@@ -6029,6 +7082,7 @@ async function listConsoleMessages2(driver, args = {}) {
|
|
|
6029
7082
|
});
|
|
6030
7083
|
}
|
|
6031
7084
|
async function getConsoleMessage2(driver, args = {}) {
|
|
7085
|
+
refuseWorkerSelector(driver, args.target);
|
|
6032
7086
|
return withPage3(driver, args.target, async (page) => {
|
|
6033
7087
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
6034
7088
|
const idx = args.index ?? 0;
|
|
@@ -6039,6 +7093,7 @@ async function getConsoleMessage2(driver, args = {}) {
|
|
|
6039
7093
|
});
|
|
6040
7094
|
}
|
|
6041
7095
|
async function listNetworkRequests2(driver, args = {}) {
|
|
7096
|
+
refuseWorkerSelector(driver, args.target);
|
|
6042
7097
|
return withPage3(driver, args.target, async (page) => {
|
|
6043
7098
|
if (args.reload)
|
|
6044
7099
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
@@ -6049,6 +7104,7 @@ async function listNetworkRequests2(driver, args = {}) {
|
|
|
6049
7104
|
});
|
|
6050
7105
|
}
|
|
6051
7106
|
async function getNetworkRequest2(driver, args = {}) {
|
|
7107
|
+
refuseWorkerSelector(driver, args.target);
|
|
6052
7108
|
if (!args.requestId && !args.url)
|
|
6053
7109
|
throw new BidiToolError("get_network_request requires requestId or url");
|
|
6054
7110
|
return withPage3(driver, args.target, async (page) => {
|
|
@@ -6139,30 +7195,1721 @@ var FIREFOX_TOOLS = {
|
|
|
6139
7195
|
...LEASE_TOOLS
|
|
6140
7196
|
};
|
|
6141
7197
|
|
|
6142
|
-
// src/
|
|
6143
|
-
var
|
|
6144
|
-
|
|
6145
|
-
|
|
6146
|
-
|
|
6147
|
-
|
|
6148
|
-
|
|
6149
|
-
|
|
6150
|
-
|
|
6151
|
-
|
|
6152
|
-
|
|
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
|
+
|
|
8871
|
+
// src/cli.ts
|
|
8872
|
+
var USAGE = `cdp-toolkit: raw single-target CDP, 29-tool chrome-devtools-mcp parity, plus a Firefox backend over WebDriver BiDi.
|
|
8873
|
+
|
|
8874
|
+
Usage:
|
|
8875
|
+
bun run src/cli.ts <tool> [--browser chrome|firefox] [--connect <port|host:port|ws-url>] [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
|
|
8876
|
+
bun run src/cli.ts --list [--browser chrome|firefox]
|
|
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
|
|
8880
|
+
|
|
8881
|
+
Examples:
|
|
8882
|
+
bun run src/cli.ts list_pages
|
|
8883
|
+
bun run src/cli.ts navigate_page --target index:0 --url https://example.com
|
|
6153
8884
|
bun run src/cli.ts click --target index:0 --uid 42
|
|
6154
8885
|
bun run src/cli.ts evaluate_script --json '{"expression":"1+2"}'
|
|
6155
8886
|
bun run src/cli.ts take_screenshot --target url:example --fullPage true
|
|
6156
8887
|
bun run src/cli.ts --browser firefox take_snapshot
|
|
6157
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.
|
|
6158
8895
|
|
|
6159
8896
|
Backend selection: --browser chrome|firefox, else the CDP_BROWSER env var, else "chrome" (default,
|
|
6160
|
-
zero behavior change for existing users). Firefox
|
|
6161
|
-
|
|
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.
|
|
6162
8907
|
|
|
6163
8908
|
Target selector grammar: active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>
|
|
6164
|
-
plus worker:<substr>
|
|
6165
|
-
|
|
8909
|
+
plus worker:<substr> (Chrome only) for evaluate_script, list_network_requests, get_network_request
|
|
8910
|
+
and list_console_messages ONLY: an MV3 extension's background service worker, e.g.
|
|
8911
|
+
worker:<extension-id>. An idle-evicted worker is started first unless wake:false. Recording a
|
|
8912
|
+
worker keeps it alive for the length of the capture.
|
|
6166
8913
|
Leases: pass --lease <token> to act on a tab another agent claimed with claim_page (MCP only).
|
|
6167
8914
|
Run list_leases to see who holds what; release_page --lease <token> gives a lease back.
|
|
6168
8915
|
Run with --list to print every tool name available under the selected backend.
|
|
@@ -6179,7 +8926,7 @@ function coerce(raw) {
|
|
|
6179
8926
|
return raw;
|
|
6180
8927
|
}
|
|
6181
8928
|
function parseArgv(argv) {
|
|
6182
|
-
const out = { list: false, capabilities: false, args: {} };
|
|
8929
|
+
const out = { list: false, capabilities: false, help: false, args: {} };
|
|
6183
8930
|
let jsonObj;
|
|
6184
8931
|
const flagPairs = [];
|
|
6185
8932
|
for (let i = 0;i < argv.length; i++) {
|
|
@@ -6192,6 +8939,10 @@ function parseArgv(argv) {
|
|
|
6192
8939
|
out.capabilities = true;
|
|
6193
8940
|
continue;
|
|
6194
8941
|
}
|
|
8942
|
+
if (token === "--help" || token === "-h") {
|
|
8943
|
+
out.help = true;
|
|
8944
|
+
continue;
|
|
8945
|
+
}
|
|
6195
8946
|
if (token.startsWith("--")) {
|
|
6196
8947
|
const key = token.slice(2);
|
|
6197
8948
|
const next = argv[i + 1];
|
|
@@ -6226,10 +8977,61 @@ function parseArgv(argv) {
|
|
|
6226
8977
|
function isToolName(name) {
|
|
6227
8978
|
return Object.prototype.hasOwnProperty.call(TOOLS, name);
|
|
6228
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
|
+
}
|
|
6229
9017
|
async function main() {
|
|
6230
9018
|
const rawArgv = process.argv.slice(2);
|
|
6231
|
-
const browser =
|
|
6232
|
-
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
|
+
}
|
|
6233
9035
|
const availability = toolAvailability(browser);
|
|
6234
9036
|
const availableSet = new Set(availability.available);
|
|
6235
9037
|
if (parsed.capabilities) {
|
|
@@ -6290,7 +9092,7 @@ async function main() {
|
|
|
6290
9092
|
`);
|
|
6291
9093
|
return 1;
|
|
6292
9094
|
}
|
|
6293
|
-
const session2 = await startFirefoxSession();
|
|
9095
|
+
const session2 = await startFirefoxSession({ endpoint });
|
|
6294
9096
|
try {
|
|
6295
9097
|
const result = await neutralFn(session2.driver, parsed.args);
|
|
6296
9098
|
process.stdout.write(`${JSON.stringify(result, null, 2)}
|