cdp-toolkit 1.4.0 → 1.7.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 +251 -0
- package/CONTRACT.md +10 -3
- package/README.md +78 -30
- package/dist/cli.js +583 -92
- package/dist/index.js +568 -77
- package/dist/mcp.js +712 -140
- package/package.json +3 -2
- package/src/bidi/driver.ts +4 -0
- package/src/capabilities.ts +1 -1
- package/src/cdp/driver.ts +3 -0
- package/src/driver.ts +4 -1
- package/src/index.ts +15 -4
- package/src/leases-tools.ts +147 -26
- package/src/leases.ts +84 -4
- package/src/manifest.ts +127 -47
- package/src/mcp.ts +5 -2
- package/src/reap.ts +114 -0
- package/src/shared-tools.ts +41 -8
- package/src/tools/screencast.ts +741 -0
package/dist/mcp.js
CHANGED
|
@@ -13,6 +13,16 @@ 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
|
+
var longLived = false;
|
|
17
|
+
function markLongLivedProcess(value = true) {
|
|
18
|
+
longLived = value;
|
|
19
|
+
}
|
|
20
|
+
function requireLease() {
|
|
21
|
+
if (!longLived)
|
|
22
|
+
return false;
|
|
23
|
+
const raw = (process.env.CDP_REQUIRE_LEASE ?? "").trim().toLowerCase();
|
|
24
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
25
|
+
}
|
|
16
26
|
function defaultLabel() {
|
|
17
27
|
return `pid-${process.pid}`;
|
|
18
28
|
}
|
|
@@ -99,7 +109,8 @@ async function claimLease(backend, targetId, opts) {
|
|
|
99
109
|
label: opts.label,
|
|
100
110
|
createdAt: now,
|
|
101
111
|
lastUsedAt: now,
|
|
102
|
-
ttlMs: opts.ttlMs ?? leaseTtlMs()
|
|
112
|
+
ttlMs: opts.ttlMs ?? leaseTtlMs(),
|
|
113
|
+
auto: opts.auto === true
|
|
103
114
|
};
|
|
104
115
|
try {
|
|
105
116
|
await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
|
|
@@ -175,8 +186,16 @@ async function assertLeaseOk(backend, targetId, ctx = {}) {
|
|
|
175
186
|
const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
|
|
176
187
|
const where = describeTarget(targetId, ctx);
|
|
177
188
|
if (token === undefined) {
|
|
178
|
-
if (!held)
|
|
189
|
+
if (!held) {
|
|
190
|
+
if (requireLease()) {
|
|
191
|
+
await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
|
|
192
|
+
}
|
|
179
193
|
return;
|
|
194
|
+
}
|
|
195
|
+
if (held.auto === true && held.pid === process.pid) {
|
|
196
|
+
await touchLease(held, now);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
180
199
|
throw new LeaseConflictError(`${where} is leased by '${held.label}' (pid ${held.pid}). Pass that lease's token as the 'lease' argument, or call release_page to free it. list_leases shows every active lease.`, targetId, held.label);
|
|
181
200
|
}
|
|
182
201
|
const parts = tokenParts(token);
|
|
@@ -184,8 +203,12 @@ async function assertLeaseOk(backend, targetId, ctx = {}) {
|
|
|
184
203
|
throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
|
|
185
204
|
}
|
|
186
205
|
if (parts.backend !== backend || parts.targetId !== targetId) {
|
|
187
|
-
if (!held)
|
|
206
|
+
if (!held) {
|
|
207
|
+
if (requireLease()) {
|
|
208
|
+
await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
|
|
209
|
+
}
|
|
188
210
|
return;
|
|
211
|
+
}
|
|
189
212
|
throw new LeaseConflictError(`the lease token you passed is for ${parts.backend} target '${parts.targetId}', not ${where}, which is leased by '${held.label}'. Resolve the tab you actually hold, or claim this one.`, targetId, held.label);
|
|
190
213
|
}
|
|
191
214
|
if (!held) {
|
|
@@ -682,6 +705,8 @@ var REQUIRED_CAPABILITIES = {
|
|
|
682
705
|
performance_trace: ["trace.performance"],
|
|
683
706
|
take_heapsnapshot: ["heap.snapshot"],
|
|
684
707
|
lighthouse_audit: ["audit.lighthouse"],
|
|
708
|
+
start_screen_recording: ["capture.screencast"],
|
|
709
|
+
stop_screen_recording: ["capture.screencast"],
|
|
685
710
|
mock_request: ["network.intercept"],
|
|
686
711
|
list_mocks: ["network.intercept"],
|
|
687
712
|
clear_mocks: ["network.intercept"]
|
|
@@ -937,7 +962,8 @@ var CDP_CAPABILITIES = new Set([
|
|
|
937
962
|
"snapshot.accessibilityTree",
|
|
938
963
|
"input.insertTextAtomic",
|
|
939
964
|
"locate.text",
|
|
940
|
-
"locate.xpath"
|
|
965
|
+
"locate.xpath",
|
|
966
|
+
"capture.screencast"
|
|
941
967
|
]);
|
|
942
968
|
|
|
943
969
|
class CdpPageDriver {
|
|
@@ -1426,6 +1452,20 @@ async function recordOrigin(backend, targetId, opts = {}) {
|
|
|
1426
1452
|
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1427
1453
|
} catch {}
|
|
1428
1454
|
}
|
|
1455
|
+
async function readOrigin(backend, targetId) {
|
|
1456
|
+
let raw;
|
|
1457
|
+
try {
|
|
1458
|
+
raw = await readFile2(originFile(backend, targetId), "utf8");
|
|
1459
|
+
} catch {
|
|
1460
|
+
return;
|
|
1461
|
+
}
|
|
1462
|
+
try {
|
|
1463
|
+
const rec = JSON.parse(raw);
|
|
1464
|
+
if (typeof rec?.targetId === "string" && typeof rec?.label === "string" && typeof rec?.createdAt === "number")
|
|
1465
|
+
return rec;
|
|
1466
|
+
} catch {}
|
|
1467
|
+
return;
|
|
1468
|
+
}
|
|
1429
1469
|
async function listOrigins(opts = {}) {
|
|
1430
1470
|
let names = [];
|
|
1431
1471
|
try {
|
|
@@ -1483,59 +1523,43 @@ async function newTrackedPage(driver, backend, opts = {}) {
|
|
|
1483
1523
|
return page;
|
|
1484
1524
|
}
|
|
1485
1525
|
|
|
1486
|
-
// src/
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
const
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
const existing = pages.find((p) => p.id === targetId);
|
|
1504
|
-
if (!existing)
|
|
1505
|
-
throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
|
|
1506
|
-
url = existing.url;
|
|
1526
|
+
// src/reap.ts
|
|
1527
|
+
function staleAgentTabs(input) {
|
|
1528
|
+
const live = new Set(input.livePageIds);
|
|
1529
|
+
const out = [];
|
|
1530
|
+
for (const row of input.leases) {
|
|
1531
|
+
if (row.backend !== input.backend)
|
|
1532
|
+
continue;
|
|
1533
|
+
if (row.unreadable !== undefined)
|
|
1534
|
+
continue;
|
|
1535
|
+
if (row.stale !== "dead-pid" && row.stale !== "expired")
|
|
1536
|
+
continue;
|
|
1537
|
+
if (!live.has(row.targetId))
|
|
1538
|
+
continue;
|
|
1539
|
+
const origin = input.origins.get(row.targetId);
|
|
1540
|
+
if (origin === undefined || origin.unreadable !== undefined)
|
|
1541
|
+
continue;
|
|
1542
|
+
out.push({ targetId: row.targetId, label: row.label, reason: row.stale });
|
|
1507
1543
|
}
|
|
1508
|
-
|
|
1509
|
-
const { record, token } = await claimLease(backend, targetId, {
|
|
1510
|
-
label,
|
|
1511
|
-
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
1512
|
-
liveIds
|
|
1513
|
-
});
|
|
1514
|
-
return {
|
|
1515
|
-
lease: token,
|
|
1516
|
-
targetId,
|
|
1517
|
-
url,
|
|
1518
|
-
label: record.label,
|
|
1519
|
-
ttlMs: record.ttlMs,
|
|
1520
|
-
expiresAt: record.lastUsedAt + record.ttlMs
|
|
1521
|
-
};
|
|
1544
|
+
return out;
|
|
1522
1545
|
}
|
|
1523
|
-
async function
|
|
1524
|
-
|
|
1525
|
-
|
|
1546
|
+
async function reapStaleAgentTabs(driver, backend) {
|
|
1547
|
+
const livePageIds = (await driver.listPages()).map((p) => p.id);
|
|
1548
|
+
const [origins, leases] = await Promise.all([
|
|
1549
|
+
originIndex(backend, undefined),
|
|
1550
|
+
listLeases({ liveIds: livePageIds, liveBackend: backend }).catch(() => [])
|
|
1551
|
+
]);
|
|
1552
|
+
const candidates = staleAgentTabs({ backend, livePageIds, origins, leases });
|
|
1553
|
+
const closed = [];
|
|
1554
|
+
for (const c of candidates) {
|
|
1555
|
+
const res = await driver.closePage(c.targetId).catch(() => ({ success: false }));
|
|
1556
|
+
if (res.success !== true)
|
|
1557
|
+
continue;
|
|
1558
|
+
await releaseLeaseFor(backend, c.targetId);
|
|
1559
|
+
closed.push(c);
|
|
1526
1560
|
}
|
|
1527
|
-
return
|
|
1561
|
+
return closed;
|
|
1528
1562
|
}
|
|
1529
|
-
async function listLeasesTool(driver, _args = {}) {
|
|
1530
|
-
const liveIds = (await driver.listPages()).map((p) => p.id);
|
|
1531
|
-
const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
|
|
1532
|
-
return { leases, count: leases.length };
|
|
1533
|
-
}
|
|
1534
|
-
var LEASE_TOOLS = {
|
|
1535
|
-
claim_page: claimPage,
|
|
1536
|
-
release_page: releasePage,
|
|
1537
|
-
list_leases: listLeasesTool
|
|
1538
|
-
};
|
|
1539
1563
|
|
|
1540
1564
|
// src/shared-tools.ts
|
|
1541
1565
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
@@ -1548,7 +1572,7 @@ class SharedToolError extends Error {
|
|
|
1548
1572
|
function target3(p) {
|
|
1549
1573
|
return { id: p.id, url: p.url, title: p.title };
|
|
1550
1574
|
}
|
|
1551
|
-
function
|
|
1575
|
+
function backendOf(driver) {
|
|
1552
1576
|
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
1553
1577
|
}
|
|
1554
1578
|
async function withPage2(driver, target, fn) {
|
|
@@ -1562,7 +1586,7 @@ async function withPage2(driver, target, fn) {
|
|
|
1562
1586
|
async function resolvePage(driver, selector) {
|
|
1563
1587
|
const pages = await driver.listPages();
|
|
1564
1588
|
const hit = await pickPage(driver, pages, selector);
|
|
1565
|
-
await assertLeaseOk(
|
|
1589
|
+
await assertLeaseOk(backendOf(driver), hit.id, { url: hit.url, title: hit.title });
|
|
1566
1590
|
return hit;
|
|
1567
1591
|
}
|
|
1568
1592
|
async function pickPage(driver, pages, selector) {
|
|
@@ -1617,8 +1641,10 @@ async function reapSet(driver, pages, all) {
|
|
|
1617
1641
|
}
|
|
1618
1642
|
}
|
|
1619
1643
|
async function listPages(driver, args = {}) {
|
|
1620
|
-
const
|
|
1621
|
-
const
|
|
1644
|
+
const reaped = requireLease() ? await reapStaleAgentTabs(driver, backendOf(driver)) : [];
|
|
1645
|
+
const reapedIds = new Set(reaped.map((r) => r.targetId));
|
|
1646
|
+
const pages = (await driver.listPages({ all: args.all })).filter((p) => !reapedIds.has(p.id));
|
|
1647
|
+
const ledger = await originIndex(backendOf(driver), await reapSet(driver, pages, args.all));
|
|
1622
1648
|
const annotated = pages.map((p) => {
|
|
1623
1649
|
const rec = ledger.get(p.id);
|
|
1624
1650
|
if (!rec)
|
|
@@ -1627,16 +1653,18 @@ async function listPages(driver, args = {}) {
|
|
|
1627
1653
|
return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
|
|
1628
1654
|
return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
|
|
1629
1655
|
});
|
|
1630
|
-
return { pages: annotated, count: annotated.length };
|
|
1656
|
+
return { pages: annotated, count: annotated.length, ...reaped.length ? { reaped } : {} };
|
|
1631
1657
|
}
|
|
1632
1658
|
async function newPage(driver, args = {}) {
|
|
1633
1659
|
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1634
|
-
const p = await newTrackedPage(driver,
|
|
1635
|
-
|
|
1660
|
+
const p = await newTrackedPage(driver, backendOf(driver), { url: args.url, label });
|
|
1661
|
+
const explicit = args.claim === true;
|
|
1662
|
+
if (!explicit && !requireLease())
|
|
1636
1663
|
return { targetId: p.id, url: p.url };
|
|
1637
|
-
const { record, token } = await claimLease(
|
|
1664
|
+
const { record, token } = await claimLease(backendOf(driver), p.id, {
|
|
1638
1665
|
label,
|
|
1639
|
-
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
|
|
1666
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
1667
|
+
auto: !explicit
|
|
1640
1668
|
});
|
|
1641
1669
|
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
1642
1670
|
}
|
|
@@ -1647,7 +1675,7 @@ async function closePage(driver, args) {
|
|
|
1647
1675
|
const res = await driver.closePage(p.id);
|
|
1648
1676
|
if (!res.success)
|
|
1649
1677
|
return { closed: p.id, success: false, leaseReleased: false };
|
|
1650
|
-
const released = await releaseLeaseFor(
|
|
1678
|
+
const released = await releaseLeaseFor(backendOf(driver), p.id);
|
|
1651
1679
|
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
1652
1680
|
}
|
|
1653
1681
|
async function writeSelectedFile(id) {
|
|
@@ -1972,6 +2000,103 @@ var SHARED_TOOLS = {
|
|
|
1972
2000
|
handle_dialog: handleDialog
|
|
1973
2001
|
};
|
|
1974
2002
|
|
|
2003
|
+
// src/leases-tools.ts
|
|
2004
|
+
class LeaseToolError extends Error {
|
|
2005
|
+
}
|
|
2006
|
+
function backendOf2(driver) {
|
|
2007
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
2008
|
+
}
|
|
2009
|
+
async function claimPage(driver, args = {}) {
|
|
2010
|
+
const hasTarget = typeof args.target === "string" && args.target.length > 0;
|
|
2011
|
+
const hasTargetId = typeof args.targetId === "string" && args.targetId.length > 0;
|
|
2012
|
+
if (hasTarget && hasTargetId) {
|
|
2013
|
+
throw new LeaseToolError("claim_page takes at most one of 'target' (any selector: active | index:N | url:<substr> | title:<substr> | <targetId>) or 'targetId' (an exact target id, kept for back-compat). Pass 'target' alone to claim an open tab, or neither to open a fresh one.");
|
|
2014
|
+
}
|
|
2015
|
+
const backend = backendOf2(driver);
|
|
2016
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
2017
|
+
const pages = await driver.listPages();
|
|
2018
|
+
let targetId;
|
|
2019
|
+
let url;
|
|
2020
|
+
let opened;
|
|
2021
|
+
if (hasTarget) {
|
|
2022
|
+
const hit = await pickPage(driver, pages, args.target).catch((err) => {
|
|
2023
|
+
throw new LeaseToolError(`claim_page: no open tab matches target '${args.target}' (${err.message}). No tab was opened: claim_page never creates one when 'target' is given.`);
|
|
2024
|
+
});
|
|
2025
|
+
targetId = hit.id;
|
|
2026
|
+
url = hit.url;
|
|
2027
|
+
opened = false;
|
|
2028
|
+
} else if (hasTargetId) {
|
|
2029
|
+
const existing = pages.find((p) => p.id === args.targetId);
|
|
2030
|
+
if (!existing)
|
|
2031
|
+
throw new LeaseToolError(`claim_page: no page target with id '${args.targetId}'`);
|
|
2032
|
+
targetId = existing.id;
|
|
2033
|
+
url = existing.url;
|
|
2034
|
+
opened = false;
|
|
2035
|
+
} else {
|
|
2036
|
+
const page = await newTrackedPage(driver, backend, { url: args.url, label });
|
|
2037
|
+
targetId = page.id;
|
|
2038
|
+
url = page.url;
|
|
2039
|
+
opened = true;
|
|
2040
|
+
}
|
|
2041
|
+
const liveIds = [...pages.map((p) => p.id), targetId];
|
|
2042
|
+
const { record, token } = await claimLease(backend, targetId, {
|
|
2043
|
+
label,
|
|
2044
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
2045
|
+
liveIds
|
|
2046
|
+
});
|
|
2047
|
+
return {
|
|
2048
|
+
lease: token,
|
|
2049
|
+
targetId,
|
|
2050
|
+
url,
|
|
2051
|
+
label: record.label,
|
|
2052
|
+
ttlMs: record.ttlMs,
|
|
2053
|
+
expiresAt: record.lastUsedAt + record.ttlMs,
|
|
2054
|
+
opened
|
|
2055
|
+
};
|
|
2056
|
+
}
|
|
2057
|
+
async function releasePage(driver, args = {}) {
|
|
2058
|
+
const hasLease = typeof args.lease === "string" && args.lease.length > 0;
|
|
2059
|
+
const hasTarget = typeof args.target === "string" && args.target.length > 0;
|
|
2060
|
+
if (hasLease === hasTarget) {
|
|
2061
|
+
throw new LeaseToolError("release_page takes exactly one of 'lease' (the token claim_page returned) or 'target' (a selector for a tab this process holds)");
|
|
2062
|
+
}
|
|
2063
|
+
const backend = backendOf2(driver);
|
|
2064
|
+
let targetId;
|
|
2065
|
+
let released;
|
|
2066
|
+
if (hasLease) {
|
|
2067
|
+
const parts = tokenParts(args.lease);
|
|
2068
|
+
if (!parts)
|
|
2069
|
+
return { released: false, closed: false };
|
|
2070
|
+
targetId = parts.targetId;
|
|
2071
|
+
released = (await releaseLease(args.lease)).released;
|
|
2072
|
+
} else {
|
|
2073
|
+
const page = await resolvePage(driver, args.target);
|
|
2074
|
+
targetId = page.id;
|
|
2075
|
+
released = (await releaseLeaseFor(backend, targetId)).released;
|
|
2076
|
+
}
|
|
2077
|
+
if (!released)
|
|
2078
|
+
return { released: false, closed: false, ...hasTarget ? { targetId } : {} };
|
|
2079
|
+
const shouldClose = args.close === false ? false : args.close === true ? true : await readOrigin(backend, targetId) !== undefined;
|
|
2080
|
+
if (!shouldClose)
|
|
2081
|
+
return { released: true, closed: false, targetId };
|
|
2082
|
+
const res = await driver.closePage(targetId).catch(() => ({ success: false }));
|
|
2083
|
+
return { released: true, closed: res.success === true, targetId };
|
|
2084
|
+
}
|
|
2085
|
+
async function listLeasesTool(driver, _args = {}) {
|
|
2086
|
+
const backend = backendOf2(driver);
|
|
2087
|
+
const reaped = requireLease() ? await reapStaleAgentTabs(driver, backend) : [];
|
|
2088
|
+
const reapedIds = new Set(reaped.map((r) => r.targetId));
|
|
2089
|
+
const liveIds = (await driver.listPages()).map((p) => p.id);
|
|
2090
|
+
const all = await listLeases({ liveIds, liveBackend: backend });
|
|
2091
|
+
const leases = all.filter((l) => !(l.backend === backend && reapedIds.has(l.targetId)));
|
|
2092
|
+
return { leases, count: leases.length, ...reaped.length ? { reaped } : {} };
|
|
2093
|
+
}
|
|
2094
|
+
var LEASE_TOOLS = {
|
|
2095
|
+
claim_page: claimPage,
|
|
2096
|
+
release_page: releasePage,
|
|
2097
|
+
list_leases: listLeasesTool
|
|
2098
|
+
};
|
|
2099
|
+
|
|
1975
2100
|
// src/tools/console.ts
|
|
1976
2101
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
1977
2102
|
|
|
@@ -2579,11 +2704,375 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
2579
2704
|
return { ...analyzeEvents(events), tracePath: args.tracePath };
|
|
2580
2705
|
}
|
|
2581
2706
|
|
|
2582
|
-
// src/tools/
|
|
2583
|
-
import {
|
|
2707
|
+
// src/tools/screencast.ts
|
|
2708
|
+
import { spawn } from "node:child_process";
|
|
2709
|
+
import { mkdir as mkdir6, rm, stat as stat2, writeFile as writeFile6 } from "node:fs/promises";
|
|
2584
2710
|
import { join as join5 } from "node:path";
|
|
2585
2711
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2586
|
-
|
|
2712
|
+
var FFMPEG_BIN = process.env.CDP_FFMPEG ?? "ffmpeg";
|
|
2713
|
+
function frameDurationsMs(ledger, stopWallclockMs) {
|
|
2714
|
+
return ledger.map((entry, i) => {
|
|
2715
|
+
const next = i + 1 < ledger.length ? ledger[i + 1].timestamp : stopWallclockMs;
|
|
2716
|
+
return Math.max(1, Math.round(next - entry.timestamp));
|
|
2717
|
+
});
|
|
2718
|
+
}
|
|
2719
|
+
var CONCAT_GRID_MS = 40;
|
|
2720
|
+
function coalesceToConcatGrid(ledger, stopWallclockMs, gridMs = CONCAT_GRID_MS) {
|
|
2721
|
+
if (!ledger.length)
|
|
2722
|
+
return [];
|
|
2723
|
+
const raw = frameDurationsMs(ledger, stopWallclockMs);
|
|
2724
|
+
let elapsed = 0;
|
|
2725
|
+
const kept = [];
|
|
2726
|
+
let lastSlot = -1;
|
|
2727
|
+
for (let i = 0;i < ledger.length; i++) {
|
|
2728
|
+
const slot = Math.round(elapsed / gridMs);
|
|
2729
|
+
if (slot > lastSlot) {
|
|
2730
|
+
kept.push({ file: ledger[i].file, slot });
|
|
2731
|
+
lastSlot = slot;
|
|
2732
|
+
}
|
|
2733
|
+
elapsed += raw[i];
|
|
2734
|
+
}
|
|
2735
|
+
const endSlot = Math.max(lastSlot + 1, Math.round(elapsed / gridMs));
|
|
2736
|
+
return kept.map((frame, i) => ({
|
|
2737
|
+
file: frame.file,
|
|
2738
|
+
durationMs: ((i + 1 < kept.length ? kept[i + 1].slot : endSlot) - frame.slot) * gridMs
|
|
2739
|
+
}));
|
|
2740
|
+
}
|
|
2741
|
+
function buildConcatManifest(frames) {
|
|
2742
|
+
if (!frames.length)
|
|
2743
|
+
throw new CdpError("cannot build a concat manifest from an empty frame ledger");
|
|
2744
|
+
const lines = ["ffconcat version 1.0"];
|
|
2745
|
+
for (const frame of frames) {
|
|
2746
|
+
lines.push(`file ${frame.file}`);
|
|
2747
|
+
lines.push(`duration ${(frame.durationMs / 1000).toFixed(6)}`);
|
|
2748
|
+
}
|
|
2749
|
+
lines.push(`file ${frames[frames.length - 1].file}`);
|
|
2750
|
+
return `${lines.join(`
|
|
2751
|
+
`)}
|
|
2752
|
+
`;
|
|
2753
|
+
}
|
|
2754
|
+
function evenDimensions(width, height) {
|
|
2755
|
+
return { width: Math.max(0, Math.trunc(width / 2) * 2), height: Math.max(0, Math.trunc(height / 2) * 2) };
|
|
2756
|
+
}
|
|
2757
|
+
function pngSize(buf) {
|
|
2758
|
+
if (buf.length < 24)
|
|
2759
|
+
return;
|
|
2760
|
+
if (buf.readUInt32BE(0) !== 2303741511 || buf.readUInt32BE(4) !== 218765834)
|
|
2761
|
+
return;
|
|
2762
|
+
if (buf.toString("latin1", 12, 16) !== "IHDR")
|
|
2763
|
+
return;
|
|
2764
|
+
return { width: buf.readUInt32BE(16), height: buf.readUInt32BE(20) };
|
|
2765
|
+
}
|
|
2766
|
+
var JPEG_SOF = new Set([192, 193, 194, 195, 197, 198, 199, 201, 202, 203, 205, 206, 207]);
|
|
2767
|
+
function jpegSize(buf) {
|
|
2768
|
+
if (buf.length < 4 || buf[0] !== 255 || buf[1] !== 216)
|
|
2769
|
+
return;
|
|
2770
|
+
let i = 2;
|
|
2771
|
+
while (i + 3 < buf.length) {
|
|
2772
|
+
if (buf[i] !== 255) {
|
|
2773
|
+
i += 1;
|
|
2774
|
+
continue;
|
|
2775
|
+
}
|
|
2776
|
+
const marker = buf[i + 1];
|
|
2777
|
+
if (marker === 255) {
|
|
2778
|
+
i += 1;
|
|
2779
|
+
continue;
|
|
2780
|
+
}
|
|
2781
|
+
if (marker === 216 || marker === 1 || marker >= 208 && marker <= 215) {
|
|
2782
|
+
i += 2;
|
|
2783
|
+
continue;
|
|
2784
|
+
}
|
|
2785
|
+
if (i + 3 >= buf.length)
|
|
2786
|
+
return;
|
|
2787
|
+
const segLen = buf.readUInt16BE(i + 2);
|
|
2788
|
+
if (JPEG_SOF.has(marker)) {
|
|
2789
|
+
if (i + 9 > buf.length)
|
|
2790
|
+
return;
|
|
2791
|
+
return { width: buf.readUInt16BE(i + 7), height: buf.readUInt16BE(i + 5) };
|
|
2792
|
+
}
|
|
2793
|
+
if (segLen < 2)
|
|
2794
|
+
return;
|
|
2795
|
+
i += 2 + segLen;
|
|
2796
|
+
}
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
function imageSize(buf) {
|
|
2800
|
+
return pngSize(buf) ?? jpegSize(buf);
|
|
2801
|
+
}
|
|
2802
|
+
var ENCODER_LADDER = [
|
|
2803
|
+
{ encoder: "hevc_videotoolbox", codec: "hevc", tag: "hvc1" },
|
|
2804
|
+
{ encoder: "h264_videotoolbox", codec: "h264" },
|
|
2805
|
+
{ encoder: "libx265", codec: "hevc", tag: "hvc1" },
|
|
2806
|
+
{ encoder: "libx264", codec: "h264" }
|
|
2807
|
+
];
|
|
2808
|
+
function ffmpegEncoderNames(encodersOutput) {
|
|
2809
|
+
const names = new Set;
|
|
2810
|
+
for (const line of encodersOutput.split(`
|
|
2811
|
+
`)) {
|
|
2812
|
+
const m = /^\s*[VAS][A-Z.]{5}\s+(\S+)/.exec(line);
|
|
2813
|
+
if (m)
|
|
2814
|
+
names.add(m[1]);
|
|
2815
|
+
}
|
|
2816
|
+
return names;
|
|
2817
|
+
}
|
|
2818
|
+
function selectEncoder(encodersOutput) {
|
|
2819
|
+
const available = ffmpegEncoderNames(encodersOutput);
|
|
2820
|
+
for (const candidate of ENCODER_LADDER) {
|
|
2821
|
+
if (available.has(candidate.encoder))
|
|
2822
|
+
return candidate;
|
|
2823
|
+
}
|
|
2824
|
+
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).");
|
|
2825
|
+
}
|
|
2826
|
+
function buildFfmpegArgs(manifestPath, outPath, choice) {
|
|
2827
|
+
return [
|
|
2828
|
+
"-hide_banner",
|
|
2829
|
+
"-loglevel",
|
|
2830
|
+
"error",
|
|
2831
|
+
"-y",
|
|
2832
|
+
"-f",
|
|
2833
|
+
"concat",
|
|
2834
|
+
"-safe",
|
|
2835
|
+
"0",
|
|
2836
|
+
"-i",
|
|
2837
|
+
manifestPath,
|
|
2838
|
+
"-vsync",
|
|
2839
|
+
"vfr",
|
|
2840
|
+
"-vf",
|
|
2841
|
+
"scale=trunc(iw/2)*2:trunc(ih/2)*2",
|
|
2842
|
+
"-c:v",
|
|
2843
|
+
choice.encoder,
|
|
2844
|
+
...choice.tag ? ["-tag:v", choice.tag] : [],
|
|
2845
|
+
"-pix_fmt",
|
|
2846
|
+
"yuv420p",
|
|
2847
|
+
"-movflags",
|
|
2848
|
+
"+faststart",
|
|
2849
|
+
"-an",
|
|
2850
|
+
outPath
|
|
2851
|
+
];
|
|
2852
|
+
}
|
|
2853
|
+
function run(bin, args) {
|
|
2854
|
+
return new Promise((resolve, reject) => {
|
|
2855
|
+
const child = spawn(bin, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2856
|
+
let stdout = "";
|
|
2857
|
+
let stderr = "";
|
|
2858
|
+
child.stdout?.on("data", (chunk) => {
|
|
2859
|
+
stdout += chunk.toString("utf8");
|
|
2860
|
+
});
|
|
2861
|
+
child.stderr?.on("data", (chunk) => {
|
|
2862
|
+
stderr += chunk.toString("utf8");
|
|
2863
|
+
});
|
|
2864
|
+
child.on("error", reject);
|
|
2865
|
+
child.on("close", (code) => resolve({ code, stdout, stderr }));
|
|
2866
|
+
});
|
|
2867
|
+
}
|
|
2868
|
+
var encoderProbe;
|
|
2869
|
+
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.";
|
|
2870
|
+
async function probeEncoder() {
|
|
2871
|
+
if (!encoderProbe) {
|
|
2872
|
+
encoderProbe = (async () => {
|
|
2873
|
+
let res;
|
|
2874
|
+
try {
|
|
2875
|
+
res = await run(FFMPEG_BIN, ["-hide_banner", "-encoders"]);
|
|
2876
|
+
} catch (err) {
|
|
2877
|
+
throw new CdpError(`${FFMPEG_MISSING} (${err.message})`);
|
|
2878
|
+
}
|
|
2879
|
+
if (res.code !== 0)
|
|
2880
|
+
throw new CdpError(`${FFMPEG_MISSING} ('-encoders' exited ${res.code}: ${res.stderr.trim().slice(0, 300)})`);
|
|
2881
|
+
return selectEncoder(`${res.stdout}
|
|
2882
|
+
${res.stderr}`);
|
|
2883
|
+
})();
|
|
2884
|
+
encoderProbe.catch(() => {
|
|
2885
|
+
encoderProbe = undefined;
|
|
2886
|
+
});
|
|
2887
|
+
}
|
|
2888
|
+
return encoderProbe;
|
|
2889
|
+
}
|
|
2890
|
+
var liveRecordings = new Map;
|
|
2891
|
+
function shortId2(id) {
|
|
2892
|
+
return id.slice(0, 8);
|
|
2893
|
+
}
|
|
2894
|
+
function assertNotRecording(targetId, activeIds) {
|
|
2895
|
+
if (activeIds.includes(targetId)) {
|
|
2896
|
+
throw new CdpError(`a screen recording is already in progress for target ${shortId2(targetId)}; call stop_screen_recording first`);
|
|
2897
|
+
}
|
|
2898
|
+
}
|
|
2899
|
+
function chooseRecordingId(activeIds, selector) {
|
|
2900
|
+
if (!activeIds.length) {
|
|
2901
|
+
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).");
|
|
2902
|
+
}
|
|
2903
|
+
if (selector === undefined || selector === "active") {
|
|
2904
|
+
if (activeIds.length === 1)
|
|
2905
|
+
return activeIds[0];
|
|
2906
|
+
throw new CdpError(`${activeIds.length} screen recordings are in progress (targets: ${activeIds.map(shortId2).join(", ")}); pass 'target' to say which one to stop`);
|
|
2907
|
+
}
|
|
2908
|
+
return activeIds.find((id) => id === selector);
|
|
2909
|
+
}
|
|
2910
|
+
function stamp3() {
|
|
2911
|
+
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
2912
|
+
}
|
|
2913
|
+
function target32(t) {
|
|
2914
|
+
return { id: t.id, url: t.url, title: t.title };
|
|
2915
|
+
}
|
|
2916
|
+
async function startScreenRecording(args = {}) {
|
|
2917
|
+
const format = args.format ?? "jpeg";
|
|
2918
|
+
if (format !== "jpeg" && format !== "png")
|
|
2919
|
+
throw new CdpError("start_screen_recording: format must be 'jpeg' or 'png'");
|
|
2920
|
+
if (args.quality !== undefined && (!Number.isFinite(args.quality) || args.quality < 0 || args.quality > 100)) {
|
|
2921
|
+
throw new CdpError("start_screen_recording: quality must be a number between 0 and 100 (jpeg only)");
|
|
2922
|
+
}
|
|
2923
|
+
const encoder = await probeEncoder();
|
|
2924
|
+
const { conn, target } = await openPage(args.target);
|
|
2925
|
+
try {
|
|
2926
|
+
assertNotRecording(target.id, [...liveRecordings.keys()]);
|
|
2927
|
+
} catch (err) {
|
|
2928
|
+
conn.close();
|
|
2929
|
+
throw err;
|
|
2930
|
+
}
|
|
2931
|
+
const spoolDir = join5(ARTIFACT_DIR4, `screencast-${shortId2(target.id)}-${stamp3()}`);
|
|
2932
|
+
try {
|
|
2933
|
+
await mkdir6(spoolDir, { recursive: true });
|
|
2934
|
+
} catch (err) {
|
|
2935
|
+
conn.close();
|
|
2936
|
+
throw new CdpError(`cannot create the screencast spool directory '${spoolDir}': ${err.message}`);
|
|
2937
|
+
}
|
|
2938
|
+
const ext = format === "png" ? "png" : "jpg";
|
|
2939
|
+
const rec = {
|
|
2940
|
+
conn,
|
|
2941
|
+
target,
|
|
2942
|
+
spoolDir,
|
|
2943
|
+
ledger: [],
|
|
2944
|
+
startedAt: Date.now(),
|
|
2945
|
+
format,
|
|
2946
|
+
encoder,
|
|
2947
|
+
writes: Promise.resolve(),
|
|
2948
|
+
dropped: 0,
|
|
2949
|
+
unwritten: new Set,
|
|
2950
|
+
off: () => {}
|
|
2951
|
+
};
|
|
2952
|
+
let seq = 0;
|
|
2953
|
+
rec.off = conn.on("Page.screencastFrame", (params) => {
|
|
2954
|
+
const sessionId = params.sessionId;
|
|
2955
|
+
if (typeof sessionId === "number") {
|
|
2956
|
+
conn.send("Page.screencastFrameAck", { sessionId }).catch(() => {});
|
|
2957
|
+
}
|
|
2958
|
+
const data = params.data;
|
|
2959
|
+
if (typeof data !== "string" || !data.length) {
|
|
2960
|
+
rec.dropped += 1;
|
|
2961
|
+
return;
|
|
2962
|
+
}
|
|
2963
|
+
const meta = params.metadata;
|
|
2964
|
+
const timestamp = typeof meta?.timestamp === "number" ? Math.round(meta.timestamp * 1000) : Date.now();
|
|
2965
|
+
const buf = Buffer.from(data, "base64");
|
|
2966
|
+
const size = imageSize(buf);
|
|
2967
|
+
if (!size || size.width <= 0 || size.height <= 0) {
|
|
2968
|
+
rec.dropped += 1;
|
|
2969
|
+
return;
|
|
2970
|
+
}
|
|
2971
|
+
seq += 1;
|
|
2972
|
+
const file = `frame-${String(seq).padStart(6, "0")}.${ext}`;
|
|
2973
|
+
rec.ledger.push({ file, timestamp, width: size.width, height: size.height });
|
|
2974
|
+
rec.writes = rec.writes.then(() => writeFile6(join5(spoolDir, file), buf)).catch(() => {
|
|
2975
|
+
rec.dropped += 1;
|
|
2976
|
+
rec.unwritten.add(file);
|
|
2977
|
+
});
|
|
2978
|
+
});
|
|
2979
|
+
try {
|
|
2980
|
+
await conn.send("Page.enable");
|
|
2981
|
+
if (args.bringToFront)
|
|
2982
|
+
await conn.send("Page.bringToFront").catch(() => {});
|
|
2983
|
+
const params = { format };
|
|
2984
|
+
if (format === "jpeg" && args.quality !== undefined)
|
|
2985
|
+
params.quality = args.quality;
|
|
2986
|
+
if (args.maxWidth !== undefined)
|
|
2987
|
+
params.maxWidth = args.maxWidth;
|
|
2988
|
+
if (args.maxHeight !== undefined)
|
|
2989
|
+
params.maxHeight = args.maxHeight;
|
|
2990
|
+
if (args.everyNthFrame !== undefined)
|
|
2991
|
+
params.everyNthFrame = args.everyNthFrame;
|
|
2992
|
+
await conn.send("Page.startScreencast", params);
|
|
2993
|
+
} catch (err) {
|
|
2994
|
+
rec.off();
|
|
2995
|
+
conn.close();
|
|
2996
|
+
await rm(spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
2997
|
+
throw err;
|
|
2998
|
+
}
|
|
2999
|
+
liveRecordings.set(target.id, rec);
|
|
3000
|
+
return {
|
|
3001
|
+
target: target32(target),
|
|
3002
|
+
format,
|
|
3003
|
+
spoolDir,
|
|
3004
|
+
encoder: encoder.encoder,
|
|
3005
|
+
codec: encoder.codec,
|
|
3006
|
+
startedAt: rec.startedAt,
|
|
3007
|
+
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."
|
|
3008
|
+
};
|
|
3009
|
+
}
|
|
3010
|
+
async function pickRecording(selector) {
|
|
3011
|
+
const chosen = chooseRecordingId([...liveRecordings.keys()], selector);
|
|
3012
|
+
if (chosen)
|
|
3013
|
+
return liveRecordings.get(chosen);
|
|
3014
|
+
const { conn, target } = await openPage(selector);
|
|
3015
|
+
conn.close();
|
|
3016
|
+
const hit = liveRecordings.get(target.id);
|
|
3017
|
+
if (!hit) {
|
|
3018
|
+
throw new CdpError(`no screen recording is in progress for target ${shortId2(target.id)} (recording: ${[...liveRecordings.keys()].map(shortId2).join(", ") || "none"})`);
|
|
3019
|
+
}
|
|
3020
|
+
return hit;
|
|
3021
|
+
}
|
|
3022
|
+
async function stopScreenRecording(args = {}) {
|
|
3023
|
+
const rec = await pickRecording(args.target);
|
|
3024
|
+
rec.off();
|
|
3025
|
+
try {
|
|
3026
|
+
await rec.conn.send("Page.stopScreencast");
|
|
3027
|
+
} catch {}
|
|
3028
|
+
const stoppedAt = Date.now();
|
|
3029
|
+
await rec.writes.catch(() => {});
|
|
3030
|
+
rec.conn.close();
|
|
3031
|
+
liveRecordings.delete(rec.target.id);
|
|
3032
|
+
const ledger = rec.unwritten.size ? rec.ledger.filter((entry) => !rec.unwritten.has(entry.file)) : rec.ledger;
|
|
3033
|
+
if (!ledger.length) {
|
|
3034
|
+
await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
3035
|
+
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.`);
|
|
3036
|
+
}
|
|
3037
|
+
await mkdir6(ARTIFACT_DIR4, { recursive: true });
|
|
3038
|
+
const outPath = args.savePath ?? join5(ARTIFACT_DIR4, `screen-recording-${shortId2(rec.target.id)}-${stamp3()}.mp4`);
|
|
3039
|
+
const frames = coalesceToConcatGrid(ledger, stoppedAt);
|
|
3040
|
+
const manifestPath = join5(rec.spoolDir, "frames.ffconcat");
|
|
3041
|
+
await writeFile6(manifestPath, buildConcatManifest(frames), "utf8");
|
|
3042
|
+
const durationMs = frames.reduce((total, f) => total + f.durationMs, 0);
|
|
3043
|
+
const { width, height } = evenDimensions(ledger[0].width, ledger[0].height);
|
|
3044
|
+
let res;
|
|
3045
|
+
try {
|
|
3046
|
+
res = await run(FFMPEG_BIN, buildFfmpegArgs(manifestPath, outPath, rec.encoder));
|
|
3047
|
+
} catch (err) {
|
|
3048
|
+
throw new CdpError(`${FFMPEG_MISSING} Spooled frames kept at ${rec.spoolDir}. (${err.message})`);
|
|
3049
|
+
}
|
|
3050
|
+
if (res.code !== 0) {
|
|
3051
|
+
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(" ")}
|
|
3052
|
+
ffmpeg said: ${res.stderr.trim().slice(0, 800)}`);
|
|
3053
|
+
}
|
|
3054
|
+
const { size } = await stat2(outPath);
|
|
3055
|
+
await rm(rec.spoolDir, { recursive: true, force: true }).catch(() => {});
|
|
3056
|
+
return {
|
|
3057
|
+
path: outPath,
|
|
3058
|
+
bytes: size,
|
|
3059
|
+
durationMs,
|
|
3060
|
+
frameCount: ledger.length,
|
|
3061
|
+
encodedFrames: frames.length,
|
|
3062
|
+
codec: rec.encoder.codec,
|
|
3063
|
+
encoder: rec.encoder.encoder,
|
|
3064
|
+
width,
|
|
3065
|
+
height,
|
|
3066
|
+
droppedFrames: rec.dropped,
|
|
3067
|
+
target: target32(rec.target)
|
|
3068
|
+
};
|
|
3069
|
+
}
|
|
3070
|
+
|
|
3071
|
+
// src/tools/heap.ts
|
|
3072
|
+
import { mkdir as mkdir7, writeFile as writeFile7 } from "node:fs/promises";
|
|
3073
|
+
import { join as join6 } from "node:path";
|
|
3074
|
+
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3075
|
+
function stamp4(targetId) {
|
|
2587
3076
|
const short = targetId.slice(0, 8);
|
|
2588
3077
|
const iso = new Date().toISOString().replace(/[:.]/g, "-");
|
|
2589
3078
|
return `${short}-${iso}`;
|
|
@@ -2606,9 +3095,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2606
3095
|
if (snapshot.length === 0) {
|
|
2607
3096
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
2608
3097
|
}
|
|
2609
|
-
await
|
|
2610
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
2611
|
-
await
|
|
3098
|
+
await mkdir7(ARTIFACT_DIR5, { recursive: true });
|
|
3099
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join6(ARTIFACT_DIR5, args.savePath) : join6(ARTIFACT_DIR5, `take_heapsnapshot-${stamp4(target.id)}.heapsnapshot`);
|
|
3100
|
+
await writeFile7(path, snapshot, "utf8");
|
|
2612
3101
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
2613
3102
|
return {
|
|
2614
3103
|
path,
|
|
@@ -2620,15 +3109,15 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2620
3109
|
}
|
|
2621
3110
|
|
|
2622
3111
|
// src/tools/lighthouse.ts
|
|
2623
|
-
import { spawn } from "node:child_process";
|
|
2624
|
-
import { mkdir as
|
|
2625
|
-
var
|
|
3112
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
3113
|
+
import { mkdir as mkdir8, readFile as readFile6, stat as stat3 } from "node:fs/promises";
|
|
3114
|
+
var ARTIFACT_DIR6 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2626
3115
|
function cdpPort() {
|
|
2627
3116
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
2628
3117
|
const m = base.match(/:(\d+)/);
|
|
2629
3118
|
return m?.[1] ? Number(m[1]) : 9222;
|
|
2630
3119
|
}
|
|
2631
|
-
function
|
|
3120
|
+
function stamp5() {
|
|
2632
3121
|
return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
|
|
2633
3122
|
}
|
|
2634
3123
|
function shortHost(url) {
|
|
@@ -2645,7 +3134,7 @@ function runProcess(cmd, args, opts) {
|
|
|
2645
3134
|
let stdout = "";
|
|
2646
3135
|
let stderr = "";
|
|
2647
3136
|
let settled = false;
|
|
2648
|
-
const child =
|
|
3137
|
+
const child = spawn2(cmd, args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
2649
3138
|
const timer = setTimeout(() => {
|
|
2650
3139
|
if (settled)
|
|
2651
3140
|
return;
|
|
@@ -2682,8 +3171,8 @@ async function lighthouseAudit(args) {
|
|
|
2682
3171
|
const url = args.url.trim();
|
|
2683
3172
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
2684
3173
|
const port = cdpPort();
|
|
2685
|
-
await
|
|
2686
|
-
const outPath = args.savePath ?? `${
|
|
3174
|
+
await mkdir8(ARTIFACT_DIR6, { recursive: true });
|
|
3175
|
+
const outPath = args.savePath ?? `${ARTIFACT_DIR6}/lighthouse-${shortHost(url)}-${stamp5()}.json`;
|
|
2687
3176
|
const formFactor = args.formFactor ?? "desktop";
|
|
2688
3177
|
const lhArgs = [
|
|
2689
3178
|
"--yes",
|
|
@@ -2738,7 +3227,7 @@ async function lighthouseAudit(args) {
|
|
|
2738
3227
|
if (Object.keys(categories).length === 0) {
|
|
2739
3228
|
throw new CdpError(`lighthouse_audit report contained no category scores (exit code ${res.code}). stderr: ${truncate(res.stderr)}`);
|
|
2740
3229
|
}
|
|
2741
|
-
const { size } = await
|
|
3230
|
+
const { size } = await stat3(outPath);
|
|
2742
3231
|
return {
|
|
2743
3232
|
path: outPath,
|
|
2744
3233
|
bytes: size,
|
|
@@ -2792,6 +3281,8 @@ var TOOLS = {
|
|
|
2792
3281
|
performance_stop_trace: performanceStopTrace,
|
|
2793
3282
|
performance_analyze_insight: performanceAnalyzeInsight,
|
|
2794
3283
|
performance_trace: performanceTrace,
|
|
3284
|
+
start_screen_recording: startScreenRecording,
|
|
3285
|
+
stop_screen_recording: stopScreenRecording,
|
|
2795
3286
|
take_heapsnapshot: takeHeapsnapshot,
|
|
2796
3287
|
lighthouse_audit: lighthouseAudit,
|
|
2797
3288
|
mock_request: mockRequest,
|
|
@@ -2812,13 +3303,13 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
2812
3303
|
var MANIFEST = [
|
|
2813
3304
|
{
|
|
2814
3305
|
name: "list_pages",
|
|
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.",
|
|
3306
|
+
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. Under CDP_REQUIRE_LEASE this call also reaps: a tab this toolkit created whose lease is stale because the owning process died or the TTL elapsed is closed, and the closed tabs are reported in an additive 'reaped' array ({targetId,label,reason}) that is present only when something was actually closed. A tab with no lease is never reaped, so tabs opened by a caller that never uses leases are untouched.",
|
|
2816
3307
|
inputSchema: {
|
|
2817
3308
|
type: "object",
|
|
2818
3309
|
properties: {
|
|
2819
3310
|
lease: {
|
|
2820
3311
|
type: "string",
|
|
2821
|
-
description: "Opaque lease token from claim_page.
|
|
3312
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
2822
3313
|
},
|
|
2823
3314
|
all: {
|
|
2824
3315
|
type: "boolean",
|
|
@@ -2837,7 +3328,7 @@ var MANIFEST = [
|
|
|
2837
3328
|
properties: {
|
|
2838
3329
|
lease: {
|
|
2839
3330
|
type: "string",
|
|
2840
|
-
description: "Opaque lease token from claim_page.
|
|
3331
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
2841
3332
|
},
|
|
2842
3333
|
url: {
|
|
2843
3334
|
type: "string",
|
|
@@ -2845,7 +3336,7 @@ var MANIFEST = [
|
|
|
2845
3336
|
},
|
|
2846
3337
|
claim: {
|
|
2847
3338
|
type: "boolean",
|
|
2848
|
-
description: "Claim the new tab atomically as part of creating it and return a lease token alongside {targetId,url}. Default false, which is byte-identical to the pre-1.2 behavior."
|
|
3339
|
+
description: "Claim the new tab atomically as part of creating it and return a lease token alongside {targetId,url}. Default false, which is byte-identical to the pre-1.2 behavior. Under CDP_REQUIRE_LEASE the new tab is claimed and a lease returned even without claim:true, because a tab nobody holds is a tab nobody may drive; passing claim:true additionally makes the lease an explicit one, which requires its token on every later call even from this same process."
|
|
2849
3340
|
},
|
|
2850
3341
|
label: {
|
|
2851
3342
|
type: "string",
|
|
@@ -2868,7 +3359,7 @@ var MANIFEST = [
|
|
|
2868
3359
|
properties: {
|
|
2869
3360
|
lease: {
|
|
2870
3361
|
type: "string",
|
|
2871
|
-
description: "Opaque lease token from claim_page.
|
|
3362
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
2872
3363
|
},
|
|
2873
3364
|
target: {
|
|
2874
3365
|
type: "string",
|
|
@@ -2889,7 +3380,7 @@ var MANIFEST = [
|
|
|
2889
3380
|
properties: {
|
|
2890
3381
|
lease: {
|
|
2891
3382
|
type: "string",
|
|
2892
|
-
description: "Opaque lease token from claim_page.
|
|
3383
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
2893
3384
|
},
|
|
2894
3385
|
target: {
|
|
2895
3386
|
type: "string",
|
|
@@ -2910,7 +3401,7 @@ var MANIFEST = [
|
|
|
2910
3401
|
properties: {
|
|
2911
3402
|
lease: {
|
|
2912
3403
|
type: "string",
|
|
2913
|
-
description: "Opaque lease token from claim_page.
|
|
3404
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
2914
3405
|
},
|
|
2915
3406
|
target: {
|
|
2916
3407
|
type: "string",
|
|
@@ -2952,7 +3443,7 @@ var MANIFEST = [
|
|
|
2952
3443
|
properties: {
|
|
2953
3444
|
lease: {
|
|
2954
3445
|
type: "string",
|
|
2955
|
-
description: "Opaque lease token from claim_page.
|
|
3446
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
2956
3447
|
},
|
|
2957
3448
|
target: {
|
|
2958
3449
|
type: "string",
|
|
@@ -2985,7 +3476,7 @@ var MANIFEST = [
|
|
|
2985
3476
|
properties: {
|
|
2986
3477
|
lease: {
|
|
2987
3478
|
type: "string",
|
|
2988
|
-
description: "Opaque lease token from claim_page.
|
|
3479
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
2989
3480
|
},
|
|
2990
3481
|
target: {
|
|
2991
3482
|
type: "string",
|
|
@@ -3022,7 +3513,7 @@ var MANIFEST = [
|
|
|
3022
3513
|
properties: {
|
|
3023
3514
|
lease: {
|
|
3024
3515
|
type: "string",
|
|
3025
|
-
description: "Opaque lease token from claim_page.
|
|
3516
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3026
3517
|
},
|
|
3027
3518
|
target: {
|
|
3028
3519
|
type: "string",
|
|
@@ -3053,7 +3544,7 @@ var MANIFEST = [
|
|
|
3053
3544
|
properties: {
|
|
3054
3545
|
lease: {
|
|
3055
3546
|
type: "string",
|
|
3056
|
-
description: "Opaque lease token from claim_page.
|
|
3547
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3057
3548
|
},
|
|
3058
3549
|
target: {
|
|
3059
3550
|
type: "string",
|
|
@@ -3109,7 +3600,7 @@ var MANIFEST = [
|
|
|
3109
3600
|
properties: {
|
|
3110
3601
|
lease: {
|
|
3111
3602
|
type: "string",
|
|
3112
|
-
description: "Opaque lease token from claim_page.
|
|
3603
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3113
3604
|
},
|
|
3114
3605
|
target: {
|
|
3115
3606
|
type: "string",
|
|
@@ -3144,7 +3635,7 @@ var MANIFEST = [
|
|
|
3144
3635
|
properties: {
|
|
3145
3636
|
lease: {
|
|
3146
3637
|
type: "string",
|
|
3147
|
-
description: "Opaque lease token from claim_page.
|
|
3638
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3148
3639
|
},
|
|
3149
3640
|
target: {
|
|
3150
3641
|
type: "string",
|
|
@@ -3167,7 +3658,7 @@ var MANIFEST = [
|
|
|
3167
3658
|
properties: {
|
|
3168
3659
|
lease: {
|
|
3169
3660
|
type: "string",
|
|
3170
|
-
description: "Opaque lease token from claim_page.
|
|
3661
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3171
3662
|
},
|
|
3172
3663
|
target: {
|
|
3173
3664
|
type: "string",
|
|
@@ -3207,7 +3698,7 @@ var MANIFEST = [
|
|
|
3207
3698
|
properties: {
|
|
3208
3699
|
lease: {
|
|
3209
3700
|
type: "string",
|
|
3210
|
-
description: "Opaque lease token from claim_page.
|
|
3701
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3211
3702
|
},
|
|
3212
3703
|
target: {
|
|
3213
3704
|
type: "string",
|
|
@@ -3234,7 +3725,7 @@ var MANIFEST = [
|
|
|
3234
3725
|
properties: {
|
|
3235
3726
|
lease: {
|
|
3236
3727
|
type: "string",
|
|
3237
|
-
description: "Opaque lease token from claim_page.
|
|
3728
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3238
3729
|
},
|
|
3239
3730
|
target: {
|
|
3240
3731
|
type: "string",
|
|
@@ -3286,7 +3777,7 @@ var MANIFEST = [
|
|
|
3286
3777
|
properties: {
|
|
3287
3778
|
lease: {
|
|
3288
3779
|
type: "string",
|
|
3289
|
-
description: "Opaque lease token from claim_page.
|
|
3780
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3290
3781
|
},
|
|
3291
3782
|
target: {
|
|
3292
3783
|
type: "string",
|
|
@@ -3319,7 +3810,7 @@ var MANIFEST = [
|
|
|
3319
3810
|
properties: {
|
|
3320
3811
|
lease: {
|
|
3321
3812
|
type: "string",
|
|
3322
|
-
description: "Opaque lease token from claim_page.
|
|
3813
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3323
3814
|
},
|
|
3324
3815
|
target: {
|
|
3325
3816
|
type: "string",
|
|
@@ -3365,7 +3856,7 @@ var MANIFEST = [
|
|
|
3365
3856
|
properties: {
|
|
3366
3857
|
lease: {
|
|
3367
3858
|
type: "string",
|
|
3368
|
-
description: "Opaque lease token from claim_page.
|
|
3859
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3369
3860
|
},
|
|
3370
3861
|
target: {
|
|
3371
3862
|
type: "string",
|
|
@@ -3398,7 +3889,7 @@ var MANIFEST = [
|
|
|
3398
3889
|
properties: {
|
|
3399
3890
|
lease: {
|
|
3400
3891
|
type: "string",
|
|
3401
|
-
description: "Opaque lease token from claim_page.
|
|
3892
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3402
3893
|
},
|
|
3403
3894
|
target: {
|
|
3404
3895
|
type: "string",
|
|
@@ -3430,7 +3921,7 @@ var MANIFEST = [
|
|
|
3430
3921
|
properties: {
|
|
3431
3922
|
lease: {
|
|
3432
3923
|
type: "string",
|
|
3433
|
-
description: "Opaque lease token from claim_page.
|
|
3924
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3434
3925
|
},
|
|
3435
3926
|
target: {
|
|
3436
3927
|
type: "string",
|
|
@@ -3469,7 +3960,7 @@ var MANIFEST = [
|
|
|
3469
3960
|
properties: {
|
|
3470
3961
|
lease: {
|
|
3471
3962
|
type: "string",
|
|
3472
|
-
description: "Opaque lease token from claim_page.
|
|
3963
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3473
3964
|
},
|
|
3474
3965
|
target: {
|
|
3475
3966
|
type: "string",
|
|
@@ -3520,7 +4011,7 @@ var MANIFEST = [
|
|
|
3520
4011
|
properties: {
|
|
3521
4012
|
lease: {
|
|
3522
4013
|
type: "string",
|
|
3523
|
-
description: "Opaque lease token from claim_page.
|
|
4014
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3524
4015
|
},
|
|
3525
4016
|
target: {
|
|
3526
4017
|
type: "string",
|
|
@@ -3620,7 +4111,7 @@ var MANIFEST = [
|
|
|
3620
4111
|
properties: {
|
|
3621
4112
|
lease: {
|
|
3622
4113
|
type: "string",
|
|
3623
|
-
description: "Opaque lease token from claim_page.
|
|
4114
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3624
4115
|
},
|
|
3625
4116
|
target: {
|
|
3626
4117
|
type: "string",
|
|
@@ -3658,7 +4149,7 @@ var MANIFEST = [
|
|
|
3658
4149
|
properties: {
|
|
3659
4150
|
lease: {
|
|
3660
4151
|
type: "string",
|
|
3661
|
-
description: "Opaque lease token from claim_page.
|
|
4152
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3662
4153
|
},
|
|
3663
4154
|
target: {
|
|
3664
4155
|
type: "string",
|
|
@@ -3695,7 +4186,7 @@ var MANIFEST = [
|
|
|
3695
4186
|
properties: {
|
|
3696
4187
|
lease: {
|
|
3697
4188
|
type: "string",
|
|
3698
|
-
description: "Opaque lease token from claim_page.
|
|
4189
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3699
4190
|
},
|
|
3700
4191
|
target: {
|
|
3701
4192
|
type: "string",
|
|
@@ -3722,7 +4213,7 @@ var MANIFEST = [
|
|
|
3722
4213
|
properties: {
|
|
3723
4214
|
lease: {
|
|
3724
4215
|
type: "string",
|
|
3725
|
-
description: "Opaque lease token from claim_page.
|
|
4216
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3726
4217
|
},
|
|
3727
4218
|
target: {
|
|
3728
4219
|
type: "string",
|
|
@@ -3745,7 +4236,7 @@ var MANIFEST = [
|
|
|
3745
4236
|
properties: {
|
|
3746
4237
|
lease: {
|
|
3747
4238
|
type: "string",
|
|
3748
|
-
description: "Opaque lease token from claim_page.
|
|
4239
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3749
4240
|
},
|
|
3750
4241
|
target: {
|
|
3751
4242
|
type: "string",
|
|
@@ -3776,7 +4267,7 @@ var MANIFEST = [
|
|
|
3776
4267
|
properties: {
|
|
3777
4268
|
lease: {
|
|
3778
4269
|
type: "string",
|
|
3779
|
-
description: "Opaque lease token from claim_page.
|
|
4270
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3780
4271
|
},
|
|
3781
4272
|
target: {
|
|
3782
4273
|
type: "string",
|
|
@@ -3811,7 +4302,7 @@ var MANIFEST = [
|
|
|
3811
4302
|
properties: {
|
|
3812
4303
|
lease: {
|
|
3813
4304
|
type: "string",
|
|
3814
|
-
description: "Opaque lease token from claim_page.
|
|
4305
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3815
4306
|
},
|
|
3816
4307
|
target: {
|
|
3817
4308
|
type: "string",
|
|
@@ -3837,7 +4328,7 @@ var MANIFEST = [
|
|
|
3837
4328
|
properties: {
|
|
3838
4329
|
lease: {
|
|
3839
4330
|
type: "string",
|
|
3840
|
-
description: "Opaque lease token from claim_page.
|
|
4331
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3841
4332
|
},
|
|
3842
4333
|
target: {
|
|
3843
4334
|
type: "string",
|
|
@@ -3856,7 +4347,7 @@ var MANIFEST = [
|
|
|
3856
4347
|
properties: {
|
|
3857
4348
|
lease: {
|
|
3858
4349
|
type: "string",
|
|
3859
|
-
description: "Opaque lease token from claim_page.
|
|
4350
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3860
4351
|
},
|
|
3861
4352
|
tracePath: {
|
|
3862
4353
|
type: "string",
|
|
@@ -3875,7 +4366,7 @@ var MANIFEST = [
|
|
|
3875
4366
|
properties: {
|
|
3876
4367
|
lease: {
|
|
3877
4368
|
type: "string",
|
|
3878
|
-
description: "Opaque lease token from claim_page.
|
|
4369
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3879
4370
|
},
|
|
3880
4371
|
target: {
|
|
3881
4372
|
type: "string",
|
|
@@ -3905,6 +4396,76 @@ var MANIFEST = [
|
|
|
3905
4396
|
additionalProperties: false
|
|
3906
4397
|
}
|
|
3907
4398
|
},
|
|
4399
|
+
{
|
|
4400
|
+
name: "start_screen_recording",
|
|
4401
|
+
description: "Start recording the target tab to video over raw CDP (Page.startScreencast), holding one persistent connection open and spooling every Page.screencastFrame to disk. Chrome emits a frame ON REPAINT, not on a clock, so the stream is variable-rate: each frame is timestamped into a ledger and stop_screen_recording turns those timestamps into per-frame display durations, which is what makes a still page hold one long frame instead of the video racing or freezing. Every frame is acked immediately (an unacked frame stalls the stream). ffmpeg is probed here, not at stop, so a missing encoder fails before a recording is captured and thrown away. Must be paired with stop_screen_recording WITHIN THE SAME PROCESS (the frames are events on this connection and cannot be re-attached from another process, so the pair works under the MCP server, not the one-process-per-call CLI). Throws if a recording is already in progress for the target; recordings on DIFFERENT targets run concurrently. Chrome only: WebDriver BiDi has no screencast primitive, so both tools are absent under --browser firefox.",
|
|
4402
|
+
inputSchema: {
|
|
4403
|
+
type: "object",
|
|
4404
|
+
properties: {
|
|
4405
|
+
lease: {
|
|
4406
|
+
type: "string",
|
|
4407
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4408
|
+
},
|
|
4409
|
+
target: {
|
|
4410
|
+
type: "string",
|
|
4411
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4412
|
+
},
|
|
4413
|
+
format: {
|
|
4414
|
+
type: "string",
|
|
4415
|
+
enum: [
|
|
4416
|
+
"jpeg",
|
|
4417
|
+
"png"
|
|
4418
|
+
],
|
|
4419
|
+
description: "Frame image format. Defaults to jpeg (far smaller per repaint); png is lossless. quality applies to jpeg only."
|
|
4420
|
+
},
|
|
4421
|
+
quality: {
|
|
4422
|
+
type: "number",
|
|
4423
|
+
description: "JPEG frame quality 0-100. Ignored for png. Omitted by default, which leaves Chrome's own default in place."
|
|
4424
|
+
},
|
|
4425
|
+
maxWidth: {
|
|
4426
|
+
type: "number",
|
|
4427
|
+
description: "Cap the streamed frame width in pixels. Also the way to pin frame size if the viewport might change mid-recording."
|
|
4428
|
+
},
|
|
4429
|
+
maxHeight: {
|
|
4430
|
+
type: "number",
|
|
4431
|
+
description: "Cap the streamed frame height in pixels."
|
|
4432
|
+
},
|
|
4433
|
+
everyNthFrame: {
|
|
4434
|
+
type: "number",
|
|
4435
|
+
description: "Capture only every Nth repaint (1 = every frame). Reduces spool size on a busy page; per-frame durations keep playback at wallclock speed either way."
|
|
4436
|
+
},
|
|
4437
|
+
bringToFront: {
|
|
4438
|
+
type: "boolean",
|
|
4439
|
+
description: "Activate the tab (Page.bringToFront) before recording. Default false. A fully backgrounded or occluded tab may never repaint, and a recording of one captures 0 frames."
|
|
4440
|
+
}
|
|
4441
|
+
},
|
|
4442
|
+
required: [],
|
|
4443
|
+
additionalProperties: false
|
|
4444
|
+
}
|
|
4445
|
+
},
|
|
4446
|
+
{
|
|
4447
|
+
name: "stop_screen_recording",
|
|
4448
|
+
description: "Stop the recording started by start_screen_recording, assemble the spooled frames into an MP4 with ffmpeg using PER-FRAME durations from the ledger (frame N is held until frame N+1 painted; the last frame is held until this call), and return {path,bytes,durationMs,frameCount,encodedFrames,codec,encoder,width,height,droppedFrames,target}. Encoder ladder, probed once at start: hevc_videotoolbox then h264_videotoolbox then libx265 then libx264, with -tag:v hvc1 on HEVC so QuickTime plays it, +faststart, yuv420p, and even dimensions forced. With one recording in flight 'target' may be omitted; with several it is required, because guessing which to stop can lose another agent's recording. Throws if nothing is recording in this process, and throws rather than writing a silent empty video when 0 frames arrived. On an ffmpeg failure the spooled frames are KEPT and the spool path plus the exact command are named in the error. Encoded video tops out at 25 fps: ffmpeg's concat demuxer represents image timestamps on a 1/25s grid, so frames captured less than 40ms apart are coalesced deliberately (frameCount reports what was captured, encodedFrames what reached the video) instead of being dropped silently by ffmpeg. durationMs is the sum of the encoded durations; the file itself runs one 40ms step longer, because the concat demuxer needs the final frame repeated for its hold to count.",
|
|
4449
|
+
inputSchema: {
|
|
4450
|
+
type: "object",
|
|
4451
|
+
properties: {
|
|
4452
|
+
lease: {
|
|
4453
|
+
type: "string",
|
|
4454
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4455
|
+
},
|
|
4456
|
+
target: {
|
|
4457
|
+
type: "string",
|
|
4458
|
+
description: "Which recording to stop: 'active' (default, valid only when exactly one recording is in flight) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4459
|
+
},
|
|
4460
|
+
savePath: {
|
|
4461
|
+
type: "string",
|
|
4462
|
+
description: "Override the output file path. Default: /tmp/cdp-toolkit/screen-recording-<targetIdShort>-<stamp>.mp4."
|
|
4463
|
+
}
|
|
4464
|
+
},
|
|
4465
|
+
required: [],
|
|
4466
|
+
additionalProperties: false
|
|
4467
|
+
}
|
|
4468
|
+
},
|
|
3908
4469
|
{
|
|
3909
4470
|
name: "take_heapsnapshot",
|
|
3910
4471
|
description: "Capture a V8 heap snapshot of the selected page target over raw CDP (HeapProfiler.takeHeapSnapshot, accumulating addHeapSnapshotChunk events) and write it as a .heapsnapshot JSON file loadable by the DevTools Memory panel; returns {path,bytes,chunks,target} only and does not parse/summarize the snapshot. Writes under /tmp/cdp-toolkit (CDP_ARTIFACT_DIR) unless savePath is given.",
|
|
@@ -3913,7 +4474,7 @@ var MANIFEST = [
|
|
|
3913
4474
|
properties: {
|
|
3914
4475
|
lease: {
|
|
3915
4476
|
type: "string",
|
|
3916
|
-
description: "Opaque lease token from claim_page.
|
|
4477
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3917
4478
|
},
|
|
3918
4479
|
target: {
|
|
3919
4480
|
type: "string",
|
|
@@ -3936,7 +4497,7 @@ var MANIFEST = [
|
|
|
3936
4497
|
properties: {
|
|
3937
4498
|
lease: {
|
|
3938
4499
|
type: "string",
|
|
3939
|
-
description: "Opaque lease token from claim_page.
|
|
4500
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3940
4501
|
},
|
|
3941
4502
|
url: {
|
|
3942
4503
|
type: "string",
|
|
@@ -3980,7 +4541,7 @@ var MANIFEST = [
|
|
|
3980
4541
|
properties: {
|
|
3981
4542
|
lease: {
|
|
3982
4543
|
type: "string",
|
|
3983
|
-
description: "Opaque lease token from claim_page.
|
|
4544
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3984
4545
|
},
|
|
3985
4546
|
target: {
|
|
3986
4547
|
type: "string",
|
|
@@ -4050,7 +4611,7 @@ var MANIFEST = [
|
|
|
4050
4611
|
properties: {
|
|
4051
4612
|
lease: {
|
|
4052
4613
|
type: "string",
|
|
4053
|
-
description: "Opaque lease token from claim_page.
|
|
4614
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4054
4615
|
},
|
|
4055
4616
|
target: {
|
|
4056
4617
|
type: "string",
|
|
@@ -4069,7 +4630,7 @@ var MANIFEST = [
|
|
|
4069
4630
|
properties: {
|
|
4070
4631
|
lease: {
|
|
4071
4632
|
type: "string",
|
|
4072
|
-
description: "Opaque lease token from claim_page.
|
|
4633
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4073
4634
|
},
|
|
4074
4635
|
target: {
|
|
4075
4636
|
type: "string",
|
|
@@ -4086,17 +4647,21 @@ var MANIFEST = [
|
|
|
4086
4647
|
},
|
|
4087
4648
|
{
|
|
4088
4649
|
name: "claim_page",
|
|
4089
|
-
description: "Take exclusive ownership of a browser tab and return an opaque lease token.
|
|
4650
|
+
description: "Take exclusive ownership of a browser tab and return an opaque lease token. Two modes. FRESH TAB: with no target and no targetId, opens a new tab (optionally at url) and claims it, so 'give me my own tab' is one call. TAKEOVER: with target, claims a tab that is already open — this is how you work in a tab the human already has open when they ask you to, instead of opening your own — resolving any target selector against the live page list and never creating anything. The answer's 'opened' flag says which happened (true only when this call created the tab), and that same creation record is what release_page consults, so a tab you took over is left open when you release it. Any later tool call against a leased tab must carry the matching token in its 'lease' argument or it is refused, naming the holder. The lease is reclaimable once its owning process dies, its ttlMs elapses without use, or its tab is closed. Refused from the CLI: a CLI invocation is one process per call, so its lease would be reclaimable immediately.",
|
|
4090
4651
|
inputSchema: {
|
|
4091
4652
|
type: "object",
|
|
4092
4653
|
properties: {
|
|
4654
|
+
target: {
|
|
4655
|
+
type: "string",
|
|
4656
|
+
description: "Take over an already-open tab, e.g. one the human has open, when asked to: pass any target selector — active | index:N | url:<substr> | title:<substr> | <targetId>. Resolved against the live page list only, so it NEVER opens a tab: a selector that matches nothing is an error, not a new blank tab. The tab is left open on release because the toolkit did not create it. Refused if another live process holds it, including a lease the gate acquired for that process automatically under CDP_REQUIRE_LEASE: this takes over unleased (human) tabs and never steals one from a live agent. Mutually exclusive with targetId."
|
|
4657
|
+
},
|
|
4093
4658
|
targetId: {
|
|
4094
4659
|
type: "string",
|
|
4095
|
-
description: "Claim this already-open page target instead of opening a new tab. Must be an exact target id, not a selector."
|
|
4660
|
+
description: "Claim this already-open page target instead of opening a new tab. Must be an exact target id, not a selector. Kept for back-compat: prefer 'target', which accepts an exact id too plus every other selector form. Mutually exclusive with target."
|
|
4096
4661
|
},
|
|
4097
4662
|
url: {
|
|
4098
4663
|
type: "string",
|
|
4099
|
-
description: "When opening a fresh tab, navigate it here. Ignored when targetId is given."
|
|
4664
|
+
description: "When opening a fresh tab, navigate it here. Ignored when target or targetId is given, since both of those claim a tab that is already open."
|
|
4100
4665
|
},
|
|
4101
4666
|
label: {
|
|
4102
4667
|
type: "string",
|
|
@@ -4113,24 +4678,30 @@ var MANIFEST = [
|
|
|
4113
4678
|
},
|
|
4114
4679
|
{
|
|
4115
4680
|
name: "release_page",
|
|
4116
|
-
description: "
|
|
4681
|
+
description: "Give a lease back, and close the tab if this toolkit opened it. Takes exactly one of 'lease' (the token claim_page returned) or 'target' (a selector for a tab this process holds, which is how you release a lease the gate acquired for you automatically, since that path never hands you a token). A tab with a creation record from this toolkit is closed; a tab that was already open and merely claimed is released and left alone. Override either way with 'close'. Idempotent: an already-released, reclaimed, or expired lease reports released:false, and a release that did not happen never closes anything, because by then the tab may belong to another agent. Answers {released, closed, targetId}.",
|
|
4117
4682
|
inputSchema: {
|
|
4118
4683
|
type: "object",
|
|
4119
4684
|
properties: {
|
|
4120
4685
|
lease: {
|
|
4121
4686
|
type: "string",
|
|
4122
|
-
description: "The opaque token claim_page (or new_page with claim:true) returned."
|
|
4687
|
+
description: "The opaque token claim_page (or new_page with claim:true) returned. Mutually exclusive with 'target'. Under CDP_REQUIRE_LEASE a lease the gate acquired for you automatically never produced a token in the first place, so give 'target' instead."
|
|
4688
|
+
},
|
|
4689
|
+
target: {
|
|
4690
|
+
type: "string",
|
|
4691
|
+
description: "Target selector for a tab this process holds: active | index:N | url:<substr> | title:<substr> | <targetId>. Mutually exclusive with 'lease'. Refused if another process holds the tab."
|
|
4692
|
+
},
|
|
4693
|
+
close: {
|
|
4694
|
+
type: "boolean",
|
|
4695
|
+
description: "Force the close decision instead of letting the creation ledger decide. true closes even a tab this toolkit did not open; false keeps an agent-created tab open. Omit for the default, which closes only tabs this toolkit created."
|
|
4123
4696
|
}
|
|
4124
4697
|
},
|
|
4125
|
-
required: [
|
|
4126
|
-
"lease"
|
|
4127
|
-
],
|
|
4698
|
+
required: [],
|
|
4128
4699
|
additionalProperties: false
|
|
4129
4700
|
}
|
|
4130
4701
|
},
|
|
4131
4702
|
{
|
|
4132
4703
|
name: "list_leases",
|
|
4133
|
-
description:
|
|
4704
|
+
description: "Enumerate every active lease for diagnosis: backend, target id, agent label, pid, createdAt, lastUsedAt, ttlMs, whether the owning process is still alive, and whether the lease is reclaimable and why. Requires no token. Never returns the lease nonce, so this cannot be used to forge a token. A row for a lease file that could not be read or parsed instead carries `unreadable` (the errno, or \"unparseable\"), with label, pid, and the timestamp fields as zero placeholders and stale always false, since stale is what marks a lease free to take and an unreadable one must never read that way. Under CDP_REQUIRE_LEASE this call reaps abandoned agent tabs exactly as list_pages does, reporting them in an additive 'reaped' array and omitting their rows from the listing.",
|
|
4134
4705
|
inputSchema: {
|
|
4135
4706
|
type: "object",
|
|
4136
4707
|
properties: {},
|
|
@@ -4678,8 +5249,8 @@ function hostFromUrl(url, toolName) {
|
|
|
4678
5249
|
function driverError2(code, message, data) {
|
|
4679
5250
|
return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
|
|
4680
5251
|
}
|
|
4681
|
-
function encodeUid2(
|
|
4682
|
-
return `bidi:${
|
|
5252
|
+
function encodeUid2(stamp6) {
|
|
5253
|
+
return `bidi:${stamp6}`;
|
|
4683
5254
|
}
|
|
4684
5255
|
function decodeStamp(uid) {
|
|
4685
5256
|
if (!uid.startsWith("bidi:"))
|
|
@@ -4786,7 +5357,7 @@ function parseSnapshotLine(line) {
|
|
|
4786
5357
|
const m = LINE_RE.exec(line);
|
|
4787
5358
|
if (!m)
|
|
4788
5359
|
return;
|
|
4789
|
-
const indent = m[1] ?? "",
|
|
5360
|
+
const indent = m[1] ?? "", stamp6 = m[2], role = m[3], nameLit = m[4], extrasRaw = m[5];
|
|
4790
5361
|
const depth = indent.length / 2;
|
|
4791
5362
|
let name;
|
|
4792
5363
|
if (nameLit) {
|
|
@@ -4814,7 +5385,7 @@ function parseSnapshotLine(line) {
|
|
|
4814
5385
|
extras[key] = val;
|
|
4815
5386
|
}
|
|
4816
5387
|
}
|
|
4817
|
-
return { uid: encodeUid2(
|
|
5388
|
+
return { uid: encodeUid2(stamp6), role, depth, ...name !== undefined ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} };
|
|
4818
5389
|
}
|
|
4819
5390
|
var INTERACTIVE_ROLES3 = new Set([
|
|
4820
5391
|
"button",
|
|
@@ -4847,8 +5418,8 @@ function sharedRefOf(node, describe) {
|
|
|
4847
5418
|
}
|
|
4848
5419
|
async function resolveElementLocator2(conn, contextId, loc) {
|
|
4849
5420
|
if ("uid" in loc) {
|
|
4850
|
-
const
|
|
4851
|
-
const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(
|
|
5421
|
+
const stamp6 = decodeStamp(loc.uid);
|
|
5422
|
+
const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp6)}"]` });
|
|
4852
5423
|
if (!nodes2.length)
|
|
4853
5424
|
throw driverError2("stale-uid", `uid does not resolve: ${loc.uid}`);
|
|
4854
5425
|
return sharedRefOf(nodes2[0], loc.uid);
|
|
@@ -5060,14 +5631,14 @@ class BidiPageDriver {
|
|
|
5060
5631
|
return loc.uid;
|
|
5061
5632
|
}
|
|
5062
5633
|
const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
|
|
5063
|
-
const
|
|
5064
|
-
if (typeof
|
|
5634
|
+
const stamp6 = await this.callFunctionValue(STAMP_ONE_SOURCE, [], ref);
|
|
5635
|
+
if (typeof stamp6 !== "string")
|
|
5065
5636
|
throw driverError2("page-error", "stamping the located element did not return a string");
|
|
5066
|
-
return encodeUid2(
|
|
5637
|
+
return encodeUid2(stamp6);
|
|
5067
5638
|
}
|
|
5068
5639
|
async resolve(uid) {
|
|
5069
|
-
const
|
|
5070
|
-
const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(
|
|
5640
|
+
const stamp6 = decodeStamp(uid);
|
|
5641
|
+
const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp6)}"]` });
|
|
5071
5642
|
if (!nodes.length)
|
|
5072
5643
|
throw driverError2("stale-uid", `uid does not resolve: ${uid}`);
|
|
5073
5644
|
}
|
|
@@ -5496,11 +6067,11 @@ function createFirefoxDriver(port, opts) {
|
|
|
5496
6067
|
}
|
|
5497
6068
|
|
|
5498
6069
|
// src/bidi/launch.ts
|
|
5499
|
-
import { spawn as
|
|
6070
|
+
import { spawn as spawn3 } from "node:child_process";
|
|
5500
6071
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5501
6072
|
import { createConnection, createServer } from "node:net";
|
|
5502
6073
|
import { tmpdir } from "node:os";
|
|
5503
|
-
import { join as
|
|
6074
|
+
import { join as join7 } from "node:path";
|
|
5504
6075
|
function resolveBinary() {
|
|
5505
6076
|
const candidates = [
|
|
5506
6077
|
process.env.FIREFOX_BIN,
|
|
@@ -5574,11 +6145,11 @@ async function launchFirefox(opts = {}) {
|
|
|
5574
6145
|
const binary = resolveBinary();
|
|
5575
6146
|
const port = await pickPort(opts.port);
|
|
5576
6147
|
const isThrowaway = opts.profilePath === undefined;
|
|
5577
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
6148
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join7(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
5578
6149
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
5579
6150
|
if (opts.headless)
|
|
5580
6151
|
args.push("--headless");
|
|
5581
|
-
const proc =
|
|
6152
|
+
const proc = spawn3(binary, args, { stdio: "ignore", detached: false });
|
|
5582
6153
|
let closed = false;
|
|
5583
6154
|
const close = async () => {
|
|
5584
6155
|
if (closed)
|
|
@@ -5844,7 +6415,7 @@ var FIREFOX_TOOLS = {
|
|
|
5844
6415
|
};
|
|
5845
6416
|
|
|
5846
6417
|
// src/mcp.ts
|
|
5847
|
-
var VERSION = "1.
|
|
6418
|
+
var VERSION = "1.7.0";
|
|
5848
6419
|
var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
|
|
5849
6420
|
var AVAILABILITY = toolAvailability(BROWSER);
|
|
5850
6421
|
var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
|
|
@@ -5901,6 +6472,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
5901
6472
|
});
|
|
5902
6473
|
});
|
|
5903
6474
|
auditCoverage();
|
|
6475
|
+
markLongLivedProcess();
|
|
5904
6476
|
await server.connect(new StdioServerTransport);
|
|
5905
6477
|
console.error(`[cdp-toolkit] MCP server v${VERSION} ready, browser=${BROWSER}, ${AVAILABILITY.available.length} tools, CDP_BASE=${BASE}`);
|
|
5906
6478
|
var shuttingDown = false;
|