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/dist/cli.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 : `pid-${process.pid}`;
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.newPage(args.url);
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 mkdir2, writeFile as writeFile2 } from "node:fs/promises";
1387
- import { join as join2 } from "node:path";
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
- return { pages, count: pages.length };
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 p = await driver.newPage(args.url);
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: typeof args.label === "string" && args.label.length ? args.label : `pid-${process.pid}`,
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 mkdir2(STATE_DIR, { recursive: true });
1481
- await writeFile2(join2(STATE_DIR, "selected"), id, "utf8");
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) => page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true }));
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 mkdir2(ARTIFACT_DIR, { recursive: true });
1910
+ await mkdir3(ARTIFACT_DIR, { recursive: true });
1637
1911
  const ext = outFormat === "jpeg" ? "jpg" : "png";
1638
- const path = args.savePath ?? join2(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
1639
- await writeFile2(path, Buffer.from(data));
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 readFile2 } from "node:fs/promises";
1979
+ import { readFile as readFile3 } from "node:fs/promises";
1703
1980
 
1704
1981
  // src/tools/recorder.ts
1705
- import { mkdir as mkdir3, appendFile, writeFile as writeFile3, copyFile } from "node:fs/promises";
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 mkdir3(ARTIFACT_DIR2, { recursive: true });
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 writeFile3(file, "");
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 readFile2(file, "utf8");
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 readFile3 } from "node:fs/promises";
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 readFile3(file, "utf8");
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 mkdir4, readFile as readFile4, writeFile as writeFile4, stat, unlink as unlink2 } from "node:fs/promises";
2078
- import { join as join3 } from "node:path";
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 join3(STATE_DIR2, "trace-state.json");
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 mkdir4(ARTIFACT_DIR3, { recursive: true });
2119
- const path = join3(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
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 writeFile4(path, body, "utf8");
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 mkdir4(STATE_DIR2, { recursive: true });
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 writeFile4(stateFilePath(), JSON.stringify(state), "utf8");
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 readFile4(stateFilePath(), "utf8");
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 unlink2(stateFilePath()).catch(() => {});
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 readFile4(args.tracePath, "utf8");
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/heap.ts
2309
- import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
2310
- import { join as join4 } from "node:path";
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
- function stamp3(targetId) {
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 mkdir5(ARTIFACT_DIR4, { recursive: true });
2336
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join4(ARTIFACT_DIR4, args.savePath) : join4(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
2337
- await writeFile5(path, snapshot, "utf8");
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 mkdir6, readFile as readFile5, stat as stat2 } from "node:fs/promises";
2351
- var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
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 stamp4() {
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 = spawn(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
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 mkdir6(ARTIFACT_DIR5, { recursive: true });
2412
- const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
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 readFile5(outPath, "utf8");
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 stat2(outPath);
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,
@@ -3030,11 +3676,42 @@ function buildSnapshotFunctionSource() {
3030
3676
  var MAX_NAME_LENGTH = 120;
3031
3677
 
3032
3678
  // src/bidi/driver.ts
3679
+ function normalizeBidiCookie(c) {
3680
+ const value = c.value.type === "base64" ? Buffer.from(c.value.value, "base64").toString("utf8") : c.value.value;
3681
+ const hasExpiry = typeof c.expiry === "number";
3682
+ return {
3683
+ name: c.name,
3684
+ value,
3685
+ domain: c.domain,
3686
+ path: c.path,
3687
+ expires: hasExpiry ? c.expiry : -1,
3688
+ size: typeof c.size === "number" ? c.size : Buffer.byteLength(`${c.name}${value}`, "utf8"),
3689
+ httpOnly: c.httpOnly === true,
3690
+ secure: c.secure === true,
3691
+ sameSite: c.sameSite ?? "default",
3692
+ session: !hasExpiry
3693
+ };
3694
+ }
3695
+ function hostFromUrl(url, toolName) {
3696
+ if (url === undefined || url === "") {
3697
+ 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.`);
3698
+ }
3699
+ let host = "";
3700
+ try {
3701
+ host = new URL(url).hostname;
3702
+ } catch {
3703
+ throw driverError2("unsupported", `${toolName} could not parse the url '${url}' to derive the 'domain' BiDi requires. Pass 'domain' explicitly.`);
3704
+ }
3705
+ if (host === "") {
3706
+ throw driverError2("unsupported", `${toolName} cannot derive a 'domain' from '${url}': that url has no host. Pass 'domain' explicitly.`);
3707
+ }
3708
+ return host;
3709
+ }
3033
3710
  function driverError2(code, message, data) {
3034
3711
  return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
3035
3712
  }
3036
- function encodeUid2(stamp5) {
3037
- return `bidi:${stamp5}`;
3713
+ function encodeUid2(stamp6) {
3714
+ return `bidi:${stamp6}`;
3038
3715
  }
3039
3716
  function decodeStamp(uid) {
3040
3717
  if (!uid.startsWith("bidi:"))
@@ -3141,7 +3818,7 @@ function parseSnapshotLine(line) {
3141
3818
  const m = LINE_RE.exec(line);
3142
3819
  if (!m)
3143
3820
  return;
3144
- const indent = m[1] ?? "", stamp5 = m[2], role = m[3], nameLit = m[4], extrasRaw = m[5];
3821
+ const indent = m[1] ?? "", stamp6 = m[2], role = m[3], nameLit = m[4], extrasRaw = m[5];
3145
3822
  const depth = indent.length / 2;
3146
3823
  let name;
3147
3824
  if (nameLit) {
@@ -3169,7 +3846,7 @@ function parseSnapshotLine(line) {
3169
3846
  extras[key] = val;
3170
3847
  }
3171
3848
  }
3172
- return { uid: encodeUid2(stamp5), role, depth, ...name !== undefined ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} };
3849
+ return { uid: encodeUid2(stamp6), role, depth, ...name !== undefined ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} };
3173
3850
  }
3174
3851
  var INTERACTIVE_ROLES3 = new Set([
3175
3852
  "button",
@@ -3202,8 +3879,8 @@ function sharedRefOf(node, describe) {
3202
3879
  }
3203
3880
  async function resolveElementLocator2(conn, contextId, loc) {
3204
3881
  if ("uid" in loc) {
3205
- const stamp5 = decodeStamp(loc.uid);
3206
- const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp5)}"]` });
3882
+ const stamp6 = decodeStamp(loc.uid);
3883
+ const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp6)}"]` });
3207
3884
  if (!nodes2.length)
3208
3885
  throw driverError2("stale-uid", `uid does not resolve: ${loc.uid}`);
3209
3886
  return sharedRefOf(nodes2[0], loc.uid);
@@ -3415,14 +4092,14 @@ class BidiPageDriver {
3415
4092
  return loc.uid;
3416
4093
  }
3417
4094
  const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
3418
- const stamp5 = await this.callFunctionValue(STAMP_ONE_SOURCE, [], ref);
3419
- if (typeof stamp5 !== "string")
4095
+ const stamp6 = await this.callFunctionValue(STAMP_ONE_SOURCE, [], ref);
4096
+ if (typeof stamp6 !== "string")
3420
4097
  throw driverError2("page-error", "stamping the located element did not return a string");
3421
- return encodeUid2(stamp5);
4098
+ return encodeUid2(stamp6);
3422
4099
  }
3423
4100
  async resolve(uid) {
3424
- const stamp5 = decodeStamp(uid);
3425
- const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp5)}"]` });
4101
+ const stamp6 = decodeStamp(uid);
4102
+ const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp6)}"]` });
3426
4103
  if (!nodes.length)
3427
4104
  throw driverError2("stale-uid", `uid does not resolve: ${uid}`);
3428
4105
  }
@@ -3638,6 +4315,49 @@ class BidiPageDriver {
3638
4315
  }
3639
4316
  return this.handleOneDialog(accept, promptText, opts?.timeoutMs ?? 15000);
3640
4317
  }
4318
+ async getCookies() {
4319
+ try {
4320
+ const { cookies } = await this.conn.send("storage.getCookies", { partition: { type: "context", context: this.contextId } });
4321
+ return (cookies ?? []).map(normalizeBidiCookie);
4322
+ } catch (e) {
4323
+ throw mapBidiError(e);
4324
+ }
4325
+ }
4326
+ async setCookie(params) {
4327
+ const domain = params.domain ?? hostFromUrl(params.url, "set_cookie");
4328
+ try {
4329
+ await this.conn.send("storage.setCookie", {
4330
+ cookie: {
4331
+ name: params.name,
4332
+ value: { type: "string", value: params.value },
4333
+ domain,
4334
+ ...params.path !== undefined ? { path: params.path } : {},
4335
+ ...params.expires !== undefined ? { expiry: params.expires } : {},
4336
+ ...params.httpOnly !== undefined ? { httpOnly: params.httpOnly } : {},
4337
+ ...params.secure !== undefined ? { secure: params.secure } : {},
4338
+ ...params.sameSite !== undefined ? { sameSite: params.sameSite } : {}
4339
+ },
4340
+ partition: { type: "context", context: this.contextId }
4341
+ });
4342
+ } catch (e) {
4343
+ throw mapBidiError(e);
4344
+ }
4345
+ }
4346
+ async deleteCookies(filter) {
4347
+ const domain = filter.domain ?? hostFromUrl(filter.url, "delete_cookies");
4348
+ try {
4349
+ await this.conn.send("storage.deleteCookies", {
4350
+ filter: {
4351
+ name: filter.name,
4352
+ domain,
4353
+ ...filter.path !== undefined ? { path: filter.path } : {}
4354
+ },
4355
+ partition: { type: "context", context: this.contextId }
4356
+ });
4357
+ } catch (e) {
4358
+ throw mapBidiError(e);
4359
+ }
4360
+ }
3641
4361
  async startBodyCapture() {
3642
4362
  let collector;
3643
4363
  try {
@@ -3808,11 +4528,11 @@ function createFirefoxDriver(port, opts) {
3808
4528
  }
3809
4529
 
3810
4530
  // src/bidi/launch.ts
3811
- import { spawn as spawn2 } from "node:child_process";
4531
+ import { spawn as spawn3 } from "node:child_process";
3812
4532
  import { mkdtempSync, rmSync } from "node:fs";
3813
4533
  import { createConnection, createServer } from "node:net";
3814
4534
  import { tmpdir } from "node:os";
3815
- import { join as join5 } from "node:path";
4535
+ import { join as join7 } from "node:path";
3816
4536
  function resolveBinary() {
3817
4537
  const candidates = [
3818
4538
  process.env.FIREFOX_BIN,
@@ -3886,11 +4606,11 @@ async function launchFirefox(opts = {}) {
3886
4606
  const binary = resolveBinary();
3887
4607
  const port = await pickPort(opts.port);
3888
4608
  const isThrowaway = opts.profilePath === undefined;
3889
- const profilePath = opts.profilePath ?? mkdtempSync(join5(tmpdir(), "cdp-toolkit-ff-profile-"));
4609
+ const profilePath = opts.profilePath ?? mkdtempSync(join7(tmpdir(), "cdp-toolkit-ff-profile-"));
3890
4610
  const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
3891
4611
  if (opts.headless)
3892
4612
  args.push("--headless");
3893
- const proc = spawn2(binary, args, { stdio: "ignore", detached: false });
4613
+ const proc = spawn3(binary, args, { stdio: "ignore", detached: false });
3894
4614
  let closed = false;
3895
4615
  const close = async () => {
3896
4616
  if (closed)