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/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
|
}
|
|
@@ -704,6 +707,31 @@ function decodeUid(uid) {
|
|
|
704
707
|
throw driverError("foreign-uid", `malformed cdp uid payload: ${uid}`);
|
|
705
708
|
return n;
|
|
706
709
|
}
|
|
710
|
+
function normalizeCdpCookie(c) {
|
|
711
|
+
const same = (c.sameSite ?? "").toLowerCase();
|
|
712
|
+
const sameSite = same === "strict" || same === "lax" || same === "none" ? same : "default";
|
|
713
|
+
return {
|
|
714
|
+
name: c.name,
|
|
715
|
+
value: c.value,
|
|
716
|
+
domain: c.domain,
|
|
717
|
+
path: c.path,
|
|
718
|
+
expires: typeof c.expires === "number" ? c.expires : -1,
|
|
719
|
+
size: typeof c.size === "number" ? c.size : Buffer.byteLength(`${c.name}${c.value}`, "utf8"),
|
|
720
|
+
httpOnly: c.httpOnly === true,
|
|
721
|
+
secure: c.secure === true,
|
|
722
|
+
sameSite,
|
|
723
|
+
session: c.session === true
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function cdpSameSite(sameSite) {
|
|
727
|
+
if (sameSite === "strict")
|
|
728
|
+
return "Strict";
|
|
729
|
+
if (sameSite === "lax")
|
|
730
|
+
return "Lax";
|
|
731
|
+
if (sameSite === "none")
|
|
732
|
+
return "None";
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
707
735
|
async function centerOf(conn, objectId) {
|
|
708
736
|
const fn = "function(){this.scrollIntoView({block:'center',inline:'center'});const r=this.getBoundingClientRect();if(r.width===0&&r.height===0)return null;return {x:r.left+r.width/2,y:r.top+r.height/2};}";
|
|
709
737
|
const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
@@ -1226,6 +1254,38 @@ class CdpPageDriver {
|
|
|
1226
1254
|
});
|
|
1227
1255
|
});
|
|
1228
1256
|
}
|
|
1257
|
+
async getCookies() {
|
|
1258
|
+
await this.ensureDomain("Network");
|
|
1259
|
+
const { cookies } = await this.conn.send("Network.getCookies");
|
|
1260
|
+
return (cookies ?? []).map(normalizeCdpCookie);
|
|
1261
|
+
}
|
|
1262
|
+
async setCookie(params) {
|
|
1263
|
+
await this.ensureDomain("Network");
|
|
1264
|
+
const sameSite = cdpSameSite(params.sameSite);
|
|
1265
|
+
const { success } = await this.conn.send("Network.setCookie", {
|
|
1266
|
+
name: params.name,
|
|
1267
|
+
value: params.value,
|
|
1268
|
+
...params.url !== undefined ? { url: params.url } : {},
|
|
1269
|
+
...params.domain !== undefined ? { domain: params.domain } : {},
|
|
1270
|
+
...params.path !== undefined ? { path: params.path } : {},
|
|
1271
|
+
...params.expires !== undefined ? { expires: params.expires } : {},
|
|
1272
|
+
...params.httpOnly !== undefined ? { httpOnly: params.httpOnly } : {},
|
|
1273
|
+
...params.secure !== undefined ? { secure: params.secure } : {},
|
|
1274
|
+
...sameSite !== undefined ? { sameSite } : {}
|
|
1275
|
+
});
|
|
1276
|
+
if (success !== true) {
|
|
1277
|
+
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.`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async deleteCookies(filter) {
|
|
1281
|
+
await this.ensureDomain("Network");
|
|
1282
|
+
await this.conn.send("Network.deleteCookies", {
|
|
1283
|
+
name: filter.name,
|
|
1284
|
+
...filter.url !== undefined ? { url: filter.url } : {},
|
|
1285
|
+
...filter.domain !== undefined ? { domain: filter.domain } : {},
|
|
1286
|
+
...filter.path !== undefined ? { path: filter.path } : {}
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1229
1289
|
async startBodyCapture() {
|
|
1230
1290
|
await this.ensureDomain("Network");
|
|
1231
1291
|
return async () => {
|
|
@@ -1285,6 +1345,10 @@ async function withBrowserConn(fn) {
|
|
|
1285
1345
|
}
|
|
1286
1346
|
}
|
|
1287
1347
|
function noSuchTarget(e) {
|
|
1348
|
+
if (e instanceof LeaseConflictError)
|
|
1349
|
+
throw e;
|
|
1350
|
+
if (typeof e?.code === "string")
|
|
1351
|
+
throw e;
|
|
1288
1352
|
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
1289
1353
|
}
|
|
1290
1354
|
|
|
@@ -1328,6 +1392,97 @@ function createCdpDriver() {
|
|
|
1328
1392
|
return new CdpBrowserDriver;
|
|
1329
1393
|
}
|
|
1330
1394
|
|
|
1395
|
+
// src/origins.ts
|
|
1396
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1397
|
+
import { join as join2 } from "node:path";
|
|
1398
|
+
function safeId2(id) {
|
|
1399
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1400
|
+
}
|
|
1401
|
+
function originFile(backend, targetId) {
|
|
1402
|
+
return join2(leaseDir(), `origin-${backend}-${safeId2(targetId)}.json`);
|
|
1403
|
+
}
|
|
1404
|
+
var ORIGIN_FILE_RE = /^origin-(chrome|firefox)-(.+)\.json$/;
|
|
1405
|
+
async function recordOrigin(backend, targetId, opts = {}) {
|
|
1406
|
+
const record = {
|
|
1407
|
+
backend,
|
|
1408
|
+
targetId,
|
|
1409
|
+
label: typeof opts.label === "string" && opts.label.length ? opts.label : defaultLabel(),
|
|
1410
|
+
pid: process.pid,
|
|
1411
|
+
createdAt: opts.now ?? Date.now()
|
|
1412
|
+
};
|
|
1413
|
+
const file = originFile(backend, targetId);
|
|
1414
|
+
try {
|
|
1415
|
+
await mkdir2(leaseDir(), { recursive: true });
|
|
1416
|
+
try {
|
|
1417
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1418
|
+
return;
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
if (err.code !== "EEXIST")
|
|
1421
|
+
throw err;
|
|
1422
|
+
}
|
|
1423
|
+
await unlink2(file).catch(() => {
|
|
1424
|
+
return;
|
|
1425
|
+
});
|
|
1426
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1427
|
+
} catch {}
|
|
1428
|
+
}
|
|
1429
|
+
async function listOrigins(opts = {}) {
|
|
1430
|
+
let names = [];
|
|
1431
|
+
try {
|
|
1432
|
+
names = await readdir2(leaseDir());
|
|
1433
|
+
} catch {
|
|
1434
|
+
return [];
|
|
1435
|
+
}
|
|
1436
|
+
const live = opts.liveIds === undefined ? undefined : new Set(opts.liveIds.map((id) => safeId2(id)));
|
|
1437
|
+
const out = [];
|
|
1438
|
+
for (const name of names) {
|
|
1439
|
+
const parts = ORIGIN_FILE_RE.exec(name);
|
|
1440
|
+
if (!parts)
|
|
1441
|
+
continue;
|
|
1442
|
+
const backend = parts[1];
|
|
1443
|
+
const nameId = parts[2];
|
|
1444
|
+
const scoped = live !== undefined && (opts.liveBackend === undefined || opts.liveBackend === backend);
|
|
1445
|
+
if (scoped && !live.has(nameId)) {
|
|
1446
|
+
await unlink2(join2(leaseDir(), name)).catch(() => {
|
|
1447
|
+
return;
|
|
1448
|
+
});
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
const fromName = { backend, targetId: nameId, label: "", pid: 0, createdAt: 0 };
|
|
1452
|
+
let rec;
|
|
1453
|
+
try {
|
|
1454
|
+
rec = JSON.parse(await readFile2(join2(leaseDir(), name), "utf8"));
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
const code = err.code;
|
|
1457
|
+
if (code === "ENOENT")
|
|
1458
|
+
continue;
|
|
1459
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (typeof rec?.targetId !== "string" || typeof rec?.label !== "string" || typeof rec?.createdAt !== "number") {
|
|
1463
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
out.push({ backend: rec.backend, targetId: rec.targetId, label: rec.label, pid: rec.pid, createdAt: rec.createdAt });
|
|
1467
|
+
}
|
|
1468
|
+
return out;
|
|
1469
|
+
}
|
|
1470
|
+
async function originIndex(backend, liveIds) {
|
|
1471
|
+
const rows = await listOrigins({ liveIds, liveBackend: backend }).catch(() => []);
|
|
1472
|
+
const index = new Map;
|
|
1473
|
+
for (const row of rows) {
|
|
1474
|
+
if (row.backend !== backend)
|
|
1475
|
+
continue;
|
|
1476
|
+
index.set(row.targetId, row);
|
|
1477
|
+
}
|
|
1478
|
+
return index;
|
|
1479
|
+
}
|
|
1480
|
+
async function newTrackedPage(driver, backend, opts = {}) {
|
|
1481
|
+
const page = await driver.newPage(opts.url);
|
|
1482
|
+
await recordOrigin(backend, page.id, { label: opts.label });
|
|
1483
|
+
return page;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1331
1486
|
// src/leases-tools.ts
|
|
1332
1487
|
class LeaseToolError extends Error {
|
|
1333
1488
|
}
|
|
@@ -1336,12 +1491,12 @@ function backendOf(driver) {
|
|
|
1336
1491
|
}
|
|
1337
1492
|
async function claimPage(driver, args = {}) {
|
|
1338
1493
|
const backend = backendOf(driver);
|
|
1339
|
-
const label = typeof args.label === "string" && args.label.length ? args.label :
|
|
1494
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1340
1495
|
const pages = await driver.listPages();
|
|
1341
1496
|
let targetId = args.targetId;
|
|
1342
1497
|
let url = args.url ?? "about:blank";
|
|
1343
1498
|
if (targetId === undefined || targetId === "") {
|
|
1344
|
-
const page = await driver
|
|
1499
|
+
const page = await newTrackedPage(driver, backend, { url: args.url, label });
|
|
1345
1500
|
targetId = page.id;
|
|
1346
1501
|
url = page.url;
|
|
1347
1502
|
} else {
|
|
@@ -1383,8 +1538,8 @@ var LEASE_TOOLS = {
|
|
|
1383
1538
|
};
|
|
1384
1539
|
|
|
1385
1540
|
// src/shared-tools.ts
|
|
1386
|
-
import { mkdir as
|
|
1387
|
-
import { join as
|
|
1541
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
1542
|
+
import { dirname, join as join3 } from "node:path";
|
|
1388
1543
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1389
1544
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1390
1545
|
|
|
@@ -1452,16 +1607,35 @@ function locatorOf(args) {
|
|
|
1452
1607
|
throw new SharedToolError("provide exactly one of { uid } or { selector }");
|
|
1453
1608
|
return hasUid ? { uid: args.uid } : { css: args.selector };
|
|
1454
1609
|
}
|
|
1610
|
+
async function reapSet(driver, pages, all) {
|
|
1611
|
+
if (all === true)
|
|
1612
|
+
return pages.map((p) => p.id);
|
|
1613
|
+
try {
|
|
1614
|
+
return (await driver.listPages({ all: true })).map((p) => p.id);
|
|
1615
|
+
} catch {
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1455
1619
|
async function listPages(driver, args = {}) {
|
|
1456
1620
|
const pages = await driver.listPages({ all: args.all });
|
|
1457
|
-
|
|
1621
|
+
const ledger = await originIndex(backendOf2(driver), await reapSet(driver, pages, args.all));
|
|
1622
|
+
const annotated = pages.map((p) => {
|
|
1623
|
+
const rec = ledger.get(p.id);
|
|
1624
|
+
if (!rec)
|
|
1625
|
+
return { ...p, origin: "unknown" };
|
|
1626
|
+
if (rec.unreadable !== undefined)
|
|
1627
|
+
return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
|
|
1628
|
+
return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
|
|
1629
|
+
});
|
|
1630
|
+
return { pages: annotated, count: annotated.length };
|
|
1458
1631
|
}
|
|
1459
1632
|
async function newPage(driver, args = {}) {
|
|
1460
|
-
const
|
|
1633
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1634
|
+
const p = await newTrackedPage(driver, backendOf2(driver), { url: args.url, label });
|
|
1461
1635
|
if (args.claim !== true)
|
|
1462
1636
|
return { targetId: p.id, url: p.url };
|
|
1463
1637
|
const { record, token } = await claimLease(backendOf2(driver), p.id, {
|
|
1464
|
-
label
|
|
1638
|
+
label,
|
|
1465
1639
|
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
|
|
1466
1640
|
});
|
|
1467
1641
|
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
@@ -1477,8 +1651,8 @@ async function closePage(driver, args) {
|
|
|
1477
1651
|
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
1478
1652
|
}
|
|
1479
1653
|
async function writeSelectedFile(id) {
|
|
1480
|
-
await
|
|
1481
|
-
await
|
|
1654
|
+
await mkdir3(STATE_DIR, { recursive: true });
|
|
1655
|
+
await writeFile3(join3(STATE_DIR, "selected"), id, "utf8");
|
|
1482
1656
|
}
|
|
1483
1657
|
async function selectPage(driver, args) {
|
|
1484
1658
|
if (args.target === undefined || args.target === "")
|
|
@@ -1518,11 +1692,108 @@ async function waitForText(driver, args) {
|
|
|
1518
1692
|
return { found: true, waitedMs: r.elapsedMs };
|
|
1519
1693
|
});
|
|
1520
1694
|
}
|
|
1695
|
+
function serializeForSink(value) {
|
|
1696
|
+
const json = JSON.stringify(value === undefined ? null : value, null, 2);
|
|
1697
|
+
return json ?? "null";
|
|
1698
|
+
}
|
|
1699
|
+
async function writeJsonSink(savePath, value) {
|
|
1700
|
+
const path = savePath.startsWith("/") ? savePath : join3(ARTIFACT_DIR, savePath);
|
|
1701
|
+
await mkdir3(dirname(path), { recursive: true });
|
|
1702
|
+
const json = serializeForSink(value);
|
|
1703
|
+
await writeFile3(path, json, "utf8");
|
|
1704
|
+
return { path, bytes: Buffer.byteLength(json, "utf8") };
|
|
1705
|
+
}
|
|
1706
|
+
function sinkRequested(savePath) {
|
|
1707
|
+
return savePath !== undefined && savePath !== "";
|
|
1708
|
+
}
|
|
1521
1709
|
async function evaluateScript(driver, args) {
|
|
1522
1710
|
if (typeof args.expression !== "string" || args.expression.length === 0) {
|
|
1523
1711
|
throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
|
|
1524
1712
|
}
|
|
1525
|
-
return withPage2(driver, args.target, (page) =>
|
|
1713
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1714
|
+
const value = await page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true });
|
|
1715
|
+
if (!sinkRequested(args.savePath))
|
|
1716
|
+
return value;
|
|
1717
|
+
const { path, bytes } = await writeJsonSink(args.savePath, value);
|
|
1718
|
+
const result = {
|
|
1719
|
+
path,
|
|
1720
|
+
bytes,
|
|
1721
|
+
type: value === null ? "null" : typeof value,
|
|
1722
|
+
target: target3(page.info)
|
|
1723
|
+
};
|
|
1724
|
+
return result;
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
function cookieDomainMatches(cookieDomain, filter) {
|
|
1728
|
+
const bare = (d) => (d.startsWith(".") ? d.slice(1) : d).toLowerCase();
|
|
1729
|
+
const c = bare(cookieDomain);
|
|
1730
|
+
const f = bare(filter);
|
|
1731
|
+
return c === f || c.endsWith(`.${f}`);
|
|
1732
|
+
}
|
|
1733
|
+
function filterCookies(cookies, filter) {
|
|
1734
|
+
return cookies.filter((c) => {
|
|
1735
|
+
if (filter.name !== undefined && filter.name !== "" && c.name !== filter.name)
|
|
1736
|
+
return false;
|
|
1737
|
+
if (filter.domain !== undefined && filter.domain !== "" && !cookieDomainMatches(c.domain, filter.domain))
|
|
1738
|
+
return false;
|
|
1739
|
+
return true;
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
async function listCookies(driver, args = {}) {
|
|
1743
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1744
|
+
const all = await page.getCookies();
|
|
1745
|
+
const cookies = filterCookies(all, { domain: args.domain, name: args.name });
|
|
1746
|
+
if (!sinkRequested(args.savePath)) {
|
|
1747
|
+
return { cookies, count: cookies.length, target: target3(page.info) };
|
|
1748
|
+
}
|
|
1749
|
+
const { path, bytes } = await writeJsonSink(args.savePath, cookies);
|
|
1750
|
+
return { path, bytes, count: cookies.length, target: target3(page.info) };
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
function assertCookieSite(args, toolName) {
|
|
1754
|
+
const hasUrl = args.url !== undefined && args.url !== "";
|
|
1755
|
+
const hasDomain = args.domain !== undefined && args.domain !== "";
|
|
1756
|
+
if (!hasUrl && !hasDomain) {
|
|
1757
|
+
throw new Error(`${toolName} requires either 'url' or 'domain': a cookie has to be attributed to a site, and neither was given.`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
function assertCookieName(name, toolName) {
|
|
1761
|
+
if (name === undefined || name === "") {
|
|
1762
|
+
throw new Error(`${toolName} requires a non-empty 'name'.`);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
async function setCookie(driver, args) {
|
|
1766
|
+
assertCookieName(args.name, "set_cookie");
|
|
1767
|
+
if (typeof args.value !== "string")
|
|
1768
|
+
throw new Error("set_cookie requires a string 'value'.");
|
|
1769
|
+
assertCookieSite(args, "set_cookie");
|
|
1770
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1771
|
+
await page.setCookie({
|
|
1772
|
+
name: args.name,
|
|
1773
|
+
value: args.value,
|
|
1774
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1775
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1776
|
+
...args.path !== undefined ? { path: args.path } : {},
|
|
1777
|
+
...args.expires !== undefined ? { expires: args.expires } : {},
|
|
1778
|
+
...args.httpOnly !== undefined ? { httpOnly: args.httpOnly } : {},
|
|
1779
|
+
...args.secure !== undefined ? { secure: args.secure } : {},
|
|
1780
|
+
...args.sameSite !== undefined ? { sameSite: args.sameSite } : {}
|
|
1781
|
+
});
|
|
1782
|
+
return { set: true, target: target3(page.info) };
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
async function deleteCookies(driver, args) {
|
|
1786
|
+
assertCookieName(args.name, "delete_cookies");
|
|
1787
|
+
assertCookieSite(args, "delete_cookies");
|
|
1788
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1789
|
+
await page.deleteCookies({
|
|
1790
|
+
name: args.name,
|
|
1791
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1792
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1793
|
+
...args.path !== undefined ? { path: args.path } : {}
|
|
1794
|
+
});
|
|
1795
|
+
return { deleted: true, target: target3(page.info) };
|
|
1796
|
+
});
|
|
1526
1797
|
}
|
|
1527
1798
|
var BARE_TOKEN_EXTRAS = new Set(["disabled", "focused"]);
|
|
1528
1799
|
var UNQUOTED_EXTRAS = new Set(["checked", "expanded", "url"]);
|
|
@@ -1633,10 +1904,10 @@ async function takeScreenshot(driver, args = {}) {
|
|
|
1633
1904
|
const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
|
|
1634
1905
|
if (data.byteLength === 0)
|
|
1635
1906
|
throw new SharedToolError("captureScreenshot returned empty data");
|
|
1636
|
-
await
|
|
1907
|
+
await mkdir3(ARTIFACT_DIR, { recursive: true });
|
|
1637
1908
|
const ext = outFormat === "jpeg" ? "jpg" : "png";
|
|
1638
|
-
const path = args.savePath ??
|
|
1639
|
-
await
|
|
1909
|
+
const path = args.savePath ?? join3(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
1910
|
+
await writeFile3(path, Buffer.from(data));
|
|
1640
1911
|
const result = {
|
|
1641
1912
|
path,
|
|
1642
1913
|
bytes: data.byteLength,
|
|
@@ -1683,6 +1954,9 @@ var SHARED_TOOLS = {
|
|
|
1683
1954
|
navigate_page: navigatePage,
|
|
1684
1955
|
wait_for: waitForText,
|
|
1685
1956
|
evaluate_script: evaluateScript,
|
|
1957
|
+
list_cookies: listCookies,
|
|
1958
|
+
set_cookie: setCookie,
|
|
1959
|
+
delete_cookies: deleteCookies,
|
|
1686
1960
|
take_snapshot: takeSnapshot,
|
|
1687
1961
|
click,
|
|
1688
1962
|
hover,
|
|
@@ -1699,10 +1973,10 @@ var SHARED_TOOLS = {
|
|
|
1699
1973
|
};
|
|
1700
1974
|
|
|
1701
1975
|
// src/tools/console.ts
|
|
1702
|
-
import { readFile as
|
|
1976
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1703
1977
|
|
|
1704
1978
|
// src/tools/recorder.ts
|
|
1705
|
-
import { mkdir as
|
|
1979
|
+
import { mkdir as mkdir4, appendFile, writeFile as writeFile4, copyFile } from "node:fs/promises";
|
|
1706
1980
|
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1707
1981
|
function recFile(targetId) {
|
|
1708
1982
|
return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
|
|
@@ -1723,11 +1997,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
1723
1997
|
async function startRecorder(target, opts = {}) {
|
|
1724
1998
|
const network = opts.network ?? true;
|
|
1725
1999
|
const consoleCap = opts.console ?? true;
|
|
1726
|
-
await
|
|
2000
|
+
await mkdir4(ARTIFACT_DIR2, { recursive: true });
|
|
1727
2001
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
1728
2002
|
const file = opts.file ?? recFile(resolved.id);
|
|
1729
2003
|
if (opts.truncate) {
|
|
1730
|
-
await
|
|
2004
|
+
await writeFile4(file, "");
|
|
1731
2005
|
}
|
|
1732
2006
|
let chain = Promise.resolve();
|
|
1733
2007
|
let dropped = 0;
|
|
@@ -1869,7 +2143,7 @@ function lineToEntry(line, index) {
|
|
|
1869
2143
|
async function readConsoleEntries(file) {
|
|
1870
2144
|
let raw;
|
|
1871
2145
|
try {
|
|
1872
|
-
raw = await
|
|
2146
|
+
raw = await readFile3(file, "utf8");
|
|
1873
2147
|
} catch {
|
|
1874
2148
|
return [];
|
|
1875
2149
|
}
|
|
@@ -1925,7 +2199,7 @@ async function getConsoleMessage(args = {}) {
|
|
|
1925
2199
|
}
|
|
1926
2200
|
|
|
1927
2201
|
// src/tools/network.ts
|
|
1928
|
-
import { readFile as
|
|
2202
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1929
2203
|
var DEFAULT_CAPTURE_MS2 = 2500;
|
|
1930
2204
|
function asHeaders(v) {
|
|
1931
2205
|
if (!v || typeof v !== "object")
|
|
@@ -1992,7 +2266,7 @@ function correlate(lines) {
|
|
|
1992
2266
|
async function readNetworkRequests(file) {
|
|
1993
2267
|
let raw;
|
|
1994
2268
|
try {
|
|
1995
|
-
raw = await
|
|
2269
|
+
raw = await readFile4(file, "utf8");
|
|
1996
2270
|
} catch {
|
|
1997
2271
|
return [];
|
|
1998
2272
|
}
|
|
@@ -2074,8 +2348,8 @@ async function getNetworkRequest(args = {}) {
|
|
|
2074
2348
|
}
|
|
2075
2349
|
|
|
2076
2350
|
// src/tools/performance.ts
|
|
2077
|
-
import { mkdir as
|
|
2078
|
-
import { join as
|
|
2351
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, stat, unlink as unlink3 } from "node:fs/promises";
|
|
2352
|
+
import { join as join4 } from "node:path";
|
|
2079
2353
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2080
2354
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
2081
2355
|
var DEFAULT_CATEGORIES = [
|
|
@@ -2093,7 +2367,7 @@ function shortId(id) {
|
|
|
2093
2367
|
return id.slice(0, 8);
|
|
2094
2368
|
}
|
|
2095
2369
|
function stateFilePath() {
|
|
2096
|
-
return
|
|
2370
|
+
return join4(STATE_DIR2, "trace-state.json");
|
|
2097
2371
|
}
|
|
2098
2372
|
function bufferTraceData(conn, sink) {
|
|
2099
2373
|
return conn.on("Tracing.dataCollected", (params) => {
|
|
@@ -2115,10 +2389,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
2115
2389
|
return sink;
|
|
2116
2390
|
}
|
|
2117
2391
|
async function writeTrace(events, targetId) {
|
|
2118
|
-
await
|
|
2119
|
-
const path =
|
|
2392
|
+
await mkdir5(ARTIFACT_DIR3, { recursive: true });
|
|
2393
|
+
const path = join4(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
2120
2394
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
2121
|
-
await
|
|
2395
|
+
await writeFile5(path, body, "utf8");
|
|
2122
2396
|
const { size } = await stat(path);
|
|
2123
2397
|
return { path, bytes: size };
|
|
2124
2398
|
}
|
|
@@ -2138,7 +2412,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
2138
2412
|
throw err;
|
|
2139
2413
|
}
|
|
2140
2414
|
liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
|
|
2141
|
-
await
|
|
2415
|
+
await mkdir5(STATE_DIR2, { recursive: true });
|
|
2142
2416
|
const state = {
|
|
2143
2417
|
targetId: target.id,
|
|
2144
2418
|
url: target.url,
|
|
@@ -2146,7 +2420,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
2146
2420
|
pid: process.pid,
|
|
2147
2421
|
categories
|
|
2148
2422
|
};
|
|
2149
|
-
await
|
|
2423
|
+
await writeFile5(stateFilePath(), JSON.stringify(state), "utf8");
|
|
2150
2424
|
return {
|
|
2151
2425
|
target: { id: target.id, url: target.url, title: target.title },
|
|
2152
2426
|
categories,
|
|
@@ -2159,7 +2433,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
2159
2433
|
if (!entry) {
|
|
2160
2434
|
let hint = "";
|
|
2161
2435
|
try {
|
|
2162
|
-
const raw = await
|
|
2436
|
+
const raw = await readFile5(stateFilePath(), "utf8");
|
|
2163
2437
|
const state = JSON.parse(raw);
|
|
2164
2438
|
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
2439
|
} catch {}
|
|
@@ -2171,7 +2445,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
2171
2445
|
} finally {
|
|
2172
2446
|
conn.close();
|
|
2173
2447
|
liveTraces.delete(target.id);
|
|
2174
|
-
await
|
|
2448
|
+
await unlink3(stateFilePath()).catch(() => {});
|
|
2175
2449
|
}
|
|
2176
2450
|
const written = await writeTrace(events, target.id);
|
|
2177
2451
|
const metrics = analyzeEvents(events);
|
|
@@ -2289,7 +2563,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
2289
2563
|
}
|
|
2290
2564
|
let raw;
|
|
2291
2565
|
try {
|
|
2292
|
-
raw = await
|
|
2566
|
+
raw = await readFile5(args.tracePath, "utf8");
|
|
2293
2567
|
} catch (err) {
|
|
2294
2568
|
throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
|
|
2295
2569
|
}
|
|
@@ -2306,8 +2580,8 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
2306
2580
|
}
|
|
2307
2581
|
|
|
2308
2582
|
// src/tools/heap.ts
|
|
2309
|
-
import { mkdir as
|
|
2310
|
-
import { join as
|
|
2583
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
|
|
2584
|
+
import { join as join5 } from "node:path";
|
|
2311
2585
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2312
2586
|
function stamp3(targetId) {
|
|
2313
2587
|
const short = targetId.slice(0, 8);
|
|
@@ -2332,9 +2606,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2332
2606
|
if (snapshot.length === 0) {
|
|
2333
2607
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
2334
2608
|
}
|
|
2335
|
-
await
|
|
2336
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
2337
|
-
await
|
|
2609
|
+
await mkdir6(ARTIFACT_DIR4, { recursive: true });
|
|
2610
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join5(ARTIFACT_DIR4, args.savePath) : join5(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
|
|
2611
|
+
await writeFile6(path, snapshot, "utf8");
|
|
2338
2612
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
2339
2613
|
return {
|
|
2340
2614
|
path,
|
|
@@ -2347,7 +2621,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2347
2621
|
|
|
2348
2622
|
// src/tools/lighthouse.ts
|
|
2349
2623
|
import { spawn } from "node:child_process";
|
|
2350
|
-
import { mkdir as
|
|
2624
|
+
import { mkdir as mkdir7, readFile as readFile6, stat as stat2 } from "node:fs/promises";
|
|
2351
2625
|
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2352
2626
|
function cdpPort() {
|
|
2353
2627
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
@@ -2408,7 +2682,7 @@ async function lighthouseAudit(args) {
|
|
|
2408
2682
|
const url = args.url.trim();
|
|
2409
2683
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
2410
2684
|
const port = cdpPort();
|
|
2411
|
-
await
|
|
2685
|
+
await mkdir7(ARTIFACT_DIR5, { recursive: true });
|
|
2412
2686
|
const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
|
|
2413
2687
|
const formFactor = args.formFactor ?? "desktop";
|
|
2414
2688
|
const lhArgs = [
|
|
@@ -2437,7 +2711,7 @@ async function lighthouseAudit(args) {
|
|
|
2437
2711
|
}
|
|
2438
2712
|
let raw;
|
|
2439
2713
|
try {
|
|
2440
|
-
raw = await
|
|
2714
|
+
raw = await readFile6(outPath, "utf8");
|
|
2441
2715
|
} catch {
|
|
2442
2716
|
raw = undefined;
|
|
2443
2717
|
}
|
|
@@ -2494,6 +2768,9 @@ var TOOLS = {
|
|
|
2494
2768
|
navigate_page: onCdp(SHARED_TOOLS.navigate_page),
|
|
2495
2769
|
wait_for: onCdp(SHARED_TOOLS.wait_for),
|
|
2496
2770
|
evaluate_script: onCdp(SHARED_TOOLS.evaluate_script),
|
|
2771
|
+
list_cookies: onCdp(SHARED_TOOLS.list_cookies),
|
|
2772
|
+
set_cookie: onCdp(SHARED_TOOLS.set_cookie),
|
|
2773
|
+
delete_cookies: onCdp(SHARED_TOOLS.delete_cookies),
|
|
2497
2774
|
take_snapshot: onCdp(SHARED_TOOLS.take_snapshot),
|
|
2498
2775
|
click: onCdp(SHARED_TOOLS.click),
|
|
2499
2776
|
hover: onCdp(SHARED_TOOLS.hover),
|
|
@@ -2535,7 +2812,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
2535
2812
|
var MANIFEST = [
|
|
2536
2813
|
{
|
|
2537
2814
|
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.",
|
|
2815
|
+
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
2816
|
inputSchema: {
|
|
2540
2817
|
type: "object",
|
|
2541
2818
|
properties: {
|
|
@@ -2702,7 +2979,7 @@ var MANIFEST = [
|
|
|
2702
2979
|
},
|
|
2703
2980
|
{
|
|
2704
2981
|
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.",
|
|
2982
|
+
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
2983
|
inputSchema: {
|
|
2707
2984
|
type: "object",
|
|
2708
2985
|
properties: {
|
|
@@ -2725,6 +3002,10 @@ var MANIFEST = [
|
|
|
2725
3002
|
args: {
|
|
2726
3003
|
type: "array",
|
|
2727
3004
|
description: "Positional JSON-serializable arguments to pass to the expression, treating it as a function. No live element/page handle is bound."
|
|
3005
|
+
},
|
|
3006
|
+
savePath: {
|
|
3007
|
+
type: "string",
|
|
3008
|
+
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
3009
|
}
|
|
2729
3010
|
},
|
|
2730
3011
|
required: [
|
|
@@ -2733,6 +3014,128 @@ var MANIFEST = [
|
|
|
2733
3014
|
additionalProperties: false
|
|
2734
3015
|
}
|
|
2735
3016
|
},
|
|
3017
|
+
{
|
|
3018
|
+
name: "list_cookies",
|
|
3019
|
+
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.",
|
|
3020
|
+
inputSchema: {
|
|
3021
|
+
type: "object",
|
|
3022
|
+
properties: {
|
|
3023
|
+
lease: {
|
|
3024
|
+
type: "string",
|
|
3025
|
+
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."
|
|
3026
|
+
},
|
|
3027
|
+
target: {
|
|
3028
|
+
type: "string",
|
|
3029
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
3030
|
+
},
|
|
3031
|
+
domain: {
|
|
3032
|
+
type: "string",
|
|
3033
|
+
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."
|
|
3034
|
+
},
|
|
3035
|
+
name: {
|
|
3036
|
+
type: "string",
|
|
3037
|
+
description: "Keep only the cookie with exactly this name. Exact match, not a substring."
|
|
3038
|
+
},
|
|
3039
|
+
savePath: {
|
|
3040
|
+
type: "string",
|
|
3041
|
+
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."
|
|
3042
|
+
}
|
|
3043
|
+
},
|
|
3044
|
+
required: [],
|
|
3045
|
+
additionalProperties: false
|
|
3046
|
+
}
|
|
3047
|
+
},
|
|
3048
|
+
{
|
|
3049
|
+
name: "set_cookie",
|
|
3050
|
+
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.",
|
|
3051
|
+
inputSchema: {
|
|
3052
|
+
type: "object",
|
|
3053
|
+
properties: {
|
|
3054
|
+
lease: {
|
|
3055
|
+
type: "string",
|
|
3056
|
+
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."
|
|
3057
|
+
},
|
|
3058
|
+
target: {
|
|
3059
|
+
type: "string",
|
|
3060
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
3061
|
+
},
|
|
3062
|
+
name: {
|
|
3063
|
+
type: "string",
|
|
3064
|
+
description: "Cookie name. Required and non-empty."
|
|
3065
|
+
},
|
|
3066
|
+
value: {
|
|
3067
|
+
type: "string",
|
|
3068
|
+
description: "Cookie value. Required. It is not echoed back in the response."
|
|
3069
|
+
},
|
|
3070
|
+
url: {
|
|
3071
|
+
type: "string",
|
|
3072
|
+
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'."
|
|
3073
|
+
},
|
|
3074
|
+
domain: {
|
|
3075
|
+
type: "string",
|
|
3076
|
+
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."
|
|
3077
|
+
},
|
|
3078
|
+
path: {
|
|
3079
|
+
type: "string",
|
|
3080
|
+
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."
|
|
3081
|
+
},
|
|
3082
|
+
expires: {
|
|
3083
|
+
type: "number",
|
|
3084
|
+
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."
|
|
3085
|
+
},
|
|
3086
|
+
httpOnly: {
|
|
3087
|
+
type: "boolean",
|
|
3088
|
+
description: "Set the httpOnly flag, making the cookie invisible to document.cookie. Only a protocol write like this one can create such a cookie."
|
|
3089
|
+
},
|
|
3090
|
+
secure: {
|
|
3091
|
+
type: "boolean",
|
|
3092
|
+
description: "Set the secure flag. Chrome declines a secure cookie on an insecure origin, and that refusal surfaces as an error."
|
|
3093
|
+
},
|
|
3094
|
+
sameSite: {
|
|
3095
|
+
type: "string",
|
|
3096
|
+
enum: ["strict", "lax", "none", "default"],
|
|
3097
|
+
description: "SameSite attribute, lowercase, matching what list_cookies reports. 'default' means the attribute is not set at all."
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
required: ["name", "value"],
|
|
3101
|
+
additionalProperties: false
|
|
3102
|
+
}
|
|
3103
|
+
},
|
|
3104
|
+
{
|
|
3105
|
+
name: "delete_cookies",
|
|
3106
|
+
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.",
|
|
3107
|
+
inputSchema: {
|
|
3108
|
+
type: "object",
|
|
3109
|
+
properties: {
|
|
3110
|
+
lease: {
|
|
3111
|
+
type: "string",
|
|
3112
|
+
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."
|
|
3113
|
+
},
|
|
3114
|
+
target: {
|
|
3115
|
+
type: "string",
|
|
3116
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
3117
|
+
},
|
|
3118
|
+
name: {
|
|
3119
|
+
type: "string",
|
|
3120
|
+
description: "Exact cookie name to delete. Required and non-empty. No wildcards: this removes a named cookie, not a swathe of them."
|
|
3121
|
+
},
|
|
3122
|
+
url: {
|
|
3123
|
+
type: "string",
|
|
3124
|
+
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."
|
|
3125
|
+
},
|
|
3126
|
+
domain: {
|
|
3127
|
+
type: "string",
|
|
3128
|
+
description: "Delete cookies with this exact domain. Give this or 'url'."
|
|
3129
|
+
},
|
|
3130
|
+
path: {
|
|
3131
|
+
type: "string",
|
|
3132
|
+
description: "Narrow the deletion to cookies with this exact path. Omit to match any path allowed by the other constraints."
|
|
3133
|
+
}
|
|
3134
|
+
},
|
|
3135
|
+
required: ["name"],
|
|
3136
|
+
additionalProperties: false
|
|
3137
|
+
}
|
|
3138
|
+
},
|
|
2736
3139
|
{
|
|
2737
3140
|
name: "take_snapshot",
|
|
2738
3141
|
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.",
|
|
@@ -4241,6 +4644,37 @@ function buildSnapshotFunctionSource() {
|
|
|
4241
4644
|
var MAX_NAME_LENGTH = 120;
|
|
4242
4645
|
|
|
4243
4646
|
// src/bidi/driver.ts
|
|
4647
|
+
function normalizeBidiCookie(c) {
|
|
4648
|
+
const value = c.value.type === "base64" ? Buffer.from(c.value.value, "base64").toString("utf8") : c.value.value;
|
|
4649
|
+
const hasExpiry = typeof c.expiry === "number";
|
|
4650
|
+
return {
|
|
4651
|
+
name: c.name,
|
|
4652
|
+
value,
|
|
4653
|
+
domain: c.domain,
|
|
4654
|
+
path: c.path,
|
|
4655
|
+
expires: hasExpiry ? c.expiry : -1,
|
|
4656
|
+
size: typeof c.size === "number" ? c.size : Buffer.byteLength(`${c.name}${value}`, "utf8"),
|
|
4657
|
+
httpOnly: c.httpOnly === true,
|
|
4658
|
+
secure: c.secure === true,
|
|
4659
|
+
sameSite: c.sameSite ?? "default",
|
|
4660
|
+
session: !hasExpiry
|
|
4661
|
+
};
|
|
4662
|
+
}
|
|
4663
|
+
function hostFromUrl(url, toolName) {
|
|
4664
|
+
if (url === undefined || url === "") {
|
|
4665
|
+
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.`);
|
|
4666
|
+
}
|
|
4667
|
+
let host = "";
|
|
4668
|
+
try {
|
|
4669
|
+
host = new URL(url).hostname;
|
|
4670
|
+
} catch {
|
|
4671
|
+
throw driverError2("unsupported", `${toolName} could not parse the url '${url}' to derive the 'domain' BiDi requires. Pass 'domain' explicitly.`);
|
|
4672
|
+
}
|
|
4673
|
+
if (host === "") {
|
|
4674
|
+
throw driverError2("unsupported", `${toolName} cannot derive a 'domain' from '${url}': that url has no host. Pass 'domain' explicitly.`);
|
|
4675
|
+
}
|
|
4676
|
+
return host;
|
|
4677
|
+
}
|
|
4244
4678
|
function driverError2(code, message, data) {
|
|
4245
4679
|
return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
|
|
4246
4680
|
}
|
|
@@ -4849,6 +5283,49 @@ class BidiPageDriver {
|
|
|
4849
5283
|
}
|
|
4850
5284
|
return this.handleOneDialog(accept, promptText, opts?.timeoutMs ?? 15000);
|
|
4851
5285
|
}
|
|
5286
|
+
async getCookies() {
|
|
5287
|
+
try {
|
|
5288
|
+
const { cookies } = await this.conn.send("storage.getCookies", { partition: { type: "context", context: this.contextId } });
|
|
5289
|
+
return (cookies ?? []).map(normalizeBidiCookie);
|
|
5290
|
+
} catch (e) {
|
|
5291
|
+
throw mapBidiError(e);
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
async setCookie(params) {
|
|
5295
|
+
const domain = params.domain ?? hostFromUrl(params.url, "set_cookie");
|
|
5296
|
+
try {
|
|
5297
|
+
await this.conn.send("storage.setCookie", {
|
|
5298
|
+
cookie: {
|
|
5299
|
+
name: params.name,
|
|
5300
|
+
value: { type: "string", value: params.value },
|
|
5301
|
+
domain,
|
|
5302
|
+
...params.path !== undefined ? { path: params.path } : {},
|
|
5303
|
+
...params.expires !== undefined ? { expiry: params.expires } : {},
|
|
5304
|
+
...params.httpOnly !== undefined ? { httpOnly: params.httpOnly } : {},
|
|
5305
|
+
...params.secure !== undefined ? { secure: params.secure } : {},
|
|
5306
|
+
...params.sameSite !== undefined ? { sameSite: params.sameSite } : {}
|
|
5307
|
+
},
|
|
5308
|
+
partition: { type: "context", context: this.contextId }
|
|
5309
|
+
});
|
|
5310
|
+
} catch (e) {
|
|
5311
|
+
throw mapBidiError(e);
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
async deleteCookies(filter) {
|
|
5315
|
+
const domain = filter.domain ?? hostFromUrl(filter.url, "delete_cookies");
|
|
5316
|
+
try {
|
|
5317
|
+
await this.conn.send("storage.deleteCookies", {
|
|
5318
|
+
filter: {
|
|
5319
|
+
name: filter.name,
|
|
5320
|
+
domain,
|
|
5321
|
+
...filter.path !== undefined ? { path: filter.path } : {}
|
|
5322
|
+
},
|
|
5323
|
+
partition: { type: "context", context: this.contextId }
|
|
5324
|
+
});
|
|
5325
|
+
} catch (e) {
|
|
5326
|
+
throw mapBidiError(e);
|
|
5327
|
+
}
|
|
5328
|
+
}
|
|
4852
5329
|
async startBodyCapture() {
|
|
4853
5330
|
let collector;
|
|
4854
5331
|
try {
|
|
@@ -5023,7 +5500,7 @@ import { spawn as spawn2 } from "node:child_process";
|
|
|
5023
5500
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5024
5501
|
import { createConnection, createServer } from "node:net";
|
|
5025
5502
|
import { tmpdir } from "node:os";
|
|
5026
|
-
import { join as
|
|
5503
|
+
import { join as join6 } from "node:path";
|
|
5027
5504
|
function resolveBinary() {
|
|
5028
5505
|
const candidates = [
|
|
5029
5506
|
process.env.FIREFOX_BIN,
|
|
@@ -5097,7 +5574,7 @@ async function launchFirefox(opts = {}) {
|
|
|
5097
5574
|
const binary = resolveBinary();
|
|
5098
5575
|
const port = await pickPort(opts.port);
|
|
5099
5576
|
const isThrowaway = opts.profilePath === undefined;
|
|
5100
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
5577
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join6(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
5101
5578
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
5102
5579
|
if (opts.headless)
|
|
5103
5580
|
args.push("--headless");
|
|
@@ -5367,7 +5844,7 @@ var FIREFOX_TOOLS = {
|
|
|
5367
5844
|
};
|
|
5368
5845
|
|
|
5369
5846
|
// src/mcp.ts
|
|
5370
|
-
var VERSION = "1.
|
|
5847
|
+
var VERSION = "1.4.0";
|
|
5371
5848
|
var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
|
|
5372
5849
|
var AVAILABILITY = toolAvailability(BROWSER);
|
|
5373
5850
|
var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
|