cdp-toolkit 1.1.0 → 1.2.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 +76 -9
- package/CONTRACT.md +7 -5
- package/README.md +67 -30
- package/dist/cli.js +410 -61
- package/dist/index.js +370 -39
- package/dist/mcp.js +609 -72
- package/package.json +2 -2
- package/src/bidi/driver.ts +31 -3
- package/src/capabilities.ts +1 -1
- package/src/cli.ts +47 -22
- package/src/client.ts +25 -7
- package/src/driver.ts +1 -1
- package/src/firefox-tools.ts +6 -3
- package/src/index.ts +6 -1
- package/src/leases-tools.ts +101 -0
- package/src/leases.ts +533 -0
- package/src/manifest.ts +211 -13
- package/src/mcp.ts +27 -19
- package/src/shared-tools.ts +52 -6
- package/src/tools/console.ts +3 -3
- package/src/tools/evaluate.ts +2 -2
- package/src/tools/heap.ts +3 -3
- package/src/tools/lighthouse.ts +4 -4
- package/src/tools/navigation.ts +1 -1
- package/src/tools/network.ts +7 -7
- package/src/tools/network_mock.ts +8 -8
- package/src/tools/performance.ts +11 -11
- package/src/tools/recorder.ts +5 -5
- package/src/tools/snapshot.ts +2 -2
package/dist/index.js
CHANGED
|
@@ -1,3 +1,255 @@
|
|
|
1
|
+
// src/leases.ts
|
|
2
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
3
|
+
import { randomBytes } from "node:crypto";
|
|
4
|
+
import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
5
|
+
import { join } from "node:path";
|
|
6
|
+
var DEFAULT_LEASE_TTL_MS = 900000;
|
|
7
|
+
function leaseDir() {
|
|
8
|
+
return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
9
|
+
}
|
|
10
|
+
function leaseTtlMs() {
|
|
11
|
+
const raw = Number(process.env.CDP_LEASE_TTL_MS);
|
|
12
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
|
|
13
|
+
}
|
|
14
|
+
function safeId(id) {
|
|
15
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
16
|
+
}
|
|
17
|
+
function leaseFile(backend, targetId) {
|
|
18
|
+
return join(leaseDir(), `lease-${backend}-${safeId(targetId)}.json`);
|
|
19
|
+
}
|
|
20
|
+
function mintToken(backend, targetId, nonce) {
|
|
21
|
+
return `${backend}:${targetId}:${nonce}`;
|
|
22
|
+
}
|
|
23
|
+
function tokenParts(token) {
|
|
24
|
+
if (typeof token !== "string")
|
|
25
|
+
return;
|
|
26
|
+
const first = token.indexOf(":");
|
|
27
|
+
const last = token.lastIndexOf(":");
|
|
28
|
+
if (first <= 0 || last <= first)
|
|
29
|
+
return;
|
|
30
|
+
const backend = token.slice(0, first);
|
|
31
|
+
if (backend !== "chrome" && backend !== "firefox")
|
|
32
|
+
return;
|
|
33
|
+
const targetId = token.slice(first + 1, last);
|
|
34
|
+
const nonce = token.slice(last + 1);
|
|
35
|
+
if (!targetId.length || !/^[0-9a-f]{24}$/.test(nonce))
|
|
36
|
+
return;
|
|
37
|
+
return { backend, targetId, nonce };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
class LeaseConflictError extends Error {
|
|
41
|
+
targetId;
|
|
42
|
+
holder;
|
|
43
|
+
constructor(message, targetId, holder) {
|
|
44
|
+
super(message);
|
|
45
|
+
this.targetId = targetId;
|
|
46
|
+
this.holder = holder;
|
|
47
|
+
this.name = "LeaseConflictError";
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
async function readLease(backend, targetId) {
|
|
51
|
+
let raw;
|
|
52
|
+
try {
|
|
53
|
+
raw = await readFile(leaseFile(backend, targetId), "utf8");
|
|
54
|
+
} catch (err) {
|
|
55
|
+
if (err.code === "ENOENT")
|
|
56
|
+
return;
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
try {
|
|
60
|
+
const rec = JSON.parse(raw);
|
|
61
|
+
if (typeof rec?.nonce === "string" && typeof rec?.pid === "number")
|
|
62
|
+
return rec;
|
|
63
|
+
} catch {}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
function isPidAlive(pid) {
|
|
67
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
68
|
+
return false;
|
|
69
|
+
try {
|
|
70
|
+
process.kill(pid, 0);
|
|
71
|
+
return true;
|
|
72
|
+
} catch (err) {
|
|
73
|
+
return err.code === "EPERM";
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function staleReason(rec, opts) {
|
|
77
|
+
if (!isPidAlive(rec.pid))
|
|
78
|
+
return "dead-pid";
|
|
79
|
+
if (opts.now - rec.lastUsedAt > rec.ttlMs)
|
|
80
|
+
return "expired";
|
|
81
|
+
if (opts.liveIds && !opts.liveIds.includes(rec.targetId))
|
|
82
|
+
return "target-gone";
|
|
83
|
+
return false;
|
|
84
|
+
}
|
|
85
|
+
async function claimLease(backend, targetId, opts) {
|
|
86
|
+
const now = opts.now ?? Date.now();
|
|
87
|
+
await mkdir(leaseDir(), { recursive: true });
|
|
88
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
89
|
+
const record = {
|
|
90
|
+
backend,
|
|
91
|
+
targetId,
|
|
92
|
+
nonce: randomBytes(12).toString("hex"),
|
|
93
|
+
pid: process.pid,
|
|
94
|
+
label: opts.label,
|
|
95
|
+
createdAt: now,
|
|
96
|
+
lastUsedAt: now,
|
|
97
|
+
ttlMs: opts.ttlMs ?? leaseTtlMs()
|
|
98
|
+
};
|
|
99
|
+
try {
|
|
100
|
+
await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
|
|
101
|
+
return { token: mintToken(backend, targetId, record.nonce), record };
|
|
102
|
+
} catch (err) {
|
|
103
|
+
if (err.code !== "EEXIST")
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
const existing = await readLease(backend, targetId);
|
|
107
|
+
if (!existing)
|
|
108
|
+
continue;
|
|
109
|
+
const stale = staleReason(existing, { now, liveIds: opts.liveIds });
|
|
110
|
+
if (!stale) {
|
|
111
|
+
throw new LeaseConflictError(`target '${targetId}' is already leased by '${existing.label}' (pid ${existing.pid}); call release_page with that lease's token, or list_leases to inspect it`, targetId, existing.label);
|
|
112
|
+
}
|
|
113
|
+
await unlink(leaseFile(backend, targetId)).catch(() => {
|
|
114
|
+
return;
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
throw new LeaseConflictError(`target '${targetId}' could not be claimed after 3 attempts: another process is claiming and releasing it concurrently`, targetId);
|
|
118
|
+
}
|
|
119
|
+
async function releaseLease(token) {
|
|
120
|
+
const parts = tokenParts(token);
|
|
121
|
+
if (!parts)
|
|
122
|
+
return { released: false };
|
|
123
|
+
const rec = await readLease(parts.backend, parts.targetId);
|
|
124
|
+
if (!rec || rec.nonce !== parts.nonce)
|
|
125
|
+
return { released: false };
|
|
126
|
+
await unlink(leaseFile(parts.backend, parts.targetId)).catch(() => {
|
|
127
|
+
return;
|
|
128
|
+
});
|
|
129
|
+
return { released: true };
|
|
130
|
+
}
|
|
131
|
+
async function releaseLeaseFor(backend, targetId) {
|
|
132
|
+
if (!await readLease(backend, targetId))
|
|
133
|
+
return { released: false };
|
|
134
|
+
await unlink(leaseFile(backend, targetId)).catch(() => {
|
|
135
|
+
return;
|
|
136
|
+
});
|
|
137
|
+
return { released: true };
|
|
138
|
+
}
|
|
139
|
+
var leaseScope = new AsyncLocalStorage;
|
|
140
|
+
function withLeaseScope(lease, fn) {
|
|
141
|
+
return leaseScope.run(lease, fn);
|
|
142
|
+
}
|
|
143
|
+
function currentLease() {
|
|
144
|
+
return leaseScope.getStore();
|
|
145
|
+
}
|
|
146
|
+
function leaseFromArgs(args) {
|
|
147
|
+
if (!args || typeof args !== "object")
|
|
148
|
+
return;
|
|
149
|
+
const lease = args.lease;
|
|
150
|
+
return typeof lease === "string" && lease.length > 0 ? lease : undefined;
|
|
151
|
+
}
|
|
152
|
+
async function touchLease(rec, now = Date.now()) {
|
|
153
|
+
const current = await readLease(rec.backend, rec.targetId).catch(() => {
|
|
154
|
+
return;
|
|
155
|
+
});
|
|
156
|
+
if (!current || current.nonce !== rec.nonce)
|
|
157
|
+
return;
|
|
158
|
+
await writeFile(leaseFile(current.backend, current.targetId), JSON.stringify({ ...current, lastUsedAt: now }), "utf8").catch(() => {
|
|
159
|
+
return;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function describeTarget(targetId, ctx) {
|
|
163
|
+
const url = ctx.url ? ` (url ${ctx.url})` : ctx.title ? ` (title ${JSON.stringify(ctx.title)})` : "";
|
|
164
|
+
return `target '${targetId}'${url}`;
|
|
165
|
+
}
|
|
166
|
+
async function assertLeaseOk(backend, targetId, ctx = {}) {
|
|
167
|
+
const token = ctx.lease ?? currentLease();
|
|
168
|
+
const now = ctx.now ?? Date.now();
|
|
169
|
+
const rec = await readLease(backend, targetId);
|
|
170
|
+
const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
|
|
171
|
+
const where = describeTarget(targetId, ctx);
|
|
172
|
+
if (token === undefined) {
|
|
173
|
+
if (!held)
|
|
174
|
+
return;
|
|
175
|
+
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);
|
|
176
|
+
}
|
|
177
|
+
const parts = tokenParts(token);
|
|
178
|
+
if (!parts) {
|
|
179
|
+
throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
|
|
180
|
+
}
|
|
181
|
+
if (parts.backend !== backend || parts.targetId !== targetId) {
|
|
182
|
+
if (!held)
|
|
183
|
+
return;
|
|
184
|
+
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);
|
|
185
|
+
}
|
|
186
|
+
if (!held) {
|
|
187
|
+
throw new LeaseConflictError(`${where} is not leased any more: the token you passed belongs to a lease that was released or reclaimed. Call claim_page again to take the tab.`, targetId);
|
|
188
|
+
}
|
|
189
|
+
if (parts.nonce !== held.nonce) {
|
|
190
|
+
throw new LeaseConflictError(`${where} was reclaimed and is now leased by '${held.label}' (pid ${held.pid}). Your token was invalidated by that reclamation. Call claim_page again if you still need this tab.`, targetId, held.label);
|
|
191
|
+
}
|
|
192
|
+
await touchLease(held, now);
|
|
193
|
+
}
|
|
194
|
+
var LEASE_FILE_RE = /^lease-(chrome|firefox)-(.+)\.json$/;
|
|
195
|
+
async function listLeases(opts = {}) {
|
|
196
|
+
const now = opts.now ?? Date.now();
|
|
197
|
+
let names = [];
|
|
198
|
+
try {
|
|
199
|
+
names = await readdir(leaseDir());
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (err.code === "ENOENT")
|
|
202
|
+
return [];
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const name of names) {
|
|
207
|
+
const parts = LEASE_FILE_RE.exec(name);
|
|
208
|
+
if (!parts)
|
|
209
|
+
continue;
|
|
210
|
+
const fromName = {
|
|
211
|
+
backend: parts[1],
|
|
212
|
+
targetId: parts[2],
|
|
213
|
+
label: "",
|
|
214
|
+
pid: 0,
|
|
215
|
+
createdAt: 0,
|
|
216
|
+
lastUsedAt: 0,
|
|
217
|
+
ttlMs: 0,
|
|
218
|
+
pidAlive: false,
|
|
219
|
+
stale: false
|
|
220
|
+
};
|
|
221
|
+
let rec;
|
|
222
|
+
try {
|
|
223
|
+
rec = JSON.parse(await readFile(join(leaseDir(), name), "utf8"));
|
|
224
|
+
} catch (err) {
|
|
225
|
+
const code = err.code;
|
|
226
|
+
if (code === "ENOENT")
|
|
227
|
+
continue;
|
|
228
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
229
|
+
continue;
|
|
230
|
+
}
|
|
231
|
+
if (typeof rec?.nonce !== "string" || typeof rec?.pid !== "number") {
|
|
232
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
233
|
+
continue;
|
|
234
|
+
}
|
|
235
|
+
out.push({
|
|
236
|
+
backend: rec.backend,
|
|
237
|
+
targetId: rec.targetId,
|
|
238
|
+
label: rec.label,
|
|
239
|
+
pid: rec.pid,
|
|
240
|
+
createdAt: rec.createdAt,
|
|
241
|
+
lastUsedAt: rec.lastUsedAt,
|
|
242
|
+
ttlMs: rec.ttlMs,
|
|
243
|
+
pidAlive: isPidAlive(rec.pid),
|
|
244
|
+
stale: staleReason(rec, {
|
|
245
|
+
now,
|
|
246
|
+
liveIds: opts.liveBackend === undefined || opts.liveBackend === rec.backend ? opts.liveIds : undefined
|
|
247
|
+
})
|
|
248
|
+
});
|
|
249
|
+
}
|
|
250
|
+
return out;
|
|
251
|
+
}
|
|
252
|
+
|
|
1
253
|
// src/client.ts
|
|
2
254
|
var BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
3
255
|
var DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
|
|
@@ -151,8 +403,13 @@ async function browserWsUrl() {
|
|
|
151
403
|
const v = await httpJson("/json/version");
|
|
152
404
|
return v.webSocketDebuggerUrl;
|
|
153
405
|
}
|
|
154
|
-
async function resolveTarget(selector) {
|
|
406
|
+
async function resolveTarget(selector, opts = {}) {
|
|
155
407
|
const targets = await listTargets();
|
|
408
|
+
const hit = pickTarget(targets, selector);
|
|
409
|
+
await assertLeaseOk("chrome", hit.id, { lease: opts.lease, url: hit.url, title: hit.title });
|
|
410
|
+
return hit;
|
|
411
|
+
}
|
|
412
|
+
function pickTarget(targets, selector) {
|
|
156
413
|
const pages = targets.filter((t) => t.type === "page");
|
|
157
414
|
if (!selector || selector === "active") {
|
|
158
415
|
if (!pages.length)
|
|
@@ -188,7 +445,7 @@ async function openBrowser(opts = {}) {
|
|
|
188
445
|
return new CdpConnection(await browserWsUrl(), opts).connect();
|
|
189
446
|
}
|
|
190
447
|
async function openPage(selector, opts = {}) {
|
|
191
|
-
const target = await resolveTarget(selector);
|
|
448
|
+
const target = await resolveTarget(selector, { lease: opts.lease });
|
|
192
449
|
const conn = await new CdpConnection(target.webSocketDebuggerUrl, opts).connect();
|
|
193
450
|
return { conn, target };
|
|
194
451
|
}
|
|
@@ -1069,9 +1326,63 @@ function createCdpDriver() {
|
|
|
1069
1326
|
return new CdpBrowserDriver;
|
|
1070
1327
|
}
|
|
1071
1328
|
|
|
1329
|
+
// src/leases-tools.ts
|
|
1330
|
+
class LeaseToolError extends Error {
|
|
1331
|
+
}
|
|
1332
|
+
function backendOf(driver) {
|
|
1333
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
1334
|
+
}
|
|
1335
|
+
async function claimPage(driver, args = {}) {
|
|
1336
|
+
const backend = backendOf(driver);
|
|
1337
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : `pid-${process.pid}`;
|
|
1338
|
+
const pages = await driver.listPages();
|
|
1339
|
+
let targetId = args.targetId;
|
|
1340
|
+
let url = args.url ?? "about:blank";
|
|
1341
|
+
if (targetId === undefined || targetId === "") {
|
|
1342
|
+
const page = await driver.newPage(args.url);
|
|
1343
|
+
targetId = page.id;
|
|
1344
|
+
url = page.url;
|
|
1345
|
+
} else {
|
|
1346
|
+
const existing = pages.find((p) => p.id === targetId);
|
|
1347
|
+
if (!existing)
|
|
1348
|
+
throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
|
|
1349
|
+
url = existing.url;
|
|
1350
|
+
}
|
|
1351
|
+
const liveIds = [...pages.map((p) => p.id), targetId];
|
|
1352
|
+
const { record, token } = await claimLease(backend, targetId, {
|
|
1353
|
+
label,
|
|
1354
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
1355
|
+
liveIds
|
|
1356
|
+
});
|
|
1357
|
+
return {
|
|
1358
|
+
lease: token,
|
|
1359
|
+
targetId,
|
|
1360
|
+
url,
|
|
1361
|
+
label: record.label,
|
|
1362
|
+
ttlMs: record.ttlMs,
|
|
1363
|
+
expiresAt: record.lastUsedAt + record.ttlMs
|
|
1364
|
+
};
|
|
1365
|
+
}
|
|
1366
|
+
async function releasePage(_driver, args) {
|
|
1367
|
+
if (typeof args.lease !== "string" || !args.lease.length) {
|
|
1368
|
+
throw new LeaseToolError("release_page requires the 'lease' token returned by claim_page");
|
|
1369
|
+
}
|
|
1370
|
+
return releaseLease(args.lease);
|
|
1371
|
+
}
|
|
1372
|
+
async function listLeasesTool(driver, _args = {}) {
|
|
1373
|
+
const liveIds = (await driver.listPages()).map((p) => p.id);
|
|
1374
|
+
const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
|
|
1375
|
+
return { leases, count: leases.length };
|
|
1376
|
+
}
|
|
1377
|
+
var LEASE_TOOLS = {
|
|
1378
|
+
claim_page: claimPage,
|
|
1379
|
+
release_page: releasePage,
|
|
1380
|
+
list_leases: listLeasesTool
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1072
1383
|
// src/shared-tools.ts
|
|
1073
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1074
|
-
import { join } from "node:path";
|
|
1384
|
+
import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1385
|
+
import { join as join2 } from "node:path";
|
|
1075
1386
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1076
1387
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1077
1388
|
|
|
@@ -1080,6 +1391,9 @@ class SharedToolError extends Error {
|
|
|
1080
1391
|
function target3(p) {
|
|
1081
1392
|
return { id: p.id, url: p.url, title: p.title };
|
|
1082
1393
|
}
|
|
1394
|
+
function backendOf2(driver) {
|
|
1395
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
1396
|
+
}
|
|
1083
1397
|
async function withPage2(driver, target, fn) {
|
|
1084
1398
|
const page = await driver.page(target);
|
|
1085
1399
|
try {
|
|
@@ -1090,6 +1404,11 @@ async function withPage2(driver, target, fn) {
|
|
|
1090
1404
|
}
|
|
1091
1405
|
async function resolvePage(driver, selector) {
|
|
1092
1406
|
const pages = await driver.listPages();
|
|
1407
|
+
const hit = await pickPage(driver, pages, selector);
|
|
1408
|
+
await assertLeaseOk(backendOf2(driver), hit.id, { url: hit.url, title: hit.title });
|
|
1409
|
+
return hit;
|
|
1410
|
+
}
|
|
1411
|
+
async function pickPage(driver, pages, selector) {
|
|
1093
1412
|
if (selector === undefined || selector === "active") {
|
|
1094
1413
|
if (!pages.length)
|
|
1095
1414
|
throw new SharedToolError("no page targets open");
|
|
@@ -1137,18 +1456,27 @@ async function listPages(driver, args = {}) {
|
|
|
1137
1456
|
}
|
|
1138
1457
|
async function newPage(driver, args = {}) {
|
|
1139
1458
|
const p = await driver.newPage(args.url);
|
|
1140
|
-
|
|
1459
|
+
if (args.claim !== true)
|
|
1460
|
+
return { targetId: p.id, url: p.url };
|
|
1461
|
+
const { record, token } = await claimLease(backendOf2(driver), p.id, {
|
|
1462
|
+
label: typeof args.label === "string" && args.label.length ? args.label : `pid-${process.pid}`,
|
|
1463
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
|
|
1464
|
+
});
|
|
1465
|
+
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
1141
1466
|
}
|
|
1142
1467
|
async function closePage(driver, args) {
|
|
1143
1468
|
if (args.target === undefined || args.target === "")
|
|
1144
1469
|
throw new SharedToolError("close_page requires an explicit target; refusing to guess which page to close");
|
|
1145
1470
|
const p = await resolvePage(driver, args.target);
|
|
1146
1471
|
const res = await driver.closePage(p.id);
|
|
1147
|
-
|
|
1472
|
+
if (!res.success)
|
|
1473
|
+
return { closed: p.id, success: false, leaseReleased: false };
|
|
1474
|
+
const released = await releaseLeaseFor(backendOf2(driver), p.id);
|
|
1475
|
+
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
1148
1476
|
}
|
|
1149
1477
|
async function writeSelectedFile(id) {
|
|
1150
|
-
await
|
|
1151
|
-
await
|
|
1478
|
+
await mkdir2(STATE_DIR, { recursive: true });
|
|
1479
|
+
await writeFile2(join2(STATE_DIR, "selected"), id, "utf8");
|
|
1152
1480
|
}
|
|
1153
1481
|
async function selectPage(driver, args) {
|
|
1154
1482
|
if (args.target === undefined || args.target === "")
|
|
@@ -1303,10 +1631,10 @@ async function takeScreenshot(driver, args = {}) {
|
|
|
1303
1631
|
const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
|
|
1304
1632
|
if (data.byteLength === 0)
|
|
1305
1633
|
throw new SharedToolError("captureScreenshot returned empty data");
|
|
1306
|
-
await
|
|
1634
|
+
await mkdir2(ARTIFACT_DIR, { recursive: true });
|
|
1307
1635
|
const ext = outFormat === "jpeg" ? "jpg" : "png";
|
|
1308
|
-
const path = args.savePath ??
|
|
1309
|
-
await
|
|
1636
|
+
const path = args.savePath ?? join2(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
1637
|
+
await writeFile2(path, Buffer.from(data));
|
|
1310
1638
|
const result = {
|
|
1311
1639
|
path,
|
|
1312
1640
|
bytes: data.byteLength,
|
|
@@ -1369,10 +1697,10 @@ var SHARED_TOOLS = {
|
|
|
1369
1697
|
};
|
|
1370
1698
|
|
|
1371
1699
|
// src/tools/console.ts
|
|
1372
|
-
import { readFile } from "node:fs/promises";
|
|
1700
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1373
1701
|
|
|
1374
1702
|
// src/tools/recorder.ts
|
|
1375
|
-
import { mkdir as
|
|
1703
|
+
import { mkdir as mkdir3, appendFile, writeFile as writeFile3, copyFile } from "node:fs/promises";
|
|
1376
1704
|
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1377
1705
|
function recFile(targetId) {
|
|
1378
1706
|
return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
|
|
@@ -1393,11 +1721,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
1393
1721
|
async function startRecorder(target, opts = {}) {
|
|
1394
1722
|
const network = opts.network ?? true;
|
|
1395
1723
|
const consoleCap = opts.console ?? true;
|
|
1396
|
-
await
|
|
1724
|
+
await mkdir3(ARTIFACT_DIR2, { recursive: true });
|
|
1397
1725
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
1398
1726
|
const file = opts.file ?? recFile(resolved.id);
|
|
1399
1727
|
if (opts.truncate) {
|
|
1400
|
-
await
|
|
1728
|
+
await writeFile3(file, "");
|
|
1401
1729
|
}
|
|
1402
1730
|
let chain = Promise.resolve();
|
|
1403
1731
|
let dropped = 0;
|
|
@@ -1539,7 +1867,7 @@ function lineToEntry(line, index) {
|
|
|
1539
1867
|
async function readConsoleEntries(file) {
|
|
1540
1868
|
let raw;
|
|
1541
1869
|
try {
|
|
1542
|
-
raw = await
|
|
1870
|
+
raw = await readFile2(file, "utf8");
|
|
1543
1871
|
} catch {
|
|
1544
1872
|
return [];
|
|
1545
1873
|
}
|
|
@@ -1595,7 +1923,7 @@ async function getConsoleMessage(args = {}) {
|
|
|
1595
1923
|
}
|
|
1596
1924
|
|
|
1597
1925
|
// src/tools/network.ts
|
|
1598
|
-
import { readFile as
|
|
1926
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1599
1927
|
var DEFAULT_CAPTURE_MS2 = 2500;
|
|
1600
1928
|
function asHeaders(v) {
|
|
1601
1929
|
if (!v || typeof v !== "object")
|
|
@@ -1662,7 +1990,7 @@ function correlate(lines) {
|
|
|
1662
1990
|
async function readNetworkRequests(file) {
|
|
1663
1991
|
let raw;
|
|
1664
1992
|
try {
|
|
1665
|
-
raw = await
|
|
1993
|
+
raw = await readFile3(file, "utf8");
|
|
1666
1994
|
} catch {
|
|
1667
1995
|
return [];
|
|
1668
1996
|
}
|
|
@@ -1744,8 +2072,8 @@ async function getNetworkRequest(args = {}) {
|
|
|
1744
2072
|
}
|
|
1745
2073
|
|
|
1746
2074
|
// src/tools/performance.ts
|
|
1747
|
-
import { mkdir as
|
|
1748
|
-
import { join as
|
|
2075
|
+
import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4, stat, unlink as unlink2 } from "node:fs/promises";
|
|
2076
|
+
import { join as join3 } from "node:path";
|
|
1749
2077
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1750
2078
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1751
2079
|
var DEFAULT_CATEGORIES = [
|
|
@@ -1763,7 +2091,7 @@ function shortId(id) {
|
|
|
1763
2091
|
return id.slice(0, 8);
|
|
1764
2092
|
}
|
|
1765
2093
|
function stateFilePath() {
|
|
1766
|
-
return
|
|
2094
|
+
return join3(STATE_DIR2, "trace-state.json");
|
|
1767
2095
|
}
|
|
1768
2096
|
function bufferTraceData(conn, sink) {
|
|
1769
2097
|
return conn.on("Tracing.dataCollected", (params) => {
|
|
@@ -1785,10 +2113,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
1785
2113
|
return sink;
|
|
1786
2114
|
}
|
|
1787
2115
|
async function writeTrace(events, targetId) {
|
|
1788
|
-
await
|
|
1789
|
-
const path =
|
|
2116
|
+
await mkdir4(ARTIFACT_DIR3, { recursive: true });
|
|
2117
|
+
const path = join3(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
1790
2118
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
1791
|
-
await
|
|
2119
|
+
await writeFile4(path, body, "utf8");
|
|
1792
2120
|
const { size } = await stat(path);
|
|
1793
2121
|
return { path, bytes: size };
|
|
1794
2122
|
}
|
|
@@ -1808,7 +2136,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
1808
2136
|
throw err;
|
|
1809
2137
|
}
|
|
1810
2138
|
liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
|
|
1811
|
-
await
|
|
2139
|
+
await mkdir4(STATE_DIR2, { recursive: true });
|
|
1812
2140
|
const state = {
|
|
1813
2141
|
targetId: target.id,
|
|
1814
2142
|
url: target.url,
|
|
@@ -1816,7 +2144,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
1816
2144
|
pid: process.pid,
|
|
1817
2145
|
categories
|
|
1818
2146
|
};
|
|
1819
|
-
await
|
|
2147
|
+
await writeFile4(stateFilePath(), JSON.stringify(state), "utf8");
|
|
1820
2148
|
return {
|
|
1821
2149
|
target: { id: target.id, url: target.url, title: target.title },
|
|
1822
2150
|
categories,
|
|
@@ -1829,7 +2157,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
1829
2157
|
if (!entry) {
|
|
1830
2158
|
let hint = "";
|
|
1831
2159
|
try {
|
|
1832
|
-
const raw = await
|
|
2160
|
+
const raw = await readFile4(stateFilePath(), "utf8");
|
|
1833
2161
|
const state = JSON.parse(raw);
|
|
1834
2162
|
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)`;
|
|
1835
2163
|
} catch {}
|
|
@@ -1841,7 +2169,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
1841
2169
|
} finally {
|
|
1842
2170
|
conn.close();
|
|
1843
2171
|
liveTraces.delete(target.id);
|
|
1844
|
-
await
|
|
2172
|
+
await unlink2(stateFilePath()).catch(() => {});
|
|
1845
2173
|
}
|
|
1846
2174
|
const written = await writeTrace(events, target.id);
|
|
1847
2175
|
const metrics = analyzeEvents(events);
|
|
@@ -1959,7 +2287,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
1959
2287
|
}
|
|
1960
2288
|
let raw;
|
|
1961
2289
|
try {
|
|
1962
|
-
raw = await
|
|
2290
|
+
raw = await readFile4(args.tracePath, "utf8");
|
|
1963
2291
|
} catch (err) {
|
|
1964
2292
|
throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
|
|
1965
2293
|
}
|
|
@@ -1976,8 +2304,8 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
1976
2304
|
}
|
|
1977
2305
|
|
|
1978
2306
|
// src/tools/heap.ts
|
|
1979
|
-
import { mkdir as
|
|
1980
|
-
import { join as
|
|
2307
|
+
import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
|
|
2308
|
+
import { join as join4 } from "node:path";
|
|
1981
2309
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1982
2310
|
function stamp3(targetId) {
|
|
1983
2311
|
const short = targetId.slice(0, 8);
|
|
@@ -2002,9 +2330,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2002
2330
|
if (snapshot.length === 0) {
|
|
2003
2331
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
2004
2332
|
}
|
|
2005
|
-
await
|
|
2006
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
2007
|
-
await
|
|
2333
|
+
await mkdir5(ARTIFACT_DIR4, { recursive: true });
|
|
2334
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join4(ARTIFACT_DIR4, args.savePath) : join4(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
|
|
2335
|
+
await writeFile5(path, snapshot, "utf8");
|
|
2008
2336
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
2009
2337
|
return {
|
|
2010
2338
|
path,
|
|
@@ -2017,7 +2345,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2017
2345
|
|
|
2018
2346
|
// src/tools/lighthouse.ts
|
|
2019
2347
|
import { spawn } from "node:child_process";
|
|
2020
|
-
import { mkdir as
|
|
2348
|
+
import { mkdir as mkdir6, readFile as readFile5, stat as stat2 } from "node:fs/promises";
|
|
2021
2349
|
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2022
2350
|
function cdpPort() {
|
|
2023
2351
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
@@ -2078,7 +2406,7 @@ async function lighthouseAudit(args) {
|
|
|
2078
2406
|
const url = args.url.trim();
|
|
2079
2407
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
2080
2408
|
const port = cdpPort();
|
|
2081
|
-
await
|
|
2409
|
+
await mkdir6(ARTIFACT_DIR5, { recursive: true });
|
|
2082
2410
|
const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
|
|
2083
2411
|
const formFactor = args.formFactor ?? "desktop";
|
|
2084
2412
|
const lhArgs = [
|
|
@@ -2107,7 +2435,7 @@ async function lighthouseAudit(args) {
|
|
|
2107
2435
|
}
|
|
2108
2436
|
let raw;
|
|
2109
2437
|
try {
|
|
2110
|
-
raw = await
|
|
2438
|
+
raw = await readFile5(outPath, "utf8");
|
|
2111
2439
|
} catch {
|
|
2112
2440
|
raw = undefined;
|
|
2113
2441
|
}
|
|
@@ -2115,7 +2443,7 @@ async function lighthouseAudit(args) {
|
|
|
2115
2443
|
if (res.signal === "SIGKILL") {
|
|
2116
2444
|
throw new CdpError(`lighthouse_audit timed out after ${timeoutMs}ms (no report written). stderr: ${truncate(res.stderr)}`);
|
|
2117
2445
|
}
|
|
2118
|
-
const hint = /not found|could not determine executable|npm ERR|E404|ERR_MODULE/i.test(res.stderr) ? " The Lighthouse CLI could not be fetched/installed via npx
|
|
2446
|
+
const hint = /not found|could not determine executable|npm ERR|E404|ERR_MODULE/i.test(res.stderr) ? " The Lighthouse CLI could not be fetched/installed via npx; ensure network access and that 'lighthouse' is installable." : "";
|
|
2119
2447
|
throw new CdpError(`lighthouse_audit produced no report (exit code ${res.code}).${hint} stderr: ${truncate(res.stderr)}`);
|
|
2120
2448
|
}
|
|
2121
2449
|
let report;
|
|
@@ -2189,7 +2517,10 @@ var TOOLS = {
|
|
|
2189
2517
|
lighthouse_audit: lighthouseAudit,
|
|
2190
2518
|
mock_request: mockRequest,
|
|
2191
2519
|
list_mocks: listMocks,
|
|
2192
|
-
clear_mocks: clearMocks
|
|
2520
|
+
clear_mocks: clearMocks,
|
|
2521
|
+
claim_page: onCdp(LEASE_TOOLS.claim_page),
|
|
2522
|
+
release_page: onCdp(LEASE_TOOLS.release_page),
|
|
2523
|
+
list_leases: onCdp(LEASE_TOOLS.list_leases)
|
|
2193
2524
|
};
|
|
2194
2525
|
var TOOL_NAMES = Object.keys(TOOLS);
|
|
2195
2526
|
export {
|