cdp-toolkit 1.2.0 → 1.6.0
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 +223 -0
- package/CONTRACT.md +4 -2
- package/README.md +67 -20
- package/dist/cli.js +783 -63
- package/dist/index.js +694 -48
- package/dist/mcp.js +982 -66
- package/package.json +5 -3
- package/src/bidi/driver.ts +140 -1
- package/src/capabilities.ts +1 -1
- package/src/cdp/driver.ts +148 -1
- package/src/driver.ts +110 -1
- package/src/firefox-tools.ts +1 -1
- package/src/index.ts +22 -4
- package/src/leases-tools.ts +9 -3
- package/src/leases.ts +9 -1
- package/src/manifest.ts +198 -2
- package/src/mcp.ts +1 -1
- package/src/origins.ts +266 -0
- package/src/shared-tools.ts +317 -11
- package/src/tools/screencast.ts +741 -0
package/dist/index.js
CHANGED
|
@@ -11,6 +11,9 @@ function leaseTtlMs() {
|
|
|
11
11
|
const raw = Number(process.env.CDP_LEASE_TTL_MS);
|
|
12
12
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
|
|
13
13
|
}
|
|
14
|
+
function defaultLabel() {
|
|
15
|
+
return `pid-${process.pid}`;
|
|
16
|
+
}
|
|
14
17
|
function safeId(id) {
|
|
15
18
|
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
16
19
|
}
|
|
@@ -677,6 +680,8 @@ var REQUIRED_CAPABILITIES = {
|
|
|
677
680
|
performance_trace: ["trace.performance"],
|
|
678
681
|
take_heapsnapshot: ["heap.snapshot"],
|
|
679
682
|
lighthouse_audit: ["audit.lighthouse"],
|
|
683
|
+
start_screen_recording: ["capture.screencast"],
|
|
684
|
+
stop_screen_recording: ["capture.screencast"],
|
|
680
685
|
mock_request: ["network.intercept"],
|
|
681
686
|
list_mocks: ["network.intercept"],
|
|
682
687
|
clear_mocks: ["network.intercept"]
|
|
@@ -702,6 +707,31 @@ function decodeUid(uid) {
|
|
|
702
707
|
throw driverError("foreign-uid", `malformed cdp uid payload: ${uid}`);
|
|
703
708
|
return n;
|
|
704
709
|
}
|
|
710
|
+
function normalizeCdpCookie(c) {
|
|
711
|
+
const same = (c.sameSite ?? "").toLowerCase();
|
|
712
|
+
const sameSite = same === "strict" || same === "lax" || same === "none" ? same : "default";
|
|
713
|
+
return {
|
|
714
|
+
name: c.name,
|
|
715
|
+
value: c.value,
|
|
716
|
+
domain: c.domain,
|
|
717
|
+
path: c.path,
|
|
718
|
+
expires: typeof c.expires === "number" ? c.expires : -1,
|
|
719
|
+
size: typeof c.size === "number" ? c.size : Buffer.byteLength(`${c.name}${c.value}`, "utf8"),
|
|
720
|
+
httpOnly: c.httpOnly === true,
|
|
721
|
+
secure: c.secure === true,
|
|
722
|
+
sameSite,
|
|
723
|
+
session: c.session === true
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function cdpSameSite(sameSite) {
|
|
727
|
+
if (sameSite === "strict")
|
|
728
|
+
return "Strict";
|
|
729
|
+
if (sameSite === "lax")
|
|
730
|
+
return "Lax";
|
|
731
|
+
if (sameSite === "none")
|
|
732
|
+
return "None";
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
705
735
|
async function centerOf(conn, objectId) {
|
|
706
736
|
const fn = "function(){this.scrollIntoView({block:'center',inline:'center'});const r=this.getBoundingClientRect();if(r.width===0&&r.height===0)return null;return {x:r.left+r.width/2,y:r.top+r.height/2};}";
|
|
707
737
|
const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
@@ -907,7 +937,8 @@ var CDP_CAPABILITIES = new Set([
|
|
|
907
937
|
"snapshot.accessibilityTree",
|
|
908
938
|
"input.insertTextAtomic",
|
|
909
939
|
"locate.text",
|
|
910
|
-
"locate.xpath"
|
|
940
|
+
"locate.xpath",
|
|
941
|
+
"capture.screencast"
|
|
911
942
|
]);
|
|
912
943
|
|
|
913
944
|
class CdpPageDriver {
|
|
@@ -1224,6 +1255,38 @@ class CdpPageDriver {
|
|
|
1224
1255
|
});
|
|
1225
1256
|
});
|
|
1226
1257
|
}
|
|
1258
|
+
async getCookies() {
|
|
1259
|
+
await this.ensureDomain("Network");
|
|
1260
|
+
const { cookies } = await this.conn.send("Network.getCookies");
|
|
1261
|
+
return (cookies ?? []).map(normalizeCdpCookie);
|
|
1262
|
+
}
|
|
1263
|
+
async setCookie(params) {
|
|
1264
|
+
await this.ensureDomain("Network");
|
|
1265
|
+
const sameSite = cdpSameSite(params.sameSite);
|
|
1266
|
+
const { success } = await this.conn.send("Network.setCookie", {
|
|
1267
|
+
name: params.name,
|
|
1268
|
+
value: params.value,
|
|
1269
|
+
...params.url !== undefined ? { url: params.url } : {},
|
|
1270
|
+
...params.domain !== undefined ? { domain: params.domain } : {},
|
|
1271
|
+
...params.path !== undefined ? { path: params.path } : {},
|
|
1272
|
+
...params.expires !== undefined ? { expires: params.expires } : {},
|
|
1273
|
+
...params.httpOnly !== undefined ? { httpOnly: params.httpOnly } : {},
|
|
1274
|
+
...params.secure !== undefined ? { secure: params.secure } : {},
|
|
1275
|
+
...sameSite !== undefined ? { sameSite } : {}
|
|
1276
|
+
});
|
|
1277
|
+
if (success !== true) {
|
|
1278
|
+
throw driverError("page-error", `Network.setCookie declined the cookie '${params.name}'. Chrome rejects a cookie whose domain does not match the url, a secure cookie on an insecure origin, and an oversized name or value.`);
|
|
1279
|
+
}
|
|
1280
|
+
}
|
|
1281
|
+
async deleteCookies(filter) {
|
|
1282
|
+
await this.ensureDomain("Network");
|
|
1283
|
+
await this.conn.send("Network.deleteCookies", {
|
|
1284
|
+
name: filter.name,
|
|
1285
|
+
...filter.url !== undefined ? { url: filter.url } : {},
|
|
1286
|
+
...filter.domain !== undefined ? { domain: filter.domain } : {},
|
|
1287
|
+
...filter.path !== undefined ? { path: filter.path } : {}
|
|
1288
|
+
});
|
|
1289
|
+
}
|
|
1227
1290
|
async startBodyCapture() {
|
|
1228
1291
|
await this.ensureDomain("Network");
|
|
1229
1292
|
return async () => {
|
|
@@ -1283,6 +1346,10 @@ async function withBrowserConn(fn) {
|
|
|
1283
1346
|
}
|
|
1284
1347
|
}
|
|
1285
1348
|
function noSuchTarget(e) {
|
|
1349
|
+
if (e instanceof LeaseConflictError)
|
|
1350
|
+
throw e;
|
|
1351
|
+
if (typeof e?.code === "string")
|
|
1352
|
+
throw e;
|
|
1286
1353
|
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
1287
1354
|
}
|
|
1288
1355
|
|
|
@@ -1326,6 +1393,97 @@ function createCdpDriver() {
|
|
|
1326
1393
|
return new CdpBrowserDriver;
|
|
1327
1394
|
}
|
|
1328
1395
|
|
|
1396
|
+
// src/origins.ts
|
|
1397
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1398
|
+
import { join as join2 } from "node:path";
|
|
1399
|
+
function safeId2(id) {
|
|
1400
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1401
|
+
}
|
|
1402
|
+
function originFile(backend, targetId) {
|
|
1403
|
+
return join2(leaseDir(), `origin-${backend}-${safeId2(targetId)}.json`);
|
|
1404
|
+
}
|
|
1405
|
+
var ORIGIN_FILE_RE = /^origin-(chrome|firefox)-(.+)\.json$/;
|
|
1406
|
+
async function recordOrigin(backend, targetId, opts = {}) {
|
|
1407
|
+
const record = {
|
|
1408
|
+
backend,
|
|
1409
|
+
targetId,
|
|
1410
|
+
label: typeof opts.label === "string" && opts.label.length ? opts.label : defaultLabel(),
|
|
1411
|
+
pid: process.pid,
|
|
1412
|
+
createdAt: opts.now ?? Date.now()
|
|
1413
|
+
};
|
|
1414
|
+
const file = originFile(backend, targetId);
|
|
1415
|
+
try {
|
|
1416
|
+
await mkdir2(leaseDir(), { recursive: true });
|
|
1417
|
+
try {
|
|
1418
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1419
|
+
return;
|
|
1420
|
+
} catch (err) {
|
|
1421
|
+
if (err.code !== "EEXIST")
|
|
1422
|
+
throw err;
|
|
1423
|
+
}
|
|
1424
|
+
await unlink2(file).catch(() => {
|
|
1425
|
+
return;
|
|
1426
|
+
});
|
|
1427
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1428
|
+
} catch {}
|
|
1429
|
+
}
|
|
1430
|
+
async function listOrigins(opts = {}) {
|
|
1431
|
+
let names = [];
|
|
1432
|
+
try {
|
|
1433
|
+
names = await readdir2(leaseDir());
|
|
1434
|
+
} catch {
|
|
1435
|
+
return [];
|
|
1436
|
+
}
|
|
1437
|
+
const live = opts.liveIds === undefined ? undefined : new Set(opts.liveIds.map((id) => safeId2(id)));
|
|
1438
|
+
const out = [];
|
|
1439
|
+
for (const name of names) {
|
|
1440
|
+
const parts = ORIGIN_FILE_RE.exec(name);
|
|
1441
|
+
if (!parts)
|
|
1442
|
+
continue;
|
|
1443
|
+
const backend = parts[1];
|
|
1444
|
+
const nameId = parts[2];
|
|
1445
|
+
const scoped = live !== undefined && (opts.liveBackend === undefined || opts.liveBackend === backend);
|
|
1446
|
+
if (scoped && !live.has(nameId)) {
|
|
1447
|
+
await unlink2(join2(leaseDir(), name)).catch(() => {
|
|
1448
|
+
return;
|
|
1449
|
+
});
|
|
1450
|
+
continue;
|
|
1451
|
+
}
|
|
1452
|
+
const fromName = { backend, targetId: nameId, label: "", pid: 0, createdAt: 0 };
|
|
1453
|
+
let rec;
|
|
1454
|
+
try {
|
|
1455
|
+
rec = JSON.parse(await readFile2(join2(leaseDir(), name), "utf8"));
|
|
1456
|
+
} catch (err) {
|
|
1457
|
+
const code = err.code;
|
|
1458
|
+
if (code === "ENOENT")
|
|
1459
|
+
continue;
|
|
1460
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
1461
|
+
continue;
|
|
1462
|
+
}
|
|
1463
|
+
if (typeof rec?.targetId !== "string" || typeof rec?.label !== "string" || typeof rec?.createdAt !== "number") {
|
|
1464
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
1465
|
+
continue;
|
|
1466
|
+
}
|
|
1467
|
+
out.push({ backend: rec.backend, targetId: rec.targetId, label: rec.label, pid: rec.pid, createdAt: rec.createdAt });
|
|
1468
|
+
}
|
|
1469
|
+
return out;
|
|
1470
|
+
}
|
|
1471
|
+
async function originIndex(backend, liveIds) {
|
|
1472
|
+
const rows = await listOrigins({ liveIds, liveBackend: backend }).catch(() => []);
|
|
1473
|
+
const index = new Map;
|
|
1474
|
+
for (const row of rows) {
|
|
1475
|
+
if (row.backend !== backend)
|
|
1476
|
+
continue;
|
|
1477
|
+
index.set(row.targetId, row);
|
|
1478
|
+
}
|
|
1479
|
+
return index;
|
|
1480
|
+
}
|
|
1481
|
+
async function newTrackedPage(driver, backend, opts = {}) {
|
|
1482
|
+
const page = await driver.newPage(opts.url);
|
|
1483
|
+
await recordOrigin(backend, page.id, { label: opts.label });
|
|
1484
|
+
return page;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1329
1487
|
// src/leases-tools.ts
|
|
1330
1488
|
class LeaseToolError extends Error {
|
|
1331
1489
|
}
|
|
@@ -1334,12 +1492,12 @@ function backendOf(driver) {
|
|
|
1334
1492
|
}
|
|
1335
1493
|
async function claimPage(driver, args = {}) {
|
|
1336
1494
|
const backend = backendOf(driver);
|
|
1337
|
-
const label = typeof args.label === "string" && args.label.length ? args.label :
|
|
1495
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1338
1496
|
const pages = await driver.listPages();
|
|
1339
1497
|
let targetId = args.targetId;
|
|
1340
1498
|
let url = args.url ?? "about:blank";
|
|
1341
1499
|
if (targetId === undefined || targetId === "") {
|
|
1342
|
-
const page = await driver
|
|
1500
|
+
const page = await newTrackedPage(driver, backend, { url: args.url, label });
|
|
1343
1501
|
targetId = page.id;
|
|
1344
1502
|
url = page.url;
|
|
1345
1503
|
} else {
|
|
@@ -1381,8 +1539,8 @@ var LEASE_TOOLS = {
|
|
|
1381
1539
|
};
|
|
1382
1540
|
|
|
1383
1541
|
// src/shared-tools.ts
|
|
1384
|
-
import { mkdir as
|
|
1385
|
-
import { join as
|
|
1542
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
1543
|
+
import { dirname, join as join3 } from "node:path";
|
|
1386
1544
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1387
1545
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1388
1546
|
|
|
@@ -1450,16 +1608,35 @@ function locatorOf(args) {
|
|
|
1450
1608
|
throw new SharedToolError("provide exactly one of { uid } or { selector }");
|
|
1451
1609
|
return hasUid ? { uid: args.uid } : { css: args.selector };
|
|
1452
1610
|
}
|
|
1611
|
+
async function reapSet(driver, pages, all) {
|
|
1612
|
+
if (all === true)
|
|
1613
|
+
return pages.map((p) => p.id);
|
|
1614
|
+
try {
|
|
1615
|
+
return (await driver.listPages({ all: true })).map((p) => p.id);
|
|
1616
|
+
} catch {
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
}
|
|
1453
1620
|
async function listPages(driver, args = {}) {
|
|
1454
1621
|
const pages = await driver.listPages({ all: args.all });
|
|
1455
|
-
|
|
1622
|
+
const ledger = await originIndex(backendOf2(driver), await reapSet(driver, pages, args.all));
|
|
1623
|
+
const annotated = pages.map((p) => {
|
|
1624
|
+
const rec = ledger.get(p.id);
|
|
1625
|
+
if (!rec)
|
|
1626
|
+
return { ...p, origin: "unknown" };
|
|
1627
|
+
if (rec.unreadable !== undefined)
|
|
1628
|
+
return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
|
|
1629
|
+
return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
|
|
1630
|
+
});
|
|
1631
|
+
return { pages: annotated, count: annotated.length };
|
|
1456
1632
|
}
|
|
1457
1633
|
async function newPage(driver, args = {}) {
|
|
1458
|
-
const
|
|
1634
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1635
|
+
const p = await newTrackedPage(driver, backendOf2(driver), { url: args.url, label });
|
|
1459
1636
|
if (args.claim !== true)
|
|
1460
1637
|
return { targetId: p.id, url: p.url };
|
|
1461
1638
|
const { record, token } = await claimLease(backendOf2(driver), p.id, {
|
|
1462
|
-
label
|
|
1639
|
+
label,
|
|
1463
1640
|
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
|
|
1464
1641
|
});
|
|
1465
1642
|
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
@@ -1475,8 +1652,8 @@ async function closePage(driver, args) {
|
|
|
1475
1652
|
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
1476
1653
|
}
|
|
1477
1654
|
async function writeSelectedFile(id) {
|
|
1478
|
-
await
|
|
1479
|
-
await
|
|
1655
|
+
await mkdir3(STATE_DIR, { recursive: true });
|
|
1656
|
+
await writeFile3(join3(STATE_DIR, "selected"), id, "utf8");
|
|
1480
1657
|
}
|
|
1481
1658
|
async function selectPage(driver, args) {
|
|
1482
1659
|
if (args.target === undefined || args.target === "")
|
|
@@ -1516,11 +1693,108 @@ async function waitForText(driver, args) {
|
|
|
1516
1693
|
return { found: true, waitedMs: r.elapsedMs };
|
|
1517
1694
|
});
|
|
1518
1695
|
}
|
|
1696
|
+
function serializeForSink(value) {
|
|
1697
|
+
const json = JSON.stringify(value === undefined ? null : value, null, 2);
|
|
1698
|
+
return json ?? "null";
|
|
1699
|
+
}
|
|
1700
|
+
async function writeJsonSink(savePath, value) {
|
|
1701
|
+
const path = savePath.startsWith("/") ? savePath : join3(ARTIFACT_DIR, savePath);
|
|
1702
|
+
await mkdir3(dirname(path), { recursive: true });
|
|
1703
|
+
const json = serializeForSink(value);
|
|
1704
|
+
await writeFile3(path, json, "utf8");
|
|
1705
|
+
return { path, bytes: Buffer.byteLength(json, "utf8") };
|
|
1706
|
+
}
|
|
1707
|
+
function sinkRequested(savePath) {
|
|
1708
|
+
return savePath !== undefined && savePath !== "";
|
|
1709
|
+
}
|
|
1519
1710
|
async function evaluateScript(driver, args) {
|
|
1520
1711
|
if (typeof args.expression !== "string" || args.expression.length === 0) {
|
|
1521
1712
|
throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
|
|
1522
1713
|
}
|
|
1523
|
-
return withPage2(driver, args.target, (page) =>
|
|
1714
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1715
|
+
const value = await page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true });
|
|
1716
|
+
if (!sinkRequested(args.savePath))
|
|
1717
|
+
return value;
|
|
1718
|
+
const { path, bytes } = await writeJsonSink(args.savePath, value);
|
|
1719
|
+
const result = {
|
|
1720
|
+
path,
|
|
1721
|
+
bytes,
|
|
1722
|
+
type: value === null ? "null" : typeof value,
|
|
1723
|
+
target: target3(page.info)
|
|
1724
|
+
};
|
|
1725
|
+
return result;
|
|
1726
|
+
});
|
|
1727
|
+
}
|
|
1728
|
+
function cookieDomainMatches(cookieDomain, filter) {
|
|
1729
|
+
const bare = (d) => (d.startsWith(".") ? d.slice(1) : d).toLowerCase();
|
|
1730
|
+
const c = bare(cookieDomain);
|
|
1731
|
+
const f = bare(filter);
|
|
1732
|
+
return c === f || c.endsWith(`.${f}`);
|
|
1733
|
+
}
|
|
1734
|
+
function filterCookies(cookies, filter) {
|
|
1735
|
+
return cookies.filter((c) => {
|
|
1736
|
+
if (filter.name !== undefined && filter.name !== "" && c.name !== filter.name)
|
|
1737
|
+
return false;
|
|
1738
|
+
if (filter.domain !== undefined && filter.domain !== "" && !cookieDomainMatches(c.domain, filter.domain))
|
|
1739
|
+
return false;
|
|
1740
|
+
return true;
|
|
1741
|
+
});
|
|
1742
|
+
}
|
|
1743
|
+
async function listCookies(driver, args = {}) {
|
|
1744
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1745
|
+
const all = await page.getCookies();
|
|
1746
|
+
const cookies = filterCookies(all, { domain: args.domain, name: args.name });
|
|
1747
|
+
if (!sinkRequested(args.savePath)) {
|
|
1748
|
+
return { cookies, count: cookies.length, target: target3(page.info) };
|
|
1749
|
+
}
|
|
1750
|
+
const { path, bytes } = await writeJsonSink(args.savePath, cookies);
|
|
1751
|
+
return { path, bytes, count: cookies.length, target: target3(page.info) };
|
|
1752
|
+
});
|
|
1753
|
+
}
|
|
1754
|
+
function assertCookieSite(args, toolName) {
|
|
1755
|
+
const hasUrl = args.url !== undefined && args.url !== "";
|
|
1756
|
+
const hasDomain = args.domain !== undefined && args.domain !== "";
|
|
1757
|
+
if (!hasUrl && !hasDomain) {
|
|
1758
|
+
throw new Error(`${toolName} requires either 'url' or 'domain': a cookie has to be attributed to a site, and neither was given.`);
|
|
1759
|
+
}
|
|
1760
|
+
}
|
|
1761
|
+
function assertCookieName(name, toolName) {
|
|
1762
|
+
if (name === undefined || name === "") {
|
|
1763
|
+
throw new Error(`${toolName} requires a non-empty 'name'.`);
|
|
1764
|
+
}
|
|
1765
|
+
}
|
|
1766
|
+
async function setCookie(driver, args) {
|
|
1767
|
+
assertCookieName(args.name, "set_cookie");
|
|
1768
|
+
if (typeof args.value !== "string")
|
|
1769
|
+
throw new Error("set_cookie requires a string 'value'.");
|
|
1770
|
+
assertCookieSite(args, "set_cookie");
|
|
1771
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1772
|
+
await page.setCookie({
|
|
1773
|
+
name: args.name,
|
|
1774
|
+
value: args.value,
|
|
1775
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1776
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1777
|
+
...args.path !== undefined ? { path: args.path } : {},
|
|
1778
|
+
...args.expires !== undefined ? { expires: args.expires } : {},
|
|
1779
|
+
...args.httpOnly !== undefined ? { httpOnly: args.httpOnly } : {},
|
|
1780
|
+
...args.secure !== undefined ? { secure: args.secure } : {},
|
|
1781
|
+
...args.sameSite !== undefined ? { sameSite: args.sameSite } : {}
|
|
1782
|
+
});
|
|
1783
|
+
return { set: true, target: target3(page.info) };
|
|
1784
|
+
});
|
|
1785
|
+
}
|
|
1786
|
+
async function deleteCookies(driver, args) {
|
|
1787
|
+
assertCookieName(args.name, "delete_cookies");
|
|
1788
|
+
assertCookieSite(args, "delete_cookies");
|
|
1789
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1790
|
+
await page.deleteCookies({
|
|
1791
|
+
name: args.name,
|
|
1792
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1793
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1794
|
+
...args.path !== undefined ? { path: args.path } : {}
|
|
1795
|
+
});
|
|
1796
|
+
return { deleted: true, target: target3(page.info) };
|
|
1797
|
+
});
|
|
1524
1798
|
}
|
|
1525
1799
|
var BARE_TOKEN_EXTRAS = new Set(["disabled", "focused"]);
|
|
1526
1800
|
var UNQUOTED_EXTRAS = new Set(["checked", "expanded", "url"]);
|
|
@@ -1631,10 +1905,10 @@ async function takeScreenshot(driver, args = {}) {
|
|
|
1631
1905
|
const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
|
|
1632
1906
|
if (data.byteLength === 0)
|
|
1633
1907
|
throw new SharedToolError("captureScreenshot returned empty data");
|
|
1634
|
-
await
|
|
1908
|
+
await mkdir3(ARTIFACT_DIR, { recursive: true });
|
|
1635
1909
|
const ext = outFormat === "jpeg" ? "jpg" : "png";
|
|
1636
|
-
const path = args.savePath ??
|
|
1637
|
-
await
|
|
1910
|
+
const path = args.savePath ?? join3(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
1911
|
+
await writeFile3(path, Buffer.from(data));
|
|
1638
1912
|
const result = {
|
|
1639
1913
|
path,
|
|
1640
1914
|
bytes: data.byteLength,
|
|
@@ -1681,6 +1955,9 @@ var SHARED_TOOLS = {
|
|
|
1681
1955
|
navigate_page: navigatePage,
|
|
1682
1956
|
wait_for: waitForText,
|
|
1683
1957
|
evaluate_script: evaluateScript,
|
|
1958
|
+
list_cookies: listCookies,
|
|
1959
|
+
set_cookie: setCookie,
|
|
1960
|
+
delete_cookies: deleteCookies,
|
|
1684
1961
|
take_snapshot: takeSnapshot,
|
|
1685
1962
|
click,
|
|
1686
1963
|
hover,
|
|
@@ -1697,10 +1974,10 @@ var SHARED_TOOLS = {
|
|
|
1697
1974
|
};
|
|
1698
1975
|
|
|
1699
1976
|
// src/tools/console.ts
|
|
1700
|
-
import { readFile as
|
|
1977
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1701
1978
|
|
|
1702
1979
|
// src/tools/recorder.ts
|
|
1703
|
-
import { mkdir as
|
|
1980
|
+
import { mkdir as mkdir4, appendFile, writeFile as writeFile4, copyFile } from "node:fs/promises";
|
|
1704
1981
|
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1705
1982
|
function recFile(targetId) {
|
|
1706
1983
|
return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
|
|
@@ -1721,11 +1998,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
1721
1998
|
async function startRecorder(target, opts = {}) {
|
|
1722
1999
|
const network = opts.network ?? true;
|
|
1723
2000
|
const consoleCap = opts.console ?? true;
|
|
1724
|
-
await
|
|
2001
|
+
await mkdir4(ARTIFACT_DIR2, { recursive: true });
|
|
1725
2002
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
1726
2003
|
const file = opts.file ?? recFile(resolved.id);
|
|
1727
2004
|
if (opts.truncate) {
|
|
1728
|
-
await
|
|
2005
|
+
await writeFile4(file, "");
|
|
1729
2006
|
}
|
|
1730
2007
|
let chain = Promise.resolve();
|
|
1731
2008
|
let dropped = 0;
|
|
@@ -1867,7 +2144,7 @@ function lineToEntry(line, index) {
|
|
|
1867
2144
|
async function readConsoleEntries(file) {
|
|
1868
2145
|
let raw;
|
|
1869
2146
|
try {
|
|
1870
|
-
raw = await
|
|
2147
|
+
raw = await readFile3(file, "utf8");
|
|
1871
2148
|
} catch {
|
|
1872
2149
|
return [];
|
|
1873
2150
|
}
|
|
@@ -1923,7 +2200,7 @@ async function getConsoleMessage(args = {}) {
|
|
|
1923
2200
|
}
|
|
1924
2201
|
|
|
1925
2202
|
// src/tools/network.ts
|
|
1926
|
-
import { readFile as
|
|
2203
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1927
2204
|
var DEFAULT_CAPTURE_MS2 = 2500;
|
|
1928
2205
|
function asHeaders(v) {
|
|
1929
2206
|
if (!v || typeof v !== "object")
|
|
@@ -1990,7 +2267,7 @@ function correlate(lines) {
|
|
|
1990
2267
|
async function readNetworkRequests(file) {
|
|
1991
2268
|
let raw;
|
|
1992
2269
|
try {
|
|
1993
|
-
raw = await
|
|
2270
|
+
raw = await readFile4(file, "utf8");
|
|
1994
2271
|
} catch {
|
|
1995
2272
|
return [];
|
|
1996
2273
|
}
|
|
@@ -2072,8 +2349,8 @@ async function getNetworkRequest(args = {}) {
|
|
|
2072
2349
|
}
|
|
2073
2350
|
|
|
2074
2351
|
// src/tools/performance.ts
|
|
2075
|
-
import { mkdir as
|
|
2076
|
-
import { join as
|
|
2352
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, stat, unlink as unlink3 } from "node:fs/promises";
|
|
2353
|
+
import { join as join4 } from "node:path";
|
|
2077
2354
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2078
2355
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
2079
2356
|
var DEFAULT_CATEGORIES = [
|
|
@@ -2091,7 +2368,7 @@ function shortId(id) {
|
|
|
2091
2368
|
return id.slice(0, 8);
|
|
2092
2369
|
}
|
|
2093
2370
|
function stateFilePath() {
|
|
2094
|
-
return
|
|
2371
|
+
return join4(STATE_DIR2, "trace-state.json");
|
|
2095
2372
|
}
|
|
2096
2373
|
function bufferTraceData(conn, sink) {
|
|
2097
2374
|
return conn.on("Tracing.dataCollected", (params) => {
|
|
@@ -2113,10 +2390,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
2113
2390
|
return sink;
|
|
2114
2391
|
}
|
|
2115
2392
|
async function writeTrace(events, targetId) {
|
|
2116
|
-
await
|
|
2117
|
-
const path =
|
|
2393
|
+
await mkdir5(ARTIFACT_DIR3, { recursive: true });
|
|
2394
|
+
const path = join4(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
2118
2395
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
2119
|
-
await
|
|
2396
|
+
await writeFile5(path, body, "utf8");
|
|
2120
2397
|
const { size } = await stat(path);
|
|
2121
2398
|
return { path, bytes: size };
|
|
2122
2399
|
}
|
|
@@ -2136,7 +2413,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
2136
2413
|
throw err;
|
|
2137
2414
|
}
|
|
2138
2415
|
liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
|
|
2139
|
-
await
|
|
2416
|
+
await mkdir5(STATE_DIR2, { recursive: true });
|
|
2140
2417
|
const state = {
|
|
2141
2418
|
targetId: target.id,
|
|
2142
2419
|
url: target.url,
|
|
@@ -2144,7 +2421,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
2144
2421
|
pid: process.pid,
|
|
2145
2422
|
categories
|
|
2146
2423
|
};
|
|
2147
|
-
await
|
|
2424
|
+
await writeFile5(stateFilePath(), JSON.stringify(state), "utf8");
|
|
2148
2425
|
return {
|
|
2149
2426
|
target: { id: target.id, url: target.url, title: target.title },
|
|
2150
2427
|
categories,
|
|
@@ -2157,7 +2434,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
2157
2434
|
if (!entry) {
|
|
2158
2435
|
let hint = "";
|
|
2159
2436
|
try {
|
|
2160
|
-
const raw = await
|
|
2437
|
+
const raw = await readFile5(stateFilePath(), "utf8");
|
|
2161
2438
|
const state = JSON.parse(raw);
|
|
2162
2439
|
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)`;
|
|
2163
2440
|
} catch {}
|
|
@@ -2169,7 +2446,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
2169
2446
|
} finally {
|
|
2170
2447
|
conn.close();
|
|
2171
2448
|
liveTraces.delete(target.id);
|
|
2172
|
-
await
|
|
2449
|
+
await unlink3(stateFilePath()).catch(() => {});
|
|
2173
2450
|
}
|
|
2174
2451
|
const written = await writeTrace(events, target.id);
|
|
2175
2452
|
const metrics = analyzeEvents(events);
|
|
@@ -2287,7 +2564,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
2287
2564
|
}
|
|
2288
2565
|
let raw;
|
|
2289
2566
|
try {
|
|
2290
|
-
raw = await
|
|
2567
|
+
raw = await readFile5(args.tracePath, "utf8");
|
|
2291
2568
|
} catch (err) {
|
|
2292
2569
|
throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
|
|
2293
2570
|
}
|
|
@@ -2303,11 +2580,375 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
2303
2580
|
return { ...analyzeEvents(events), tracePath: args.tracePath };
|
|
2304
2581
|
}
|
|
2305
2582
|
|
|
2306
|
-
// src/tools/
|
|
2307
|
-
import {
|
|
2308
|
-
import {
|
|
2583
|
+
// src/tools/screencast.ts
|
|
2584
|
+
import { spawn } from "node:child_process";
|
|
2585
|
+
import { mkdir as mkdir6, rm, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
2586
|
+
import { join as join5 } from "node:path";
|
|
2309
2587
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2310
|
-
|
|
2588
|
+
var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
|
|
2589
|
+
function frameDurationsMs(ledger, stopWallclockMs) {
|
|
2590
|
+
return ledger.map((entry, i) => {
|
|
2591
|
+
const next = i + 1 < ledger.length ? ledger[i + 1].timestamp : stopWallclockMs;
|
|
2592
|
+
return Math.max(1, Math.round(next - entry.timestamp));
|
|
2593
|
+
});
|
|
2594
|
+
}
|
|
2595
|
+
var CONCAT_GRID_MS = 40;
|
|
2596
|
+
function coalesceToConcatGrid(ledger, stopWallclockMs, gridMs = CONCAT_GRID_MS) {
|
|
2597
|
+
if (!ledger.length)
|
|
2598
|
+
return [];
|
|
2599
|
+
const raw = frameDurationsMs(ledger, stopWallclockMs);
|
|
2600
|
+
let elapsed = 0;
|
|
2601
|
+
const kept = [];
|
|
2602
|
+
let lastSlot = -1;
|
|
2603
|
+
for (let i = 0;i < ledger.length; i++) {
|
|
2604
|
+
const slot = Math.round(elapsed / gridMs);
|
|
2605
|
+
if (slot > lastSlot) {
|
|
2606
|
+
kept.push({ file: ledger[i].file, slot });
|
|
2607
|
+
lastSlot = slot;
|
|
2608
|
+
}
|
|
2609
|
+
elapsed += raw[i];
|
|
2610
|
+
}
|
|
2611
|
+
const endSlot = Math.max(lastSlot + 1, Math.round(elapsed / gridMs));
|
|
2612
|
+
return kept.map((frame, i) => ({
|
|
2613
|
+
file: frame.file,
|
|
2614
|
+
durationMs: ((i + 1 < kept.length ? kept[i + 1].slot : endSlot) - frame.slot) * gridMs
|
|
2615
|
+
}));
|
|
2616
|
+
}
|
|
2617
|
+
function buildConcatManifest(frames) {
|
|
2618
|
+
if (!frames.length)
|
|
2619
|
+
throw new CdpError("cannot build a concat manifest from an empty frame ledger");
|
|
2620
|
+
const lines = ["ffconcat version 1.0"];
|
|
2621
|
+
for (const frame of frames) {
|
|
2622
|
+
lines.push(`file ${frame.file}`);
|
|
2623
|
+
lines.push(`duration ${(frame.durationMs / 1000).toFixed(6)}`);
|
|
2624
|
+
}
|
|
2625
|
+
lines.push(`file ${frames[frames.length - 1].file}`);
|
|
2626
|
+
return `${lines.join(`
|
|
2627
|
+
`)}
|
|
2628
|
+
`;
|
|
2629
|
+
}
|
|
2630
|
+
function evenDimensions(width, height) {
|
|
2631
|
+
return { width: Math.max(0, Math.trunc(width / 2) * 2), height: Math.max(0, Math.trunc(height / 2) * 2) };
|
|
2632
|
+
}
|
|
2633
|
+
function pngSize(buf) {
|
|
2634
|
+
if (buf.length < 24)
|
|
2635
|
+
return;
|
|
2636
|
+
if (buf.readUInt32BE(0) !== 2303741511 || buf.readUInt32BE(4) !== 218765834)
|
|
2637
|
+
return;
|
|
2638
|
+
if (buf.toString("latin1", 12, 16) !== "IHDR")
|
|
2639
|
+
return;
|
|
2640
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
2641
|
+
}
|
|
2642
|
+
var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
|
|
2643
|
+
function jpegSize(buf) {
|
|
2644
|
+
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
|
|
2645
|
+
return;
|
|
2646
|
+
let i = 2;
|
|
2647
|
+
while (i + 3 < buf.length) {
|
|
2648
|
+
if (buf[i] !== 255) {
|
|
2649
|
+
i += 1;
|
|
2650
|
+
continue;
|
|
2651
|
+
}
|
|
2652
|
+
const marker = buf[i + 1];
|
|
2653
|
+
if (marker === 255) {
|
|
2654
|
+
i += 1;
|
|
2655
|
+
continue;
|
|
2656
|
+
}
|
|
2657
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
2658
|
+
i += 2;
|
|
2659
|
+
continue;
|
|
2660
|
+
}
|
|
2661
|
+
if (i + 3 >= buf.length)
|
|
2662
|
+
return;
|
|
2663
|
+
const segLen = buf.readUInt16BE(i + 2);
|
|
2664
|
+
if (JPEG_SOF.has(marker)) {
|
|
2665
|
+
if (i + 9 > buf.length)
|
|
2666
|
+
return;
|
|
2667
|
+
return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
|
|
2668
|
+
}
|
|
2669
|
+
if (segLen < 2)
|
|
2670
|
+
return;
|
|
2671
|
+
i += 2 + segLen;
|
|
2672
|
+
}
|
|
2673
|
+
return;
|
|
2674
|
+
}
|
|
2675
|
+
function imageSize(buf) {
|
|
2676
|
+
return pngSize(buf) ?? jpegSize(buf);
|
|
2677
|
+
}
|
|
2678
|
+
var ENCODER_LADDER = [
|
|
2679
|
+
{ encoder: "hevc_videotoolbox", codec: "hevc", tag: "hvc1" },
|
|
2680
|
+
{ encoder: "h264_videotoolbox", codec: "h264" },
|
|
2681
|
+
{ encoder: "libx265", codec: "hevc", tag: "hvc1" },
|
|
2682
|
+
{ encoder: "libx264", codec: "h264" }
|
|
2683
|
+
];
|
|
2684
|
+
function ffmpegEncoderNames(encodersOutput) {
|
|
2685
|
+
const names = new Set;
|
|
2686
|
+
for (const line of encodersOutput.split(`
|
|
2687
|
+
`)) {
|
|
2688
|
+
const m = /^\s*[VAS][A-Z.]{5}\s+(\S+)/.exec(line);
|
|
2689
|
+
if (m)
|
|
2690
|
+
names.add(m[1]);
|
|
2691
|
+
}
|
|
2692
|
+
return names;
|
|
2693
|
+
}
|
|
2694
|
+
function selectEncoder(encodersOutput) {
|
|
2695
|
+
const available = ffmpegEncoderNames(encodersOutput);
|
|
2696
|
+
for (const candidate of ENCODER_LADDER) {
|
|
2697
|
+
if (available.has(candidate.encoder))
|
|
2698
|
+
return candidate;
|
|
2699
|
+
}
|
|
2700
|
+
throw new CdpError(`ffmpeg reports none of the supported video encoders (${ENCODER_LADDER.map((c) => c.encoder).join(", ")}). ` + "Install a full ffmpeg build: 'brew install ffmpeg' (macOS) or 'apt install ffmpeg' (Debian/Ubuntu).");
|
|
2701
|
+
}
|
|
2702
|
+
function buildFfmpegArgs(manifestPath, outPath, choice) {
|
|
2703
|
+
return [
|
|
2704
|
+
"-hide_banner",
|
|
2705
|
+
"-loglevel",
|
|
2706
|
+
"error",
|
|
2707
|
+
"-y",
|
|
2708
|
+
"-f",
|
|
2709
|
+
"concat",
|
|
2710
|
+
"-safe",
|
|
2711
|
+
"0",
|
|
2712
|
+
"-i",
|
|
2713
|
+
manifestPath,
|
|
2714
|
+
"-vsync",
|
|
2715
|
+
"vfr",
|
|
2716
|
+
"-vf",
|
|
2717
|
+
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
2718
|
+
"-c:v",
|
|
2719
|
+
choice.encoder,
|
|
2720
|
+
...choice.tag ? ["-tag:v", choice.tag] : [],
|
|
2721
|
+
"-pix_fmt",
|
|
2722
|
+
"yuv420p",
|
|
2723
|
+
"-movflags",
|
|
2724
|
+
"+faststart",
|
|
2725
|
+
"-an",
|
|
2726
|
+
outPath
|
|
2727
|
+
];
|
|
2728
|
+
}
|
|
2729
|
+
function run(bin, args) {
|
|
2730
|
+
return new Promise((resolve, reject) => {
|
|
2731
|
+
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2732
|
+
let stdout = "";
|
|
2733
|
+
let stderr = "";
|
|
2734
|
+
child.stdout?.on("data", (chunk) => {
|
|
2735
|
+
stdout += chunk.toString("utf8");
|
|
2736
|
+
});
|
|
2737
|
+
child.stderr?.on("data", (chunk) => {
|
|
2738
|
+
stderr += chunk.toString("utf8");
|
|
2739
|
+
});
|
|
2740
|
+
child.on("error", reject);
|
|
2741
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
2742
|
+
});
|
|
2743
|
+
}
|
|
2744
|
+
var encoderProbe;
|
|
2745
|
+
var FFMPEG_MISSING = `ffmpeg is required to encode a screen recording but '${FFMPEG_BIN}' could not be executed. ` + "Install it with 'brew install ffmpeg' (macOS) or 'apt install ffmpeg' (Debian/Ubuntu), " + "or point CDP_FFMPEG at the binary.";
|
|
2746
|
+
async function probeEncoder() {
|
|
2747
|
+
if (!encoderProbe) {
|
|
2748
|
+
encoderProbe = (async () => {
|
|
2749
|
+
let res;
|
|
2750
|
+
try {
|
|
2751
|
+
res = await run(FFMPEG_BIN, ["-hide_banner", "-encoders"]);
|
|
2752
|
+
} catch (err) {
|
|
2753
|
+
throw new CdpError(`${FFMPEG_MISSING} (${err.message})`);
|
|
2754
|
+
}
|
|
2755
|
+
if (res.code !== 0)
|
|
2756
|
+
throw new CdpError(`${FFMPEG_MISSING} ('-encoders' exited ${res.code}: ${res.stderr.trim().slice(0, 300)})`);
|
|
2757
|
+
return selectEncoder(`${res.stdout}
|
|
2758
|
+
${res.stderr}`);
|
|
2759
|
+
})();
|
|
2760
|
+
encoderProbe.catch(() => {
|
|
2761
|
+
encoderProbe = undefined;
|
|
2762
|
+
});
|
|
2763
|
+
}
|
|
2764
|
+
return encoderProbe;
|
|
2765
|
+
}
|
|
2766
|
+
var liveRecordings = new Map;
|
|
2767
|
+
function shortId2(id) {
|
|
2768
|
+
return id.slice(0, 8);
|
|
2769
|
+
}
|
|
2770
|
+
function assertNotRecording(targetId, activeIds) {
|
|
2771
|
+
if (activeIds.includes(targetId)) {
|
|
2772
|
+
throw new CdpError(`a screen recording is already in progress for target ${shortId2(targetId)}; call stop_screen_recording first`);
|
|
2773
|
+
}
|
|
2774
|
+
}
|
|
2775
|
+
function chooseRecordingId(activeIds, selector) {
|
|
2776
|
+
if (!activeIds.length) {
|
|
2777
|
+
throw new CdpError("no screen recording is in progress in this process. start_screen_recording parks a live CDP connection in memory, so a recording cannot be stopped from a different process than the one that started it (the CLI is one process per call: use the MCP server for the pair).");
|
|
2778
|
+
}
|
|
2779
|
+
if (selector === undefined || selector === "active") {
|
|
2780
|
+
if (activeIds.length === 1)
|
|
2781
|
+
return activeIds[0];
|
|
2782
|
+
throw new CdpError(`${activeIds.length} screen recordings are in progress (targets: ${activeIds.map(shortId2).join(", ")}); pass 'target' to say which one to stop`);
|
|
2783
|
+
}
|
|
2784
|
+
return activeIds.find((id) => id === selector);
|
|
2785
|
+
}
|
|
2786
|
+
function stamp3() {
|
|
2787
|
+
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
2788
|
+
}
|
|
2789
|
+
function target32(t) {
|
|
2790
|
+
return { id: t.id, url: t.url, title: t.title };
|
|
2791
|
+
}
|
|
2792
|
+
async function startScreenRecording(args = {}) {
|
|
2793
|
+
const format = args.format ?? "jpeg";
|
|
2794
|
+
if (format !== "jpeg" && format !== "png")
|
|
2795
|
+
throw new CdpError("start_screen_recording: format must be 'jpeg' or 'png'");
|
|
2796
|
+
if (args.quality !== undefined && (!Number.isFinite(args.quality) || args.quality < 0 || args.quality > 100)) {
|
|
2797
|
+
throw new CdpError("start_screen_recording: quality must be a number between 0 and 100 (jpeg only)");
|
|
2798
|
+
}
|
|
2799
|
+
const encoder = await probeEncoder();
|
|
2800
|
+
const { conn, target } = await openPage(args.target);
|
|
2801
|
+
try {
|
|
2802
|
+
assertNotRecording(target.id, [...liveRecordings.keys()]);
|
|
2803
|
+
} catch (err) {
|
|
2804
|
+
conn.close();
|
|
2805
|
+
throw err;
|
|
2806
|
+
}
|
|
2807
|
+
const spoolDir = join5(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
|
|
2808
|
+
try {
|
|
2809
|
+
await mkdir6(spoolDir, { recursive: true });
|
|
2810
|
+
} catch (err) {
|
|
2811
|
+
conn.close();
|
|
2812
|
+
throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
|
|
2813
|
+
}
|
|
2814
|
+
const ext = format === "png" ? "png" : "jpg";
|
|
2815
|
+
const rec = {
|
|
2816
|
+
conn,
|
|
2817
|
+
target,
|
|
2818
|
+
spoolDir,
|
|
2819
|
+
ledger: [],
|
|
2820
|
+
startedAt: Date.now(),
|
|
2821
|
+
format,
|
|
2822
|
+
encoder,
|
|
2823
|
+
writes: Promise.resolve(),
|
|
2824
|
+
dropped: 0,
|
|
2825
|
+
unwritten: new Set,
|
|
2826
|
+
off: () => {}
|
|
2827
|
+
};
|
|
2828
|
+
let seq = 0;
|
|
2829
|
+
rec.off = conn.on("Page.screencastFrame", (params) => {
|
|
2830
|
+
const sessionId = params.sessionId;
|
|
2831
|
+
if (typeof sessionId === "number") {
|
|
2832
|
+
conn.send("Page.screencastFrameAck", { sessionId }).catch(() => {});
|
|
2833
|
+
}
|
|
2834
|
+
const data = params.data;
|
|
2835
|
+
if (typeof data !== "string" || !data.length) {
|
|
2836
|
+
rec.dropped += 1;
|
|
2837
|
+
return;
|
|
2838
|
+
}
|
|
2839
|
+
const meta = params.metadata;
|
|
2840
|
+
const timestamp = typeof meta?.timestamp === "number" ? Math.round(meta.timestamp * 1000) : Date.now();
|
|
2841
|
+
const buf = Buffer.from(data, "base64");
|
|
2842
|
+
const size = imageSize(buf);
|
|
2843
|
+
if (!size || size.width <= 0 || size.height <= 0) {
|
|
2844
|
+
rec.dropped += 1;
|
|
2845
|
+
return;
|
|
2846
|
+
}
|
|
2847
|
+
seq += 1;
|
|
2848
|
+
const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
|
|
2849
|
+
rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
|
|
2850
|
+
rec.writes = rec.writes.then(() => writeFile6(join5(spoolDir, file), buf)).catch(() => {
|
|
2851
|
+
rec.dropped += 1;
|
|
2852
|
+
rec.unwritten.add(file);
|
|
2853
|
+
});
|
|
2854
|
+
});
|
|
2855
|
+
try {
|
|
2856
|
+
await conn.send("Page.enable");
|
|
2857
|
+
if (args.bringToFront)
|
|
2858
|
+
await conn.send("Page.bringToFront").catch(() => {});
|
|
2859
|
+
const params = { format };
|
|
2860
|
+
if (format === "jpeg" && args.quality !== undefined)
|
|
2861
|
+
params.quality = args.quality;
|
|
2862
|
+
if (args.maxWidth !== undefined)
|
|
2863
|
+
params.maxWidth = args.maxWidth;
|
|
2864
|
+
if (args.maxHeight !== undefined)
|
|
2865
|
+
params.maxHeight = args.maxHeight;
|
|
2866
|
+
if (args.everyNthFrame !== undefined)
|
|
2867
|
+
params.everyNthFrame = args.everyNthFrame;
|
|
2868
|
+
await conn.send("Page.startScreencast", params);
|
|
2869
|
+
} catch (err) {
|
|
2870
|
+
rec.off();
|
|
2871
|
+
conn.close();
|
|
2872
|
+
await rm(spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
2873
|
+
throw err;
|
|
2874
|
+
}
|
|
2875
|
+
liveRecordings.set(target.id, rec);
|
|
2876
|
+
return {
|
|
2877
|
+
target: target32(target),
|
|
2878
|
+
format,
|
|
2879
|
+
spoolDir,
|
|
2880
|
+
encoder: encoder.encoder,
|
|
2881
|
+
codec: encoder.codec,
|
|
2882
|
+
startedAt: rec.startedAt,
|
|
2883
|
+
note: "Recording. Call stop_screen_recording in THIS process to encode the MP4; frames only arrive on repaint, so a still page yields one long-held frame."
|
|
2884
|
+
};
|
|
2885
|
+
}
|
|
2886
|
+
async function pickRecording(selector) {
|
|
2887
|
+
const chosen = chooseRecordingId([...liveRecordings.keys()], selector);
|
|
2888
|
+
if (chosen)
|
|
2889
|
+
return liveRecordings.get(chosen);
|
|
2890
|
+
const { conn, target } = await openPage(selector);
|
|
2891
|
+
conn.close();
|
|
2892
|
+
const hit = liveRecordings.get(target.id);
|
|
2893
|
+
if (!hit) {
|
|
2894
|
+
throw new CdpError(`no screen recording is in progress for target ${shortId2(target.id)} (recording: ${[...liveRecordings.keys()].map(shortId2).join(", ") || "none"})`);
|
|
2895
|
+
}
|
|
2896
|
+
return hit;
|
|
2897
|
+
}
|
|
2898
|
+
async function stopScreenRecording(args = {}) {
|
|
2899
|
+
const rec = await pickRecording(args.target);
|
|
2900
|
+
rec.off();
|
|
2901
|
+
try {
|
|
2902
|
+
await rec.conn.send("Page.stopScreencast");
|
|
2903
|
+
} catch {}
|
|
2904
|
+
const stoppedAt = Date.now();
|
|
2905
|
+
await rec.writes.catch(() => {});
|
|
2906
|
+
rec.conn.close();
|
|
2907
|
+
liveRecordings.delete(rec.target.id);
|
|
2908
|
+
const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
|
|
2909
|
+
if (!ledger.length) {
|
|
2910
|
+
await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
2911
|
+
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.`);
|
|
2912
|
+
}
|
|
2913
|
+
await mkdir6(ARTIFACT_DIR4, { recursive: true });
|
|
2914
|
+
const outPath = args.savePath ?? join5(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
|
|
2915
|
+
const frames = coalesceToConcatGrid(ledger, stoppedAt);
|
|
2916
|
+
const manifestPath = join5(rec.spoolDir, "frames.ffconcat");
|
|
2917
|
+
await writeFile6(manifestPath, buildConcatManifest(frames), "utf8");
|
|
2918
|
+
const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
|
|
2919
|
+
const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
|
|
2920
|
+
let res;
|
|
2921
|
+
try {
|
|
2922
|
+
res = await run(FFMPEG_BIN, buildFfmpegArgs(manifestPath, outPath, rec.encoder));
|
|
2923
|
+
} catch (err) {
|
|
2924
|
+
throw new CdpError(`${FFMPEG_MISSING} Spooled frames kept at ${rec.spoolDir}. (${err.message})`);
|
|
2925
|
+
}
|
|
2926
|
+
if (res.code !== 0) {
|
|
2927
|
+
throw new CdpError(`ffmpeg (${rec.encoder.encoder}) exited ${res.code} encoding ${frames.length} frames. Spooled frames kept at ${rec.spoolDir}, re-run by hand with: ` + `${FFMPEG_BIN} ${buildFfmpegArgs(manifestPath, outPath, rec.encoder).join(" ")}
|
|
2928
|
+
ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
2929
|
+
}
|
|
2930
|
+
const { size } = await stat2(outPath);
|
|
2931
|
+
await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
2932
|
+
return {
|
|
2933
|
+
path: outPath,
|
|
2934
|
+
bytes: size,
|
|
2935
|
+
durationMs,
|
|
2936
|
+
frameCount: ledger.length,
|
|
2937
|
+
encodedFrames: frames.length,
|
|
2938
|
+
codec: rec.encoder.codec,
|
|
2939
|
+
encoder: rec.encoder.encoder,
|
|
2940
|
+
width,
|
|
2941
|
+
height,
|
|
2942
|
+
droppedFrames: rec.dropped,
|
|
2943
|
+
target: target32(rec.target)
|
|
2944
|
+
};
|
|
2945
|
+
}
|
|
2946
|
+
|
|
2947
|
+
// src/tools/heap.ts
|
|
2948
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
|
|
2949
|
+
import { join as join6 } from "node:path";
|
|
2950
|
+
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2951
|
+
function stamp4(targetId) {
|
|
2311
2952
|
const short = targetId.slice(0, 8);
|
|
2312
2953
|
const iso = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2313
2954
|
return `${short}-${iso}`;
|
|
@@ -2330,9 +2971,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2330
2971
|
if (snapshot.length === 0) {
|
|
2331
2972
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
2332
2973
|
}
|
|
2333
|
-
await
|
|
2334
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
2335
|
-
await
|
|
2974
|
+
await mkdir7(ARTIFACT_DIR5, { recursive: true });
|
|
2975
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join6(ARTIFACT_DIR5, args.savePath) : join6(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
|
|
2976
|
+
await writeFile7(path, snapshot, "utf8");
|
|
2336
2977
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
2337
2978
|
return {
|
|
2338
2979
|
path,
|
|
@@ -2344,15 +2985,15 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2344
2985
|
}
|
|
2345
2986
|
|
|
2346
2987
|
// src/tools/lighthouse.ts
|
|
2347
|
-
import { spawn } from "node:child_process";
|
|
2348
|
-
import { mkdir as
|
|
2349
|
-
var
|
|
2988
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2989
|
+
import { mkdir as mkdir8, readFile as readFile6, stat as stat3 } from "node:fs/promises";
|
|
2990
|
+
var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2350
2991
|
function cdpPort() {
|
|
2351
2992
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
2352
2993
|
const m = base.match(/:(\d+)/);
|
|
2353
2994
|
return m?.[1] ? Number(m[1]) : 9222;
|
|
2354
2995
|
}
|
|
2355
|
-
function
|
|
2996
|
+
function stamp5() {
|
|
2356
2997
|
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
2357
2998
|
}
|
|
2358
2999
|
function shortHost(url) {
|
|
@@ -2369,7 +3010,7 @@ function runProcess(cmd, args, opts) {
|
|
|
2369
3010
|
let stdout = "";
|
|
2370
3011
|
let stderr = "";
|
|
2371
3012
|
let settled = false;
|
|
2372
|
-
const child =
|
|
3013
|
+
const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2373
3014
|
const timer = setTimeout(() => {
|
|
2374
3015
|
if (settled)
|
|
2375
3016
|
return;
|
|
@@ -2406,8 +3047,8 @@ async function lighthouseAudit(args) {
|
|
|
2406
3047
|
const url = args.url.trim();
|
|
2407
3048
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
2408
3049
|
const port = cdpPort();
|
|
2409
|
-
await
|
|
2410
|
-
const outPath = args.savePath ?? `${
|
|
3050
|
+
await mkdir8(ARTIFACT_DIR6, { recursive: true });
|
|
3051
|
+
const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
|
|
2411
3052
|
const formFactor = args.formFactor ?? "desktop";
|
|
2412
3053
|
const lhArgs = [
|
|
2413
3054
|
"--yes",
|
|
@@ -2435,7 +3076,7 @@ async function lighthouseAudit(args) {
|
|
|
2435
3076
|
}
|
|
2436
3077
|
let raw;
|
|
2437
3078
|
try {
|
|
2438
|
-
raw = await
|
|
3079
|
+
raw = await readFile6(outPath, "utf8");
|
|
2439
3080
|
} catch {
|
|
2440
3081
|
raw = undefined;
|
|
2441
3082
|
}
|
|
@@ -2462,7 +3103,7 @@ async function lighthouseAudit(args) {
|
|
|
2462
3103
|
if (Object.keys(categories).length === 0) {
|
|
2463
3104
|
throw new CdpError(`lighthouse_audit report contained no category scores (exit code ${res.code}). stderr: ${truncate(res.stderr)}`);
|
|
2464
3105
|
}
|
|
2465
|
-
const { size } = await
|
|
3106
|
+
const { size } = await stat3(outPath);
|
|
2466
3107
|
return {
|
|
2467
3108
|
path: outPath,
|
|
2468
3109
|
bytes: size,
|
|
@@ -2492,6 +3133,9 @@ var TOOLS = {
|
|
|
2492
3133
|
navigate_page: onCdp(SHARED_TOOLS.navigate_page),
|
|
2493
3134
|
wait_for: onCdp(SHARED_TOOLS.wait_for),
|
|
2494
3135
|
evaluate_script: onCdp(SHARED_TOOLS.evaluate_script),
|
|
3136
|
+
list_cookies: onCdp(SHARED_TOOLS.list_cookies),
|
|
3137
|
+
set_cookie: onCdp(SHARED_TOOLS.set_cookie),
|
|
3138
|
+
delete_cookies: onCdp(SHARED_TOOLS.delete_cookies),
|
|
2495
3139
|
take_snapshot: onCdp(SHARED_TOOLS.take_snapshot),
|
|
2496
3140
|
click: onCdp(SHARED_TOOLS.click),
|
|
2497
3141
|
hover: onCdp(SHARED_TOOLS.hover),
|
|
@@ -2513,6 +3157,8 @@ var TOOLS = {
|
|
|
2513
3157
|
performance_stop_trace: performanceStopTrace,
|
|
2514
3158
|
performance_analyze_insight: performanceAnalyzeInsight,
|
|
2515
3159
|
performance_trace: performanceTrace,
|
|
3160
|
+
start_screen_recording: startScreenRecording,
|
|
3161
|
+
stop_screen_recording: stopScreenRecording,
|
|
2516
3162
|
take_heapsnapshot: takeHeapsnapshot,
|
|
2517
3163
|
lighthouse_audit: lighthouseAudit,
|
|
2518
3164
|
mock_request: mockRequest,
|