cdp-toolkit 1.2.0 → 1.4.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 +127 -0
- package/CONTRACT.md +1 -1
- package/README.md +53 -10
- package/dist/cli.js +392 -41
- package/dist/index.js +316 -39
- package/dist/mcp.js +521 -44
- package/package.json +4 -3
- package/src/bidi/driver.ts +136 -1
- package/src/cdp/driver.ts +145 -1
- package/src/driver.ts +106 -0
- package/src/firefox-tools.ts +1 -1
- package/src/index.ts +9 -2
- package/src/leases-tools.ts +8 -2
- package/src/leases.ts +8 -0
- package/src/manifest.ts +128 -2
- package/src/mcp.ts +1 -1
- package/src/origins.ts +266 -0
- package/src/shared-tools.ts +315 -10
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
|
}
|
|
@@ -702,6 +705,31 @@ function decodeUid(uid) {
|
|
|
702
705
|
throw driverError("foreign-uid", `malformed cdp uid payload: ${uid}`);
|
|
703
706
|
return n;
|
|
704
707
|
}
|
|
708
|
+
function normalizeCdpCookie(c) {
|
|
709
|
+
const same = (c.sameSite ?? "").toLowerCase();
|
|
710
|
+
const sameSite = same === "strict" || same === "lax" || same === "none" ? same : "default";
|
|
711
|
+
return {
|
|
712
|
+
name: c.name,
|
|
713
|
+
value: c.value,
|
|
714
|
+
domain: c.domain,
|
|
715
|
+
path: c.path,
|
|
716
|
+
expires: typeof c.expires === "number" ? c.expires : -1,
|
|
717
|
+
size: typeof c.size === "number" ? c.size : Buffer.byteLength(`${c.name}${c.value}`, "utf8"),
|
|
718
|
+
httpOnly: c.httpOnly === true,
|
|
719
|
+
secure: c.secure === true,
|
|
720
|
+
sameSite,
|
|
721
|
+
session: c.session === true
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function cdpSameSite(sameSite) {
|
|
725
|
+
if (sameSite === "strict")
|
|
726
|
+
return "Strict";
|
|
727
|
+
if (sameSite === "lax")
|
|
728
|
+
return "Lax";
|
|
729
|
+
if (sameSite === "none")
|
|
730
|
+
return "None";
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
705
733
|
async function centerOf(conn, objectId) {
|
|
706
734
|
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
735
|
const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
@@ -1224,6 +1252,38 @@ class CdpPageDriver {
|
|
|
1224
1252
|
});
|
|
1225
1253
|
});
|
|
1226
1254
|
}
|
|
1255
|
+
async getCookies() {
|
|
1256
|
+
await this.ensureDomain("Network");
|
|
1257
|
+
const { cookies } = await this.conn.send("Network.getCookies");
|
|
1258
|
+
return (cookies ?? []).map(normalizeCdpCookie);
|
|
1259
|
+
}
|
|
1260
|
+
async setCookie(params) {
|
|
1261
|
+
await this.ensureDomain("Network");
|
|
1262
|
+
const sameSite = cdpSameSite(params.sameSite);
|
|
1263
|
+
const { success } = await this.conn.send("Network.setCookie", {
|
|
1264
|
+
name: params.name,
|
|
1265
|
+
value: params.value,
|
|
1266
|
+
...params.url !== undefined ? { url: params.url } : {},
|
|
1267
|
+
...params.domain !== undefined ? { domain: params.domain } : {},
|
|
1268
|
+
...params.path !== undefined ? { path: params.path } : {},
|
|
1269
|
+
...params.expires !== undefined ? { expires: params.expires } : {},
|
|
1270
|
+
...params.httpOnly !== undefined ? { httpOnly: params.httpOnly } : {},
|
|
1271
|
+
...params.secure !== undefined ? { secure: params.secure } : {},
|
|
1272
|
+
...sameSite !== undefined ? { sameSite } : {}
|
|
1273
|
+
});
|
|
1274
|
+
if (success !== true) {
|
|
1275
|
+
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.`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
async deleteCookies(filter) {
|
|
1279
|
+
await this.ensureDomain("Network");
|
|
1280
|
+
await this.conn.send("Network.deleteCookies", {
|
|
1281
|
+
name: filter.name,
|
|
1282
|
+
...filter.url !== undefined ? { url: filter.url } : {},
|
|
1283
|
+
...filter.domain !== undefined ? { domain: filter.domain } : {},
|
|
1284
|
+
...filter.path !== undefined ? { path: filter.path } : {}
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
1227
1287
|
async startBodyCapture() {
|
|
1228
1288
|
await this.ensureDomain("Network");
|
|
1229
1289
|
return async () => {
|
|
@@ -1283,6 +1343,10 @@ async function withBrowserConn(fn) {
|
|
|
1283
1343
|
}
|
|
1284
1344
|
}
|
|
1285
1345
|
function noSuchTarget(e) {
|
|
1346
|
+
if (e instanceof LeaseConflictError)
|
|
1347
|
+
throw e;
|
|
1348
|
+
if (typeof e?.code === "string")
|
|
1349
|
+
throw e;
|
|
1286
1350
|
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
1287
1351
|
}
|
|
1288
1352
|
|
|
@@ -1326,6 +1390,97 @@ function createCdpDriver() {
|
|
|
1326
1390
|
return new CdpBrowserDriver;
|
|
1327
1391
|
}
|
|
1328
1392
|
|
|
1393
|
+
// src/origins.ts
|
|
1394
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1395
|
+
import { join as join2 } from "node:path";
|
|
1396
|
+
function safeId2(id) {
|
|
1397
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1398
|
+
}
|
|
1399
|
+
function originFile(backend, targetId) {
|
|
1400
|
+
return join2(leaseDir(), `origin-${backend}-${safeId2(targetId)}.json`);
|
|
1401
|
+
}
|
|
1402
|
+
var ORIGIN_FILE_RE = /^origin-(chrome|firefox)-(.+)\.json$/;
|
|
1403
|
+
async function recordOrigin(backend, targetId, opts = {}) {
|
|
1404
|
+
const record = {
|
|
1405
|
+
backend,
|
|
1406
|
+
targetId,
|
|
1407
|
+
label: typeof opts.label === "string" && opts.label.length ? opts.label : defaultLabel(),
|
|
1408
|
+
pid: process.pid,
|
|
1409
|
+
createdAt: opts.now ?? Date.now()
|
|
1410
|
+
};
|
|
1411
|
+
const file = originFile(backend, targetId);
|
|
1412
|
+
try {
|
|
1413
|
+
await mkdir2(leaseDir(), { recursive: true });
|
|
1414
|
+
try {
|
|
1415
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1416
|
+
return;
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
if (err.code !== "EEXIST")
|
|
1419
|
+
throw err;
|
|
1420
|
+
}
|
|
1421
|
+
await unlink2(file).catch(() => {
|
|
1422
|
+
return;
|
|
1423
|
+
});
|
|
1424
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1425
|
+
} catch {}
|
|
1426
|
+
}
|
|
1427
|
+
async function listOrigins(opts = {}) {
|
|
1428
|
+
let names = [];
|
|
1429
|
+
try {
|
|
1430
|
+
names = await readdir2(leaseDir());
|
|
1431
|
+
} catch {
|
|
1432
|
+
return [];
|
|
1433
|
+
}
|
|
1434
|
+
const live = opts.liveIds === undefined ? undefined : new Set(opts.liveIds.map((id) => safeId2(id)));
|
|
1435
|
+
const out = [];
|
|
1436
|
+
for (const name of names) {
|
|
1437
|
+
const parts = ORIGIN_FILE_RE.exec(name);
|
|
1438
|
+
if (!parts)
|
|
1439
|
+
continue;
|
|
1440
|
+
const backend = parts[1];
|
|
1441
|
+
const nameId = parts[2];
|
|
1442
|
+
const scoped = live !== undefined && (opts.liveBackend === undefined || opts.liveBackend === backend);
|
|
1443
|
+
if (scoped && !live.has(nameId)) {
|
|
1444
|
+
await unlink2(join2(leaseDir(), name)).catch(() => {
|
|
1445
|
+
return;
|
|
1446
|
+
});
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
const fromName = { backend, targetId: nameId, label: "", pid: 0, createdAt: 0 };
|
|
1450
|
+
let rec;
|
|
1451
|
+
try {
|
|
1452
|
+
rec = JSON.parse(await readFile2(join2(leaseDir(), name), "utf8"));
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
const code = err.code;
|
|
1455
|
+
if (code === "ENOENT")
|
|
1456
|
+
continue;
|
|
1457
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (typeof rec?.targetId !== "string" || typeof rec?.label !== "string" || typeof rec?.createdAt !== "number") {
|
|
1461
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
out.push({ backend: rec.backend, targetId: rec.targetId, label: rec.label, pid: rec.pid, createdAt: rec.createdAt });
|
|
1465
|
+
}
|
|
1466
|
+
return out;
|
|
1467
|
+
}
|
|
1468
|
+
async function originIndex(backend, liveIds) {
|
|
1469
|
+
const rows = await listOrigins({ liveIds, liveBackend: backend }).catch(() => []);
|
|
1470
|
+
const index = new Map;
|
|
1471
|
+
for (const row of rows) {
|
|
1472
|
+
if (row.backend !== backend)
|
|
1473
|
+
continue;
|
|
1474
|
+
index.set(row.targetId, row);
|
|
1475
|
+
}
|
|
1476
|
+
return index;
|
|
1477
|
+
}
|
|
1478
|
+
async function newTrackedPage(driver, backend, opts = {}) {
|
|
1479
|
+
const page = await driver.newPage(opts.url);
|
|
1480
|
+
await recordOrigin(backend, page.id, { label: opts.label });
|
|
1481
|
+
return page;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1329
1484
|
// src/leases-tools.ts
|
|
1330
1485
|
class LeaseToolError extends Error {
|
|
1331
1486
|
}
|
|
@@ -1334,12 +1489,12 @@ function backendOf(driver) {
|
|
|
1334
1489
|
}
|
|
1335
1490
|
async function claimPage(driver, args = {}) {
|
|
1336
1491
|
const backend = backendOf(driver);
|
|
1337
|
-
const label = typeof args.label === "string" && args.label.length ? args.label :
|
|
1492
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1338
1493
|
const pages = await driver.listPages();
|
|
1339
1494
|
let targetId = args.targetId;
|
|
1340
1495
|
let url = args.url ?? "about:blank";
|
|
1341
1496
|
if (targetId === undefined || targetId === "") {
|
|
1342
|
-
const page = await driver
|
|
1497
|
+
const page = await newTrackedPage(driver, backend, { url: args.url, label });
|
|
1343
1498
|
targetId = page.id;
|
|
1344
1499
|
url = page.url;
|
|
1345
1500
|
} else {
|
|
@@ -1381,8 +1536,8 @@ var LEASE_TOOLS = {
|
|
|
1381
1536
|
};
|
|
1382
1537
|
|
|
1383
1538
|
// src/shared-tools.ts
|
|
1384
|
-
import { mkdir as
|
|
1385
|
-
import { join as
|
|
1539
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
1540
|
+
import { dirname, join as join3 } from "node:path";
|
|
1386
1541
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1387
1542
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1388
1543
|
|
|
@@ -1450,16 +1605,35 @@ function locatorOf(args) {
|
|
|
1450
1605
|
throw new SharedToolError("provide exactly one of { uid } or { selector }");
|
|
1451
1606
|
return hasUid ? { uid: args.uid } : { css: args.selector };
|
|
1452
1607
|
}
|
|
1608
|
+
async function reapSet(driver, pages, all) {
|
|
1609
|
+
if (all === true)
|
|
1610
|
+
return pages.map((p) => p.id);
|
|
1611
|
+
try {
|
|
1612
|
+
return (await driver.listPages({ all: true })).map((p) => p.id);
|
|
1613
|
+
} catch {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1453
1617
|
async function listPages(driver, args = {}) {
|
|
1454
1618
|
const pages = await driver.listPages({ all: args.all });
|
|
1455
|
-
|
|
1619
|
+
const ledger = await originIndex(backendOf2(driver), await reapSet(driver, pages, args.all));
|
|
1620
|
+
const annotated = pages.map((p) => {
|
|
1621
|
+
const rec = ledger.get(p.id);
|
|
1622
|
+
if (!rec)
|
|
1623
|
+
return { ...p, origin: "unknown" };
|
|
1624
|
+
if (rec.unreadable !== undefined)
|
|
1625
|
+
return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
|
|
1626
|
+
return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
|
|
1627
|
+
});
|
|
1628
|
+
return { pages: annotated, count: annotated.length };
|
|
1456
1629
|
}
|
|
1457
1630
|
async function newPage(driver, args = {}) {
|
|
1458
|
-
const
|
|
1631
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1632
|
+
const p = await newTrackedPage(driver, backendOf2(driver), { url: args.url, label });
|
|
1459
1633
|
if (args.claim !== true)
|
|
1460
1634
|
return { targetId: p.id, url: p.url };
|
|
1461
1635
|
const { record, token } = await claimLease(backendOf2(driver), p.id, {
|
|
1462
|
-
label
|
|
1636
|
+
label,
|
|
1463
1637
|
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
|
|
1464
1638
|
});
|
|
1465
1639
|
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
@@ -1475,8 +1649,8 @@ async function closePage(driver, args) {
|
|
|
1475
1649
|
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
1476
1650
|
}
|
|
1477
1651
|
async function writeSelectedFile(id) {
|
|
1478
|
-
await
|
|
1479
|
-
await
|
|
1652
|
+
await mkdir3(STATE_DIR, { recursive: true });
|
|
1653
|
+
await writeFile3(join3(STATE_DIR, "selected"), id, "utf8");
|
|
1480
1654
|
}
|
|
1481
1655
|
async function selectPage(driver, args) {
|
|
1482
1656
|
if (args.target === undefined || args.target === "")
|
|
@@ -1516,11 +1690,108 @@ async function waitForText(driver, args) {
|
|
|
1516
1690
|
return { found: true, waitedMs: r.elapsedMs };
|
|
1517
1691
|
});
|
|
1518
1692
|
}
|
|
1693
|
+
function serializeForSink(value) {
|
|
1694
|
+
const json = JSON.stringify(value === undefined ? null : value, null, 2);
|
|
1695
|
+
return json ?? "null";
|
|
1696
|
+
}
|
|
1697
|
+
async function writeJsonSink(savePath, value) {
|
|
1698
|
+
const path = savePath.startsWith("/") ? savePath : join3(ARTIFACT_DIR, savePath);
|
|
1699
|
+
await mkdir3(dirname(path), { recursive: true });
|
|
1700
|
+
const json = serializeForSink(value);
|
|
1701
|
+
await writeFile3(path, json, "utf8");
|
|
1702
|
+
return { path, bytes: Buffer.byteLength(json, "utf8") };
|
|
1703
|
+
}
|
|
1704
|
+
function sinkRequested(savePath) {
|
|
1705
|
+
return savePath !== undefined && savePath !== "";
|
|
1706
|
+
}
|
|
1519
1707
|
async function evaluateScript(driver, args) {
|
|
1520
1708
|
if (typeof args.expression !== "string" || args.expression.length === 0) {
|
|
1521
1709
|
throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
|
|
1522
1710
|
}
|
|
1523
|
-
return withPage2(driver, args.target, (page) =>
|
|
1711
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1712
|
+
const value = await page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true });
|
|
1713
|
+
if (!sinkRequested(args.savePath))
|
|
1714
|
+
return value;
|
|
1715
|
+
const { path, bytes } = await writeJsonSink(args.savePath, value);
|
|
1716
|
+
const result = {
|
|
1717
|
+
path,
|
|
1718
|
+
bytes,
|
|
1719
|
+
type: value === null ? "null" : typeof value,
|
|
1720
|
+
target: target3(page.info)
|
|
1721
|
+
};
|
|
1722
|
+
return result;
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
function cookieDomainMatches(cookieDomain, filter) {
|
|
1726
|
+
const bare = (d) => (d.startsWith(".") ? d.slice(1) : d).toLowerCase();
|
|
1727
|
+
const c = bare(cookieDomain);
|
|
1728
|
+
const f = bare(filter);
|
|
1729
|
+
return c === f || c.endsWith(`.${f}`);
|
|
1730
|
+
}
|
|
1731
|
+
function filterCookies(cookies, filter) {
|
|
1732
|
+
return cookies.filter((c) => {
|
|
1733
|
+
if (filter.name !== undefined && filter.name !== "" && c.name !== filter.name)
|
|
1734
|
+
return false;
|
|
1735
|
+
if (filter.domain !== undefined && filter.domain !== "" && !cookieDomainMatches(c.domain, filter.domain))
|
|
1736
|
+
return false;
|
|
1737
|
+
return true;
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
async function listCookies(driver, args = {}) {
|
|
1741
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1742
|
+
const all = await page.getCookies();
|
|
1743
|
+
const cookies = filterCookies(all, { domain: args.domain, name: args.name });
|
|
1744
|
+
if (!sinkRequested(args.savePath)) {
|
|
1745
|
+
return { cookies, count: cookies.length, target: target3(page.info) };
|
|
1746
|
+
}
|
|
1747
|
+
const { path, bytes } = await writeJsonSink(args.savePath, cookies);
|
|
1748
|
+
return { path, bytes, count: cookies.length, target: target3(page.info) };
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
function assertCookieSite(args, toolName) {
|
|
1752
|
+
const hasUrl = args.url !== undefined && args.url !== "";
|
|
1753
|
+
const hasDomain = args.domain !== undefined && args.domain !== "";
|
|
1754
|
+
if (!hasUrl && !hasDomain) {
|
|
1755
|
+
throw new Error(`${toolName} requires either 'url' or 'domain': a cookie has to be attributed to a site, and neither was given.`);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function assertCookieName(name, toolName) {
|
|
1759
|
+
if (name === undefined || name === "") {
|
|
1760
|
+
throw new Error(`${toolName} requires a non-empty 'name'.`);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
async function setCookie(driver, args) {
|
|
1764
|
+
assertCookieName(args.name, "set_cookie");
|
|
1765
|
+
if (typeof args.value !== "string")
|
|
1766
|
+
throw new Error("set_cookie requires a string 'value'.");
|
|
1767
|
+
assertCookieSite(args, "set_cookie");
|
|
1768
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1769
|
+
await page.setCookie({
|
|
1770
|
+
name: args.name,
|
|
1771
|
+
value: args.value,
|
|
1772
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1773
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1774
|
+
...args.path !== undefined ? { path: args.path } : {},
|
|
1775
|
+
...args.expires !== undefined ? { expires: args.expires } : {},
|
|
1776
|
+
...args.httpOnly !== undefined ? { httpOnly: args.httpOnly } : {},
|
|
1777
|
+
...args.secure !== undefined ? { secure: args.secure } : {},
|
|
1778
|
+
...args.sameSite !== undefined ? { sameSite: args.sameSite } : {}
|
|
1779
|
+
});
|
|
1780
|
+
return { set: true, target: target3(page.info) };
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
async function deleteCookies(driver, args) {
|
|
1784
|
+
assertCookieName(args.name, "delete_cookies");
|
|
1785
|
+
assertCookieSite(args, "delete_cookies");
|
|
1786
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1787
|
+
await page.deleteCookies({
|
|
1788
|
+
name: args.name,
|
|
1789
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1790
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1791
|
+
...args.path !== undefined ? { path: args.path } : {}
|
|
1792
|
+
});
|
|
1793
|
+
return { deleted: true, target: target3(page.info) };
|
|
1794
|
+
});
|
|
1524
1795
|
}
|
|
1525
1796
|
var BARE_TOKEN_EXTRAS = new Set(["disabled", "focused"]);
|
|
1526
1797
|
var UNQUOTED_EXTRAS = new Set(["checked", "expanded", "url"]);
|
|
@@ -1631,10 +1902,10 @@ async function takeScreenshot(driver, args = {}) {
|
|
|
1631
1902
|
const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
|
|
1632
1903
|
if (data.byteLength === 0)
|
|
1633
1904
|
throw new SharedToolError("captureScreenshot returned empty data");
|
|
1634
|
-
await
|
|
1905
|
+
await mkdir3(ARTIFACT_DIR, { recursive: true });
|
|
1635
1906
|
const ext = outFormat === "jpeg" ? "jpg" : "png";
|
|
1636
|
-
const path = args.savePath ??
|
|
1637
|
-
await
|
|
1907
|
+
const path = args.savePath ?? join3(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
1908
|
+
await writeFile3(path, Buffer.from(data));
|
|
1638
1909
|
const result = {
|
|
1639
1910
|
path,
|
|
1640
1911
|
bytes: data.byteLength,
|
|
@@ -1681,6 +1952,9 @@ var SHARED_TOOLS = {
|
|
|
1681
1952
|
navigate_page: navigatePage,
|
|
1682
1953
|
wait_for: waitForText,
|
|
1683
1954
|
evaluate_script: evaluateScript,
|
|
1955
|
+
list_cookies: listCookies,
|
|
1956
|
+
set_cookie: setCookie,
|
|
1957
|
+
delete_cookies: deleteCookies,
|
|
1684
1958
|
take_snapshot: takeSnapshot,
|
|
1685
1959
|
click,
|
|
1686
1960
|
hover,
|
|
@@ -1697,10 +1971,10 @@ var SHARED_TOOLS = {
|
|
|
1697
1971
|
};
|
|
1698
1972
|
|
|
1699
1973
|
// src/tools/console.ts
|
|
1700
|
-
import { readFile as
|
|
1974
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1701
1975
|
|
|
1702
1976
|
// src/tools/recorder.ts
|
|
1703
|
-
import { mkdir as
|
|
1977
|
+
import { mkdir as mkdir4, appendFile, writeFile as writeFile4, copyFile } from "node:fs/promises";
|
|
1704
1978
|
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1705
1979
|
function recFile(targetId) {
|
|
1706
1980
|
return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
|
|
@@ -1721,11 +1995,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
1721
1995
|
async function startRecorder(target, opts = {}) {
|
|
1722
1996
|
const network = opts.network ?? true;
|
|
1723
1997
|
const consoleCap = opts.console ?? true;
|
|
1724
|
-
await
|
|
1998
|
+
await mkdir4(ARTIFACT_DIR2, { recursive: true });
|
|
1725
1999
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
1726
2000
|
const file = opts.file ?? recFile(resolved.id);
|
|
1727
2001
|
if (opts.truncate) {
|
|
1728
|
-
await
|
|
2002
|
+
await writeFile4(file, "");
|
|
1729
2003
|
}
|
|
1730
2004
|
let chain = Promise.resolve();
|
|
1731
2005
|
let dropped = 0;
|
|
@@ -1867,7 +2141,7 @@ function lineToEntry(line, index) {
|
|
|
1867
2141
|
async function readConsoleEntries(file) {
|
|
1868
2142
|
let raw;
|
|
1869
2143
|
try {
|
|
1870
|
-
raw = await
|
|
2144
|
+
raw = await readFile3(file, "utf8");
|
|
1871
2145
|
} catch {
|
|
1872
2146
|
return [];
|
|
1873
2147
|
}
|
|
@@ -1923,7 +2197,7 @@ async function getConsoleMessage(args = {}) {
|
|
|
1923
2197
|
}
|
|
1924
2198
|
|
|
1925
2199
|
// src/tools/network.ts
|
|
1926
|
-
import { readFile as
|
|
2200
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1927
2201
|
var DEFAULT_CAPTURE_MS2 = 2500;
|
|
1928
2202
|
function asHeaders(v) {
|
|
1929
2203
|
if (!v || typeof v !== "object")
|
|
@@ -1990,7 +2264,7 @@ function correlate(lines) {
|
|
|
1990
2264
|
async function readNetworkRequests(file) {
|
|
1991
2265
|
let raw;
|
|
1992
2266
|
try {
|
|
1993
|
-
raw = await
|
|
2267
|
+
raw = await readFile4(file, "utf8");
|
|
1994
2268
|
} catch {
|
|
1995
2269
|
return [];
|
|
1996
2270
|
}
|
|
@@ -2072,8 +2346,8 @@ async function getNetworkRequest(args = {}) {
|
|
|
2072
2346
|
}
|
|
2073
2347
|
|
|
2074
2348
|
// src/tools/performance.ts
|
|
2075
|
-
import { mkdir as
|
|
2076
|
-
import { join as
|
|
2349
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, stat, unlink as unlink3 } from "node:fs/promises";
|
|
2350
|
+
import { join as join4 } from "node:path";
|
|
2077
2351
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2078
2352
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
2079
2353
|
var DEFAULT_CATEGORIES = [
|
|
@@ -2091,7 +2365,7 @@ function shortId(id) {
|
|
|
2091
2365
|
return id.slice(0, 8);
|
|
2092
2366
|
}
|
|
2093
2367
|
function stateFilePath() {
|
|
2094
|
-
return
|
|
2368
|
+
return join4(STATE_DIR2, "trace-state.json");
|
|
2095
2369
|
}
|
|
2096
2370
|
function bufferTraceData(conn, sink) {
|
|
2097
2371
|
return conn.on("Tracing.dataCollected", (params) => {
|
|
@@ -2113,10 +2387,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
2113
2387
|
return sink;
|
|
2114
2388
|
}
|
|
2115
2389
|
async function writeTrace(events, targetId) {
|
|
2116
|
-
await
|
|
2117
|
-
const path =
|
|
2390
|
+
await mkdir5(ARTIFACT_DIR3, { recursive: true });
|
|
2391
|
+
const path = join4(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
2118
2392
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
2119
|
-
await
|
|
2393
|
+
await writeFile5(path, body, "utf8");
|
|
2120
2394
|
const { size } = await stat(path);
|
|
2121
2395
|
return { path, bytes: size };
|
|
2122
2396
|
}
|
|
@@ -2136,7 +2410,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
2136
2410
|
throw err;
|
|
2137
2411
|
}
|
|
2138
2412
|
liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
|
|
2139
|
-
await
|
|
2413
|
+
await mkdir5(STATE_DIR2, { recursive: true });
|
|
2140
2414
|
const state = {
|
|
2141
2415
|
targetId: target.id,
|
|
2142
2416
|
url: target.url,
|
|
@@ -2144,7 +2418,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
2144
2418
|
pid: process.pid,
|
|
2145
2419
|
categories
|
|
2146
2420
|
};
|
|
2147
|
-
await
|
|
2421
|
+
await writeFile5(stateFilePath(), JSON.stringify(state), "utf8");
|
|
2148
2422
|
return {
|
|
2149
2423
|
target: { id: target.id, url: target.url, title: target.title },
|
|
2150
2424
|
categories,
|
|
@@ -2157,7 +2431,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
2157
2431
|
if (!entry) {
|
|
2158
2432
|
let hint = "";
|
|
2159
2433
|
try {
|
|
2160
|
-
const raw = await
|
|
2434
|
+
const raw = await readFile5(stateFilePath(), "utf8");
|
|
2161
2435
|
const state = JSON.parse(raw);
|
|
2162
2436
|
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
2437
|
} catch {}
|
|
@@ -2169,7 +2443,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
2169
2443
|
} finally {
|
|
2170
2444
|
conn.close();
|
|
2171
2445
|
liveTraces.delete(target.id);
|
|
2172
|
-
await
|
|
2446
|
+
await unlink3(stateFilePath()).catch(() => {});
|
|
2173
2447
|
}
|
|
2174
2448
|
const written = await writeTrace(events, target.id);
|
|
2175
2449
|
const metrics = analyzeEvents(events);
|
|
@@ -2287,7 +2561,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
2287
2561
|
}
|
|
2288
2562
|
let raw;
|
|
2289
2563
|
try {
|
|
2290
|
-
raw = await
|
|
2564
|
+
raw = await readFile5(args.tracePath, "utf8");
|
|
2291
2565
|
} catch (err) {
|
|
2292
2566
|
throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
|
|
2293
2567
|
}
|
|
@@ -2304,8 +2578,8 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
2304
2578
|
}
|
|
2305
2579
|
|
|
2306
2580
|
// src/tools/heap.ts
|
|
2307
|
-
import { mkdir as
|
|
2308
|
-
import { join as
|
|
2581
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
|
|
2582
|
+
import { join as join5 } from "node:path";
|
|
2309
2583
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2310
2584
|
function stamp3(targetId) {
|
|
2311
2585
|
const short = targetId.slice(0, 8);
|
|
@@ -2330,9 +2604,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2330
2604
|
if (snapshot.length === 0) {
|
|
2331
2605
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
2332
2606
|
}
|
|
2333
|
-
await
|
|
2334
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
2335
|
-
await
|
|
2607
|
+
await mkdir6(ARTIFACT_DIR4, { recursive: true });
|
|
2608
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join5(ARTIFACT_DIR4, args.savePath) : join5(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
|
|
2609
|
+
await writeFile6(path, snapshot, "utf8");
|
|
2336
2610
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
2337
2611
|
return {
|
|
2338
2612
|
path,
|
|
@@ -2345,7 +2619,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2345
2619
|
|
|
2346
2620
|
// src/tools/lighthouse.ts
|
|
2347
2621
|
import { spawn } from "node:child_process";
|
|
2348
|
-
import { mkdir as
|
|
2622
|
+
import { mkdir as mkdir7, readFile as readFile6, stat as stat2 } from "node:fs/promises";
|
|
2349
2623
|
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2350
2624
|
function cdpPort() {
|
|
2351
2625
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
@@ -2406,7 +2680,7 @@ async function lighthouseAudit(args) {
|
|
|
2406
2680
|
const url = args.url.trim();
|
|
2407
2681
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
2408
2682
|
const port = cdpPort();
|
|
2409
|
-
await
|
|
2683
|
+
await mkdir7(ARTIFACT_DIR5, { recursive: true });
|
|
2410
2684
|
const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
|
|
2411
2685
|
const formFactor = args.formFactor ?? "desktop";
|
|
2412
2686
|
const lhArgs = [
|
|
@@ -2435,7 +2709,7 @@ async function lighthouseAudit(args) {
|
|
|
2435
2709
|
}
|
|
2436
2710
|
let raw;
|
|
2437
2711
|
try {
|
|
2438
|
-
raw = await
|
|
2712
|
+
raw = await readFile6(outPath, "utf8");
|
|
2439
2713
|
} catch {
|
|
2440
2714
|
raw = undefined;
|
|
2441
2715
|
}
|
|
@@ -2492,6 +2766,9 @@ var TOOLS = {
|
|
|
2492
2766
|
navigate_page: onCdp(SHARED_TOOLS.navigate_page),
|
|
2493
2767
|
wait_for: onCdp(SHARED_TOOLS.wait_for),
|
|
2494
2768
|
evaluate_script: onCdp(SHARED_TOOLS.evaluate_script),
|
|
2769
|
+
list_cookies: onCdp(SHARED_TOOLS.list_cookies),
|
|
2770
|
+
set_cookie: onCdp(SHARED_TOOLS.set_cookie),
|
|
2771
|
+
delete_cookies: onCdp(SHARED_TOOLS.delete_cookies),
|
|
2495
2772
|
take_snapshot: onCdp(SHARED_TOOLS.take_snapshot),
|
|
2496
2773
|
click: onCdp(SHARED_TOOLS.click),
|
|
2497
2774
|
hover: onCdp(SHARED_TOOLS.hover),
|