cdp-toolkit 1.1.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +203 -9
- package/CONTRACT.md +7 -5
- package/README.md +113 -33
- package/dist/cli.js +764 -64
- package/dist/index.js +650 -42
- package/dist/mcp.js +1091 -77
- package/package.json +4 -3
- package/src/bidi/driver.ts +167 -4
- package/src/capabilities.ts +1 -1
- package/src/cdp/driver.ts +145 -1
- package/src/cli.ts +47 -22
- package/src/client.ts +25 -7
- package/src/driver.ts +107 -1
- package/src/firefox-tools.ts +6 -3
- package/src/index.ts +14 -2
- package/src/leases-tools.ts +107 -0
- package/src/leases.ts +541 -0
- package/src/manifest.ts +339 -15
- package/src/mcp.ts +27 -19
- package/src/origins.ts +266 -0
- package/src/shared-tools.ts +365 -14
- 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,258 @@
|
|
|
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 defaultLabel() {
|
|
15
|
+
return `pid-${process.pid}`;
|
|
16
|
+
}
|
|
17
|
+
function safeId(id) {
|
|
18
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
19
|
+
}
|
|
20
|
+
function leaseFile(backend, targetId) {
|
|
21
|
+
return join(leaseDir(), `lease-${backend}-${safeId(targetId)}.json`);
|
|
22
|
+
}
|
|
23
|
+
function mintToken(backend, targetId, nonce) {
|
|
24
|
+
return `${backend}:${targetId}:${nonce}`;
|
|
25
|
+
}
|
|
26
|
+
function tokenParts(token) {
|
|
27
|
+
if (typeof token !== "string")
|
|
28
|
+
return;
|
|
29
|
+
const first = token.indexOf(":");
|
|
30
|
+
const last = token.lastIndexOf(":");
|
|
31
|
+
if (first <= 0 || last <= first)
|
|
32
|
+
return;
|
|
33
|
+
const backend = token.slice(0, first);
|
|
34
|
+
if (backend !== "chrome" && backend !== "firefox")
|
|
35
|
+
return;
|
|
36
|
+
const targetId = token.slice(first + 1, last);
|
|
37
|
+
const nonce = token.slice(last + 1);
|
|
38
|
+
if (!targetId.length || !/^[0-9a-f]{24}$/.test(nonce))
|
|
39
|
+
return;
|
|
40
|
+
return { backend, targetId, nonce };
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
class LeaseConflictError extends Error {
|
|
44
|
+
targetId;
|
|
45
|
+
holder;
|
|
46
|
+
constructor(message, targetId, holder) {
|
|
47
|
+
super(message);
|
|
48
|
+
this.targetId = targetId;
|
|
49
|
+
this.holder = holder;
|
|
50
|
+
this.name = "LeaseConflictError";
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
async function readLease(backend, targetId) {
|
|
54
|
+
let raw;
|
|
55
|
+
try {
|
|
56
|
+
raw = await readFile(leaseFile(backend, targetId), "utf8");
|
|
57
|
+
} catch (err) {
|
|
58
|
+
if (err.code === "ENOENT")
|
|
59
|
+
return;
|
|
60
|
+
throw err;
|
|
61
|
+
}
|
|
62
|
+
try {
|
|
63
|
+
const rec = JSON.parse(raw);
|
|
64
|
+
if (typeof rec?.nonce === "string" && typeof rec?.pid === "number")
|
|
65
|
+
return rec;
|
|
66
|
+
} catch {}
|
|
67
|
+
return;
|
|
68
|
+
}
|
|
69
|
+
function isPidAlive(pid) {
|
|
70
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
71
|
+
return false;
|
|
72
|
+
try {
|
|
73
|
+
process.kill(pid, 0);
|
|
74
|
+
return true;
|
|
75
|
+
} catch (err) {
|
|
76
|
+
return err.code === "EPERM";
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function staleReason(rec, opts) {
|
|
80
|
+
if (!isPidAlive(rec.pid))
|
|
81
|
+
return "dead-pid";
|
|
82
|
+
if (opts.now - rec.lastUsedAt > rec.ttlMs)
|
|
83
|
+
return "expired";
|
|
84
|
+
if (opts.liveIds && !opts.liveIds.includes(rec.targetId))
|
|
85
|
+
return "target-gone";
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
async function claimLease(backend, targetId, opts) {
|
|
89
|
+
const now = opts.now ?? Date.now();
|
|
90
|
+
await mkdir(leaseDir(), { recursive: true });
|
|
91
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
92
|
+
const record = {
|
|
93
|
+
backend,
|
|
94
|
+
targetId,
|
|
95
|
+
nonce: randomBytes(12).toString("hex"),
|
|
96
|
+
pid: process.pid,
|
|
97
|
+
label: opts.label,
|
|
98
|
+
createdAt: now,
|
|
99
|
+
lastUsedAt: now,
|
|
100
|
+
ttlMs: opts.ttlMs ?? leaseTtlMs()
|
|
101
|
+
};
|
|
102
|
+
try {
|
|
103
|
+
await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
|
|
104
|
+
return { token: mintToken(backend, targetId, record.nonce), record };
|
|
105
|
+
} catch (err) {
|
|
106
|
+
if (err.code !== "EEXIST")
|
|
107
|
+
throw err;
|
|
108
|
+
}
|
|
109
|
+
const existing = await readLease(backend, targetId);
|
|
110
|
+
if (!existing)
|
|
111
|
+
continue;
|
|
112
|
+
const stale = staleReason(existing, { now, liveIds: opts.liveIds });
|
|
113
|
+
if (!stale) {
|
|
114
|
+
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);
|
|
115
|
+
}
|
|
116
|
+
await unlink(leaseFile(backend, targetId)).catch(() => {
|
|
117
|
+
return;
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
throw new LeaseConflictError(`target '${targetId}' could not be claimed after 3 attempts: another process is claiming and releasing it concurrently`, targetId);
|
|
121
|
+
}
|
|
122
|
+
async function releaseLease(token) {
|
|
123
|
+
const parts = tokenParts(token);
|
|
124
|
+
if (!parts)
|
|
125
|
+
return { released: false };
|
|
126
|
+
const rec = await readLease(parts.backend, parts.targetId);
|
|
127
|
+
if (!rec || rec.nonce !== parts.nonce)
|
|
128
|
+
return { released: false };
|
|
129
|
+
await unlink(leaseFile(parts.backend, parts.targetId)).catch(() => {
|
|
130
|
+
return;
|
|
131
|
+
});
|
|
132
|
+
return { released: true };
|
|
133
|
+
}
|
|
134
|
+
async function releaseLeaseFor(backend, targetId) {
|
|
135
|
+
if (!await readLease(backend, targetId))
|
|
136
|
+
return { released: false };
|
|
137
|
+
await unlink(leaseFile(backend, targetId)).catch(() => {
|
|
138
|
+
return;
|
|
139
|
+
});
|
|
140
|
+
return { released: true };
|
|
141
|
+
}
|
|
142
|
+
var leaseScope = new AsyncLocalStorage;
|
|
143
|
+
function withLeaseScope(lease, fn) {
|
|
144
|
+
return leaseScope.run(lease, fn);
|
|
145
|
+
}
|
|
146
|
+
function currentLease() {
|
|
147
|
+
return leaseScope.getStore();
|
|
148
|
+
}
|
|
149
|
+
function leaseFromArgs(args) {
|
|
150
|
+
if (!args || typeof args !== "object")
|
|
151
|
+
return;
|
|
152
|
+
const lease = args.lease;
|
|
153
|
+
return typeof lease === "string" && lease.length > 0 ? lease : undefined;
|
|
154
|
+
}
|
|
155
|
+
async function touchLease(rec, now = Date.now()) {
|
|
156
|
+
const current = await readLease(rec.backend, rec.targetId).catch(() => {
|
|
157
|
+
return;
|
|
158
|
+
});
|
|
159
|
+
if (!current || current.nonce !== rec.nonce)
|
|
160
|
+
return;
|
|
161
|
+
await writeFile(leaseFile(current.backend, current.targetId), JSON.stringify({ ...current, lastUsedAt: now }), "utf8").catch(() => {
|
|
162
|
+
return;
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
function describeTarget(targetId, ctx) {
|
|
166
|
+
const url = ctx.url ? ` (url ${ctx.url})` : ctx.title ? ` (title ${JSON.stringify(ctx.title)})` : "";
|
|
167
|
+
return `target '${targetId}'${url}`;
|
|
168
|
+
}
|
|
169
|
+
async function assertLeaseOk(backend, targetId, ctx = {}) {
|
|
170
|
+
const token = ctx.lease ?? currentLease();
|
|
171
|
+
const now = ctx.now ?? Date.now();
|
|
172
|
+
const rec = await readLease(backend, targetId);
|
|
173
|
+
const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
|
|
174
|
+
const where = describeTarget(targetId, ctx);
|
|
175
|
+
if (token === undefined) {
|
|
176
|
+
if (!held)
|
|
177
|
+
return;
|
|
178
|
+
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);
|
|
179
|
+
}
|
|
180
|
+
const parts = tokenParts(token);
|
|
181
|
+
if (!parts) {
|
|
182
|
+
throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
|
|
183
|
+
}
|
|
184
|
+
if (parts.backend !== backend || parts.targetId !== targetId) {
|
|
185
|
+
if (!held)
|
|
186
|
+
return;
|
|
187
|
+
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);
|
|
188
|
+
}
|
|
189
|
+
if (!held) {
|
|
190
|
+
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);
|
|
191
|
+
}
|
|
192
|
+
if (parts.nonce !== held.nonce) {
|
|
193
|
+
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);
|
|
194
|
+
}
|
|
195
|
+
await touchLease(held, now);
|
|
196
|
+
}
|
|
197
|
+
var LEASE_FILE_RE = /^lease-(chrome|firefox)-(.+)\.json$/;
|
|
198
|
+
async function listLeases(opts = {}) {
|
|
199
|
+
const now = opts.now ?? Date.now();
|
|
200
|
+
let names = [];
|
|
201
|
+
try {
|
|
202
|
+
names = await readdir(leaseDir());
|
|
203
|
+
} catch (err) {
|
|
204
|
+
if (err.code === "ENOENT")
|
|
205
|
+
return [];
|
|
206
|
+
throw err;
|
|
207
|
+
}
|
|
208
|
+
const out = [];
|
|
209
|
+
for (const name of names) {
|
|
210
|
+
const parts = LEASE_FILE_RE.exec(name);
|
|
211
|
+
if (!parts)
|
|
212
|
+
continue;
|
|
213
|
+
const fromName = {
|
|
214
|
+
backend: parts[1],
|
|
215
|
+
targetId: parts[2],
|
|
216
|
+
label: "",
|
|
217
|
+
pid: 0,
|
|
218
|
+
createdAt: 0,
|
|
219
|
+
lastUsedAt: 0,
|
|
220
|
+
ttlMs: 0,
|
|
221
|
+
pidAlive: false,
|
|
222
|
+
stale: false
|
|
223
|
+
};
|
|
224
|
+
let rec;
|
|
225
|
+
try {
|
|
226
|
+
rec = JSON.parse(await readFile(join(leaseDir(), name), "utf8"));
|
|
227
|
+
} catch (err) {
|
|
228
|
+
const code = err.code;
|
|
229
|
+
if (code === "ENOENT")
|
|
230
|
+
continue;
|
|
231
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
232
|
+
continue;
|
|
233
|
+
}
|
|
234
|
+
if (typeof rec?.nonce !== "string" || typeof rec?.pid !== "number") {
|
|
235
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
236
|
+
continue;
|
|
237
|
+
}
|
|
238
|
+
out.push({
|
|
239
|
+
backend: rec.backend,
|
|
240
|
+
targetId: rec.targetId,
|
|
241
|
+
label: rec.label,
|
|
242
|
+
pid: rec.pid,
|
|
243
|
+
createdAt: rec.createdAt,
|
|
244
|
+
lastUsedAt: rec.lastUsedAt,
|
|
245
|
+
ttlMs: rec.ttlMs,
|
|
246
|
+
pidAlive: isPidAlive(rec.pid),
|
|
247
|
+
stale: staleReason(rec, {
|
|
248
|
+
now,
|
|
249
|
+
liveIds: opts.liveBackend === undefined || opts.liveBackend === rec.backend ? opts.liveIds : undefined
|
|
250
|
+
})
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
return out;
|
|
254
|
+
}
|
|
255
|
+
|
|
1
256
|
// src/client.ts
|
|
2
257
|
var BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
3
258
|
var DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
|
|
@@ -151,8 +406,13 @@ async function browserWsUrl() {
|
|
|
151
406
|
const v = await httpJson("/json/version");
|
|
152
407
|
return v.webSocketDebuggerUrl;
|
|
153
408
|
}
|
|
154
|
-
async function resolveTarget(selector) {
|
|
409
|
+
async function resolveTarget(selector, opts = {}) {
|
|
155
410
|
const targets = await listTargets();
|
|
411
|
+
const hit = pickTarget(targets, selector);
|
|
412
|
+
await assertLeaseOk("chrome", hit.id, { lease: opts.lease, url: hit.url, title: hit.title });
|
|
413
|
+
return hit;
|
|
414
|
+
}
|
|
415
|
+
function pickTarget(targets, selector) {
|
|
156
416
|
const pages = targets.filter((t) => t.type === "page");
|
|
157
417
|
if (!selector || selector === "active") {
|
|
158
418
|
if (!pages.length)
|
|
@@ -188,7 +448,7 @@ async function openBrowser(opts = {}) {
|
|
|
188
448
|
return new CdpConnection(await browserWsUrl(), opts).connect();
|
|
189
449
|
}
|
|
190
450
|
async function openPage(selector, opts = {}) {
|
|
191
|
-
const target = await resolveTarget(selector);
|
|
451
|
+
const target = await resolveTarget(selector, { lease: opts.lease });
|
|
192
452
|
const conn = await new CdpConnection(target.webSocketDebuggerUrl, opts).connect();
|
|
193
453
|
return { conn, target };
|
|
194
454
|
}
|
|
@@ -445,6 +705,31 @@ function decodeUid(uid) {
|
|
|
445
705
|
throw driverError("foreign-uid", `malformed cdp uid payload: ${uid}`);
|
|
446
706
|
return n;
|
|
447
707
|
}
|
|
708
|
+
function normalizeCdpCookie(c) {
|
|
709
|
+
const same = (c.sameSite ?? "").toLowerCase();
|
|
710
|
+
const sameSite = same === "strict" || same === "lax" || same === "none" ? same : "default";
|
|
711
|
+
return {
|
|
712
|
+
name: c.name,
|
|
713
|
+
value: c.value,
|
|
714
|
+
domain: c.domain,
|
|
715
|
+
path: c.path,
|
|
716
|
+
expires: typeof c.expires === "number" ? c.expires : -1,
|
|
717
|
+
size: typeof c.size === "number" ? c.size : Buffer.byteLength(`${c.name}${c.value}`, "utf8"),
|
|
718
|
+
httpOnly: c.httpOnly === true,
|
|
719
|
+
secure: c.secure === true,
|
|
720
|
+
sameSite,
|
|
721
|
+
session: c.session === true
|
|
722
|
+
};
|
|
723
|
+
}
|
|
724
|
+
function cdpSameSite(sameSite) {
|
|
725
|
+
if (sameSite === "strict")
|
|
726
|
+
return "Strict";
|
|
727
|
+
if (sameSite === "lax")
|
|
728
|
+
return "Lax";
|
|
729
|
+
if (sameSite === "none")
|
|
730
|
+
return "None";
|
|
731
|
+
return;
|
|
732
|
+
}
|
|
448
733
|
async function centerOf(conn, objectId) {
|
|
449
734
|
const fn = "function(){this.scrollIntoView({block:'center',inline:'center'});const r=this.getBoundingClientRect();if(r.width===0&&r.height===0)return null;return {x:r.left+r.width/2,y:r.top+r.height/2};}";
|
|
450
735
|
const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
@@ -967,6 +1252,38 @@ class CdpPageDriver {
|
|
|
967
1252
|
});
|
|
968
1253
|
});
|
|
969
1254
|
}
|
|
1255
|
+
async getCookies() {
|
|
1256
|
+
await this.ensureDomain("Network");
|
|
1257
|
+
const { cookies } = await this.conn.send("Network.getCookies");
|
|
1258
|
+
return (cookies ?? []).map(normalizeCdpCookie);
|
|
1259
|
+
}
|
|
1260
|
+
async setCookie(params) {
|
|
1261
|
+
await this.ensureDomain("Network");
|
|
1262
|
+
const sameSite = cdpSameSite(params.sameSite);
|
|
1263
|
+
const { success } = await this.conn.send("Network.setCookie", {
|
|
1264
|
+
name: params.name,
|
|
1265
|
+
value: params.value,
|
|
1266
|
+
...params.url !== undefined ? { url: params.url } : {},
|
|
1267
|
+
...params.domain !== undefined ? { domain: params.domain } : {},
|
|
1268
|
+
...params.path !== undefined ? { path: params.path } : {},
|
|
1269
|
+
...params.expires !== undefined ? { expires: params.expires } : {},
|
|
1270
|
+
...params.httpOnly !== undefined ? { httpOnly: params.httpOnly } : {},
|
|
1271
|
+
...params.secure !== undefined ? { secure: params.secure } : {},
|
|
1272
|
+
...sameSite !== undefined ? { sameSite } : {}
|
|
1273
|
+
});
|
|
1274
|
+
if (success !== true) {
|
|
1275
|
+
throw driverError("page-error", `Network.setCookie declined the cookie '${params.name}'. Chrome rejects a cookie whose domain does not match the url, a secure cookie on an insecure origin, and an oversized name or value.`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
async deleteCookies(filter) {
|
|
1279
|
+
await this.ensureDomain("Network");
|
|
1280
|
+
await this.conn.send("Network.deleteCookies", {
|
|
1281
|
+
name: filter.name,
|
|
1282
|
+
...filter.url !== undefined ? { url: filter.url } : {},
|
|
1283
|
+
...filter.domain !== undefined ? { domain: filter.domain } : {},
|
|
1284
|
+
...filter.path !== undefined ? { path: filter.path } : {}
|
|
1285
|
+
});
|
|
1286
|
+
}
|
|
970
1287
|
async startBodyCapture() {
|
|
971
1288
|
await this.ensureDomain("Network");
|
|
972
1289
|
return async () => {
|
|
@@ -1026,6 +1343,10 @@ async function withBrowserConn(fn) {
|
|
|
1026
1343
|
}
|
|
1027
1344
|
}
|
|
1028
1345
|
function noSuchTarget(e) {
|
|
1346
|
+
if (e instanceof LeaseConflictError)
|
|
1347
|
+
throw e;
|
|
1348
|
+
if (typeof e?.code === "string")
|
|
1349
|
+
throw e;
|
|
1029
1350
|
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
1030
1351
|
}
|
|
1031
1352
|
|
|
@@ -1069,9 +1390,154 @@ function createCdpDriver() {
|
|
|
1069
1390
|
return new CdpBrowserDriver;
|
|
1070
1391
|
}
|
|
1071
1392
|
|
|
1393
|
+
// src/origins.ts
|
|
1394
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1395
|
+
import { join as join2 } from "node:path";
|
|
1396
|
+
function safeId2(id) {
|
|
1397
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1398
|
+
}
|
|
1399
|
+
function originFile(backend, targetId) {
|
|
1400
|
+
return join2(leaseDir(), `origin-${backend}-${safeId2(targetId)}.json`);
|
|
1401
|
+
}
|
|
1402
|
+
var ORIGIN_FILE_RE = /^origin-(chrome|firefox)-(.+)\.json$/;
|
|
1403
|
+
async function recordOrigin(backend, targetId, opts = {}) {
|
|
1404
|
+
const record = {
|
|
1405
|
+
backend,
|
|
1406
|
+
targetId,
|
|
1407
|
+
label: typeof opts.label === "string" && opts.label.length ? opts.label : defaultLabel(),
|
|
1408
|
+
pid: process.pid,
|
|
1409
|
+
createdAt: opts.now ?? Date.now()
|
|
1410
|
+
};
|
|
1411
|
+
const file = originFile(backend, targetId);
|
|
1412
|
+
try {
|
|
1413
|
+
await mkdir2(leaseDir(), { recursive: true });
|
|
1414
|
+
try {
|
|
1415
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1416
|
+
return;
|
|
1417
|
+
} catch (err) {
|
|
1418
|
+
if (err.code !== "EEXIST")
|
|
1419
|
+
throw err;
|
|
1420
|
+
}
|
|
1421
|
+
await unlink2(file).catch(() => {
|
|
1422
|
+
return;
|
|
1423
|
+
});
|
|
1424
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1425
|
+
} catch {}
|
|
1426
|
+
}
|
|
1427
|
+
async function listOrigins(opts = {}) {
|
|
1428
|
+
let names = [];
|
|
1429
|
+
try {
|
|
1430
|
+
names = await readdir2(leaseDir());
|
|
1431
|
+
} catch {
|
|
1432
|
+
return [];
|
|
1433
|
+
}
|
|
1434
|
+
const live = opts.liveIds === undefined ? undefined : new Set(opts.liveIds.map((id) => safeId2(id)));
|
|
1435
|
+
const out = [];
|
|
1436
|
+
for (const name of names) {
|
|
1437
|
+
const parts = ORIGIN_FILE_RE.exec(name);
|
|
1438
|
+
if (!parts)
|
|
1439
|
+
continue;
|
|
1440
|
+
const backend = parts[1];
|
|
1441
|
+
const nameId = parts[2];
|
|
1442
|
+
const scoped = live !== undefined && (opts.liveBackend === undefined || opts.liveBackend === backend);
|
|
1443
|
+
if (scoped && !live.has(nameId)) {
|
|
1444
|
+
await unlink2(join2(leaseDir(), name)).catch(() => {
|
|
1445
|
+
return;
|
|
1446
|
+
});
|
|
1447
|
+
continue;
|
|
1448
|
+
}
|
|
1449
|
+
const fromName = { backend, targetId: nameId, label: "", pid: 0, createdAt: 0 };
|
|
1450
|
+
let rec;
|
|
1451
|
+
try {
|
|
1452
|
+
rec = JSON.parse(await readFile2(join2(leaseDir(), name), "utf8"));
|
|
1453
|
+
} catch (err) {
|
|
1454
|
+
const code = err.code;
|
|
1455
|
+
if (code === "ENOENT")
|
|
1456
|
+
continue;
|
|
1457
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
1458
|
+
continue;
|
|
1459
|
+
}
|
|
1460
|
+
if (typeof rec?.targetId !== "string" || typeof rec?.label !== "string" || typeof rec?.createdAt !== "number") {
|
|
1461
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
1462
|
+
continue;
|
|
1463
|
+
}
|
|
1464
|
+
out.push({ backend: rec.backend, targetId: rec.targetId, label: rec.label, pid: rec.pid, createdAt: rec.createdAt });
|
|
1465
|
+
}
|
|
1466
|
+
return out;
|
|
1467
|
+
}
|
|
1468
|
+
async function originIndex(backend, liveIds) {
|
|
1469
|
+
const rows = await listOrigins({ liveIds, liveBackend: backend }).catch(() => []);
|
|
1470
|
+
const index = new Map;
|
|
1471
|
+
for (const row of rows) {
|
|
1472
|
+
if (row.backend !== backend)
|
|
1473
|
+
continue;
|
|
1474
|
+
index.set(row.targetId, row);
|
|
1475
|
+
}
|
|
1476
|
+
return index;
|
|
1477
|
+
}
|
|
1478
|
+
async function newTrackedPage(driver, backend, opts = {}) {
|
|
1479
|
+
const page = await driver.newPage(opts.url);
|
|
1480
|
+
await recordOrigin(backend, page.id, { label: opts.label });
|
|
1481
|
+
return page;
|
|
1482
|
+
}
|
|
1483
|
+
|
|
1484
|
+
// src/leases-tools.ts
|
|
1485
|
+
class LeaseToolError extends Error {
|
|
1486
|
+
}
|
|
1487
|
+
function backendOf(driver) {
|
|
1488
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
1489
|
+
}
|
|
1490
|
+
async function claimPage(driver, args = {}) {
|
|
1491
|
+
const backend = backendOf(driver);
|
|
1492
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1493
|
+
const pages = await driver.listPages();
|
|
1494
|
+
let targetId = args.targetId;
|
|
1495
|
+
let url = args.url ?? "about:blank";
|
|
1496
|
+
if (targetId === undefined || targetId === "") {
|
|
1497
|
+
const page = await newTrackedPage(driver, backend, { url: args.url, label });
|
|
1498
|
+
targetId = page.id;
|
|
1499
|
+
url = page.url;
|
|
1500
|
+
} else {
|
|
1501
|
+
const existing = pages.find((p) => p.id === targetId);
|
|
1502
|
+
if (!existing)
|
|
1503
|
+
throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
|
|
1504
|
+
url = existing.url;
|
|
1505
|
+
}
|
|
1506
|
+
const liveIds = [...pages.map((p) => p.id), targetId];
|
|
1507
|
+
const { record, token } = await claimLease(backend, targetId, {
|
|
1508
|
+
label,
|
|
1509
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
1510
|
+
liveIds
|
|
1511
|
+
});
|
|
1512
|
+
return {
|
|
1513
|
+
lease: token,
|
|
1514
|
+
targetId,
|
|
1515
|
+
url,
|
|
1516
|
+
label: record.label,
|
|
1517
|
+
ttlMs: record.ttlMs,
|
|
1518
|
+
expiresAt: record.lastUsedAt + record.ttlMs
|
|
1519
|
+
};
|
|
1520
|
+
}
|
|
1521
|
+
async function releasePage(_driver, args) {
|
|
1522
|
+
if (typeof args.lease !== "string" || !args.lease.length) {
|
|
1523
|
+
throw new LeaseToolError("release_page requires the 'lease' token returned by claim_page");
|
|
1524
|
+
}
|
|
1525
|
+
return releaseLease(args.lease);
|
|
1526
|
+
}
|
|
1527
|
+
async function listLeasesTool(driver, _args = {}) {
|
|
1528
|
+
const liveIds = (await driver.listPages()).map((p) => p.id);
|
|
1529
|
+
const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
|
|
1530
|
+
return { leases, count: leases.length };
|
|
1531
|
+
}
|
|
1532
|
+
var LEASE_TOOLS = {
|
|
1533
|
+
claim_page: claimPage,
|
|
1534
|
+
release_page: releasePage,
|
|
1535
|
+
list_leases: listLeasesTool
|
|
1536
|
+
};
|
|
1537
|
+
|
|
1072
1538
|
// src/shared-tools.ts
|
|
1073
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1074
|
-
import { join } from "node:path";
|
|
1539
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
1540
|
+
import { dirname, join as join3 } from "node:path";
|
|
1075
1541
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1076
1542
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1077
1543
|
|
|
@@ -1080,6 +1546,9 @@ class SharedToolError extends Error {
|
|
|
1080
1546
|
function target3(p) {
|
|
1081
1547
|
return { id: p.id, url: p.url, title: p.title };
|
|
1082
1548
|
}
|
|
1549
|
+
function backendOf2(driver) {
|
|
1550
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
1551
|
+
}
|
|
1083
1552
|
async function withPage2(driver, target, fn) {
|
|
1084
1553
|
const page = await driver.page(target);
|
|
1085
1554
|
try {
|
|
@@ -1090,6 +1559,11 @@ async function withPage2(driver, target, fn) {
|
|
|
1090
1559
|
}
|
|
1091
1560
|
async function resolvePage(driver, selector) {
|
|
1092
1561
|
const pages = await driver.listPages();
|
|
1562
|
+
const hit = await pickPage(driver, pages, selector);
|
|
1563
|
+
await assertLeaseOk(backendOf2(driver), hit.id, { url: hit.url, title: hit.title });
|
|
1564
|
+
return hit;
|
|
1565
|
+
}
|
|
1566
|
+
async function pickPage(driver, pages, selector) {
|
|
1093
1567
|
if (selector === undefined || selector === "active") {
|
|
1094
1568
|
if (!pages.length)
|
|
1095
1569
|
throw new SharedToolError("no page targets open");
|
|
@@ -1131,24 +1605,52 @@ function locatorOf(args) {
|
|
|
1131
1605
|
throw new SharedToolError("provide exactly one of { uid } or { selector }");
|
|
1132
1606
|
return hasUid ? { uid: args.uid } : { css: args.selector };
|
|
1133
1607
|
}
|
|
1608
|
+
async function reapSet(driver, pages, all) {
|
|
1609
|
+
if (all === true)
|
|
1610
|
+
return pages.map((p) => p.id);
|
|
1611
|
+
try {
|
|
1612
|
+
return (await driver.listPages({ all: true })).map((p) => p.id);
|
|
1613
|
+
} catch {
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
}
|
|
1134
1617
|
async function listPages(driver, args = {}) {
|
|
1135
1618
|
const pages = await driver.listPages({ all: args.all });
|
|
1136
|
-
|
|
1619
|
+
const ledger = await originIndex(backendOf2(driver), await reapSet(driver, pages, args.all));
|
|
1620
|
+
const annotated = pages.map((p) => {
|
|
1621
|
+
const rec = ledger.get(p.id);
|
|
1622
|
+
if (!rec)
|
|
1623
|
+
return { ...p, origin: "unknown" };
|
|
1624
|
+
if (rec.unreadable !== undefined)
|
|
1625
|
+
return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
|
|
1626
|
+
return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
|
|
1627
|
+
});
|
|
1628
|
+
return { pages: annotated, count: annotated.length };
|
|
1137
1629
|
}
|
|
1138
1630
|
async function newPage(driver, args = {}) {
|
|
1139
|
-
const
|
|
1140
|
-
|
|
1631
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1632
|
+
const p = await newTrackedPage(driver, backendOf2(driver), { url: args.url, label });
|
|
1633
|
+
if (args.claim !== true)
|
|
1634
|
+
return { targetId: p.id, url: p.url };
|
|
1635
|
+
const { record, token } = await claimLease(backendOf2(driver), p.id, {
|
|
1636
|
+
label,
|
|
1637
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
|
|
1638
|
+
});
|
|
1639
|
+
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
1141
1640
|
}
|
|
1142
1641
|
async function closePage(driver, args) {
|
|
1143
1642
|
if (args.target === undefined || args.target === "")
|
|
1144
1643
|
throw new SharedToolError("close_page requires an explicit target; refusing to guess which page to close");
|
|
1145
1644
|
const p = await resolvePage(driver, args.target);
|
|
1146
1645
|
const res = await driver.closePage(p.id);
|
|
1147
|
-
|
|
1646
|
+
if (!res.success)
|
|
1647
|
+
return { closed: p.id, success: false, leaseReleased: false };
|
|
1648
|
+
const released = await releaseLeaseFor(backendOf2(driver), p.id);
|
|
1649
|
+
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
1148
1650
|
}
|
|
1149
1651
|
async function writeSelectedFile(id) {
|
|
1150
|
-
await
|
|
1151
|
-
await
|
|
1652
|
+
await mkdir3(STATE_DIR, { recursive: true });
|
|
1653
|
+
await writeFile3(join3(STATE_DIR, "selected"), id, "utf8");
|
|
1152
1654
|
}
|
|
1153
1655
|
async function selectPage(driver, args) {
|
|
1154
1656
|
if (args.target === undefined || args.target === "")
|
|
@@ -1188,11 +1690,108 @@ async function waitForText(driver, args) {
|
|
|
1188
1690
|
return { found: true, waitedMs: r.elapsedMs };
|
|
1189
1691
|
});
|
|
1190
1692
|
}
|
|
1693
|
+
function serializeForSink(value) {
|
|
1694
|
+
const json = JSON.stringify(value === undefined ? null : value, null, 2);
|
|
1695
|
+
return json ?? "null";
|
|
1696
|
+
}
|
|
1697
|
+
async function writeJsonSink(savePath, value) {
|
|
1698
|
+
const path = savePath.startsWith("/") ? savePath : join3(ARTIFACT_DIR, savePath);
|
|
1699
|
+
await mkdir3(dirname(path), { recursive: true });
|
|
1700
|
+
const json = serializeForSink(value);
|
|
1701
|
+
await writeFile3(path, json, "utf8");
|
|
1702
|
+
return { path, bytes: Buffer.byteLength(json, "utf8") };
|
|
1703
|
+
}
|
|
1704
|
+
function sinkRequested(savePath) {
|
|
1705
|
+
return savePath !== undefined && savePath !== "";
|
|
1706
|
+
}
|
|
1191
1707
|
async function evaluateScript(driver, args) {
|
|
1192
1708
|
if (typeof args.expression !== "string" || args.expression.length === 0) {
|
|
1193
1709
|
throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
|
|
1194
1710
|
}
|
|
1195
|
-
return withPage2(driver, args.target, (page) =>
|
|
1711
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1712
|
+
const value = await page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true });
|
|
1713
|
+
if (!sinkRequested(args.savePath))
|
|
1714
|
+
return value;
|
|
1715
|
+
const { path, bytes } = await writeJsonSink(args.savePath, value);
|
|
1716
|
+
const result = {
|
|
1717
|
+
path,
|
|
1718
|
+
bytes,
|
|
1719
|
+
type: value === null ? "null" : typeof value,
|
|
1720
|
+
target: target3(page.info)
|
|
1721
|
+
};
|
|
1722
|
+
return result;
|
|
1723
|
+
});
|
|
1724
|
+
}
|
|
1725
|
+
function cookieDomainMatches(cookieDomain, filter) {
|
|
1726
|
+
const bare = (d) => (d.startsWith(".") ? d.slice(1) : d).toLowerCase();
|
|
1727
|
+
const c = bare(cookieDomain);
|
|
1728
|
+
const f = bare(filter);
|
|
1729
|
+
return c === f || c.endsWith(`.${f}`);
|
|
1730
|
+
}
|
|
1731
|
+
function filterCookies(cookies, filter) {
|
|
1732
|
+
return cookies.filter((c) => {
|
|
1733
|
+
if (filter.name !== undefined && filter.name !== "" && c.name !== filter.name)
|
|
1734
|
+
return false;
|
|
1735
|
+
if (filter.domain !== undefined && filter.domain !== "" && !cookieDomainMatches(c.domain, filter.domain))
|
|
1736
|
+
return false;
|
|
1737
|
+
return true;
|
|
1738
|
+
});
|
|
1739
|
+
}
|
|
1740
|
+
async function listCookies(driver, args = {}) {
|
|
1741
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1742
|
+
const all = await page.getCookies();
|
|
1743
|
+
const cookies = filterCookies(all, { domain: args.domain, name: args.name });
|
|
1744
|
+
if (!sinkRequested(args.savePath)) {
|
|
1745
|
+
return { cookies, count: cookies.length, target: target3(page.info) };
|
|
1746
|
+
}
|
|
1747
|
+
const { path, bytes } = await writeJsonSink(args.savePath, cookies);
|
|
1748
|
+
return { path, bytes, count: cookies.length, target: target3(page.info) };
|
|
1749
|
+
});
|
|
1750
|
+
}
|
|
1751
|
+
function assertCookieSite(args, toolName) {
|
|
1752
|
+
const hasUrl = args.url !== undefined && args.url !== "";
|
|
1753
|
+
const hasDomain = args.domain !== undefined && args.domain !== "";
|
|
1754
|
+
if (!hasUrl && !hasDomain) {
|
|
1755
|
+
throw new Error(`${toolName} requires either 'url' or 'domain': a cookie has to be attributed to a site, and neither was given.`);
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
function assertCookieName(name, toolName) {
|
|
1759
|
+
if (name === undefined || name === "") {
|
|
1760
|
+
throw new Error(`${toolName} requires a non-empty 'name'.`);
|
|
1761
|
+
}
|
|
1762
|
+
}
|
|
1763
|
+
async function setCookie(driver, args) {
|
|
1764
|
+
assertCookieName(args.name, "set_cookie");
|
|
1765
|
+
if (typeof args.value !== "string")
|
|
1766
|
+
throw new Error("set_cookie requires a string 'value'.");
|
|
1767
|
+
assertCookieSite(args, "set_cookie");
|
|
1768
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1769
|
+
await page.setCookie({
|
|
1770
|
+
name: args.name,
|
|
1771
|
+
value: args.value,
|
|
1772
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1773
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1774
|
+
...args.path !== undefined ? { path: args.path } : {},
|
|
1775
|
+
...args.expires !== undefined ? { expires: args.expires } : {},
|
|
1776
|
+
...args.httpOnly !== undefined ? { httpOnly: args.httpOnly } : {},
|
|
1777
|
+
...args.secure !== undefined ? { secure: args.secure } : {},
|
|
1778
|
+
...args.sameSite !== undefined ? { sameSite: args.sameSite } : {}
|
|
1779
|
+
});
|
|
1780
|
+
return { set: true, target: target3(page.info) };
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
async function deleteCookies(driver, args) {
|
|
1784
|
+
assertCookieName(args.name, "delete_cookies");
|
|
1785
|
+
assertCookieSite(args, "delete_cookies");
|
|
1786
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1787
|
+
await page.deleteCookies({
|
|
1788
|
+
name: args.name,
|
|
1789
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1790
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1791
|
+
...args.path !== undefined ? { path: args.path } : {}
|
|
1792
|
+
});
|
|
1793
|
+
return { deleted: true, target: target3(page.info) };
|
|
1794
|
+
});
|
|
1196
1795
|
}
|
|
1197
1796
|
var BARE_TOKEN_EXTRAS = new Set(["disabled", "focused"]);
|
|
1198
1797
|
var UNQUOTED_EXTRAS = new Set(["checked", "expanded", "url"]);
|
|
@@ -1303,10 +1902,10 @@ async function takeScreenshot(driver, args = {}) {
|
|
|
1303
1902
|
const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
|
|
1304
1903
|
if (data.byteLength === 0)
|
|
1305
1904
|
throw new SharedToolError("captureScreenshot returned empty data");
|
|
1306
|
-
await
|
|
1905
|
+
await mkdir3(ARTIFACT_DIR, { recursive: true });
|
|
1307
1906
|
const ext = outFormat === "jpeg" ? "jpg" : "png";
|
|
1308
|
-
const path = args.savePath ??
|
|
1309
|
-
await
|
|
1907
|
+
const path = args.savePath ?? join3(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
1908
|
+
await writeFile3(path, Buffer.from(data));
|
|
1310
1909
|
const result = {
|
|
1311
1910
|
path,
|
|
1312
1911
|
bytes: data.byteLength,
|
|
@@ -1353,6 +1952,9 @@ var SHARED_TOOLS = {
|
|
|
1353
1952
|
navigate_page: navigatePage,
|
|
1354
1953
|
wait_for: waitForText,
|
|
1355
1954
|
evaluate_script: evaluateScript,
|
|
1955
|
+
list_cookies: listCookies,
|
|
1956
|
+
set_cookie: setCookie,
|
|
1957
|
+
delete_cookies: deleteCookies,
|
|
1356
1958
|
take_snapshot: takeSnapshot,
|
|
1357
1959
|
click,
|
|
1358
1960
|
hover,
|
|
@@ -1369,10 +1971,10 @@ var SHARED_TOOLS = {
|
|
|
1369
1971
|
};
|
|
1370
1972
|
|
|
1371
1973
|
// src/tools/console.ts
|
|
1372
|
-
import { readFile } from "node:fs/promises";
|
|
1974
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1373
1975
|
|
|
1374
1976
|
// src/tools/recorder.ts
|
|
1375
|
-
import { mkdir as
|
|
1977
|
+
import { mkdir as mkdir4, appendFile, writeFile as writeFile4, copyFile } from "node:fs/promises";
|
|
1376
1978
|
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1377
1979
|
function recFile(targetId) {
|
|
1378
1980
|
return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
|
|
@@ -1393,11 +1995,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
1393
1995
|
async function startRecorder(target, opts = {}) {
|
|
1394
1996
|
const network = opts.network ?? true;
|
|
1395
1997
|
const consoleCap = opts.console ?? true;
|
|
1396
|
-
await
|
|
1998
|
+
await mkdir4(ARTIFACT_DIR2, { recursive: true });
|
|
1397
1999
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
1398
2000
|
const file = opts.file ?? recFile(resolved.id);
|
|
1399
2001
|
if (opts.truncate) {
|
|
1400
|
-
await
|
|
2002
|
+
await writeFile4(file, "");
|
|
1401
2003
|
}
|
|
1402
2004
|
let chain = Promise.resolve();
|
|
1403
2005
|
let dropped = 0;
|
|
@@ -1539,7 +2141,7 @@ function lineToEntry(line, index) {
|
|
|
1539
2141
|
async function readConsoleEntries(file) {
|
|
1540
2142
|
let raw;
|
|
1541
2143
|
try {
|
|
1542
|
-
raw = await
|
|
2144
|
+
raw = await readFile3(file, "utf8");
|
|
1543
2145
|
} catch {
|
|
1544
2146
|
return [];
|
|
1545
2147
|
}
|
|
@@ -1595,7 +2197,7 @@ async function getConsoleMessage(args = {}) {
|
|
|
1595
2197
|
}
|
|
1596
2198
|
|
|
1597
2199
|
// src/tools/network.ts
|
|
1598
|
-
import { readFile as
|
|
2200
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1599
2201
|
var DEFAULT_CAPTURE_MS2 = 2500;
|
|
1600
2202
|
function asHeaders(v) {
|
|
1601
2203
|
if (!v || typeof v !== "object")
|
|
@@ -1662,7 +2264,7 @@ function correlate(lines) {
|
|
|
1662
2264
|
async function readNetworkRequests(file) {
|
|
1663
2265
|
let raw;
|
|
1664
2266
|
try {
|
|
1665
|
-
raw = await
|
|
2267
|
+
raw = await readFile4(file, "utf8");
|
|
1666
2268
|
} catch {
|
|
1667
2269
|
return [];
|
|
1668
2270
|
}
|
|
@@ -1744,8 +2346,8 @@ async function getNetworkRequest(args = {}) {
|
|
|
1744
2346
|
}
|
|
1745
2347
|
|
|
1746
2348
|
// src/tools/performance.ts
|
|
1747
|
-
import { mkdir as
|
|
1748
|
-
import { join as
|
|
2349
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, stat, unlink as unlink3 } from "node:fs/promises";
|
|
2350
|
+
import { join as join4 } from "node:path";
|
|
1749
2351
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1750
2352
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1751
2353
|
var DEFAULT_CATEGORIES = [
|
|
@@ -1763,7 +2365,7 @@ function shortId(id) {
|
|
|
1763
2365
|
return id.slice(0, 8);
|
|
1764
2366
|
}
|
|
1765
2367
|
function stateFilePath() {
|
|
1766
|
-
return
|
|
2368
|
+
return join4(STATE_DIR2, "trace-state.json");
|
|
1767
2369
|
}
|
|
1768
2370
|
function bufferTraceData(conn, sink) {
|
|
1769
2371
|
return conn.on("Tracing.dataCollected", (params) => {
|
|
@@ -1785,10 +2387,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
1785
2387
|
return sink;
|
|
1786
2388
|
}
|
|
1787
2389
|
async function writeTrace(events, targetId) {
|
|
1788
|
-
await
|
|
1789
|
-
const path =
|
|
2390
|
+
await mkdir5(ARTIFACT_DIR3, { recursive: true });
|
|
2391
|
+
const path = join4(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
1790
2392
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
1791
|
-
await
|
|
2393
|
+
await writeFile5(path, body, "utf8");
|
|
1792
2394
|
const { size } = await stat(path);
|
|
1793
2395
|
return { path, bytes: size };
|
|
1794
2396
|
}
|
|
@@ -1808,7 +2410,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
1808
2410
|
throw err;
|
|
1809
2411
|
}
|
|
1810
2412
|
liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
|
|
1811
|
-
await
|
|
2413
|
+
await mkdir5(STATE_DIR2, { recursive: true });
|
|
1812
2414
|
const state = {
|
|
1813
2415
|
targetId: target.id,
|
|
1814
2416
|
url: target.url,
|
|
@@ -1816,7 +2418,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
1816
2418
|
pid: process.pid,
|
|
1817
2419
|
categories
|
|
1818
2420
|
};
|
|
1819
|
-
await
|
|
2421
|
+
await writeFile5(stateFilePath(), JSON.stringify(state), "utf8");
|
|
1820
2422
|
return {
|
|
1821
2423
|
target: { id: target.id, url: target.url, title: target.title },
|
|
1822
2424
|
categories,
|
|
@@ -1829,7 +2431,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
1829
2431
|
if (!entry) {
|
|
1830
2432
|
let hint = "";
|
|
1831
2433
|
try {
|
|
1832
|
-
const raw = await
|
|
2434
|
+
const raw = await readFile5(stateFilePath(), "utf8");
|
|
1833
2435
|
const state = JSON.parse(raw);
|
|
1834
2436
|
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
2437
|
} catch {}
|
|
@@ -1841,7 +2443,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
1841
2443
|
} finally {
|
|
1842
2444
|
conn.close();
|
|
1843
2445
|
liveTraces.delete(target.id);
|
|
1844
|
-
await
|
|
2446
|
+
await unlink3(stateFilePath()).catch(() => {});
|
|
1845
2447
|
}
|
|
1846
2448
|
const written = await writeTrace(events, target.id);
|
|
1847
2449
|
const metrics = analyzeEvents(events);
|
|
@@ -1959,7 +2561,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
1959
2561
|
}
|
|
1960
2562
|
let raw;
|
|
1961
2563
|
try {
|
|
1962
|
-
raw = await
|
|
2564
|
+
raw = await readFile5(args.tracePath, "utf8");
|
|
1963
2565
|
} catch (err) {
|
|
1964
2566
|
throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
|
|
1965
2567
|
}
|
|
@@ -1976,8 +2578,8 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
1976
2578
|
}
|
|
1977
2579
|
|
|
1978
2580
|
// src/tools/heap.ts
|
|
1979
|
-
import { mkdir as
|
|
1980
|
-
import { join as
|
|
2581
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
|
|
2582
|
+
import { join as join5 } from "node:path";
|
|
1981
2583
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1982
2584
|
function stamp3(targetId) {
|
|
1983
2585
|
const short = targetId.slice(0, 8);
|
|
@@ -2002,9 +2604,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2002
2604
|
if (snapshot.length === 0) {
|
|
2003
2605
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
2004
2606
|
}
|
|
2005
|
-
await
|
|
2006
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
2007
|
-
await
|
|
2607
|
+
await mkdir6(ARTIFACT_DIR4, { recursive: true });
|
|
2608
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join5(ARTIFACT_DIR4, args.savePath) : join5(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
|
|
2609
|
+
await writeFile6(path, snapshot, "utf8");
|
|
2008
2610
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
2009
2611
|
return {
|
|
2010
2612
|
path,
|
|
@@ -2017,7 +2619,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2017
2619
|
|
|
2018
2620
|
// src/tools/lighthouse.ts
|
|
2019
2621
|
import { spawn } from "node:child_process";
|
|
2020
|
-
import { mkdir as
|
|
2622
|
+
import { mkdir as mkdir7, readFile as readFile6, stat as stat2 } from "node:fs/promises";
|
|
2021
2623
|
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2022
2624
|
function cdpPort() {
|
|
2023
2625
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
@@ -2078,7 +2680,7 @@ async function lighthouseAudit(args) {
|
|
|
2078
2680
|
const url = args.url.trim();
|
|
2079
2681
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
2080
2682
|
const port = cdpPort();
|
|
2081
|
-
await
|
|
2683
|
+
await mkdir7(ARTIFACT_DIR5, { recursive: true });
|
|
2082
2684
|
const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
|
|
2083
2685
|
const formFactor = args.formFactor ?? "desktop";
|
|
2084
2686
|
const lhArgs = [
|
|
@@ -2107,7 +2709,7 @@ async function lighthouseAudit(args) {
|
|
|
2107
2709
|
}
|
|
2108
2710
|
let raw;
|
|
2109
2711
|
try {
|
|
2110
|
-
raw = await
|
|
2712
|
+
raw = await readFile6(outPath, "utf8");
|
|
2111
2713
|
} catch {
|
|
2112
2714
|
raw = undefined;
|
|
2113
2715
|
}
|
|
@@ -2115,7 +2717,7 @@ async function lighthouseAudit(args) {
|
|
|
2115
2717
|
if (res.signal === "SIGKILL") {
|
|
2116
2718
|
throw new CdpError(`lighthouse_audit timed out after ${timeoutMs}ms (no report written). stderr: ${truncate(res.stderr)}`);
|
|
2117
2719
|
}
|
|
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
|
|
2720
|
+
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
2721
|
throw new CdpError(`lighthouse_audit produced no report (exit code ${res.code}).${hint} stderr: ${truncate(res.stderr)}`);
|
|
2120
2722
|
}
|
|
2121
2723
|
let report;
|
|
@@ -2164,6 +2766,9 @@ var TOOLS = {
|
|
|
2164
2766
|
navigate_page: onCdp(SHARED_TOOLS.navigate_page),
|
|
2165
2767
|
wait_for: onCdp(SHARED_TOOLS.wait_for),
|
|
2166
2768
|
evaluate_script: onCdp(SHARED_TOOLS.evaluate_script),
|
|
2769
|
+
list_cookies: onCdp(SHARED_TOOLS.list_cookies),
|
|
2770
|
+
set_cookie: onCdp(SHARED_TOOLS.set_cookie),
|
|
2771
|
+
delete_cookies: onCdp(SHARED_TOOLS.delete_cookies),
|
|
2167
2772
|
take_snapshot: onCdp(SHARED_TOOLS.take_snapshot),
|
|
2168
2773
|
click: onCdp(SHARED_TOOLS.click),
|
|
2169
2774
|
hover: onCdp(SHARED_TOOLS.hover),
|
|
@@ -2189,7 +2794,10 @@ var TOOLS = {
|
|
|
2189
2794
|
lighthouse_audit: lighthouseAudit,
|
|
2190
2795
|
mock_request: mockRequest,
|
|
2191
2796
|
list_mocks: listMocks,
|
|
2192
|
-
clear_mocks: clearMocks
|
|
2797
|
+
clear_mocks: clearMocks,
|
|
2798
|
+
claim_page: onCdp(LEASE_TOOLS.claim_page),
|
|
2799
|
+
release_page: onCdp(LEASE_TOOLS.release_page),
|
|
2800
|
+
list_leases: onCdp(LEASE_TOOLS.list_leases)
|
|
2193
2801
|
};
|
|
2194
2802
|
var TOOL_NAMES = Object.keys(TOOLS);
|
|
2195
2803
|
export {
|