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/mcp.js
CHANGED
|
@@ -13,6 +13,9 @@ function leaseTtlMs() {
|
|
|
13
13
|
const raw = Number(process.env.CDP_LEASE_TTL_MS);
|
|
14
14
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
|
|
15
15
|
}
|
|
16
|
+
function defaultLabel() {
|
|
17
|
+
return `pid-${process.pid}`;
|
|
18
|
+
}
|
|
16
19
|
function safeId(id) {
|
|
17
20
|
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
18
21
|
}
|
|
@@ -679,6 +682,8 @@ var REQUIRED_CAPABILITIES = {
|
|
|
679
682
|
performance_trace: ["trace.performance"],
|
|
680
683
|
take_heapsnapshot: ["heap.snapshot"],
|
|
681
684
|
lighthouse_audit: ["audit.lighthouse"],
|
|
685
|
+
start_screen_recording: ["capture.screencast"],
|
|
686
|
+
stop_screen_recording: ["capture.screencast"],
|
|
682
687
|
mock_request: ["network.intercept"],
|
|
683
688
|
list_mocks: ["network.intercept"],
|
|
684
689
|
clear_mocks: ["network.intercept"]
|
|
@@ -704,6 +709,31 @@ function decodeUid(uid) {
|
|
|
704
709
|
throw driverError("foreign-uid", `malformed cdp uid payload: ${uid}`);
|
|
705
710
|
return n;
|
|
706
711
|
}
|
|
712
|
+
function normalizeCdpCookie(c) {
|
|
713
|
+
const same = (c.sameSite ?? "").toLowerCase();
|
|
714
|
+
const sameSite = same === "strict" || same === "lax" || same === "none" ? same : "default";
|
|
715
|
+
return {
|
|
716
|
+
name: c.name,
|
|
717
|
+
value: c.value,
|
|
718
|
+
domain: c.domain,
|
|
719
|
+
path: c.path,
|
|
720
|
+
expires: typeof c.expires === "number" ? c.expires : -1,
|
|
721
|
+
size: typeof c.size === "number" ? c.size : Buffer.byteLength(`${c.name}${c.value}`, "utf8"),
|
|
722
|
+
httpOnly: c.httpOnly === true,
|
|
723
|
+
secure: c.secure === true,
|
|
724
|
+
sameSite,
|
|
725
|
+
session: c.session === true
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
function cdpSameSite(sameSite) {
|
|
729
|
+
if (sameSite === "strict")
|
|
730
|
+
return "Strict";
|
|
731
|
+
if (sameSite === "lax")
|
|
732
|
+
return "Lax";
|
|
733
|
+
if (sameSite === "none")
|
|
734
|
+
return "None";
|
|
735
|
+
return;
|
|
736
|
+
}
|
|
707
737
|
async function centerOf(conn, objectId) {
|
|
708
738
|
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};}";
|
|
709
739
|
const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
@@ -909,7 +939,8 @@ var CDP_CAPABILITIES = new Set([
|
|
|
909
939
|
"snapshot.accessibilityTree",
|
|
910
940
|
"input.insertTextAtomic",
|
|
911
941
|
"locate.text",
|
|
912
|
-
"locate.xpath"
|
|
942
|
+
"locate.xpath",
|
|
943
|
+
"capture.screencast"
|
|
913
944
|
]);
|
|
914
945
|
|
|
915
946
|
class CdpPageDriver {
|
|
@@ -1226,6 +1257,38 @@ class CdpPageDriver {
|
|
|
1226
1257
|
});
|
|
1227
1258
|
});
|
|
1228
1259
|
}
|
|
1260
|
+
async getCookies() {
|
|
1261
|
+
await this.ensureDomain("Network");
|
|
1262
|
+
const { cookies } = await this.conn.send("Network.getCookies");
|
|
1263
|
+
return (cookies ?? []).map(normalizeCdpCookie);
|
|
1264
|
+
}
|
|
1265
|
+
async setCookie(params) {
|
|
1266
|
+
await this.ensureDomain("Network");
|
|
1267
|
+
const sameSite = cdpSameSite(params.sameSite);
|
|
1268
|
+
const { success } = await this.conn.send("Network.setCookie", {
|
|
1269
|
+
name: params.name,
|
|
1270
|
+
value: params.value,
|
|
1271
|
+
...params.url !== undefined ? { url: params.url } : {},
|
|
1272
|
+
...params.domain !== undefined ? { domain: params.domain } : {},
|
|
1273
|
+
...params.path !== undefined ? { path: params.path } : {},
|
|
1274
|
+
...params.expires !== undefined ? { expires: params.expires } : {},
|
|
1275
|
+
...params.httpOnly !== undefined ? { httpOnly: params.httpOnly } : {},
|
|
1276
|
+
...params.secure !== undefined ? { secure: params.secure } : {},
|
|
1277
|
+
...sameSite !== undefined ? { sameSite } : {}
|
|
1278
|
+
});
|
|
1279
|
+
if (success !== true) {
|
|
1280
|
+
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.`);
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
async deleteCookies(filter) {
|
|
1284
|
+
await this.ensureDomain("Network");
|
|
1285
|
+
await this.conn.send("Network.deleteCookies", {
|
|
1286
|
+
name: filter.name,
|
|
1287
|
+
...filter.url !== undefined ? { url: filter.url } : {},
|
|
1288
|
+
...filter.domain !== undefined ? { domain: filter.domain } : {},
|
|
1289
|
+
...filter.path !== undefined ? { path: filter.path } : {}
|
|
1290
|
+
});
|
|
1291
|
+
}
|
|
1229
1292
|
async startBodyCapture() {
|
|
1230
1293
|
await this.ensureDomain("Network");
|
|
1231
1294
|
return async () => {
|
|
@@ -1285,6 +1348,10 @@ async function withBrowserConn(fn) {
|
|
|
1285
1348
|
}
|
|
1286
1349
|
}
|
|
1287
1350
|
function noSuchTarget(e) {
|
|
1351
|
+
if (e instanceof LeaseConflictError)
|
|
1352
|
+
throw e;
|
|
1353
|
+
if (typeof e?.code === "string")
|
|
1354
|
+
throw e;
|
|
1288
1355
|
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
1289
1356
|
}
|
|
1290
1357
|
|
|
@@ -1328,6 +1395,97 @@ function createCdpDriver() {
|
|
|
1328
1395
|
return new CdpBrowserDriver;
|
|
1329
1396
|
}
|
|
1330
1397
|
|
|
1398
|
+
// src/origins.ts
|
|
1399
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1400
|
+
import { join as join2 } from "node:path";
|
|
1401
|
+
function safeId2(id) {
|
|
1402
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1403
|
+
}
|
|
1404
|
+
function originFile(backend, targetId) {
|
|
1405
|
+
return join2(leaseDir(), `origin-${backend}-${safeId2(targetId)}.json`);
|
|
1406
|
+
}
|
|
1407
|
+
var ORIGIN_FILE_RE = /^origin-(chrome|firefox)-(.+)\.json$/;
|
|
1408
|
+
async function recordOrigin(backend, targetId, opts = {}) {
|
|
1409
|
+
const record = {
|
|
1410
|
+
backend,
|
|
1411
|
+
targetId,
|
|
1412
|
+
label: typeof opts.label === "string" && opts.label.length ? opts.label : defaultLabel(),
|
|
1413
|
+
pid: process.pid,
|
|
1414
|
+
createdAt: opts.now ?? Date.now()
|
|
1415
|
+
};
|
|
1416
|
+
const file = originFile(backend, targetId);
|
|
1417
|
+
try {
|
|
1418
|
+
await mkdir2(leaseDir(), { recursive: true });
|
|
1419
|
+
try {
|
|
1420
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1421
|
+
return;
|
|
1422
|
+
} catch (err) {
|
|
1423
|
+
if (err.code !== "EEXIST")
|
|
1424
|
+
throw err;
|
|
1425
|
+
}
|
|
1426
|
+
await unlink2(file).catch(() => {
|
|
1427
|
+
return;
|
|
1428
|
+
});
|
|
1429
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1430
|
+
} catch {}
|
|
1431
|
+
}
|
|
1432
|
+
async function listOrigins(opts = {}) {
|
|
1433
|
+
let names = [];
|
|
1434
|
+
try {
|
|
1435
|
+
names = await readdir2(leaseDir());
|
|
1436
|
+
} catch {
|
|
1437
|
+
return [];
|
|
1438
|
+
}
|
|
1439
|
+
const live = opts.liveIds === undefined ? undefined : new Set(opts.liveIds.map((id) => safeId2(id)));
|
|
1440
|
+
const out = [];
|
|
1441
|
+
for (const name of names) {
|
|
1442
|
+
const parts = ORIGIN_FILE_RE.exec(name);
|
|
1443
|
+
if (!parts)
|
|
1444
|
+
continue;
|
|
1445
|
+
const backend = parts[1];
|
|
1446
|
+
const nameId = parts[2];
|
|
1447
|
+
const scoped = live !== undefined && (opts.liveBackend === undefined || opts.liveBackend === backend);
|
|
1448
|
+
if (scoped && !live.has(nameId)) {
|
|
1449
|
+
await unlink2(join2(leaseDir(), name)).catch(() => {
|
|
1450
|
+
return;
|
|
1451
|
+
});
|
|
1452
|
+
continue;
|
|
1453
|
+
}
|
|
1454
|
+
const fromName = { backend, targetId: nameId, label: "", pid: 0, createdAt: 0 };
|
|
1455
|
+
let rec;
|
|
1456
|
+
try {
|
|
1457
|
+
rec = JSON.parse(await readFile2(join2(leaseDir(), name), "utf8"));
|
|
1458
|
+
} catch (err) {
|
|
1459
|
+
const code = err.code;
|
|
1460
|
+
if (code === "ENOENT")
|
|
1461
|
+
continue;
|
|
1462
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
1463
|
+
continue;
|
|
1464
|
+
}
|
|
1465
|
+
if (typeof rec?.targetId !== "string" || typeof rec?.label !== "string" || typeof rec?.createdAt !== "number") {
|
|
1466
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
1467
|
+
continue;
|
|
1468
|
+
}
|
|
1469
|
+
out.push({ backend: rec.backend, targetId: rec.targetId, label: rec.label, pid: rec.pid, createdAt: rec.createdAt });
|
|
1470
|
+
}
|
|
1471
|
+
return out;
|
|
1472
|
+
}
|
|
1473
|
+
async function originIndex(backend, liveIds) {
|
|
1474
|
+
const rows = await listOrigins({ liveIds, liveBackend: backend }).catch(() => []);
|
|
1475
|
+
const index = new Map;
|
|
1476
|
+
for (const row of rows) {
|
|
1477
|
+
if (row.backend !== backend)
|
|
1478
|
+
continue;
|
|
1479
|
+
index.set(row.targetId, row);
|
|
1480
|
+
}
|
|
1481
|
+
return index;
|
|
1482
|
+
}
|
|
1483
|
+
async function newTrackedPage(driver, backend, opts = {}) {
|
|
1484
|
+
const page = await driver.newPage(opts.url);
|
|
1485
|
+
await recordOrigin(backend, page.id, { label: opts.label });
|
|
1486
|
+
return page;
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1331
1489
|
// src/leases-tools.ts
|
|
1332
1490
|
class LeaseToolError extends Error {
|
|
1333
1491
|
}
|
|
@@ -1336,12 +1494,12 @@ function backendOf(driver) {
|
|
|
1336
1494
|
}
|
|
1337
1495
|
async function claimPage(driver, args = {}) {
|
|
1338
1496
|
const backend = backendOf(driver);
|
|
1339
|
-
const label = typeof args.label === "string" && args.label.length ? args.label :
|
|
1497
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1340
1498
|
const pages = await driver.listPages();
|
|
1341
1499
|
let targetId = args.targetId;
|
|
1342
1500
|
let url = args.url ?? "about:blank";
|
|
1343
1501
|
if (targetId === undefined || targetId === "") {
|
|
1344
|
-
const page = await driver
|
|
1502
|
+
const page = await newTrackedPage(driver, backend, { url: args.url, label });
|
|
1345
1503
|
targetId = page.id;
|
|
1346
1504
|
url = page.url;
|
|
1347
1505
|
} else {
|
|
@@ -1383,8 +1541,8 @@ var LEASE_TOOLS = {
|
|
|
1383
1541
|
};
|
|
1384
1542
|
|
|
1385
1543
|
// src/shared-tools.ts
|
|
1386
|
-
import { mkdir as
|
|
1387
|
-
import { join as
|
|
1544
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
1545
|
+
import { dirname, join as join3 } from "node:path";
|
|
1388
1546
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1389
1547
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1390
1548
|
|
|
@@ -1452,16 +1610,35 @@ function locatorOf(args) {
|
|
|
1452
1610
|
throw new SharedToolError("provide exactly one of { uid } or { selector }");
|
|
1453
1611
|
return hasUid ? { uid: args.uid } : { css: args.selector };
|
|
1454
1612
|
}
|
|
1613
|
+
async function reapSet(driver, pages, all) {
|
|
1614
|
+
if (all === true)
|
|
1615
|
+
return pages.map((p) => p.id);
|
|
1616
|
+
try {
|
|
1617
|
+
return (await driver.listPages({ all: true })).map((p) => p.id);
|
|
1618
|
+
} catch {
|
|
1619
|
+
return;
|
|
1620
|
+
}
|
|
1621
|
+
}
|
|
1455
1622
|
async function listPages(driver, args = {}) {
|
|
1456
1623
|
const pages = await driver.listPages({ all: args.all });
|
|
1457
|
-
|
|
1624
|
+
const ledger = await originIndex(backendOf2(driver), await reapSet(driver, pages, args.all));
|
|
1625
|
+
const annotated = pages.map((p) => {
|
|
1626
|
+
const rec = ledger.get(p.id);
|
|
1627
|
+
if (!rec)
|
|
1628
|
+
return { ...p, origin: "unknown" };
|
|
1629
|
+
if (rec.unreadable !== undefined)
|
|
1630
|
+
return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
|
|
1631
|
+
return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
|
|
1632
|
+
});
|
|
1633
|
+
return { pages: annotated, count: annotated.length };
|
|
1458
1634
|
}
|
|
1459
1635
|
async function newPage(driver, args = {}) {
|
|
1460
|
-
const
|
|
1636
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1637
|
+
const p = await newTrackedPage(driver, backendOf2(driver), { url: args.url, label });
|
|
1461
1638
|
if (args.claim !== true)
|
|
1462
1639
|
return { targetId: p.id, url: p.url };
|
|
1463
1640
|
const { record, token } = await claimLease(backendOf2(driver), p.id, {
|
|
1464
|
-
label
|
|
1641
|
+
label,
|
|
1465
1642
|
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
|
|
1466
1643
|
});
|
|
1467
1644
|
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
@@ -1477,8 +1654,8 @@ async function closePage(driver, args) {
|
|
|
1477
1654
|
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
1478
1655
|
}
|
|
1479
1656
|
async function writeSelectedFile(id) {
|
|
1480
|
-
await
|
|
1481
|
-
await
|
|
1657
|
+
await mkdir3(STATE_DIR, { recursive: true });
|
|
1658
|
+
await writeFile3(join3(STATE_DIR, "selected"), id, "utf8");
|
|
1482
1659
|
}
|
|
1483
1660
|
async function selectPage(driver, args) {
|
|
1484
1661
|
if (args.target === undefined || args.target === "")
|
|
@@ -1518,11 +1695,108 @@ async function waitForText(driver, args) {
|
|
|
1518
1695
|
return { found: true, waitedMs: r.elapsedMs };
|
|
1519
1696
|
});
|
|
1520
1697
|
}
|
|
1698
|
+
function serializeForSink(value) {
|
|
1699
|
+
const json = JSON.stringify(value === undefined ? null : value, null, 2);
|
|
1700
|
+
return json ?? "null";
|
|
1701
|
+
}
|
|
1702
|
+
async function writeJsonSink(savePath, value) {
|
|
1703
|
+
const path = savePath.startsWith("/") ? savePath : join3(ARTIFACT_DIR, savePath);
|
|
1704
|
+
await mkdir3(dirname(path), { recursive: true });
|
|
1705
|
+
const json = serializeForSink(value);
|
|
1706
|
+
await writeFile3(path, json, "utf8");
|
|
1707
|
+
return { path, bytes: Buffer.byteLength(json, "utf8") };
|
|
1708
|
+
}
|
|
1709
|
+
function sinkRequested(savePath) {
|
|
1710
|
+
return savePath !== undefined && savePath !== "";
|
|
1711
|
+
}
|
|
1521
1712
|
async function evaluateScript(driver, args) {
|
|
1522
1713
|
if (typeof args.expression !== "string" || args.expression.length === 0) {
|
|
1523
1714
|
throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
|
|
1524
1715
|
}
|
|
1525
|
-
return withPage2(driver, args.target, (page) =>
|
|
1716
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1717
|
+
const value = await page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true });
|
|
1718
|
+
if (!sinkRequested(args.savePath))
|
|
1719
|
+
return value;
|
|
1720
|
+
const { path, bytes } = await writeJsonSink(args.savePath, value);
|
|
1721
|
+
const result = {
|
|
1722
|
+
path,
|
|
1723
|
+
bytes,
|
|
1724
|
+
type: value === null ? "null" : typeof value,
|
|
1725
|
+
target: target3(page.info)
|
|
1726
|
+
};
|
|
1727
|
+
return result;
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
function cookieDomainMatches(cookieDomain, filter) {
|
|
1731
|
+
const bare = (d) => (d.startsWith(".") ? d.slice(1) : d).toLowerCase();
|
|
1732
|
+
const c = bare(cookieDomain);
|
|
1733
|
+
const f = bare(filter);
|
|
1734
|
+
return c === f || c.endsWith(`.${f}`);
|
|
1735
|
+
}
|
|
1736
|
+
function filterCookies(cookies, filter) {
|
|
1737
|
+
return cookies.filter((c) => {
|
|
1738
|
+
if (filter.name !== undefined && filter.name !== "" && c.name !== filter.name)
|
|
1739
|
+
return false;
|
|
1740
|
+
if (filter.domain !== undefined && filter.domain !== "" && !cookieDomainMatches(c.domain, filter.domain))
|
|
1741
|
+
return false;
|
|
1742
|
+
return true;
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
async function listCookies(driver, args = {}) {
|
|
1746
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1747
|
+
const all = await page.getCookies();
|
|
1748
|
+
const cookies = filterCookies(all, { domain: args.domain, name: args.name });
|
|
1749
|
+
if (!sinkRequested(args.savePath)) {
|
|
1750
|
+
return { cookies, count: cookies.length, target: target3(page.info) };
|
|
1751
|
+
}
|
|
1752
|
+
const { path, bytes } = await writeJsonSink(args.savePath, cookies);
|
|
1753
|
+
return { path, bytes, count: cookies.length, target: target3(page.info) };
|
|
1754
|
+
});
|
|
1755
|
+
}
|
|
1756
|
+
function assertCookieSite(args, toolName) {
|
|
1757
|
+
const hasUrl = args.url !== undefined && args.url !== "";
|
|
1758
|
+
const hasDomain = args.domain !== undefined && args.domain !== "";
|
|
1759
|
+
if (!hasUrl && !hasDomain) {
|
|
1760
|
+
throw new Error(`${toolName} requires either 'url' or 'domain': a cookie has to be attributed to a site, and neither was given.`);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
function assertCookieName(name, toolName) {
|
|
1764
|
+
if (name === undefined || name === "") {
|
|
1765
|
+
throw new Error(`${toolName} requires a non-empty 'name'.`);
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
async function setCookie(driver, args) {
|
|
1769
|
+
assertCookieName(args.name, "set_cookie");
|
|
1770
|
+
if (typeof args.value !== "string")
|
|
1771
|
+
throw new Error("set_cookie requires a string 'value'.");
|
|
1772
|
+
assertCookieSite(args, "set_cookie");
|
|
1773
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1774
|
+
await page.setCookie({
|
|
1775
|
+
name: args.name,
|
|
1776
|
+
value: args.value,
|
|
1777
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1778
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1779
|
+
...args.path !== undefined ? { path: args.path } : {},
|
|
1780
|
+
...args.expires !== undefined ? { expires: args.expires } : {},
|
|
1781
|
+
...args.httpOnly !== undefined ? { httpOnly: args.httpOnly } : {},
|
|
1782
|
+
...args.secure !== undefined ? { secure: args.secure } : {},
|
|
1783
|
+
...args.sameSite !== undefined ? { sameSite: args.sameSite } : {}
|
|
1784
|
+
});
|
|
1785
|
+
return { set: true, target: target3(page.info) };
|
|
1786
|
+
});
|
|
1787
|
+
}
|
|
1788
|
+
async function deleteCookies(driver, args) {
|
|
1789
|
+
assertCookieName(args.name, "delete_cookies");
|
|
1790
|
+
assertCookieSite(args, "delete_cookies");
|
|
1791
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1792
|
+
await page.deleteCookies({
|
|
1793
|
+
name: args.name,
|
|
1794
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1795
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1796
|
+
...args.path !== undefined ? { path: args.path } : {}
|
|
1797
|
+
});
|
|
1798
|
+
return { deleted: true, target: target3(page.info) };
|
|
1799
|
+
});
|
|
1526
1800
|
}
|
|
1527
1801
|
var BARE_TOKEN_EXTRAS = new Set(["disabled", "focused"]);
|
|
1528
1802
|
var UNQUOTED_EXTRAS = new Set(["checked", "expanded", "url"]);
|
|
@@ -1633,10 +1907,10 @@ async function takeScreenshot(driver, args = {}) {
|
|
|
1633
1907
|
const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
|
|
1634
1908
|
if (data.byteLength === 0)
|
|
1635
1909
|
throw new SharedToolError("captureScreenshot returned empty data");
|
|
1636
|
-
await
|
|
1910
|
+
await mkdir3(ARTIFACT_DIR, { recursive: true });
|
|
1637
1911
|
const ext = outFormat === "jpeg" ? "jpg" : "png";
|
|
1638
|
-
const path = args.savePath ??
|
|
1639
|
-
await
|
|
1912
|
+
const path = args.savePath ?? join3(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
1913
|
+
await writeFile3(path, Buffer.from(data));
|
|
1640
1914
|
const result = {
|
|
1641
1915
|
path,
|
|
1642
1916
|
bytes: data.byteLength,
|
|
@@ -1683,6 +1957,9 @@ var SHARED_TOOLS = {
|
|
|
1683
1957
|
navigate_page: navigatePage,
|
|
1684
1958
|
wait_for: waitForText,
|
|
1685
1959
|
evaluate_script: evaluateScript,
|
|
1960
|
+
list_cookies: listCookies,
|
|
1961
|
+
set_cookie: setCookie,
|
|
1962
|
+
delete_cookies: deleteCookies,
|
|
1686
1963
|
take_snapshot: takeSnapshot,
|
|
1687
1964
|
click,
|
|
1688
1965
|
hover,
|
|
@@ -1699,10 +1976,10 @@ var SHARED_TOOLS = {
|
|
|
1699
1976
|
};
|
|
1700
1977
|
|
|
1701
1978
|
// src/tools/console.ts
|
|
1702
|
-
import { readFile as
|
|
1979
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1703
1980
|
|
|
1704
1981
|
// src/tools/recorder.ts
|
|
1705
|
-
import { mkdir as
|
|
1982
|
+
import { mkdir as mkdir4, appendFile, writeFile as writeFile4, copyFile } from "node:fs/promises";
|
|
1706
1983
|
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1707
1984
|
function recFile(targetId) {
|
|
1708
1985
|
return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
|
|
@@ -1723,11 +2000,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
1723
2000
|
async function startRecorder(target, opts = {}) {
|
|
1724
2001
|
const network = opts.network ?? true;
|
|
1725
2002
|
const consoleCap = opts.console ?? true;
|
|
1726
|
-
await
|
|
2003
|
+
await mkdir4(ARTIFACT_DIR2, { recursive: true });
|
|
1727
2004
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
1728
2005
|
const file = opts.file ?? recFile(resolved.id);
|
|
1729
2006
|
if (opts.truncate) {
|
|
1730
|
-
await
|
|
2007
|
+
await writeFile4(file, "");
|
|
1731
2008
|
}
|
|
1732
2009
|
let chain = Promise.resolve();
|
|
1733
2010
|
let dropped = 0;
|
|
@@ -1869,7 +2146,7 @@ function lineToEntry(line, index) {
|
|
|
1869
2146
|
async function readConsoleEntries(file) {
|
|
1870
2147
|
let raw;
|
|
1871
2148
|
try {
|
|
1872
|
-
raw = await
|
|
2149
|
+
raw = await readFile3(file, "utf8");
|
|
1873
2150
|
} catch {
|
|
1874
2151
|
return [];
|
|
1875
2152
|
}
|
|
@@ -1925,7 +2202,7 @@ async function getConsoleMessage(args = {}) {
|
|
|
1925
2202
|
}
|
|
1926
2203
|
|
|
1927
2204
|
// src/tools/network.ts
|
|
1928
|
-
import { readFile as
|
|
2205
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1929
2206
|
var DEFAULT_CAPTURE_MS2 = 2500;
|
|
1930
2207
|
function asHeaders(v) {
|
|
1931
2208
|
if (!v || typeof v !== "object")
|
|
@@ -1992,7 +2269,7 @@ function correlate(lines) {
|
|
|
1992
2269
|
async function readNetworkRequests(file) {
|
|
1993
2270
|
let raw;
|
|
1994
2271
|
try {
|
|
1995
|
-
raw = await
|
|
2272
|
+
raw = await readFile4(file, "utf8");
|
|
1996
2273
|
} catch {
|
|
1997
2274
|
return [];
|
|
1998
2275
|
}
|
|
@@ -2074,8 +2351,8 @@ async function getNetworkRequest(args = {}) {
|
|
|
2074
2351
|
}
|
|
2075
2352
|
|
|
2076
2353
|
// src/tools/performance.ts
|
|
2077
|
-
import { mkdir as
|
|
2078
|
-
import { join as
|
|
2354
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, stat, unlink as unlink3 } from "node:fs/promises";
|
|
2355
|
+
import { join as join4 } from "node:path";
|
|
2079
2356
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2080
2357
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
2081
2358
|
var DEFAULT_CATEGORIES = [
|
|
@@ -2093,7 +2370,7 @@ function shortId(id) {
|
|
|
2093
2370
|
return id.slice(0, 8);
|
|
2094
2371
|
}
|
|
2095
2372
|
function stateFilePath() {
|
|
2096
|
-
return
|
|
2373
|
+
return join4(STATE_DIR2, "trace-state.json");
|
|
2097
2374
|
}
|
|
2098
2375
|
function bufferTraceData(conn, sink) {
|
|
2099
2376
|
return conn.on("Tracing.dataCollected", (params) => {
|
|
@@ -2115,10 +2392,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
2115
2392
|
return sink;
|
|
2116
2393
|
}
|
|
2117
2394
|
async function writeTrace(events, targetId) {
|
|
2118
|
-
await
|
|
2119
|
-
const path =
|
|
2395
|
+
await mkdir5(ARTIFACT_DIR3, { recursive: true });
|
|
2396
|
+
const path = join4(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
2120
2397
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
2121
|
-
await
|
|
2398
|
+
await writeFile5(path, body, "utf8");
|
|
2122
2399
|
const { size } = await stat(path);
|
|
2123
2400
|
return { path, bytes: size };
|
|
2124
2401
|
}
|
|
@@ -2138,7 +2415,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
2138
2415
|
throw err;
|
|
2139
2416
|
}
|
|
2140
2417
|
liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
|
|
2141
|
-
await
|
|
2418
|
+
await mkdir5(STATE_DIR2, { recursive: true });
|
|
2142
2419
|
const state = {
|
|
2143
2420
|
targetId: target.id,
|
|
2144
2421
|
url: target.url,
|
|
@@ -2146,7 +2423,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
2146
2423
|
pid: process.pid,
|
|
2147
2424
|
categories
|
|
2148
2425
|
};
|
|
2149
|
-
await
|
|
2426
|
+
await writeFile5(stateFilePath(), JSON.stringify(state), "utf8");
|
|
2150
2427
|
return {
|
|
2151
2428
|
target: { id: target.id, url: target.url, title: target.title },
|
|
2152
2429
|
categories,
|
|
@@ -2159,7 +2436,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
2159
2436
|
if (!entry) {
|
|
2160
2437
|
let hint = "";
|
|
2161
2438
|
try {
|
|
2162
|
-
const raw = await
|
|
2439
|
+
const raw = await readFile5(stateFilePath(), "utf8");
|
|
2163
2440
|
const state = JSON.parse(raw);
|
|
2164
2441
|
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)`;
|
|
2165
2442
|
} catch {}
|
|
@@ -2171,7 +2448,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
2171
2448
|
} finally {
|
|
2172
2449
|
conn.close();
|
|
2173
2450
|
liveTraces.delete(target.id);
|
|
2174
|
-
await
|
|
2451
|
+
await unlink3(stateFilePath()).catch(() => {});
|
|
2175
2452
|
}
|
|
2176
2453
|
const written = await writeTrace(events, target.id);
|
|
2177
2454
|
const metrics = analyzeEvents(events);
|
|
@@ -2289,7 +2566,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
2289
2566
|
}
|
|
2290
2567
|
let raw;
|
|
2291
2568
|
try {
|
|
2292
|
-
raw = await
|
|
2569
|
+
raw = await readFile5(args.tracePath, "utf8");
|
|
2293
2570
|
} catch (err) {
|
|
2294
2571
|
throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
|
|
2295
2572
|
}
|
|
@@ -2305,11 +2582,375 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
2305
2582
|
return { ...analyzeEvents(events), tracePath: args.tracePath };
|
|
2306
2583
|
}
|
|
2307
2584
|
|
|
2308
|
-
// src/tools/
|
|
2309
|
-
import {
|
|
2310
|
-
import {
|
|
2585
|
+
// src/tools/screencast.ts
|
|
2586
|
+
import { spawn } from "node:child_process";
|
|
2587
|
+
import { mkdir as mkdir6, rm, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
2588
|
+
import { join as join5 } from "node:path";
|
|
2311
2589
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2312
|
-
|
|
2590
|
+
var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
|
|
2591
|
+
function frameDurationsMs(ledger, stopWallclockMs) {
|
|
2592
|
+
return ledger.map((entry, i) => {
|
|
2593
|
+
const next = i + 1 < ledger.length ? ledger[i + 1].timestamp : stopWallclockMs;
|
|
2594
|
+
return Math.max(1, Math.round(next - entry.timestamp));
|
|
2595
|
+
});
|
|
2596
|
+
}
|
|
2597
|
+
var CONCAT_GRID_MS = 40;
|
|
2598
|
+
function coalesceToConcatGrid(ledger, stopWallclockMs, gridMs = CONCAT_GRID_MS) {
|
|
2599
|
+
if (!ledger.length)
|
|
2600
|
+
return [];
|
|
2601
|
+
const raw = frameDurationsMs(ledger, stopWallclockMs);
|
|
2602
|
+
let elapsed = 0;
|
|
2603
|
+
const kept = [];
|
|
2604
|
+
let lastSlot = -1;
|
|
2605
|
+
for (let i = 0;i < ledger.length; i++) {
|
|
2606
|
+
const slot = Math.round(elapsed / gridMs);
|
|
2607
|
+
if (slot > lastSlot) {
|
|
2608
|
+
kept.push({ file: ledger[i].file, slot });
|
|
2609
|
+
lastSlot = slot;
|
|
2610
|
+
}
|
|
2611
|
+
elapsed += raw[i];
|
|
2612
|
+
}
|
|
2613
|
+
const endSlot = Math.max(lastSlot + 1, Math.round(elapsed / gridMs));
|
|
2614
|
+
return kept.map((frame, i) => ({
|
|
2615
|
+
file: frame.file,
|
|
2616
|
+
durationMs: ((i + 1 < kept.length ? kept[i + 1].slot : endSlot) - frame.slot) * gridMs
|
|
2617
|
+
}));
|
|
2618
|
+
}
|
|
2619
|
+
function buildConcatManifest(frames) {
|
|
2620
|
+
if (!frames.length)
|
|
2621
|
+
throw new CdpError("cannot build a concat manifest from an empty frame ledger");
|
|
2622
|
+
const lines = ["ffconcat version 1.0"];
|
|
2623
|
+
for (const frame of frames) {
|
|
2624
|
+
lines.push(`file ${frame.file}`);
|
|
2625
|
+
lines.push(`duration ${(frame.durationMs / 1000).toFixed(6)}`);
|
|
2626
|
+
}
|
|
2627
|
+
lines.push(`file ${frames[frames.length - 1].file}`);
|
|
2628
|
+
return `${lines.join(`
|
|
2629
|
+
`)}
|
|
2630
|
+
`;
|
|
2631
|
+
}
|
|
2632
|
+
function evenDimensions(width, height) {
|
|
2633
|
+
return { width: Math.max(0, Math.trunc(width / 2) * 2), height: Math.max(0, Math.trunc(height / 2) * 2) };
|
|
2634
|
+
}
|
|
2635
|
+
function pngSize(buf) {
|
|
2636
|
+
if (buf.length < 24)
|
|
2637
|
+
return;
|
|
2638
|
+
if (buf.readUInt32BE(0) !== 2303741511 || buf.readUInt32BE(4) !== 218765834)
|
|
2639
|
+
return;
|
|
2640
|
+
if (buf.toString("latin1", 12, 16) !== "IHDR")
|
|
2641
|
+
return;
|
|
2642
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
2643
|
+
}
|
|
2644
|
+
var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
|
|
2645
|
+
function jpegSize(buf) {
|
|
2646
|
+
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
|
|
2647
|
+
return;
|
|
2648
|
+
let i = 2;
|
|
2649
|
+
while (i + 3 < buf.length) {
|
|
2650
|
+
if (buf[i] !== 255) {
|
|
2651
|
+
i += 1;
|
|
2652
|
+
continue;
|
|
2653
|
+
}
|
|
2654
|
+
const marker = buf[i + 1];
|
|
2655
|
+
if (marker === 255) {
|
|
2656
|
+
i += 1;
|
|
2657
|
+
continue;
|
|
2658
|
+
}
|
|
2659
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
2660
|
+
i += 2;
|
|
2661
|
+
continue;
|
|
2662
|
+
}
|
|
2663
|
+
if (i + 3 >= buf.length)
|
|
2664
|
+
return;
|
|
2665
|
+
const segLen = buf.readUInt16BE(i + 2);
|
|
2666
|
+
if (JPEG_SOF.has(marker)) {
|
|
2667
|
+
if (i + 9 > buf.length)
|
|
2668
|
+
return;
|
|
2669
|
+
return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
|
|
2670
|
+
}
|
|
2671
|
+
if (segLen < 2)
|
|
2672
|
+
return;
|
|
2673
|
+
i += 2 + segLen;
|
|
2674
|
+
}
|
|
2675
|
+
return;
|
|
2676
|
+
}
|
|
2677
|
+
function imageSize(buf) {
|
|
2678
|
+
return pngSize(buf) ?? jpegSize(buf);
|
|
2679
|
+
}
|
|
2680
|
+
var ENCODER_LADDER = [
|
|
2681
|
+
{ encoder: "hevc_videotoolbox", codec: "hevc", tag: "hvc1" },
|
|
2682
|
+
{ encoder: "h264_videotoolbox", codec: "h264" },
|
|
2683
|
+
{ encoder: "libx265", codec: "hevc", tag: "hvc1" },
|
|
2684
|
+
{ encoder: "libx264", codec: "h264" }
|
|
2685
|
+
];
|
|
2686
|
+
function ffmpegEncoderNames(encodersOutput) {
|
|
2687
|
+
const names = new Set;
|
|
2688
|
+
for (const line of encodersOutput.split(`
|
|
2689
|
+
`)) {
|
|
2690
|
+
const m = /^\s*[VAS][A-Z.]{5}\s+(\S+)/.exec(line);
|
|
2691
|
+
if (m)
|
|
2692
|
+
names.add(m[1]);
|
|
2693
|
+
}
|
|
2694
|
+
return names;
|
|
2695
|
+
}
|
|
2696
|
+
function selectEncoder(encodersOutput) {
|
|
2697
|
+
const available = ffmpegEncoderNames(encodersOutput);
|
|
2698
|
+
for (const candidate of ENCODER_LADDER) {
|
|
2699
|
+
if (available.has(candidate.encoder))
|
|
2700
|
+
return candidate;
|
|
2701
|
+
}
|
|
2702
|
+
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).");
|
|
2703
|
+
}
|
|
2704
|
+
function buildFfmpegArgs(manifestPath, outPath, choice) {
|
|
2705
|
+
return [
|
|
2706
|
+
"-hide_banner",
|
|
2707
|
+
"-loglevel",
|
|
2708
|
+
"error",
|
|
2709
|
+
"-y",
|
|
2710
|
+
"-f",
|
|
2711
|
+
"concat",
|
|
2712
|
+
"-safe",
|
|
2713
|
+
"0",
|
|
2714
|
+
"-i",
|
|
2715
|
+
manifestPath,
|
|
2716
|
+
"-vsync",
|
|
2717
|
+
"vfr",
|
|
2718
|
+
"-vf",
|
|
2719
|
+
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
2720
|
+
"-c:v",
|
|
2721
|
+
choice.encoder,
|
|
2722
|
+
...choice.tag ? ["-tag:v", choice.tag] : [],
|
|
2723
|
+
"-pix_fmt",
|
|
2724
|
+
"yuv420p",
|
|
2725
|
+
"-movflags",
|
|
2726
|
+
"+faststart",
|
|
2727
|
+
"-an",
|
|
2728
|
+
outPath
|
|
2729
|
+
];
|
|
2730
|
+
}
|
|
2731
|
+
function run(bin, args) {
|
|
2732
|
+
return new Promise((resolve, reject) => {
|
|
2733
|
+
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2734
|
+
let stdout = "";
|
|
2735
|
+
let stderr = "";
|
|
2736
|
+
child.stdout?.on("data", (chunk) => {
|
|
2737
|
+
stdout += chunk.toString("utf8");
|
|
2738
|
+
});
|
|
2739
|
+
child.stderr?.on("data", (chunk) => {
|
|
2740
|
+
stderr += chunk.toString("utf8");
|
|
2741
|
+
});
|
|
2742
|
+
child.on("error", reject);
|
|
2743
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
2744
|
+
});
|
|
2745
|
+
}
|
|
2746
|
+
var encoderProbe;
|
|
2747
|
+
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.";
|
|
2748
|
+
async function probeEncoder() {
|
|
2749
|
+
if (!encoderProbe) {
|
|
2750
|
+
encoderProbe = (async () => {
|
|
2751
|
+
let res;
|
|
2752
|
+
try {
|
|
2753
|
+
res = await run(FFMPEG_BIN, ["-hide_banner", "-encoders"]);
|
|
2754
|
+
} catch (err) {
|
|
2755
|
+
throw new CdpError(`${FFMPEG_MISSING} (${err.message})`);
|
|
2756
|
+
}
|
|
2757
|
+
if (res.code !== 0)
|
|
2758
|
+
throw new CdpError(`${FFMPEG_MISSING} ('-encoders' exited ${res.code}: ${res.stderr.trim().slice(0, 300)})`);
|
|
2759
|
+
return selectEncoder(`${res.stdout}
|
|
2760
|
+
${res.stderr}`);
|
|
2761
|
+
})();
|
|
2762
|
+
encoderProbe.catch(() => {
|
|
2763
|
+
encoderProbe = undefined;
|
|
2764
|
+
});
|
|
2765
|
+
}
|
|
2766
|
+
return encoderProbe;
|
|
2767
|
+
}
|
|
2768
|
+
var liveRecordings = new Map;
|
|
2769
|
+
function shortId2(id) {
|
|
2770
|
+
return id.slice(0, 8);
|
|
2771
|
+
}
|
|
2772
|
+
function assertNotRecording(targetId, activeIds) {
|
|
2773
|
+
if (activeIds.includes(targetId)) {
|
|
2774
|
+
throw new CdpError(`a screen recording is already in progress for target ${shortId2(targetId)}; call stop_screen_recording first`);
|
|
2775
|
+
}
|
|
2776
|
+
}
|
|
2777
|
+
function chooseRecordingId(activeIds, selector) {
|
|
2778
|
+
if (!activeIds.length) {
|
|
2779
|
+
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).");
|
|
2780
|
+
}
|
|
2781
|
+
if (selector === undefined || selector === "active") {
|
|
2782
|
+
if (activeIds.length === 1)
|
|
2783
|
+
return activeIds[0];
|
|
2784
|
+
throw new CdpError(`${activeIds.length} screen recordings are in progress (targets: ${activeIds.map(shortId2).join(", ")}); pass 'target' to say which one to stop`);
|
|
2785
|
+
}
|
|
2786
|
+
return activeIds.find((id) => id === selector);
|
|
2787
|
+
}
|
|
2788
|
+
function stamp3() {
|
|
2789
|
+
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
2790
|
+
}
|
|
2791
|
+
function target32(t) {
|
|
2792
|
+
return { id: t.id, url: t.url, title: t.title };
|
|
2793
|
+
}
|
|
2794
|
+
async function startScreenRecording(args = {}) {
|
|
2795
|
+
const format = args.format ?? "jpeg";
|
|
2796
|
+
if (format !== "jpeg" && format !== "png")
|
|
2797
|
+
throw new CdpError("start_screen_recording: format must be 'jpeg' or 'png'");
|
|
2798
|
+
if (args.quality !== undefined && (!Number.isFinite(args.quality) || args.quality < 0 || args.quality > 100)) {
|
|
2799
|
+
throw new CdpError("start_screen_recording: quality must be a number between 0 and 100 (jpeg only)");
|
|
2800
|
+
}
|
|
2801
|
+
const encoder = await probeEncoder();
|
|
2802
|
+
const { conn, target } = await openPage(args.target);
|
|
2803
|
+
try {
|
|
2804
|
+
assertNotRecording(target.id, [...liveRecordings.keys()]);
|
|
2805
|
+
} catch (err) {
|
|
2806
|
+
conn.close();
|
|
2807
|
+
throw err;
|
|
2808
|
+
}
|
|
2809
|
+
const spoolDir = join5(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
|
|
2810
|
+
try {
|
|
2811
|
+
await mkdir6(spoolDir, { recursive: true });
|
|
2812
|
+
} catch (err) {
|
|
2813
|
+
conn.close();
|
|
2814
|
+
throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
|
|
2815
|
+
}
|
|
2816
|
+
const ext = format === "png" ? "png" : "jpg";
|
|
2817
|
+
const rec = {
|
|
2818
|
+
conn,
|
|
2819
|
+
target,
|
|
2820
|
+
spoolDir,
|
|
2821
|
+
ledger: [],
|
|
2822
|
+
startedAt: Date.now(),
|
|
2823
|
+
format,
|
|
2824
|
+
encoder,
|
|
2825
|
+
writes: Promise.resolve(),
|
|
2826
|
+
dropped: 0,
|
|
2827
|
+
unwritten: new Set,
|
|
2828
|
+
off: () => {}
|
|
2829
|
+
};
|
|
2830
|
+
let seq = 0;
|
|
2831
|
+
rec.off = conn.on("Page.screencastFrame", (params) => {
|
|
2832
|
+
const sessionId = params.sessionId;
|
|
2833
|
+
if (typeof sessionId === "number") {
|
|
2834
|
+
conn.send("Page.screencastFrameAck", { sessionId }).catch(() => {});
|
|
2835
|
+
}
|
|
2836
|
+
const data = params.data;
|
|
2837
|
+
if (typeof data !== "string" || !data.length) {
|
|
2838
|
+
rec.dropped += 1;
|
|
2839
|
+
return;
|
|
2840
|
+
}
|
|
2841
|
+
const meta = params.metadata;
|
|
2842
|
+
const timestamp = typeof meta?.timestamp === "number" ? Math.round(meta.timestamp * 1000) : Date.now();
|
|
2843
|
+
const buf = Buffer.from(data, "base64");
|
|
2844
|
+
const size = imageSize(buf);
|
|
2845
|
+
if (!size || size.width <= 0 || size.height <= 0) {
|
|
2846
|
+
rec.dropped += 1;
|
|
2847
|
+
return;
|
|
2848
|
+
}
|
|
2849
|
+
seq += 1;
|
|
2850
|
+
const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
|
|
2851
|
+
rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
|
|
2852
|
+
rec.writes = rec.writes.then(() => writeFile6(join5(spoolDir, file), buf)).catch(() => {
|
|
2853
|
+
rec.dropped += 1;
|
|
2854
|
+
rec.unwritten.add(file);
|
|
2855
|
+
});
|
|
2856
|
+
});
|
|
2857
|
+
try {
|
|
2858
|
+
await conn.send("Page.enable");
|
|
2859
|
+
if (args.bringToFront)
|
|
2860
|
+
await conn.send("Page.bringToFront").catch(() => {});
|
|
2861
|
+
const params = { format };
|
|
2862
|
+
if (format === "jpeg" && args.quality !== undefined)
|
|
2863
|
+
params.quality = args.quality;
|
|
2864
|
+
if (args.maxWidth !== undefined)
|
|
2865
|
+
params.maxWidth = args.maxWidth;
|
|
2866
|
+
if (args.maxHeight !== undefined)
|
|
2867
|
+
params.maxHeight = args.maxHeight;
|
|
2868
|
+
if (args.everyNthFrame !== undefined)
|
|
2869
|
+
params.everyNthFrame = args.everyNthFrame;
|
|
2870
|
+
await conn.send("Page.startScreencast", params);
|
|
2871
|
+
} catch (err) {
|
|
2872
|
+
rec.off();
|
|
2873
|
+
conn.close();
|
|
2874
|
+
await rm(spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
2875
|
+
throw err;
|
|
2876
|
+
}
|
|
2877
|
+
liveRecordings.set(target.id, rec);
|
|
2878
|
+
return {
|
|
2879
|
+
target: target32(target),
|
|
2880
|
+
format,
|
|
2881
|
+
spoolDir,
|
|
2882
|
+
encoder: encoder.encoder,
|
|
2883
|
+
codec: encoder.codec,
|
|
2884
|
+
startedAt: rec.startedAt,
|
|
2885
|
+
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."
|
|
2886
|
+
};
|
|
2887
|
+
}
|
|
2888
|
+
async function pickRecording(selector) {
|
|
2889
|
+
const chosen = chooseRecordingId([...liveRecordings.keys()], selector);
|
|
2890
|
+
if (chosen)
|
|
2891
|
+
return liveRecordings.get(chosen);
|
|
2892
|
+
const { conn, target } = await openPage(selector);
|
|
2893
|
+
conn.close();
|
|
2894
|
+
const hit = liveRecordings.get(target.id);
|
|
2895
|
+
if (!hit) {
|
|
2896
|
+
throw new CdpError(`no screen recording is in progress for target ${shortId2(target.id)} (recording: ${[...liveRecordings.keys()].map(shortId2).join(", ") || "none"})`);
|
|
2897
|
+
}
|
|
2898
|
+
return hit;
|
|
2899
|
+
}
|
|
2900
|
+
async function stopScreenRecording(args = {}) {
|
|
2901
|
+
const rec = await pickRecording(args.target);
|
|
2902
|
+
rec.off();
|
|
2903
|
+
try {
|
|
2904
|
+
await rec.conn.send("Page.stopScreencast");
|
|
2905
|
+
} catch {}
|
|
2906
|
+
const stoppedAt = Date.now();
|
|
2907
|
+
await rec.writes.catch(() => {});
|
|
2908
|
+
rec.conn.close();
|
|
2909
|
+
liveRecordings.delete(rec.target.id);
|
|
2910
|
+
const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
|
|
2911
|
+
if (!ledger.length) {
|
|
2912
|
+
await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
2913
|
+
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.`);
|
|
2914
|
+
}
|
|
2915
|
+
await mkdir6(ARTIFACT_DIR4, { recursive: true });
|
|
2916
|
+
const outPath = args.savePath ?? join5(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
|
|
2917
|
+
const frames = coalesceToConcatGrid(ledger, stoppedAt);
|
|
2918
|
+
const manifestPath = join5(rec.spoolDir, "frames.ffconcat");
|
|
2919
|
+
await writeFile6(manifestPath, buildConcatManifest(frames), "utf8");
|
|
2920
|
+
const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
|
|
2921
|
+
const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
|
|
2922
|
+
let res;
|
|
2923
|
+
try {
|
|
2924
|
+
res = await run(FFMPEG_BIN, buildFfmpegArgs(manifestPath, outPath, rec.encoder));
|
|
2925
|
+
} catch (err) {
|
|
2926
|
+
throw new CdpError(`${FFMPEG_MISSING} Spooled frames kept at ${rec.spoolDir}. (${err.message})`);
|
|
2927
|
+
}
|
|
2928
|
+
if (res.code !== 0) {
|
|
2929
|
+
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(" ")}
|
|
2930
|
+
ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
2931
|
+
}
|
|
2932
|
+
const { size } = await stat2(outPath);
|
|
2933
|
+
await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
2934
|
+
return {
|
|
2935
|
+
path: outPath,
|
|
2936
|
+
bytes: size,
|
|
2937
|
+
durationMs,
|
|
2938
|
+
frameCount: ledger.length,
|
|
2939
|
+
encodedFrames: frames.length,
|
|
2940
|
+
codec: rec.encoder.codec,
|
|
2941
|
+
encoder: rec.encoder.encoder,
|
|
2942
|
+
width,
|
|
2943
|
+
height,
|
|
2944
|
+
droppedFrames: rec.dropped,
|
|
2945
|
+
target: target32(rec.target)
|
|
2946
|
+
};
|
|
2947
|
+
}
|
|
2948
|
+
|
|
2949
|
+
// src/tools/heap.ts
|
|
2950
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
|
|
2951
|
+
import { join as join6 } from "node:path";
|
|
2952
|
+
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2953
|
+
function stamp4(targetId) {
|
|
2313
2954
|
const short = targetId.slice(0, 8);
|
|
2314
2955
|
const iso = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2315
2956
|
return `${short}-${iso}`;
|
|
@@ -2332,9 +2973,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2332
2973
|
if (snapshot.length === 0) {
|
|
2333
2974
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
2334
2975
|
}
|
|
2335
|
-
await
|
|
2336
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
2337
|
-
await
|
|
2976
|
+
await mkdir7(ARTIFACT_DIR5, { recursive: true });
|
|
2977
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join6(ARTIFACT_DIR5, args.savePath) : join6(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
|
|
2978
|
+
await writeFile7(path, snapshot, "utf8");
|
|
2338
2979
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
2339
2980
|
return {
|
|
2340
2981
|
path,
|
|
@@ -2346,15 +2987,15 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2346
2987
|
}
|
|
2347
2988
|
|
|
2348
2989
|
// src/tools/lighthouse.ts
|
|
2349
|
-
import { spawn } from "node:child_process";
|
|
2350
|
-
import { mkdir as
|
|
2351
|
-
var
|
|
2990
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
2991
|
+
import { mkdir as mkdir8, readFile as readFile6, stat as stat3 } from "node:fs/promises";
|
|
2992
|
+
var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2352
2993
|
function cdpPort() {
|
|
2353
2994
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
2354
2995
|
const m = base.match(/:(\d+)/);
|
|
2355
2996
|
return m?.[1] ? Number(m[1]) : 9222;
|
|
2356
2997
|
}
|
|
2357
|
-
function
|
|
2998
|
+
function stamp5() {
|
|
2358
2999
|
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
2359
3000
|
}
|
|
2360
3001
|
function shortHost(url) {
|
|
@@ -2371,7 +3012,7 @@ function runProcess(cmd, args, opts) {
|
|
|
2371
3012
|
let stdout = "";
|
|
2372
3013
|
let stderr = "";
|
|
2373
3014
|
let settled = false;
|
|
2374
|
-
const child =
|
|
3015
|
+
const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2375
3016
|
const timer = setTimeout(() => {
|
|
2376
3017
|
if (settled)
|
|
2377
3018
|
return;
|
|
@@ -2408,8 +3049,8 @@ async function lighthouseAudit(args) {
|
|
|
2408
3049
|
const url = args.url.trim();
|
|
2409
3050
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
2410
3051
|
const port = cdpPort();
|
|
2411
|
-
await
|
|
2412
|
-
const outPath = args.savePath ?? `${
|
|
3052
|
+
await mkdir8(ARTIFACT_DIR6, { recursive: true });
|
|
3053
|
+
const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
|
|
2413
3054
|
const formFactor = args.formFactor ?? "desktop";
|
|
2414
3055
|
const lhArgs = [
|
|
2415
3056
|
"--yes",
|
|
@@ -2437,7 +3078,7 @@ async function lighthouseAudit(args) {
|
|
|
2437
3078
|
}
|
|
2438
3079
|
let raw;
|
|
2439
3080
|
try {
|
|
2440
|
-
raw = await
|
|
3081
|
+
raw = await readFile6(outPath, "utf8");
|
|
2441
3082
|
} catch {
|
|
2442
3083
|
raw = undefined;
|
|
2443
3084
|
}
|
|
@@ -2464,7 +3105,7 @@ async function lighthouseAudit(args) {
|
|
|
2464
3105
|
if (Object.keys(categories).length === 0) {
|
|
2465
3106
|
throw new CdpError(`lighthouse_audit report contained no category scores (exit code ${res.code}). stderr: ${truncate(res.stderr)}`);
|
|
2466
3107
|
}
|
|
2467
|
-
const { size } = await
|
|
3108
|
+
const { size } = await stat3(outPath);
|
|
2468
3109
|
return {
|
|
2469
3110
|
path: outPath,
|
|
2470
3111
|
bytes: size,
|
|
@@ -2494,6 +3135,9 @@ var TOOLS = {
|
|
|
2494
3135
|
navigate_page: onCdp(SHARED_TOOLS.navigate_page),
|
|
2495
3136
|
wait_for: onCdp(SHARED_TOOLS.wait_for),
|
|
2496
3137
|
evaluate_script: onCdp(SHARED_TOOLS.evaluate_script),
|
|
3138
|
+
list_cookies: onCdp(SHARED_TOOLS.list_cookies),
|
|
3139
|
+
set_cookie: onCdp(SHARED_TOOLS.set_cookie),
|
|
3140
|
+
delete_cookies: onCdp(SHARED_TOOLS.delete_cookies),
|
|
2497
3141
|
take_snapshot: onCdp(SHARED_TOOLS.take_snapshot),
|
|
2498
3142
|
click: onCdp(SHARED_TOOLS.click),
|
|
2499
3143
|
hover: onCdp(SHARED_TOOLS.hover),
|
|
@@ -2515,6 +3159,8 @@ var TOOLS = {
|
|
|
2515
3159
|
performance_stop_trace: performanceStopTrace,
|
|
2516
3160
|
performance_analyze_insight: performanceAnalyzeInsight,
|
|
2517
3161
|
performance_trace: performanceTrace,
|
|
3162
|
+
start_screen_recording: startScreenRecording,
|
|
3163
|
+
stop_screen_recording: stopScreenRecording,
|
|
2518
3164
|
take_heapsnapshot: takeHeapsnapshot,
|
|
2519
3165
|
lighthouse_audit: lighthouseAudit,
|
|
2520
3166
|
mock_request: mockRequest,
|
|
@@ -2535,7 +3181,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
2535
3181
|
var MANIFEST = [
|
|
2536
3182
|
{
|
|
2537
3183
|
name: "list_pages",
|
|
2538
|
-
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.",
|
|
3184
|
+
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.",
|
|
2539
3185
|
inputSchema: {
|
|
2540
3186
|
type: "object",
|
|
2541
3187
|
properties: {
|
|
@@ -2702,7 +3348,7 @@ var MANIFEST = [
|
|
|
2702
3348
|
},
|
|
2703
3349
|
{
|
|
2704
3350
|
name: "evaluate_script",
|
|
2705
|
-
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.",
|
|
3351
|
+
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.",
|
|
2706
3352
|
inputSchema: {
|
|
2707
3353
|
type: "object",
|
|
2708
3354
|
properties: {
|
|
@@ -2725,6 +3371,10 @@ var MANIFEST = [
|
|
|
2725
3371
|
args: {
|
|
2726
3372
|
type: "array",
|
|
2727
3373
|
description: "Positional JSON-serializable arguments to pass to the expression, treating it as a function. No live element/page handle is bound."
|
|
3374
|
+
},
|
|
3375
|
+
savePath: {
|
|
3376
|
+
type: "string",
|
|
3377
|
+
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."
|
|
2728
3378
|
}
|
|
2729
3379
|
},
|
|
2730
3380
|
required: [
|
|
@@ -2733,6 +3383,128 @@ var MANIFEST = [
|
|
|
2733
3383
|
additionalProperties: false
|
|
2734
3384
|
}
|
|
2735
3385
|
},
|
|
3386
|
+
{
|
|
3387
|
+
name: "list_cookies",
|
|
3388
|
+
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.",
|
|
3389
|
+
inputSchema: {
|
|
3390
|
+
type: "object",
|
|
3391
|
+
properties: {
|
|
3392
|
+
lease: {
|
|
3393
|
+
type: "string",
|
|
3394
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs."
|
|
3395
|
+
},
|
|
3396
|
+
target: {
|
|
3397
|
+
type: "string",
|
|
3398
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
3399
|
+
},
|
|
3400
|
+
domain: {
|
|
3401
|
+
type: "string",
|
|
3402
|
+
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."
|
|
3403
|
+
},
|
|
3404
|
+
name: {
|
|
3405
|
+
type: "string",
|
|
3406
|
+
description: "Keep only the cookie with exactly this name. Exact match, not a substring."
|
|
3407
|
+
},
|
|
3408
|
+
savePath: {
|
|
3409
|
+
type: "string",
|
|
3410
|
+
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."
|
|
3411
|
+
}
|
|
3412
|
+
},
|
|
3413
|
+
required: [],
|
|
3414
|
+
additionalProperties: false
|
|
3415
|
+
}
|
|
3416
|
+
},
|
|
3417
|
+
{
|
|
3418
|
+
name: "set_cookie",
|
|
3419
|
+
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.",
|
|
3420
|
+
inputSchema: {
|
|
3421
|
+
type: "object",
|
|
3422
|
+
properties: {
|
|
3423
|
+
lease: {
|
|
3424
|
+
type: "string",
|
|
3425
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs."
|
|
3426
|
+
},
|
|
3427
|
+
target: {
|
|
3428
|
+
type: "string",
|
|
3429
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
3430
|
+
},
|
|
3431
|
+
name: {
|
|
3432
|
+
type: "string",
|
|
3433
|
+
description: "Cookie name. Required and non-empty."
|
|
3434
|
+
},
|
|
3435
|
+
value: {
|
|
3436
|
+
type: "string",
|
|
3437
|
+
description: "Cookie value. Required. It is not echoed back in the response."
|
|
3438
|
+
},
|
|
3439
|
+
url: {
|
|
3440
|
+
type: "string",
|
|
3441
|
+
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'."
|
|
3442
|
+
},
|
|
3443
|
+
domain: {
|
|
3444
|
+
type: "string",
|
|
3445
|
+
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."
|
|
3446
|
+
},
|
|
3447
|
+
path: {
|
|
3448
|
+
type: "string",
|
|
3449
|
+
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."
|
|
3450
|
+
},
|
|
3451
|
+
expires: {
|
|
3452
|
+
type: "number",
|
|
3453
|
+
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."
|
|
3454
|
+
},
|
|
3455
|
+
httpOnly: {
|
|
3456
|
+
type: "boolean",
|
|
3457
|
+
description: "Set the httpOnly flag, making the cookie invisible to document.cookie. Only a protocol write like this one can create such a cookie."
|
|
3458
|
+
},
|
|
3459
|
+
secure: {
|
|
3460
|
+
type: "boolean",
|
|
3461
|
+
description: "Set the secure flag. Chrome declines a secure cookie on an insecure origin, and that refusal surfaces as an error."
|
|
3462
|
+
},
|
|
3463
|
+
sameSite: {
|
|
3464
|
+
type: "string",
|
|
3465
|
+
enum: ["strict", "lax", "none", "default"],
|
|
3466
|
+
description: "SameSite attribute, lowercase, matching what list_cookies reports. 'default' means the attribute is not set at all."
|
|
3467
|
+
}
|
|
3468
|
+
},
|
|
3469
|
+
required: ["name", "value"],
|
|
3470
|
+
additionalProperties: false
|
|
3471
|
+
}
|
|
3472
|
+
},
|
|
3473
|
+
{
|
|
3474
|
+
name: "delete_cookies",
|
|
3475
|
+
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.",
|
|
3476
|
+
inputSchema: {
|
|
3477
|
+
type: "object",
|
|
3478
|
+
properties: {
|
|
3479
|
+
lease: {
|
|
3480
|
+
type: "string",
|
|
3481
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs."
|
|
3482
|
+
},
|
|
3483
|
+
target: {
|
|
3484
|
+
type: "string",
|
|
3485
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
3486
|
+
},
|
|
3487
|
+
name: {
|
|
3488
|
+
type: "string",
|
|
3489
|
+
description: "Exact cookie name to delete. Required and non-empty. No wildcards: this removes a named cookie, not a swathe of them."
|
|
3490
|
+
},
|
|
3491
|
+
url: {
|
|
3492
|
+
type: "string",
|
|
3493
|
+
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."
|
|
3494
|
+
},
|
|
3495
|
+
domain: {
|
|
3496
|
+
type: "string",
|
|
3497
|
+
description: "Delete cookies with this exact domain. Give this or 'url'."
|
|
3498
|
+
},
|
|
3499
|
+
path: {
|
|
3500
|
+
type: "string",
|
|
3501
|
+
description: "Narrow the deletion to cookies with this exact path. Omit to match any path allowed by the other constraints."
|
|
3502
|
+
}
|
|
3503
|
+
},
|
|
3504
|
+
required: ["name"],
|
|
3505
|
+
additionalProperties: false
|
|
3506
|
+
}
|
|
3507
|
+
},
|
|
2736
3508
|
{
|
|
2737
3509
|
name: "take_snapshot",
|
|
2738
3510
|
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.",
|
|
@@ -3502,6 +4274,76 @@ var MANIFEST = [
|
|
|
3502
4274
|
additionalProperties: false
|
|
3503
4275
|
}
|
|
3504
4276
|
},
|
|
4277
|
+
{
|
|
4278
|
+
name: "start_screen_recording",
|
|
4279
|
+
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.",
|
|
4280
|
+
inputSchema: {
|
|
4281
|
+
type: "object",
|
|
4282
|
+
properties: {
|
|
4283
|
+
lease: {
|
|
4284
|
+
type: "string",
|
|
4285
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
4286
|
+
},
|
|
4287
|
+
target: {
|
|
4288
|
+
type: "string",
|
|
4289
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4290
|
+
},
|
|
4291
|
+
format: {
|
|
4292
|
+
type: "string",
|
|
4293
|
+
enum: [
|
|
4294
|
+
"jpeg",
|
|
4295
|
+
"png"
|
|
4296
|
+
],
|
|
4297
|
+
description: "Frame image format. Defaults to jpeg (far smaller per repaint); png is lossless. quality applies to jpeg only."
|
|
4298
|
+
},
|
|
4299
|
+
quality: {
|
|
4300
|
+
type: "number",
|
|
4301
|
+
description: "JPEG frame quality 0-100. Ignored for png. Omitted by default, which leaves Chrome's own default in place."
|
|
4302
|
+
},
|
|
4303
|
+
maxWidth: {
|
|
4304
|
+
type: "number",
|
|
4305
|
+
description: "Cap the streamed frame width in pixels. Also the way to pin frame size if the viewport might change mid-recording."
|
|
4306
|
+
},
|
|
4307
|
+
maxHeight: {
|
|
4308
|
+
type: "number",
|
|
4309
|
+
description: "Cap the streamed frame height in pixels."
|
|
4310
|
+
},
|
|
4311
|
+
everyNthFrame: {
|
|
4312
|
+
type: "number",
|
|
4313
|
+
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."
|
|
4314
|
+
},
|
|
4315
|
+
bringToFront: {
|
|
4316
|
+
type: "boolean",
|
|
4317
|
+
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."
|
|
4318
|
+
}
|
|
4319
|
+
},
|
|
4320
|
+
required: [],
|
|
4321
|
+
additionalProperties: false
|
|
4322
|
+
}
|
|
4323
|
+
},
|
|
4324
|
+
{
|
|
4325
|
+
name: "stop_screen_recording",
|
|
4326
|
+
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.",
|
|
4327
|
+
inputSchema: {
|
|
4328
|
+
type: "object",
|
|
4329
|
+
properties: {
|
|
4330
|
+
lease: {
|
|
4331
|
+
type: "string",
|
|
4332
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
4333
|
+
},
|
|
4334
|
+
target: {
|
|
4335
|
+
type: "string",
|
|
4336
|
+
description: "Which recording to stop: 'active' (default, valid only when exactly one recording is in flight) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4337
|
+
},
|
|
4338
|
+
savePath: {
|
|
4339
|
+
type: "string",
|
|
4340
|
+
description: "Override the output file path. Default: /tmp/cdp-toolkit/screen-recording-<targetIdShort>-<stamp>.mp4."
|
|
4341
|
+
}
|
|
4342
|
+
},
|
|
4343
|
+
required: [],
|
|
4344
|
+
additionalProperties: false
|
|
4345
|
+
}
|
|
4346
|
+
},
|
|
3505
4347
|
{
|
|
3506
4348
|
name: "take_heapsnapshot",
|
|
3507
4349
|
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.",
|
|
@@ -4241,11 +5083,42 @@ function buildSnapshotFunctionSource() {
|
|
|
4241
5083
|
var MAX_NAME_LENGTH = 120;
|
|
4242
5084
|
|
|
4243
5085
|
// src/bidi/driver.ts
|
|
5086
|
+
function normalizeBidiCookie(c) {
|
|
5087
|
+
const value = c.value.type === "base64" ? Buffer.from(c.value.value, "base64").toString("utf8") : c.value.value;
|
|
5088
|
+
const hasExpiry = typeof c.expiry === "number";
|
|
5089
|
+
return {
|
|
5090
|
+
name: c.name,
|
|
5091
|
+
value,
|
|
5092
|
+
domain: c.domain,
|
|
5093
|
+
path: c.path,
|
|
5094
|
+
expires: hasExpiry ? c.expiry : -1,
|
|
5095
|
+
size: typeof c.size === "number" ? c.size : Buffer.byteLength(`${c.name}${value}`, "utf8"),
|
|
5096
|
+
httpOnly: c.httpOnly === true,
|
|
5097
|
+
secure: c.secure === true,
|
|
5098
|
+
sameSite: c.sameSite ?? "default",
|
|
5099
|
+
session: !hasExpiry
|
|
5100
|
+
};
|
|
5101
|
+
}
|
|
5102
|
+
function hostFromUrl(url, toolName) {
|
|
5103
|
+
if (url === undefined || url === "") {
|
|
5104
|
+
throw driverError2("unsupported", `${toolName} on Firefox needs a 'domain': BiDi's storage cookie calls have no 'url' parameter, and no url was given to derive one from.`);
|
|
5105
|
+
}
|
|
5106
|
+
let host = "";
|
|
5107
|
+
try {
|
|
5108
|
+
host = new URL(url).hostname;
|
|
5109
|
+
} catch {
|
|
5110
|
+
throw driverError2("unsupported", `${toolName} could not parse the url '${url}' to derive the 'domain' BiDi requires. Pass 'domain' explicitly.`);
|
|
5111
|
+
}
|
|
5112
|
+
if (host === "") {
|
|
5113
|
+
throw driverError2("unsupported", `${toolName} cannot derive a 'domain' from '${url}': that url has no host. Pass 'domain' explicitly.`);
|
|
5114
|
+
}
|
|
5115
|
+
return host;
|
|
5116
|
+
}
|
|
4244
5117
|
function driverError2(code, message, data) {
|
|
4245
5118
|
return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
|
|
4246
5119
|
}
|
|
4247
|
-
function encodeUid2(
|
|
4248
|
-
return `bidi:${
|
|
5120
|
+
function encodeUid2(stamp6) {
|
|
5121
|
+
return `bidi:${stamp6}`;
|
|
4249
5122
|
}
|
|
4250
5123
|
function decodeStamp(uid) {
|
|
4251
5124
|
if (!uid.startsWith("bidi:"))
|
|
@@ -4352,7 +5225,7 @@ function parseSnapshotLine(line) {
|
|
|
4352
5225
|
const m = LINE_RE.exec(line);
|
|
4353
5226
|
if (!m)
|
|
4354
5227
|
return;
|
|
4355
|
-
const indent = m[1] ?? "",
|
|
5228
|
+
const indent = m[1] ?? "", stamp6 = m[2], role = m[3], nameLit = m[4], extrasRaw = m[5];
|
|
4356
5229
|
const depth = indent.length / 2;
|
|
4357
5230
|
let name;
|
|
4358
5231
|
if (nameLit) {
|
|
@@ -4380,7 +5253,7 @@ function parseSnapshotLine(line) {
|
|
|
4380
5253
|
extras[key] = val;
|
|
4381
5254
|
}
|
|
4382
5255
|
}
|
|
4383
|
-
return { uid: encodeUid2(
|
|
5256
|
+
return { uid: encodeUid2(stamp6), role, depth, ...name !== undefined ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} };
|
|
4384
5257
|
}
|
|
4385
5258
|
var INTERACTIVE_ROLES3 = new Set([
|
|
4386
5259
|
"button",
|
|
@@ -4413,8 +5286,8 @@ function sharedRefOf(node, describe) {
|
|
|
4413
5286
|
}
|
|
4414
5287
|
async function resolveElementLocator2(conn, contextId, loc) {
|
|
4415
5288
|
if ("uid" in loc) {
|
|
4416
|
-
const
|
|
4417
|
-
const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(
|
|
5289
|
+
const stamp6 = decodeStamp(loc.uid);
|
|
5290
|
+
const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp6)}"]` });
|
|
4418
5291
|
if (!nodes2.length)
|
|
4419
5292
|
throw driverError2("stale-uid", `uid does not resolve: ${loc.uid}`);
|
|
4420
5293
|
return sharedRefOf(nodes2[0], loc.uid);
|
|
@@ -4626,14 +5499,14 @@ class BidiPageDriver {
|
|
|
4626
5499
|
return loc.uid;
|
|
4627
5500
|
}
|
|
4628
5501
|
const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
|
|
4629
|
-
const
|
|
4630
|
-
if (typeof
|
|
5502
|
+
const stamp6 = await this.callFunctionValue(STAMP_ONE_SOURCE, [], ref);
|
|
5503
|
+
if (typeof stamp6 !== "string")
|
|
4631
5504
|
throw driverError2("page-error", "stamping the located element did not return a string");
|
|
4632
|
-
return encodeUid2(
|
|
5505
|
+
return encodeUid2(stamp6);
|
|
4633
5506
|
}
|
|
4634
5507
|
async resolve(uid) {
|
|
4635
|
-
const
|
|
4636
|
-
const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(
|
|
5508
|
+
const stamp6 = decodeStamp(uid);
|
|
5509
|
+
const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp6)}"]` });
|
|
4637
5510
|
if (!nodes.length)
|
|
4638
5511
|
throw driverError2("stale-uid", `uid does not resolve: ${uid}`);
|
|
4639
5512
|
}
|
|
@@ -4849,6 +5722,49 @@ class BidiPageDriver {
|
|
|
4849
5722
|
}
|
|
4850
5723
|
return this.handleOneDialog(accept, promptText, opts?.timeoutMs ?? 15000);
|
|
4851
5724
|
}
|
|
5725
|
+
async getCookies() {
|
|
5726
|
+
try {
|
|
5727
|
+
const { cookies } = await this.conn.send("storage.getCookies", { partition: { type: "context", context: this.contextId } });
|
|
5728
|
+
return (cookies ?? []).map(normalizeBidiCookie);
|
|
5729
|
+
} catch (e) {
|
|
5730
|
+
throw mapBidiError(e);
|
|
5731
|
+
}
|
|
5732
|
+
}
|
|
5733
|
+
async setCookie(params) {
|
|
5734
|
+
const domain = params.domain ?? hostFromUrl(params.url, "set_cookie");
|
|
5735
|
+
try {
|
|
5736
|
+
await this.conn.send("storage.setCookie", {
|
|
5737
|
+
cookie: {
|
|
5738
|
+
name: params.name,
|
|
5739
|
+
value: { type: "string", value: params.value },
|
|
5740
|
+
domain,
|
|
5741
|
+
...params.path !== undefined ? { path: params.path } : {},
|
|
5742
|
+
...params.expires !== undefined ? { expiry: params.expires } : {},
|
|
5743
|
+
...params.httpOnly !== undefined ? { httpOnly: params.httpOnly } : {},
|
|
5744
|
+
...params.secure !== undefined ? { secure: params.secure } : {},
|
|
5745
|
+
...params.sameSite !== undefined ? { sameSite: params.sameSite } : {}
|
|
5746
|
+
},
|
|
5747
|
+
partition: { type: "context", context: this.contextId }
|
|
5748
|
+
});
|
|
5749
|
+
} catch (e) {
|
|
5750
|
+
throw mapBidiError(e);
|
|
5751
|
+
}
|
|
5752
|
+
}
|
|
5753
|
+
async deleteCookies(filter) {
|
|
5754
|
+
const domain = filter.domain ?? hostFromUrl(filter.url, "delete_cookies");
|
|
5755
|
+
try {
|
|
5756
|
+
await this.conn.send("storage.deleteCookies", {
|
|
5757
|
+
filter: {
|
|
5758
|
+
name: filter.name,
|
|
5759
|
+
domain,
|
|
5760
|
+
...filter.path !== undefined ? { path: filter.path } : {}
|
|
5761
|
+
},
|
|
5762
|
+
partition: { type: "context", context: this.contextId }
|
|
5763
|
+
});
|
|
5764
|
+
} catch (e) {
|
|
5765
|
+
throw mapBidiError(e);
|
|
5766
|
+
}
|
|
5767
|
+
}
|
|
4852
5768
|
async startBodyCapture() {
|
|
4853
5769
|
let collector;
|
|
4854
5770
|
try {
|
|
@@ -5019,11 +5935,11 @@ function createFirefoxDriver(port, opts) {
|
|
|
5019
5935
|
}
|
|
5020
5936
|
|
|
5021
5937
|
// src/bidi/launch.ts
|
|
5022
|
-
import { spawn as
|
|
5938
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
5023
5939
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5024
5940
|
import { createConnection, createServer } from "node:net";
|
|
5025
5941
|
import { tmpdir } from "node:os";
|
|
5026
|
-
import { join as
|
|
5942
|
+
import { join as join7 } from "node:path";
|
|
5027
5943
|
function resolveBinary() {
|
|
5028
5944
|
const candidates = [
|
|
5029
5945
|
process.env.FIREFOX_BIN,
|
|
@@ -5097,11 +6013,11 @@ async function launchFirefox(opts = {}) {
|
|
|
5097
6013
|
const binary = resolveBinary();
|
|
5098
6014
|
const port = await pickPort(opts.port);
|
|
5099
6015
|
const isThrowaway = opts.profilePath === undefined;
|
|
5100
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
6016
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join7(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
5101
6017
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
5102
6018
|
if (opts.headless)
|
|
5103
6019
|
args.push("--headless");
|
|
5104
|
-
const proc =
|
|
6020
|
+
const proc = spawn3(binary, args, { stdio: "ignore", detached: false });
|
|
5105
6021
|
let closed = false;
|
|
5106
6022
|
const close = async () => {
|
|
5107
6023
|
if (closed)
|
|
@@ -5367,7 +6283,7 @@ var FIREFOX_TOOLS = {
|
|
|
5367
6283
|
};
|
|
5368
6284
|
|
|
5369
6285
|
// src/mcp.ts
|
|
5370
|
-
var VERSION = "1.
|
|
6286
|
+
var VERSION = "1.6.0";
|
|
5371
6287
|
var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
|
|
5372
6288
|
var AVAILABILITY = toolAvailability(BROWSER);
|
|
5373
6289
|
var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
|