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/mcp.js
CHANGED
|
@@ -1,5 +1,260 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
|
+
// src/leases.ts
|
|
4
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
5
|
+
import { randomBytes } from "node:crypto";
|
|
6
|
+
import { mkdir, readFile, readdir, unlink, writeFile } from "node:fs/promises";
|
|
7
|
+
import { join } from "node:path";
|
|
8
|
+
var DEFAULT_LEASE_TTL_MS = 900000;
|
|
9
|
+
function leaseDir() {
|
|
10
|
+
return process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
11
|
+
}
|
|
12
|
+
function leaseTtlMs() {
|
|
13
|
+
const raw = Number(process.env.CDP_LEASE_TTL_MS);
|
|
14
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
|
|
15
|
+
}
|
|
16
|
+
function defaultLabel() {
|
|
17
|
+
return `pid-${process.pid}`;
|
|
18
|
+
}
|
|
19
|
+
function safeId(id) {
|
|
20
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
21
|
+
}
|
|
22
|
+
function leaseFile(backend, targetId) {
|
|
23
|
+
return join(leaseDir(), `lease-${backend}-${safeId(targetId)}.json`);
|
|
24
|
+
}
|
|
25
|
+
function mintToken(backend, targetId, nonce) {
|
|
26
|
+
return `${backend}:${targetId}:${nonce}`;
|
|
27
|
+
}
|
|
28
|
+
function tokenParts(token) {
|
|
29
|
+
if (typeof token !== "string")
|
|
30
|
+
return;
|
|
31
|
+
const first = token.indexOf(":");
|
|
32
|
+
const last = token.lastIndexOf(":");
|
|
33
|
+
if (first <= 0 || last <= first)
|
|
34
|
+
return;
|
|
35
|
+
const backend = token.slice(0, first);
|
|
36
|
+
if (backend !== "chrome" && backend !== "firefox")
|
|
37
|
+
return;
|
|
38
|
+
const targetId = token.slice(first + 1, last);
|
|
39
|
+
const nonce = token.slice(last + 1);
|
|
40
|
+
if (!targetId.length || !/^[0-9a-f]{24}$/.test(nonce))
|
|
41
|
+
return;
|
|
42
|
+
return { backend, targetId, nonce };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
class LeaseConflictError extends Error {
|
|
46
|
+
targetId;
|
|
47
|
+
holder;
|
|
48
|
+
constructor(message, targetId, holder) {
|
|
49
|
+
super(message);
|
|
50
|
+
this.targetId = targetId;
|
|
51
|
+
this.holder = holder;
|
|
52
|
+
this.name = "LeaseConflictError";
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function readLease(backend, targetId) {
|
|
56
|
+
let raw;
|
|
57
|
+
try {
|
|
58
|
+
raw = await readFile(leaseFile(backend, targetId), "utf8");
|
|
59
|
+
} catch (err) {
|
|
60
|
+
if (err.code === "ENOENT")
|
|
61
|
+
return;
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
try {
|
|
65
|
+
const rec = JSON.parse(raw);
|
|
66
|
+
if (typeof rec?.nonce === "string" && typeof rec?.pid === "number")
|
|
67
|
+
return rec;
|
|
68
|
+
} catch {}
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
function isPidAlive(pid) {
|
|
72
|
+
if (!Number.isInteger(pid) || pid <= 0)
|
|
73
|
+
return false;
|
|
74
|
+
try {
|
|
75
|
+
process.kill(pid, 0);
|
|
76
|
+
return true;
|
|
77
|
+
} catch (err) {
|
|
78
|
+
return err.code === "EPERM";
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
function staleReason(rec, opts) {
|
|
82
|
+
if (!isPidAlive(rec.pid))
|
|
83
|
+
return "dead-pid";
|
|
84
|
+
if (opts.now - rec.lastUsedAt > rec.ttlMs)
|
|
85
|
+
return "expired";
|
|
86
|
+
if (opts.liveIds && !opts.liveIds.includes(rec.targetId))
|
|
87
|
+
return "target-gone";
|
|
88
|
+
return false;
|
|
89
|
+
}
|
|
90
|
+
async function claimLease(backend, targetId, opts) {
|
|
91
|
+
const now = opts.now ?? Date.now();
|
|
92
|
+
await mkdir(leaseDir(), { recursive: true });
|
|
93
|
+
for (let attempt = 0;attempt < 3; attempt++) {
|
|
94
|
+
const record = {
|
|
95
|
+
backend,
|
|
96
|
+
targetId,
|
|
97
|
+
nonce: randomBytes(12).toString("hex"),
|
|
98
|
+
pid: process.pid,
|
|
99
|
+
label: opts.label,
|
|
100
|
+
createdAt: now,
|
|
101
|
+
lastUsedAt: now,
|
|
102
|
+
ttlMs: opts.ttlMs ?? leaseTtlMs()
|
|
103
|
+
};
|
|
104
|
+
try {
|
|
105
|
+
await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
|
|
106
|
+
return { token: mintToken(backend, targetId, record.nonce), record };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
if (err.code !== "EEXIST")
|
|
109
|
+
throw err;
|
|
110
|
+
}
|
|
111
|
+
const existing = await readLease(backend, targetId);
|
|
112
|
+
if (!existing)
|
|
113
|
+
continue;
|
|
114
|
+
const stale = staleReason(existing, { now, liveIds: opts.liveIds });
|
|
115
|
+
if (!stale) {
|
|
116
|
+
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);
|
|
117
|
+
}
|
|
118
|
+
await unlink(leaseFile(backend, targetId)).catch(() => {
|
|
119
|
+
return;
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
throw new LeaseConflictError(`target '${targetId}' could not be claimed after 3 attempts: another process is claiming and releasing it concurrently`, targetId);
|
|
123
|
+
}
|
|
124
|
+
async function releaseLease(token) {
|
|
125
|
+
const parts = tokenParts(token);
|
|
126
|
+
if (!parts)
|
|
127
|
+
return { released: false };
|
|
128
|
+
const rec = await readLease(parts.backend, parts.targetId);
|
|
129
|
+
if (!rec || rec.nonce !== parts.nonce)
|
|
130
|
+
return { released: false };
|
|
131
|
+
await unlink(leaseFile(parts.backend, parts.targetId)).catch(() => {
|
|
132
|
+
return;
|
|
133
|
+
});
|
|
134
|
+
return { released: true };
|
|
135
|
+
}
|
|
136
|
+
async function releaseLeaseFor(backend, targetId) {
|
|
137
|
+
if (!await readLease(backend, targetId))
|
|
138
|
+
return { released: false };
|
|
139
|
+
await unlink(leaseFile(backend, targetId)).catch(() => {
|
|
140
|
+
return;
|
|
141
|
+
});
|
|
142
|
+
return { released: true };
|
|
143
|
+
}
|
|
144
|
+
var leaseScope = new AsyncLocalStorage;
|
|
145
|
+
function withLeaseScope(lease, fn) {
|
|
146
|
+
return leaseScope.run(lease, fn);
|
|
147
|
+
}
|
|
148
|
+
function currentLease() {
|
|
149
|
+
return leaseScope.getStore();
|
|
150
|
+
}
|
|
151
|
+
function leaseFromArgs(args) {
|
|
152
|
+
if (!args || typeof args !== "object")
|
|
153
|
+
return;
|
|
154
|
+
const lease = args.lease;
|
|
155
|
+
return typeof lease === "string" && lease.length > 0 ? lease : undefined;
|
|
156
|
+
}
|
|
157
|
+
async function touchLease(rec, now = Date.now()) {
|
|
158
|
+
const current = await readLease(rec.backend, rec.targetId).catch(() => {
|
|
159
|
+
return;
|
|
160
|
+
});
|
|
161
|
+
if (!current || current.nonce !== rec.nonce)
|
|
162
|
+
return;
|
|
163
|
+
await writeFile(leaseFile(current.backend, current.targetId), JSON.stringify({ ...current, lastUsedAt: now }), "utf8").catch(() => {
|
|
164
|
+
return;
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
function describeTarget(targetId, ctx) {
|
|
168
|
+
const url = ctx.url ? ` (url ${ctx.url})` : ctx.title ? ` (title ${JSON.stringify(ctx.title)})` : "";
|
|
169
|
+
return `target '${targetId}'${url}`;
|
|
170
|
+
}
|
|
171
|
+
async function assertLeaseOk(backend, targetId, ctx = {}) {
|
|
172
|
+
const token = ctx.lease ?? currentLease();
|
|
173
|
+
const now = ctx.now ?? Date.now();
|
|
174
|
+
const rec = await readLease(backend, targetId);
|
|
175
|
+
const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
|
|
176
|
+
const where = describeTarget(targetId, ctx);
|
|
177
|
+
if (token === undefined) {
|
|
178
|
+
if (!held)
|
|
179
|
+
return;
|
|
180
|
+
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
|
+
}
|
|
182
|
+
const parts = tokenParts(token);
|
|
183
|
+
if (!parts) {
|
|
184
|
+
throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
|
|
185
|
+
}
|
|
186
|
+
if (parts.backend !== backend || parts.targetId !== targetId) {
|
|
187
|
+
if (!held)
|
|
188
|
+
return;
|
|
189
|
+
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
|
+
}
|
|
191
|
+
if (!held) {
|
|
192
|
+
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);
|
|
193
|
+
}
|
|
194
|
+
if (parts.nonce !== held.nonce) {
|
|
195
|
+
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);
|
|
196
|
+
}
|
|
197
|
+
await touchLease(held, now);
|
|
198
|
+
}
|
|
199
|
+
var LEASE_FILE_RE = /^lease-(chrome|firefox)-(.+)\.json$/;
|
|
200
|
+
async function listLeases(opts = {}) {
|
|
201
|
+
const now = opts.now ?? Date.now();
|
|
202
|
+
let names = [];
|
|
203
|
+
try {
|
|
204
|
+
names = await readdir(leaseDir());
|
|
205
|
+
} catch (err) {
|
|
206
|
+
if (err.code === "ENOENT")
|
|
207
|
+
return [];
|
|
208
|
+
throw err;
|
|
209
|
+
}
|
|
210
|
+
const out = [];
|
|
211
|
+
for (const name of names) {
|
|
212
|
+
const parts = LEASE_FILE_RE.exec(name);
|
|
213
|
+
if (!parts)
|
|
214
|
+
continue;
|
|
215
|
+
const fromName = {
|
|
216
|
+
backend: parts[1],
|
|
217
|
+
targetId: parts[2],
|
|
218
|
+
label: "",
|
|
219
|
+
pid: 0,
|
|
220
|
+
createdAt: 0,
|
|
221
|
+
lastUsedAt: 0,
|
|
222
|
+
ttlMs: 0,
|
|
223
|
+
pidAlive: false,
|
|
224
|
+
stale: false
|
|
225
|
+
};
|
|
226
|
+
let rec;
|
|
227
|
+
try {
|
|
228
|
+
rec = JSON.parse(await readFile(join(leaseDir(), name), "utf8"));
|
|
229
|
+
} catch (err) {
|
|
230
|
+
const code = err.code;
|
|
231
|
+
if (code === "ENOENT")
|
|
232
|
+
continue;
|
|
233
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
234
|
+
continue;
|
|
235
|
+
}
|
|
236
|
+
if (typeof rec?.nonce !== "string" || typeof rec?.pid !== "number") {
|
|
237
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
out.push({
|
|
241
|
+
backend: rec.backend,
|
|
242
|
+
targetId: rec.targetId,
|
|
243
|
+
label: rec.label,
|
|
244
|
+
pid: rec.pid,
|
|
245
|
+
createdAt: rec.createdAt,
|
|
246
|
+
lastUsedAt: rec.lastUsedAt,
|
|
247
|
+
ttlMs: rec.ttlMs,
|
|
248
|
+
pidAlive: isPidAlive(rec.pid),
|
|
249
|
+
stale: staleReason(rec, {
|
|
250
|
+
now,
|
|
251
|
+
liveIds: opts.liveBackend === undefined || opts.liveBackend === rec.backend ? opts.liveIds : undefined
|
|
252
|
+
})
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
return out;
|
|
256
|
+
}
|
|
257
|
+
|
|
3
258
|
// src/client.ts
|
|
4
259
|
var BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
5
260
|
var DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
|
|
@@ -153,8 +408,13 @@ async function browserWsUrl() {
|
|
|
153
408
|
const v = await httpJson("/json/version");
|
|
154
409
|
return v.webSocketDebuggerUrl;
|
|
155
410
|
}
|
|
156
|
-
async function resolveTarget(selector) {
|
|
411
|
+
async function resolveTarget(selector, opts = {}) {
|
|
157
412
|
const targets = await listTargets();
|
|
413
|
+
const hit = pickTarget(targets, selector);
|
|
414
|
+
await assertLeaseOk("chrome", hit.id, { lease: opts.lease, url: hit.url, title: hit.title });
|
|
415
|
+
return hit;
|
|
416
|
+
}
|
|
417
|
+
function pickTarget(targets, selector) {
|
|
158
418
|
const pages = targets.filter((t) => t.type === "page");
|
|
159
419
|
if (!selector || selector === "active") {
|
|
160
420
|
if (!pages.length)
|
|
@@ -190,7 +450,7 @@ async function openBrowser(opts = {}) {
|
|
|
190
450
|
return new CdpConnection(await browserWsUrl(), opts).connect();
|
|
191
451
|
}
|
|
192
452
|
async function openPage(selector, opts = {}) {
|
|
193
|
-
const target = await resolveTarget(selector);
|
|
453
|
+
const target = await resolveTarget(selector, { lease: opts.lease });
|
|
194
454
|
const conn = await new CdpConnection(target.webSocketDebuggerUrl, opts).connect();
|
|
195
455
|
return { conn, target };
|
|
196
456
|
}
|
|
@@ -447,6 +707,31 @@ function decodeUid(uid) {
|
|
|
447
707
|
throw driverError("foreign-uid", `malformed cdp uid payload: ${uid}`);
|
|
448
708
|
return n;
|
|
449
709
|
}
|
|
710
|
+
function normalizeCdpCookie(c) {
|
|
711
|
+
const same = (c.sameSite ?? "").toLowerCase();
|
|
712
|
+
const sameSite = same === "strict" || same === "lax" || same === "none" ? same : "default";
|
|
713
|
+
return {
|
|
714
|
+
name: c.name,
|
|
715
|
+
value: c.value,
|
|
716
|
+
domain: c.domain,
|
|
717
|
+
path: c.path,
|
|
718
|
+
expires: typeof c.expires === "number" ? c.expires : -1,
|
|
719
|
+
size: typeof c.size === "number" ? c.size : Buffer.byteLength(`${c.name}${c.value}`, "utf8"),
|
|
720
|
+
httpOnly: c.httpOnly === true,
|
|
721
|
+
secure: c.secure === true,
|
|
722
|
+
sameSite,
|
|
723
|
+
session: c.session === true
|
|
724
|
+
};
|
|
725
|
+
}
|
|
726
|
+
function cdpSameSite(sameSite) {
|
|
727
|
+
if (sameSite === "strict")
|
|
728
|
+
return "Strict";
|
|
729
|
+
if (sameSite === "lax")
|
|
730
|
+
return "Lax";
|
|
731
|
+
if (sameSite === "none")
|
|
732
|
+
return "None";
|
|
733
|
+
return;
|
|
734
|
+
}
|
|
450
735
|
async function centerOf(conn, objectId) {
|
|
451
736
|
const fn = "function(){this.scrollIntoView({block:'center',inline:'center'});const r=this.getBoundingClientRect();if(r.width===0&&r.height===0)return null;return {x:r.left+r.width/2,y:r.top+r.height/2};}";
|
|
452
737
|
const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
@@ -969,6 +1254,38 @@ class CdpPageDriver {
|
|
|
969
1254
|
});
|
|
970
1255
|
});
|
|
971
1256
|
}
|
|
1257
|
+
async getCookies() {
|
|
1258
|
+
await this.ensureDomain("Network");
|
|
1259
|
+
const { cookies } = await this.conn.send("Network.getCookies");
|
|
1260
|
+
return (cookies ?? []).map(normalizeCdpCookie);
|
|
1261
|
+
}
|
|
1262
|
+
async setCookie(params) {
|
|
1263
|
+
await this.ensureDomain("Network");
|
|
1264
|
+
const sameSite = cdpSameSite(params.sameSite);
|
|
1265
|
+
const { success } = await this.conn.send("Network.setCookie", {
|
|
1266
|
+
name: params.name,
|
|
1267
|
+
value: params.value,
|
|
1268
|
+
...params.url !== undefined ? { url: params.url } : {},
|
|
1269
|
+
...params.domain !== undefined ? { domain: params.domain } : {},
|
|
1270
|
+
...params.path !== undefined ? { path: params.path } : {},
|
|
1271
|
+
...params.expires !== undefined ? { expires: params.expires } : {},
|
|
1272
|
+
...params.httpOnly !== undefined ? { httpOnly: params.httpOnly } : {},
|
|
1273
|
+
...params.secure !== undefined ? { secure: params.secure } : {},
|
|
1274
|
+
...sameSite !== undefined ? { sameSite } : {}
|
|
1275
|
+
});
|
|
1276
|
+
if (success !== true) {
|
|
1277
|
+
throw driverError("page-error", `Network.setCookie declined the cookie '${params.name}'. Chrome rejects a cookie whose domain does not match the url, a secure cookie on an insecure origin, and an oversized name or value.`);
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
async deleteCookies(filter) {
|
|
1281
|
+
await this.ensureDomain("Network");
|
|
1282
|
+
await this.conn.send("Network.deleteCookies", {
|
|
1283
|
+
name: filter.name,
|
|
1284
|
+
...filter.url !== undefined ? { url: filter.url } : {},
|
|
1285
|
+
...filter.domain !== undefined ? { domain: filter.domain } : {},
|
|
1286
|
+
...filter.path !== undefined ? { path: filter.path } : {}
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
972
1289
|
async startBodyCapture() {
|
|
973
1290
|
await this.ensureDomain("Network");
|
|
974
1291
|
return async () => {
|
|
@@ -1028,6 +1345,10 @@ async function withBrowserConn(fn) {
|
|
|
1028
1345
|
}
|
|
1029
1346
|
}
|
|
1030
1347
|
function noSuchTarget(e) {
|
|
1348
|
+
if (e instanceof LeaseConflictError)
|
|
1349
|
+
throw e;
|
|
1350
|
+
if (typeof e?.code === "string")
|
|
1351
|
+
throw e;
|
|
1031
1352
|
throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
|
|
1032
1353
|
}
|
|
1033
1354
|
|
|
@@ -1071,9 +1392,154 @@ function createCdpDriver() {
|
|
|
1071
1392
|
return new CdpBrowserDriver;
|
|
1072
1393
|
}
|
|
1073
1394
|
|
|
1395
|
+
// src/origins.ts
|
|
1396
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1397
|
+
import { join as join2 } from "node:path";
|
|
1398
|
+
function safeId2(id) {
|
|
1399
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1400
|
+
}
|
|
1401
|
+
function originFile(backend, targetId) {
|
|
1402
|
+
return join2(leaseDir(), `origin-${backend}-${safeId2(targetId)}.json`);
|
|
1403
|
+
}
|
|
1404
|
+
var ORIGIN_FILE_RE = /^origin-(chrome|firefox)-(.+)\.json$/;
|
|
1405
|
+
async function recordOrigin(backend, targetId, opts = {}) {
|
|
1406
|
+
const record = {
|
|
1407
|
+
backend,
|
|
1408
|
+
targetId,
|
|
1409
|
+
label: typeof opts.label === "string" && opts.label.length ? opts.label : defaultLabel(),
|
|
1410
|
+
pid: process.pid,
|
|
1411
|
+
createdAt: opts.now ?? Date.now()
|
|
1412
|
+
};
|
|
1413
|
+
const file = originFile(backend, targetId);
|
|
1414
|
+
try {
|
|
1415
|
+
await mkdir2(leaseDir(), { recursive: true });
|
|
1416
|
+
try {
|
|
1417
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1418
|
+
return;
|
|
1419
|
+
} catch (err) {
|
|
1420
|
+
if (err.code !== "EEXIST")
|
|
1421
|
+
throw err;
|
|
1422
|
+
}
|
|
1423
|
+
await unlink2(file).catch(() => {
|
|
1424
|
+
return;
|
|
1425
|
+
});
|
|
1426
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1427
|
+
} catch {}
|
|
1428
|
+
}
|
|
1429
|
+
async function listOrigins(opts = {}) {
|
|
1430
|
+
let names = [];
|
|
1431
|
+
try {
|
|
1432
|
+
names = await readdir2(leaseDir());
|
|
1433
|
+
} catch {
|
|
1434
|
+
return [];
|
|
1435
|
+
}
|
|
1436
|
+
const live = opts.liveIds === undefined ? undefined : new Set(opts.liveIds.map((id) => safeId2(id)));
|
|
1437
|
+
const out = [];
|
|
1438
|
+
for (const name of names) {
|
|
1439
|
+
const parts = ORIGIN_FILE_RE.exec(name);
|
|
1440
|
+
if (!parts)
|
|
1441
|
+
continue;
|
|
1442
|
+
const backend = parts[1];
|
|
1443
|
+
const nameId = parts[2];
|
|
1444
|
+
const scoped = live !== undefined && (opts.liveBackend === undefined || opts.liveBackend === backend);
|
|
1445
|
+
if (scoped && !live.has(nameId)) {
|
|
1446
|
+
await unlink2(join2(leaseDir(), name)).catch(() => {
|
|
1447
|
+
return;
|
|
1448
|
+
});
|
|
1449
|
+
continue;
|
|
1450
|
+
}
|
|
1451
|
+
const fromName = { backend, targetId: nameId, label: "", pid: 0, createdAt: 0 };
|
|
1452
|
+
let rec;
|
|
1453
|
+
try {
|
|
1454
|
+
rec = JSON.parse(await readFile2(join2(leaseDir(), name), "utf8"));
|
|
1455
|
+
} catch (err) {
|
|
1456
|
+
const code = err.code;
|
|
1457
|
+
if (code === "ENOENT")
|
|
1458
|
+
continue;
|
|
1459
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
1460
|
+
continue;
|
|
1461
|
+
}
|
|
1462
|
+
if (typeof rec?.targetId !== "string" || typeof rec?.label !== "string" || typeof rec?.createdAt !== "number") {
|
|
1463
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
1464
|
+
continue;
|
|
1465
|
+
}
|
|
1466
|
+
out.push({ backend: rec.backend, targetId: rec.targetId, label: rec.label, pid: rec.pid, createdAt: rec.createdAt });
|
|
1467
|
+
}
|
|
1468
|
+
return out;
|
|
1469
|
+
}
|
|
1470
|
+
async function originIndex(backend, liveIds) {
|
|
1471
|
+
const rows = await listOrigins({ liveIds, liveBackend: backend }).catch(() => []);
|
|
1472
|
+
const index = new Map;
|
|
1473
|
+
for (const row of rows) {
|
|
1474
|
+
if (row.backend !== backend)
|
|
1475
|
+
continue;
|
|
1476
|
+
index.set(row.targetId, row);
|
|
1477
|
+
}
|
|
1478
|
+
return index;
|
|
1479
|
+
}
|
|
1480
|
+
async function newTrackedPage(driver, backend, opts = {}) {
|
|
1481
|
+
const page = await driver.newPage(opts.url);
|
|
1482
|
+
await recordOrigin(backend, page.id, { label: opts.label });
|
|
1483
|
+
return page;
|
|
1484
|
+
}
|
|
1485
|
+
|
|
1486
|
+
// src/leases-tools.ts
|
|
1487
|
+
class LeaseToolError extends Error {
|
|
1488
|
+
}
|
|
1489
|
+
function backendOf(driver) {
|
|
1490
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
1491
|
+
}
|
|
1492
|
+
async function claimPage(driver, args = {}) {
|
|
1493
|
+
const backend = backendOf(driver);
|
|
1494
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1495
|
+
const pages = await driver.listPages();
|
|
1496
|
+
let targetId = args.targetId;
|
|
1497
|
+
let url = args.url ?? "about:blank";
|
|
1498
|
+
if (targetId === undefined || targetId === "") {
|
|
1499
|
+
const page = await newTrackedPage(driver, backend, { url: args.url, label });
|
|
1500
|
+
targetId = page.id;
|
|
1501
|
+
url = page.url;
|
|
1502
|
+
} else {
|
|
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;
|
|
1507
|
+
}
|
|
1508
|
+
const liveIds = [...pages.map((p) => p.id), targetId];
|
|
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
|
+
};
|
|
1522
|
+
}
|
|
1523
|
+
async function releasePage(_driver, args) {
|
|
1524
|
+
if (typeof args.lease !== "string" || !args.lease.length) {
|
|
1525
|
+
throw new LeaseToolError("release_page requires the 'lease' token returned by claim_page");
|
|
1526
|
+
}
|
|
1527
|
+
return releaseLease(args.lease);
|
|
1528
|
+
}
|
|
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
|
+
|
|
1074
1540
|
// src/shared-tools.ts
|
|
1075
|
-
import { mkdir, writeFile } from "node:fs/promises";
|
|
1076
|
-
import { join } from "node:path";
|
|
1541
|
+
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
1542
|
+
import { dirname, join as join3 } from "node:path";
|
|
1077
1543
|
var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1078
1544
|
var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1079
1545
|
|
|
@@ -1082,6 +1548,9 @@ class SharedToolError extends Error {
|
|
|
1082
1548
|
function target3(p) {
|
|
1083
1549
|
return { id: p.id, url: p.url, title: p.title };
|
|
1084
1550
|
}
|
|
1551
|
+
function backendOf2(driver) {
|
|
1552
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
1553
|
+
}
|
|
1085
1554
|
async function withPage2(driver, target, fn) {
|
|
1086
1555
|
const page = await driver.page(target);
|
|
1087
1556
|
try {
|
|
@@ -1092,6 +1561,11 @@ async function withPage2(driver, target, fn) {
|
|
|
1092
1561
|
}
|
|
1093
1562
|
async function resolvePage(driver, selector) {
|
|
1094
1563
|
const pages = await driver.listPages();
|
|
1564
|
+
const hit = await pickPage(driver, pages, selector);
|
|
1565
|
+
await assertLeaseOk(backendOf2(driver), hit.id, { url: hit.url, title: hit.title });
|
|
1566
|
+
return hit;
|
|
1567
|
+
}
|
|
1568
|
+
async function pickPage(driver, pages, selector) {
|
|
1095
1569
|
if (selector === undefined || selector === "active") {
|
|
1096
1570
|
if (!pages.length)
|
|
1097
1571
|
throw new SharedToolError("no page targets open");
|
|
@@ -1133,24 +1607,52 @@ function locatorOf(args) {
|
|
|
1133
1607
|
throw new SharedToolError("provide exactly one of { uid } or { selector }");
|
|
1134
1608
|
return hasUid ? { uid: args.uid } : { css: args.selector };
|
|
1135
1609
|
}
|
|
1610
|
+
async function reapSet(driver, pages, all) {
|
|
1611
|
+
if (all === true)
|
|
1612
|
+
return pages.map((p) => p.id);
|
|
1613
|
+
try {
|
|
1614
|
+
return (await driver.listPages({ all: true })).map((p) => p.id);
|
|
1615
|
+
} catch {
|
|
1616
|
+
return;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1136
1619
|
async function listPages(driver, args = {}) {
|
|
1137
1620
|
const pages = await driver.listPages({ all: args.all });
|
|
1138
|
-
|
|
1621
|
+
const ledger = await originIndex(backendOf2(driver), await reapSet(driver, pages, args.all));
|
|
1622
|
+
const annotated = pages.map((p) => {
|
|
1623
|
+
const rec = ledger.get(p.id);
|
|
1624
|
+
if (!rec)
|
|
1625
|
+
return { ...p, origin: "unknown" };
|
|
1626
|
+
if (rec.unreadable !== undefined)
|
|
1627
|
+
return { ...p, origin: "unknown", originUnreadable: rec.unreadable };
|
|
1628
|
+
return { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
|
|
1629
|
+
});
|
|
1630
|
+
return { pages: annotated, count: annotated.length };
|
|
1139
1631
|
}
|
|
1140
1632
|
async function newPage(driver, args = {}) {
|
|
1141
|
-
const
|
|
1142
|
-
|
|
1633
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1634
|
+
const p = await newTrackedPage(driver, backendOf2(driver), { url: args.url, label });
|
|
1635
|
+
if (args.claim !== true)
|
|
1636
|
+
return { targetId: p.id, url: p.url };
|
|
1637
|
+
const { record, token } = await claimLease(backendOf2(driver), p.id, {
|
|
1638
|
+
label,
|
|
1639
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
|
|
1640
|
+
});
|
|
1641
|
+
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
1143
1642
|
}
|
|
1144
1643
|
async function closePage(driver, args) {
|
|
1145
1644
|
if (args.target === undefined || args.target === "")
|
|
1146
1645
|
throw new SharedToolError("close_page requires an explicit target; refusing to guess which page to close");
|
|
1147
1646
|
const p = await resolvePage(driver, args.target);
|
|
1148
1647
|
const res = await driver.closePage(p.id);
|
|
1149
|
-
|
|
1648
|
+
if (!res.success)
|
|
1649
|
+
return { closed: p.id, success: false, leaseReleased: false };
|
|
1650
|
+
const released = await releaseLeaseFor(backendOf2(driver), p.id);
|
|
1651
|
+
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
1150
1652
|
}
|
|
1151
1653
|
async function writeSelectedFile(id) {
|
|
1152
|
-
await
|
|
1153
|
-
await
|
|
1654
|
+
await mkdir3(STATE_DIR, { recursive: true });
|
|
1655
|
+
await writeFile3(join3(STATE_DIR, "selected"), id, "utf8");
|
|
1154
1656
|
}
|
|
1155
1657
|
async function selectPage(driver, args) {
|
|
1156
1658
|
if (args.target === undefined || args.target === "")
|
|
@@ -1190,11 +1692,108 @@ async function waitForText(driver, args) {
|
|
|
1190
1692
|
return { found: true, waitedMs: r.elapsedMs };
|
|
1191
1693
|
});
|
|
1192
1694
|
}
|
|
1695
|
+
function serializeForSink(value) {
|
|
1696
|
+
const json = JSON.stringify(value === undefined ? null : value, null, 2);
|
|
1697
|
+
return json ?? "null";
|
|
1698
|
+
}
|
|
1699
|
+
async function writeJsonSink(savePath, value) {
|
|
1700
|
+
const path = savePath.startsWith("/") ? savePath : join3(ARTIFACT_DIR, savePath);
|
|
1701
|
+
await mkdir3(dirname(path), { recursive: true });
|
|
1702
|
+
const json = serializeForSink(value);
|
|
1703
|
+
await writeFile3(path, json, "utf8");
|
|
1704
|
+
return { path, bytes: Buffer.byteLength(json, "utf8") };
|
|
1705
|
+
}
|
|
1706
|
+
function sinkRequested(savePath) {
|
|
1707
|
+
return savePath !== undefined && savePath !== "";
|
|
1708
|
+
}
|
|
1193
1709
|
async function evaluateScript(driver, args) {
|
|
1194
1710
|
if (typeof args.expression !== "string" || args.expression.length === 0) {
|
|
1195
1711
|
throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
|
|
1196
1712
|
}
|
|
1197
|
-
return withPage2(driver, args.target, (page) =>
|
|
1713
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1714
|
+
const value = await page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true });
|
|
1715
|
+
if (!sinkRequested(args.savePath))
|
|
1716
|
+
return value;
|
|
1717
|
+
const { path, bytes } = await writeJsonSink(args.savePath, value);
|
|
1718
|
+
const result = {
|
|
1719
|
+
path,
|
|
1720
|
+
bytes,
|
|
1721
|
+
type: value === null ? "null" : typeof value,
|
|
1722
|
+
target: target3(page.info)
|
|
1723
|
+
};
|
|
1724
|
+
return result;
|
|
1725
|
+
});
|
|
1726
|
+
}
|
|
1727
|
+
function cookieDomainMatches(cookieDomain, filter) {
|
|
1728
|
+
const bare = (d) => (d.startsWith(".") ? d.slice(1) : d).toLowerCase();
|
|
1729
|
+
const c = bare(cookieDomain);
|
|
1730
|
+
const f = bare(filter);
|
|
1731
|
+
return c === f || c.endsWith(`.${f}`);
|
|
1732
|
+
}
|
|
1733
|
+
function filterCookies(cookies, filter) {
|
|
1734
|
+
return cookies.filter((c) => {
|
|
1735
|
+
if (filter.name !== undefined && filter.name !== "" && c.name !== filter.name)
|
|
1736
|
+
return false;
|
|
1737
|
+
if (filter.domain !== undefined && filter.domain !== "" && !cookieDomainMatches(c.domain, filter.domain))
|
|
1738
|
+
return false;
|
|
1739
|
+
return true;
|
|
1740
|
+
});
|
|
1741
|
+
}
|
|
1742
|
+
async function listCookies(driver, args = {}) {
|
|
1743
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1744
|
+
const all = await page.getCookies();
|
|
1745
|
+
const cookies = filterCookies(all, { domain: args.domain, name: args.name });
|
|
1746
|
+
if (!sinkRequested(args.savePath)) {
|
|
1747
|
+
return { cookies, count: cookies.length, target: target3(page.info) };
|
|
1748
|
+
}
|
|
1749
|
+
const { path, bytes } = await writeJsonSink(args.savePath, cookies);
|
|
1750
|
+
return { path, bytes, count: cookies.length, target: target3(page.info) };
|
|
1751
|
+
});
|
|
1752
|
+
}
|
|
1753
|
+
function assertCookieSite(args, toolName) {
|
|
1754
|
+
const hasUrl = args.url !== undefined && args.url !== "";
|
|
1755
|
+
const hasDomain = args.domain !== undefined && args.domain !== "";
|
|
1756
|
+
if (!hasUrl && !hasDomain) {
|
|
1757
|
+
throw new Error(`${toolName} requires either 'url' or 'domain': a cookie has to be attributed to a site, and neither was given.`);
|
|
1758
|
+
}
|
|
1759
|
+
}
|
|
1760
|
+
function assertCookieName(name, toolName) {
|
|
1761
|
+
if (name === undefined || name === "") {
|
|
1762
|
+
throw new Error(`${toolName} requires a non-empty 'name'.`);
|
|
1763
|
+
}
|
|
1764
|
+
}
|
|
1765
|
+
async function setCookie(driver, args) {
|
|
1766
|
+
assertCookieName(args.name, "set_cookie");
|
|
1767
|
+
if (typeof args.value !== "string")
|
|
1768
|
+
throw new Error("set_cookie requires a string 'value'.");
|
|
1769
|
+
assertCookieSite(args, "set_cookie");
|
|
1770
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1771
|
+
await page.setCookie({
|
|
1772
|
+
name: args.name,
|
|
1773
|
+
value: args.value,
|
|
1774
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1775
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1776
|
+
...args.path !== undefined ? { path: args.path } : {},
|
|
1777
|
+
...args.expires !== undefined ? { expires: args.expires } : {},
|
|
1778
|
+
...args.httpOnly !== undefined ? { httpOnly: args.httpOnly } : {},
|
|
1779
|
+
...args.secure !== undefined ? { secure: args.secure } : {},
|
|
1780
|
+
...args.sameSite !== undefined ? { sameSite: args.sameSite } : {}
|
|
1781
|
+
});
|
|
1782
|
+
return { set: true, target: target3(page.info) };
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
async function deleteCookies(driver, args) {
|
|
1786
|
+
assertCookieName(args.name, "delete_cookies");
|
|
1787
|
+
assertCookieSite(args, "delete_cookies");
|
|
1788
|
+
return withPage2(driver, args.target, async (page) => {
|
|
1789
|
+
await page.deleteCookies({
|
|
1790
|
+
name: args.name,
|
|
1791
|
+
...args.url !== undefined ? { url: args.url } : {},
|
|
1792
|
+
...args.domain !== undefined ? { domain: args.domain } : {},
|
|
1793
|
+
...args.path !== undefined ? { path: args.path } : {}
|
|
1794
|
+
});
|
|
1795
|
+
return { deleted: true, target: target3(page.info) };
|
|
1796
|
+
});
|
|
1198
1797
|
}
|
|
1199
1798
|
var BARE_TOKEN_EXTRAS = new Set(["disabled", "focused"]);
|
|
1200
1799
|
var UNQUOTED_EXTRAS = new Set(["checked", "expanded", "url"]);
|
|
@@ -1305,10 +1904,10 @@ async function takeScreenshot(driver, args = {}) {
|
|
|
1305
1904
|
const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
|
|
1306
1905
|
if (data.byteLength === 0)
|
|
1307
1906
|
throw new SharedToolError("captureScreenshot returned empty data");
|
|
1308
|
-
await
|
|
1907
|
+
await mkdir3(ARTIFACT_DIR, { recursive: true });
|
|
1309
1908
|
const ext = outFormat === "jpeg" ? "jpg" : "png";
|
|
1310
|
-
const path = args.savePath ??
|
|
1311
|
-
await
|
|
1909
|
+
const path = args.savePath ?? join3(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
|
|
1910
|
+
await writeFile3(path, Buffer.from(data));
|
|
1312
1911
|
const result = {
|
|
1313
1912
|
path,
|
|
1314
1913
|
bytes: data.byteLength,
|
|
@@ -1355,6 +1954,9 @@ var SHARED_TOOLS = {
|
|
|
1355
1954
|
navigate_page: navigatePage,
|
|
1356
1955
|
wait_for: waitForText,
|
|
1357
1956
|
evaluate_script: evaluateScript,
|
|
1957
|
+
list_cookies: listCookies,
|
|
1958
|
+
set_cookie: setCookie,
|
|
1959
|
+
delete_cookies: deleteCookies,
|
|
1358
1960
|
take_snapshot: takeSnapshot,
|
|
1359
1961
|
click,
|
|
1360
1962
|
hover,
|
|
@@ -1371,10 +1973,10 @@ var SHARED_TOOLS = {
|
|
|
1371
1973
|
};
|
|
1372
1974
|
|
|
1373
1975
|
// src/tools/console.ts
|
|
1374
|
-
import { readFile } from "node:fs/promises";
|
|
1976
|
+
import { readFile as readFile3 } from "node:fs/promises";
|
|
1375
1977
|
|
|
1376
1978
|
// src/tools/recorder.ts
|
|
1377
|
-
import { mkdir as
|
|
1979
|
+
import { mkdir as mkdir4, appendFile, writeFile as writeFile4, copyFile } from "node:fs/promises";
|
|
1378
1980
|
var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1379
1981
|
function recFile(targetId) {
|
|
1380
1982
|
return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
|
|
@@ -1395,11 +1997,11 @@ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Lo
|
|
|
1395
1997
|
async function startRecorder(target, opts = {}) {
|
|
1396
1998
|
const network = opts.network ?? true;
|
|
1397
1999
|
const consoleCap = opts.console ?? true;
|
|
1398
|
-
await
|
|
2000
|
+
await mkdir4(ARTIFACT_DIR2, { recursive: true });
|
|
1399
2001
|
const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
|
|
1400
2002
|
const file = opts.file ?? recFile(resolved.id);
|
|
1401
2003
|
if (opts.truncate) {
|
|
1402
|
-
await
|
|
2004
|
+
await writeFile4(file, "");
|
|
1403
2005
|
}
|
|
1404
2006
|
let chain = Promise.resolve();
|
|
1405
2007
|
let dropped = 0;
|
|
@@ -1541,7 +2143,7 @@ function lineToEntry(line, index) {
|
|
|
1541
2143
|
async function readConsoleEntries(file) {
|
|
1542
2144
|
let raw;
|
|
1543
2145
|
try {
|
|
1544
|
-
raw = await
|
|
2146
|
+
raw = await readFile3(file, "utf8");
|
|
1545
2147
|
} catch {
|
|
1546
2148
|
return [];
|
|
1547
2149
|
}
|
|
@@ -1597,7 +2199,7 @@ async function getConsoleMessage(args = {}) {
|
|
|
1597
2199
|
}
|
|
1598
2200
|
|
|
1599
2201
|
// src/tools/network.ts
|
|
1600
|
-
import { readFile as
|
|
2202
|
+
import { readFile as readFile4 } from "node:fs/promises";
|
|
1601
2203
|
var DEFAULT_CAPTURE_MS2 = 2500;
|
|
1602
2204
|
function asHeaders(v) {
|
|
1603
2205
|
if (!v || typeof v !== "object")
|
|
@@ -1664,7 +2266,7 @@ function correlate(lines) {
|
|
|
1664
2266
|
async function readNetworkRequests(file) {
|
|
1665
2267
|
let raw;
|
|
1666
2268
|
try {
|
|
1667
|
-
raw = await
|
|
2269
|
+
raw = await readFile4(file, "utf8");
|
|
1668
2270
|
} catch {
|
|
1669
2271
|
return [];
|
|
1670
2272
|
}
|
|
@@ -1746,8 +2348,8 @@ async function getNetworkRequest(args = {}) {
|
|
|
1746
2348
|
}
|
|
1747
2349
|
|
|
1748
2350
|
// src/tools/performance.ts
|
|
1749
|
-
import { mkdir as
|
|
1750
|
-
import { join as
|
|
2351
|
+
import { mkdir as mkdir5, readFile as readFile5, writeFile as writeFile5, stat, unlink as unlink3 } from "node:fs/promises";
|
|
2352
|
+
import { join as join4 } from "node:path";
|
|
1751
2353
|
var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1752
2354
|
var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
|
|
1753
2355
|
var DEFAULT_CATEGORIES = [
|
|
@@ -1765,7 +2367,7 @@ function shortId(id) {
|
|
|
1765
2367
|
return id.slice(0, 8);
|
|
1766
2368
|
}
|
|
1767
2369
|
function stateFilePath() {
|
|
1768
|
-
return
|
|
2370
|
+
return join4(STATE_DIR2, "trace-state.json");
|
|
1769
2371
|
}
|
|
1770
2372
|
function bufferTraceData(conn, sink) {
|
|
1771
2373
|
return conn.on("Tracing.dataCollected", (params) => {
|
|
@@ -1787,10 +2389,10 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
|
|
|
1787
2389
|
return sink;
|
|
1788
2390
|
}
|
|
1789
2391
|
async function writeTrace(events, targetId) {
|
|
1790
|
-
await
|
|
1791
|
-
const path =
|
|
2392
|
+
await mkdir5(ARTIFACT_DIR3, { recursive: true });
|
|
2393
|
+
const path = join4(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
|
|
1792
2394
|
const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
|
|
1793
|
-
await
|
|
2395
|
+
await writeFile5(path, body, "utf8");
|
|
1794
2396
|
const { size } = await stat(path);
|
|
1795
2397
|
return { path, bytes: size };
|
|
1796
2398
|
}
|
|
@@ -1810,7 +2412,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
1810
2412
|
throw err;
|
|
1811
2413
|
}
|
|
1812
2414
|
liveTraces.set(target.id, { conn, target, events, startedAt: Date.now() });
|
|
1813
|
-
await
|
|
2415
|
+
await mkdir5(STATE_DIR2, { recursive: true });
|
|
1814
2416
|
const state = {
|
|
1815
2417
|
targetId: target.id,
|
|
1816
2418
|
url: target.url,
|
|
@@ -1818,7 +2420,7 @@ async function performanceStartTrace(args = {}) {
|
|
|
1818
2420
|
pid: process.pid,
|
|
1819
2421
|
categories
|
|
1820
2422
|
};
|
|
1821
|
-
await
|
|
2423
|
+
await writeFile5(stateFilePath(), JSON.stringify(state), "utf8");
|
|
1822
2424
|
return {
|
|
1823
2425
|
target: { id: target.id, url: target.url, title: target.title },
|
|
1824
2426
|
categories,
|
|
@@ -1831,7 +2433,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
1831
2433
|
if (!entry) {
|
|
1832
2434
|
let hint = "";
|
|
1833
2435
|
try {
|
|
1834
|
-
const raw = await
|
|
2436
|
+
const raw = await readFile5(stateFilePath(), "utf8");
|
|
1835
2437
|
const state = JSON.parse(raw);
|
|
1836
2438
|
hint = state.pid === process.pid ? ` (state file lists target ${shortId(state.targetId)} but its live connection is gone)` : ` (trace was started by a DIFFERENT process pid=${state.pid}; live recordings cannot cross process boundaries)`;
|
|
1837
2439
|
} catch {}
|
|
@@ -1843,7 +2445,7 @@ async function performanceStopTrace(args = {}) {
|
|
|
1843
2445
|
} finally {
|
|
1844
2446
|
conn.close();
|
|
1845
2447
|
liveTraces.delete(target.id);
|
|
1846
|
-
await
|
|
2448
|
+
await unlink3(stateFilePath()).catch(() => {});
|
|
1847
2449
|
}
|
|
1848
2450
|
const written = await writeTrace(events, target.id);
|
|
1849
2451
|
const metrics = analyzeEvents(events);
|
|
@@ -1961,7 +2563,7 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
1961
2563
|
}
|
|
1962
2564
|
let raw;
|
|
1963
2565
|
try {
|
|
1964
|
-
raw = await
|
|
2566
|
+
raw = await readFile5(args.tracePath, "utf8");
|
|
1965
2567
|
} catch (err) {
|
|
1966
2568
|
throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
|
|
1967
2569
|
}
|
|
@@ -1978,8 +2580,8 @@ async function performanceAnalyzeInsight(args = {}) {
|
|
|
1978
2580
|
}
|
|
1979
2581
|
|
|
1980
2582
|
// src/tools/heap.ts
|
|
1981
|
-
import { mkdir as
|
|
1982
|
-
import { join as
|
|
2583
|
+
import { mkdir as mkdir6, writeFile as writeFile6 } from "node:fs/promises";
|
|
2584
|
+
import { join as join5 } from "node:path";
|
|
1983
2585
|
var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
1984
2586
|
function stamp3(targetId) {
|
|
1985
2587
|
const short = targetId.slice(0, 8);
|
|
@@ -2004,9 +2606,9 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2004
2606
|
if (snapshot.length === 0) {
|
|
2005
2607
|
throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
|
|
2006
2608
|
}
|
|
2007
|
-
await
|
|
2008
|
-
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath :
|
|
2009
|
-
await
|
|
2609
|
+
await mkdir6(ARTIFACT_DIR4, { recursive: true });
|
|
2610
|
+
const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join5(ARTIFACT_DIR4, args.savePath) : join5(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
|
|
2611
|
+
await writeFile6(path, snapshot, "utf8");
|
|
2010
2612
|
const bytes = Buffer.byteLength(snapshot, "utf8");
|
|
2011
2613
|
return {
|
|
2012
2614
|
path,
|
|
@@ -2019,7 +2621,7 @@ async function takeHeapsnapshot(args = {}) {
|
|
|
2019
2621
|
|
|
2020
2622
|
// src/tools/lighthouse.ts
|
|
2021
2623
|
import { spawn } from "node:child_process";
|
|
2022
|
-
import { mkdir as
|
|
2624
|
+
import { mkdir as mkdir7, readFile as readFile6, stat as stat2 } from "node:fs/promises";
|
|
2023
2625
|
var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
2024
2626
|
function cdpPort() {
|
|
2025
2627
|
const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
@@ -2080,7 +2682,7 @@ async function lighthouseAudit(args) {
|
|
|
2080
2682
|
const url = args.url.trim();
|
|
2081
2683
|
const timeoutMs = args.timeoutMs ?? 120000;
|
|
2082
2684
|
const port = cdpPort();
|
|
2083
|
-
await
|
|
2685
|
+
await mkdir7(ARTIFACT_DIR5, { recursive: true });
|
|
2084
2686
|
const outPath = args.savePath ?? `${ARTIFACT_DIR5}/lighthouse-${shortHost(url)}-${stamp4()}.json`;
|
|
2085
2687
|
const formFactor = args.formFactor ?? "desktop";
|
|
2086
2688
|
const lhArgs = [
|
|
@@ -2109,7 +2711,7 @@ async function lighthouseAudit(args) {
|
|
|
2109
2711
|
}
|
|
2110
2712
|
let raw;
|
|
2111
2713
|
try {
|
|
2112
|
-
raw = await
|
|
2714
|
+
raw = await readFile6(outPath, "utf8");
|
|
2113
2715
|
} catch {
|
|
2114
2716
|
raw = undefined;
|
|
2115
2717
|
}
|
|
@@ -2117,7 +2719,7 @@ async function lighthouseAudit(args) {
|
|
|
2117
2719
|
if (res.signal === "SIGKILL") {
|
|
2118
2720
|
throw new CdpError(`lighthouse_audit timed out after ${timeoutMs}ms (no report written). stderr: ${truncate(res.stderr)}`);
|
|
2119
2721
|
}
|
|
2120
|
-
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
|
|
2722
|
+
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." : "";
|
|
2121
2723
|
throw new CdpError(`lighthouse_audit produced no report (exit code ${res.code}).${hint} stderr: ${truncate(res.stderr)}`);
|
|
2122
2724
|
}
|
|
2123
2725
|
let report;
|
|
@@ -2166,6 +2768,9 @@ var TOOLS = {
|
|
|
2166
2768
|
navigate_page: onCdp(SHARED_TOOLS.navigate_page),
|
|
2167
2769
|
wait_for: onCdp(SHARED_TOOLS.wait_for),
|
|
2168
2770
|
evaluate_script: onCdp(SHARED_TOOLS.evaluate_script),
|
|
2771
|
+
list_cookies: onCdp(SHARED_TOOLS.list_cookies),
|
|
2772
|
+
set_cookie: onCdp(SHARED_TOOLS.set_cookie),
|
|
2773
|
+
delete_cookies: onCdp(SHARED_TOOLS.delete_cookies),
|
|
2169
2774
|
take_snapshot: onCdp(SHARED_TOOLS.take_snapshot),
|
|
2170
2775
|
click: onCdp(SHARED_TOOLS.click),
|
|
2171
2776
|
hover: onCdp(SHARED_TOOLS.hover),
|
|
@@ -2191,7 +2796,10 @@ var TOOLS = {
|
|
|
2191
2796
|
lighthouse_audit: lighthouseAudit,
|
|
2192
2797
|
mock_request: mockRequest,
|
|
2193
2798
|
list_mocks: listMocks,
|
|
2194
|
-
clear_mocks: clearMocks
|
|
2799
|
+
clear_mocks: clearMocks,
|
|
2800
|
+
claim_page: onCdp(LEASE_TOOLS.claim_page),
|
|
2801
|
+
release_page: onCdp(LEASE_TOOLS.release_page),
|
|
2802
|
+
list_leases: onCdp(LEASE_TOOLS.list_leases)
|
|
2195
2803
|
};
|
|
2196
2804
|
var TOOL_NAMES = Object.keys(TOOLS);
|
|
2197
2805
|
|
|
@@ -2204,10 +2812,14 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
2204
2812
|
var MANIFEST = [
|
|
2205
2813
|
{
|
|
2206
2814
|
name: "list_pages",
|
|
2207
|
-
description: "Enumerate browser page targets via the CDP browser endpoint (GET /json/list). By default returns only page-type tabs; set 'all' to also include workers and background pages. Each entry carries the targetId used as a target selector elsewhere.",
|
|
2815
|
+
description: "Enumerate browser page targets via the CDP browser endpoint (GET /json/list). By default returns only page-type tabs; set 'all' to also include workers and background pages. Each entry carries the targetId used as a target selector elsewhere, plus an 'origin' field recording where the tab came from: 'agent' means this toolkit created it (the entry then also carries the creating 'label' and 'createdAt'), and this stays true after the creating agent releases its lease or dies, which is what makes a stray agent tab findable later. 'unknown' means there is no creation record. It never says 'human': the toolkit cannot prove a person opened a tab, so 'unknown' is the honest word for a tab it did not create, one opened before the toolkit ran, or one whose record could not be written. A tab whose record exists but could not be read reports origin 'unknown' plus 'originUnreadable', so a broken record is never mistaken for no record.",
|
|
2208
2816
|
inputSchema: {
|
|
2209
2817
|
type: "object",
|
|
2210
2818
|
properties: {
|
|
2819
|
+
lease: {
|
|
2820
|
+
type: "string",
|
|
2821
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
2822
|
+
},
|
|
2211
2823
|
all: {
|
|
2212
2824
|
type: "boolean",
|
|
2213
2825
|
description: "Include non-page targets (service/shared workers, background pages) when true; otherwise only page-type tabs are listed."
|
|
@@ -2223,9 +2835,25 @@ var MANIFEST = [
|
|
|
2223
2835
|
inputSchema: {
|
|
2224
2836
|
type: "object",
|
|
2225
2837
|
properties: {
|
|
2838
|
+
lease: {
|
|
2839
|
+
type: "string",
|
|
2840
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
2841
|
+
},
|
|
2226
2842
|
url: {
|
|
2227
2843
|
type: "string",
|
|
2228
2844
|
description: "URL to open in the new tab; defaults to about:blank."
|
|
2845
|
+
},
|
|
2846
|
+
claim: {
|
|
2847
|
+
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."
|
|
2849
|
+
},
|
|
2850
|
+
label: {
|
|
2851
|
+
type: "string",
|
|
2852
|
+
description: "Agent label recorded on the lease when claim:true, surfaced in conflict errors and list_leases. Defaults to pid-<pid>."
|
|
2853
|
+
},
|
|
2854
|
+
ttlMs: {
|
|
2855
|
+
type: "number",
|
|
2856
|
+
description: "How long the lease taken by claim:true survives without use before it is reclaimable. Defaults to CDP_LEASE_TTL_MS, else 900000 (15 minutes). Every checked call refreshes it, so an active agent never expires. Ignored without claim:true."
|
|
2229
2857
|
}
|
|
2230
2858
|
},
|
|
2231
2859
|
required: [],
|
|
@@ -2238,6 +2866,10 @@ var MANIFEST = [
|
|
|
2238
2866
|
inputSchema: {
|
|
2239
2867
|
type: "object",
|
|
2240
2868
|
properties: {
|
|
2869
|
+
lease: {
|
|
2870
|
+
type: "string",
|
|
2871
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
2872
|
+
},
|
|
2241
2873
|
target: {
|
|
2242
2874
|
type: "string",
|
|
2243
2875
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2255,6 +2887,10 @@ var MANIFEST = [
|
|
|
2255
2887
|
inputSchema: {
|
|
2256
2888
|
type: "object",
|
|
2257
2889
|
properties: {
|
|
2890
|
+
lease: {
|
|
2891
|
+
type: "string",
|
|
2892
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
2893
|
+
},
|
|
2258
2894
|
target: {
|
|
2259
2895
|
type: "string",
|
|
2260
2896
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2272,6 +2908,10 @@ var MANIFEST = [
|
|
|
2272
2908
|
inputSchema: {
|
|
2273
2909
|
type: "object",
|
|
2274
2910
|
properties: {
|
|
2911
|
+
lease: {
|
|
2912
|
+
type: "string",
|
|
2913
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
2914
|
+
},
|
|
2275
2915
|
target: {
|
|
2276
2916
|
type: "string",
|
|
2277
2917
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2306,10 +2946,14 @@ var MANIFEST = [
|
|
|
2306
2946
|
},
|
|
2307
2947
|
{
|
|
2308
2948
|
name: "wait_for",
|
|
2309
|
-
description: "Poll a target page until the given substring appears in document.body.innerText (Runtime.evaluate on a fixed interval), or throw on timeout. Text-substring waiting only
|
|
2949
|
+
description: "Poll a target page until the given substring appears in document.body.innerText (Runtime.evaluate on a fixed interval), or throw on timeout. Text-substring waiting only: no aria/role/selector or event variants; throws rather than returning {found:false}.",
|
|
2310
2950
|
inputSchema: {
|
|
2311
2951
|
type: "object",
|
|
2312
2952
|
properties: {
|
|
2953
|
+
lease: {
|
|
2954
|
+
type: "string",
|
|
2955
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
2956
|
+
},
|
|
2313
2957
|
target: {
|
|
2314
2958
|
type: "string",
|
|
2315
2959
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2335,10 +2979,14 @@ var MANIFEST = [
|
|
|
2335
2979
|
},
|
|
2336
2980
|
{
|
|
2337
2981
|
name: "evaluate_script",
|
|
2338
|
-
description: "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string.",
|
|
2982
|
+
description: "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string. Pass 'savePath' to write the evaluated value to a JSON file instead: the response then carries only {path,bytes,type,target} and the value itself never appears in it, in any form. That is the way to read a credential (a JWT or session token out of localStorage, for example) without putting the secret into the caller's transcript.",
|
|
2339
2983
|
inputSchema: {
|
|
2340
2984
|
type: "object",
|
|
2341
2985
|
properties: {
|
|
2986
|
+
lease: {
|
|
2987
|
+
type: "string",
|
|
2988
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
2989
|
+
},
|
|
2342
2990
|
target: {
|
|
2343
2991
|
type: "string",
|
|
2344
2992
|
description: "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring."
|
|
@@ -2354,6 +3002,10 @@ var MANIFEST = [
|
|
|
2354
3002
|
args: {
|
|
2355
3003
|
type: "array",
|
|
2356
3004
|
description: "Positional JSON-serializable arguments to pass to the expression, treating it as a function. No live element/page handle is bound."
|
|
3005
|
+
},
|
|
3006
|
+
savePath: {
|
|
3007
|
+
type: "string",
|
|
3008
|
+
description: "Write the evaluated value to this file as JSON and KEEP IT OUT OF THE RESPONSE: with savePath set the result is {path,bytes,type,target} only, with no copy, preview or truncation of the value. Use it to read credentials without putting them in a transcript. An absolute path (starting with /) is used as-is; a relative path is resolved under the artifact dir (/tmp/cdp-toolkit). Missing parent directories are created. Omit it to get the value back inline, exactly as before."
|
|
2357
3009
|
}
|
|
2358
3010
|
},
|
|
2359
3011
|
required: [
|
|
@@ -2362,12 +3014,138 @@ var MANIFEST = [
|
|
|
2362
3014
|
additionalProperties: false
|
|
2363
3015
|
}
|
|
2364
3016
|
},
|
|
3017
|
+
{
|
|
3018
|
+
name: "list_cookies",
|
|
3019
|
+
description: "Read the cookie store for the target page, INCLUDING httpOnly cookies, which document.cookie cannot see and an evaluate_script call therefore cannot reach. Each cookie carries name, value, domain, path, expires (Unix seconds, -1 for a session cookie), size, httpOnly, secure, sameSite ('strict'|'lax'|'none'|'default') and session. The read is page-scoped, not browser-wide: it returns the cookies of the resolved tab (Chrome: Network.getCookies; Firefox: storage.getCookies partitioned by that browsing context), so point at a page on the site whose cookies you want. Filter with 'domain' and/or 'name'. Pass 'savePath' to write the cookie array to a JSON file instead: the response is then {path,bytes,count,target} only, with no cookie value in it in any form, which is how to capture a session cookie without putting the credential in the caller's transcript.",
|
|
3020
|
+
inputSchema: {
|
|
3021
|
+
type: "object",
|
|
3022
|
+
properties: {
|
|
3023
|
+
lease: {
|
|
3024
|
+
type: "string",
|
|
3025
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs."
|
|
3026
|
+
},
|
|
3027
|
+
target: {
|
|
3028
|
+
type: "string",
|
|
3029
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
3030
|
+
},
|
|
3031
|
+
domain: {
|
|
3032
|
+
type: "string",
|
|
3033
|
+
description: "Keep only cookies for this domain. A leading dot is ignored on both sides and subdomains of the given domain match too, so 'example.test' matches '.example.test' and 'app.example.test'. No wildcards."
|
|
3034
|
+
},
|
|
3035
|
+
name: {
|
|
3036
|
+
type: "string",
|
|
3037
|
+
description: "Keep only the cookie with exactly this name. Exact match, not a substring."
|
|
3038
|
+
},
|
|
3039
|
+
savePath: {
|
|
3040
|
+
type: "string",
|
|
3041
|
+
description: "Write the cookie array to this file as JSON and KEEP THE VALUES OUT OF THE RESPONSE: with savePath set the result is {path,bytes,count,target} only, with no copy, preview or truncation of any cookie value. An absolute path (starting with /) is used as-is; a relative path is resolved under the artifact dir (/tmp/cdp-toolkit). Missing parent directories are created. Omit it to get the cookies back inline."
|
|
3042
|
+
}
|
|
3043
|
+
},
|
|
3044
|
+
required: [],
|
|
3045
|
+
additionalProperties: false
|
|
3046
|
+
}
|
|
3047
|
+
},
|
|
3048
|
+
{
|
|
3049
|
+
name: "set_cookie",
|
|
3050
|
+
description: "Write one cookie into the target page's cookie store, INCLUDING an httpOnly or secure cookie, which document.cookie cannot create and an evaluate_script call therefore cannot either. Chrome uses Network.setCookie, Firefox uses storage.setCookie partitioned by the resolved browsing context. Either 'url' or 'domain' is REQUIRED: a cookie has to be attributed to a site and the call is refused with an error when neither is given, rather than guessing the current page's origin. The response is {set:true,target} and never echoes the value back, so a credential you just supplied does not land in the transcript twice. A 'set:true' is earned, not assumed: Chrome answers Network.setCookie with success:false when it declines a cookie (a domain the url does not belong to, a secure cookie on an insecure origin, an oversized value) and that refusal is raised as an error. On Firefox, 'domain' is derived from 'url' when only the url was given, because BiDi has no url parameter; a url with no host, such as about:blank or a data URL, is an error rather than a silent no-op. Read the result back with list_cookies when you need proof.",
|
|
3051
|
+
inputSchema: {
|
|
3052
|
+
type: "object",
|
|
3053
|
+
properties: {
|
|
3054
|
+
lease: {
|
|
3055
|
+
type: "string",
|
|
3056
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs."
|
|
3057
|
+
},
|
|
3058
|
+
target: {
|
|
3059
|
+
type: "string",
|
|
3060
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
3061
|
+
},
|
|
3062
|
+
name: {
|
|
3063
|
+
type: "string",
|
|
3064
|
+
description: "Cookie name. Required and non-empty."
|
|
3065
|
+
},
|
|
3066
|
+
value: {
|
|
3067
|
+
type: "string",
|
|
3068
|
+
description: "Cookie value. Required. It is not echoed back in the response."
|
|
3069
|
+
},
|
|
3070
|
+
url: {
|
|
3071
|
+
type: "string",
|
|
3072
|
+
description: "The URL the cookie is set for, for example 'https://example.com/'. Chrome derives domain, path and secure from it. Firefox derives only the domain from its host. Give this or 'domain'."
|
|
3073
|
+
},
|
|
3074
|
+
domain: {
|
|
3075
|
+
type: "string",
|
|
3076
|
+
description: "Cookie domain, for example 'example.com' or '.example.com' for subdomains. Give this or 'url'. When both are given, this wins on Firefox and Chrome applies its own url plus domain consistency rule."
|
|
3077
|
+
},
|
|
3078
|
+
path: {
|
|
3079
|
+
type: "string",
|
|
3080
|
+
description: "Cookie path. Passed through exactly as given and NOT defaulted to '/': with a 'url' Chrome derives the path from it, so an invented default here would widen a cookie you meant to scope narrowly."
|
|
3081
|
+
},
|
|
3082
|
+
expires: {
|
|
3083
|
+
type: "number",
|
|
3084
|
+
description: "Expiry as Unix time in SECONDS, matching what list_cookies reports. Omit it for a session cookie, which is what both backends do when no expiry is supplied."
|
|
3085
|
+
},
|
|
3086
|
+
httpOnly: {
|
|
3087
|
+
type: "boolean",
|
|
3088
|
+
description: "Set the httpOnly flag, making the cookie invisible to document.cookie. Only a protocol write like this one can create such a cookie."
|
|
3089
|
+
},
|
|
3090
|
+
secure: {
|
|
3091
|
+
type: "boolean",
|
|
3092
|
+
description: "Set the secure flag. Chrome declines a secure cookie on an insecure origin, and that refusal surfaces as an error."
|
|
3093
|
+
},
|
|
3094
|
+
sameSite: {
|
|
3095
|
+
type: "string",
|
|
3096
|
+
enum: ["strict", "lax", "none", "default"],
|
|
3097
|
+
description: "SameSite attribute, lowercase, matching what list_cookies reports. 'default' means the attribute is not set at all."
|
|
3098
|
+
}
|
|
3099
|
+
},
|
|
3100
|
+
required: ["name", "value"],
|
|
3101
|
+
additionalProperties: false
|
|
3102
|
+
}
|
|
3103
|
+
},
|
|
3104
|
+
{
|
|
3105
|
+
name: "delete_cookies",
|
|
3106
|
+
description: "Delete the named cookie from the target page's cookie store, httpOnly cookies included. Chrome uses Network.deleteCookies, Firefox uses storage.deleteCookies with a filter, partitioned by the resolved browsing context. Both 'name' and one of 'url' or 'domain' are REQUIRED: without a site constraint the call would delete by name across the whole partition, so it is refused with an error instead. Narrow further with 'path'. The response is {deleted:true,target} and carries NO count, because neither protocol reports how many cookies it removed and a number here would be invented; call list_cookies before and after when you need a real count. 'deleted:true' means the backend accepted and performed the deletion, not that a matching cookie existed, since deleting an absent cookie is a success on both backends.",
|
|
3107
|
+
inputSchema: {
|
|
3108
|
+
type: "object",
|
|
3109
|
+
properties: {
|
|
3110
|
+
lease: {
|
|
3111
|
+
type: "string",
|
|
3112
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs."
|
|
3113
|
+
},
|
|
3114
|
+
target: {
|
|
3115
|
+
type: "string",
|
|
3116
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
3117
|
+
},
|
|
3118
|
+
name: {
|
|
3119
|
+
type: "string",
|
|
3120
|
+
description: "Exact cookie name to delete. Required and non-empty. No wildcards: this removes a named cookie, not a swathe of them."
|
|
3121
|
+
},
|
|
3122
|
+
url: {
|
|
3123
|
+
type: "string",
|
|
3124
|
+
description: "Delete cookies matching this URL, for example 'https://example.com/'. Give this or 'domain'. On Firefox only the host is used, because BiDi's filter has no url field."
|
|
3125
|
+
},
|
|
3126
|
+
domain: {
|
|
3127
|
+
type: "string",
|
|
3128
|
+
description: "Delete cookies with this exact domain. Give this or 'url'."
|
|
3129
|
+
},
|
|
3130
|
+
path: {
|
|
3131
|
+
type: "string",
|
|
3132
|
+
description: "Narrow the deletion to cookies with this exact path. Omit to match any path allowed by the other constraints."
|
|
3133
|
+
}
|
|
3134
|
+
},
|
|
3135
|
+
required: ["name"],
|
|
3136
|
+
additionalProperties: false
|
|
3137
|
+
}
|
|
3138
|
+
},
|
|
2365
3139
|
{
|
|
2366
3140
|
name: "take_snapshot",
|
|
2367
|
-
description: "Capture the page's accessibility tree (Accessibility.getFullAXTree) as a compact indented text tree where each line is prefixed with [uid]
|
|
3141
|
+
description: "Capture the page's accessibility tree (Accessibility.getFullAXTree) as a compact indented text tree where each line is prefixed with [uid], the node's CDP backendDOMNodeId. These uids are the stateless element references that every interaction tool (click/hover/fill/etc.) feeds back to resolve a live DOM node (via DOM.resolveNode({ backendNodeId: uid })), so run this first to discover uids.",
|
|
2368
3142
|
inputSchema: {
|
|
2369
3143
|
type: "object",
|
|
2370
3144
|
properties: {
|
|
3145
|
+
lease: {
|
|
3146
|
+
type: "string",
|
|
3147
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3148
|
+
},
|
|
2371
3149
|
target: {
|
|
2372
3150
|
type: "string",
|
|
2373
3151
|
description: "Target page selector: 'active' (default, = first page-type target) | 'index:N' (Nth page-type target, 0-based) | 'url:<substring>' | 'title:<substring>' | '<32-hex targetId>' (exact target by id)."
|
|
@@ -2387,6 +3165,10 @@ var MANIFEST = [
|
|
|
2387
3165
|
inputSchema: {
|
|
2388
3166
|
type: "object",
|
|
2389
3167
|
properties: {
|
|
3168
|
+
lease: {
|
|
3169
|
+
type: "string",
|
|
3170
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3171
|
+
},
|
|
2390
3172
|
target: {
|
|
2391
3173
|
type: "string",
|
|
2392
3174
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2423,6 +3205,10 @@ var MANIFEST = [
|
|
|
2423
3205
|
inputSchema: {
|
|
2424
3206
|
type: "object",
|
|
2425
3207
|
properties: {
|
|
3208
|
+
lease: {
|
|
3209
|
+
type: "string",
|
|
3210
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3211
|
+
},
|
|
2426
3212
|
target: {
|
|
2427
3213
|
type: "string",
|
|
2428
3214
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2446,6 +3232,10 @@ var MANIFEST = [
|
|
|
2446
3232
|
inputSchema: {
|
|
2447
3233
|
type: "object",
|
|
2448
3234
|
properties: {
|
|
3235
|
+
lease: {
|
|
3236
|
+
type: "string",
|
|
3237
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3238
|
+
},
|
|
2449
3239
|
target: {
|
|
2450
3240
|
type: "string",
|
|
2451
3241
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2494,6 +3284,10 @@ var MANIFEST = [
|
|
|
2494
3284
|
inputSchema: {
|
|
2495
3285
|
type: "object",
|
|
2496
3286
|
properties: {
|
|
3287
|
+
lease: {
|
|
3288
|
+
type: "string",
|
|
3289
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3290
|
+
},
|
|
2497
3291
|
target: {
|
|
2498
3292
|
type: "string",
|
|
2499
3293
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2523,6 +3317,10 @@ var MANIFEST = [
|
|
|
2523
3317
|
inputSchema: {
|
|
2524
3318
|
type: "object",
|
|
2525
3319
|
properties: {
|
|
3320
|
+
lease: {
|
|
3321
|
+
type: "string",
|
|
3322
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3323
|
+
},
|
|
2526
3324
|
target: {
|
|
2527
3325
|
type: "string",
|
|
2528
3326
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2565,6 +3363,10 @@ var MANIFEST = [
|
|
|
2565
3363
|
inputSchema: {
|
|
2566
3364
|
type: "object",
|
|
2567
3365
|
properties: {
|
|
3366
|
+
lease: {
|
|
3367
|
+
type: "string",
|
|
3368
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3369
|
+
},
|
|
2568
3370
|
target: {
|
|
2569
3371
|
type: "string",
|
|
2570
3372
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2594,6 +3396,10 @@ var MANIFEST = [
|
|
|
2594
3396
|
inputSchema: {
|
|
2595
3397
|
type: "object",
|
|
2596
3398
|
properties: {
|
|
3399
|
+
lease: {
|
|
3400
|
+
type: "string",
|
|
3401
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3402
|
+
},
|
|
2597
3403
|
target: {
|
|
2598
3404
|
type: "string",
|
|
2599
3405
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2622,6 +3428,10 @@ var MANIFEST = [
|
|
|
2622
3428
|
inputSchema: {
|
|
2623
3429
|
type: "object",
|
|
2624
3430
|
properties: {
|
|
3431
|
+
lease: {
|
|
3432
|
+
type: "string",
|
|
3433
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3434
|
+
},
|
|
2625
3435
|
target: {
|
|
2626
3436
|
type: "string",
|
|
2627
3437
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2653,10 +3463,14 @@ var MANIFEST = [
|
|
|
2653
3463
|
},
|
|
2654
3464
|
{
|
|
2655
3465
|
name: "take_screenshot",
|
|
2656
|
-
description: "Capture the viewport (default), the full scrollable page (fullPage), or a single element (uid or selector
|
|
3466
|
+
description: "Capture the viewport (default), the full scrollable page (fullPage), or a single element (uid or selector, exactly one and mutually exclusive) via raw CDP Page.captureScreenshot. Writes a PNG/JPEG under /tmp/cdp-toolkit (override with savePath) and returns {path,bytes,format,target}; raw base64 is only included when returnBase64 is set. quality applies to jpeg only.",
|
|
2657
3467
|
inputSchema: {
|
|
2658
3468
|
type: "object",
|
|
2659
3469
|
properties: {
|
|
3470
|
+
lease: {
|
|
3471
|
+
type: "string",
|
|
3472
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3473
|
+
},
|
|
2660
3474
|
target: {
|
|
2661
3475
|
type: "string",
|
|
2662
3476
|
description: "Target page selector. undefined or 'active' -> first page target; '<32-hex targetId>' -> exact target by id; 'index:N' -> Nth page target (0-based); 'url:<substring>' -> first page whose url contains substring; 'title:<substring>' -> first page whose title contains substring."
|
|
@@ -2679,7 +3493,7 @@ var MANIFEST = [
|
|
|
2679
3493
|
},
|
|
2680
3494
|
uid: {
|
|
2681
3495
|
type: "number",
|
|
2682
|
-
description: "Element to clip to
|
|
3496
|
+
description: "Element to clip to: a CDP backendDOMNodeId obtained from take_snapshot. Mutually exclusive with selector."
|
|
2683
3497
|
},
|
|
2684
3498
|
selector: {
|
|
2685
3499
|
type: "string",
|
|
@@ -2704,6 +3518,10 @@ var MANIFEST = [
|
|
|
2704
3518
|
inputSchema: {
|
|
2705
3519
|
type: "object",
|
|
2706
3520
|
properties: {
|
|
3521
|
+
lease: {
|
|
3522
|
+
type: "string",
|
|
3523
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3524
|
+
},
|
|
2707
3525
|
target: {
|
|
2708
3526
|
type: "string",
|
|
2709
3527
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2800,6 +3618,10 @@ var MANIFEST = [
|
|
|
2800
3618
|
inputSchema: {
|
|
2801
3619
|
type: "object",
|
|
2802
3620
|
properties: {
|
|
3621
|
+
lease: {
|
|
3622
|
+
type: "string",
|
|
3623
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3624
|
+
},
|
|
2803
3625
|
target: {
|
|
2804
3626
|
type: "string",
|
|
2805
3627
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2834,6 +3656,10 @@ var MANIFEST = [
|
|
|
2834
3656
|
inputSchema: {
|
|
2835
3657
|
type: "object",
|
|
2836
3658
|
properties: {
|
|
3659
|
+
lease: {
|
|
3660
|
+
type: "string",
|
|
3661
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3662
|
+
},
|
|
2837
3663
|
target: {
|
|
2838
3664
|
type: "string",
|
|
2839
3665
|
description: "Target page selector: 'active' (or omitted) for the first page-type target | 'index:N' (0-based) | 'url:<substring>' | 'title:<substring>' | a 32-hex <targetId>."
|
|
@@ -2867,6 +3693,10 @@ var MANIFEST = [
|
|
|
2867
3693
|
inputSchema: {
|
|
2868
3694
|
type: "object",
|
|
2869
3695
|
properties: {
|
|
3696
|
+
lease: {
|
|
3697
|
+
type: "string",
|
|
3698
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3699
|
+
},
|
|
2870
3700
|
target: {
|
|
2871
3701
|
type: "string",
|
|
2872
3702
|
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
@@ -2886,10 +3716,14 @@ var MANIFEST = [
|
|
|
2886
3716
|
},
|
|
2887
3717
|
{
|
|
2888
3718
|
name: "get_console_message",
|
|
2889
|
-
description: "Return a single console entry by zero-based index from the target's existing console buffer. Throws if the index is out of range
|
|
3719
|
+
description: "Return a single console entry by zero-based index from the target's existing console buffer. Throws if the index is out of range; run list_console_messages (optionally with reload:true) first to populate the buffer.",
|
|
2890
3720
|
inputSchema: {
|
|
2891
3721
|
type: "object",
|
|
2892
3722
|
properties: {
|
|
3723
|
+
lease: {
|
|
3724
|
+
type: "string",
|
|
3725
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3726
|
+
},
|
|
2893
3727
|
target: {
|
|
2894
3728
|
type: "string",
|
|
2895
3729
|
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
@@ -2909,6 +3743,10 @@ var MANIFEST = [
|
|
|
2909
3743
|
inputSchema: {
|
|
2910
3744
|
type: "object",
|
|
2911
3745
|
properties: {
|
|
3746
|
+
lease: {
|
|
3747
|
+
type: "string",
|
|
3748
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3749
|
+
},
|
|
2912
3750
|
target: {
|
|
2913
3751
|
type: "string",
|
|
2914
3752
|
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
@@ -2932,17 +3770,21 @@ var MANIFEST = [
|
|
|
2932
3770
|
},
|
|
2933
3771
|
{
|
|
2934
3772
|
name: "get_network_request",
|
|
2935
|
-
description: "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh reload capture and is matched by url ONLY (reload re-mints requestIds, so a carried-over requestId cannot fetch a body
|
|
3773
|
+
description: "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh reload capture and is matched by url ONLY (reload re-mints requestIds, so a carried-over requestId cannot fetch a body, so it returns metadata plus bodyUnavailableReason).",
|
|
2936
3774
|
inputSchema: {
|
|
2937
3775
|
type: "object",
|
|
2938
3776
|
properties: {
|
|
3777
|
+
lease: {
|
|
3778
|
+
type: "string",
|
|
3779
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3780
|
+
},
|
|
2939
3781
|
target: {
|
|
2940
3782
|
type: "string",
|
|
2941
3783
|
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
2942
3784
|
},
|
|
2943
3785
|
requestId: {
|
|
2944
3786
|
type: "string",
|
|
2945
|
-
description: "Match by exact requestId (metadata only
|
|
3787
|
+
description: "Match by exact requestId (metadata only, cannot fetch a body, since reload re-mints requestIds)."
|
|
2946
3788
|
},
|
|
2947
3789
|
url: {
|
|
2948
3790
|
type: "string",
|
|
@@ -2967,6 +3809,10 @@ var MANIFEST = [
|
|
|
2967
3809
|
inputSchema: {
|
|
2968
3810
|
type: "object",
|
|
2969
3811
|
properties: {
|
|
3812
|
+
lease: {
|
|
3813
|
+
type: "string",
|
|
3814
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3815
|
+
},
|
|
2970
3816
|
target: {
|
|
2971
3817
|
type: "string",
|
|
2972
3818
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -2985,10 +3831,14 @@ var MANIFEST = [
|
|
|
2985
3831
|
},
|
|
2986
3832
|
{
|
|
2987
3833
|
name: "performance_stop_trace",
|
|
2988
|
-
description: "Stop the in-process trace started by performance_start_trace, drain buffered Tracing.dataCollected events, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics}. Throws if no live trace exists in this process (e.g. start ran in a different process)
|
|
3834
|
+
description: "Stop the in-process trace started by performance_start_trace, drain buffered Tracing.dataCollected events, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics}. Throws if no live trace exists in this process (e.g. start ran in a different process); use performance_trace instead. The 'target' arg is accepted only for API symmetry; at most one trace is ever live per process.",
|
|
2989
3835
|
inputSchema: {
|
|
2990
3836
|
type: "object",
|
|
2991
3837
|
properties: {
|
|
3838
|
+
lease: {
|
|
3839
|
+
type: "string",
|
|
3840
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3841
|
+
},
|
|
2992
3842
|
target: {
|
|
2993
3843
|
type: "string",
|
|
2994
3844
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -3000,10 +3850,14 @@ var MANIFEST = [
|
|
|
3000
3850
|
},
|
|
3001
3851
|
{
|
|
3002
3852
|
name: "performance_analyze_insight",
|
|
3003
|
-
description: "CDP-native approximation of the DevTools insight analyzer: read a trace JSON file (bare array or {traceEvents:[...]}) at the given tracePath and return headline metrics (FCP/LCP/CLS/TBT, long tasks, layout shifts). Requires an explicit tracePath returned by performance_trace/performance_stop_trace
|
|
3853
|
+
description: "CDP-native approximation of the DevTools insight analyzer: read a trace JSON file (bare array or {traceEvents:[...]}) at the given tracePath and return headline metrics (FCP/LCP/CLS/TBT, long tasks, layout shifts). Requires an explicit tracePath returned by performance_trace/performance_stop_trace; there is no implicit 'latest trace'. Numbers approximate DevTools (no main-thread attribution or frame-scoped LCP).",
|
|
3004
3854
|
inputSchema: {
|
|
3005
3855
|
type: "object",
|
|
3006
3856
|
properties: {
|
|
3857
|
+
lease: {
|
|
3858
|
+
type: "string",
|
|
3859
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3860
|
+
},
|
|
3007
3861
|
tracePath: {
|
|
3008
3862
|
type: "string",
|
|
3009
3863
|
description: "Filesystem path to a trace JSON file written by performance_trace or performance_stop_trace (bare array OR {traceEvents:[...]}). Required."
|
|
@@ -3015,10 +3869,14 @@ var MANIFEST = [
|
|
|
3015
3869
|
},
|
|
3016
3870
|
{
|
|
3017
3871
|
name: "performance_trace",
|
|
3018
|
-
description: "PRIMARY one-shot trace: start tracing, optionally reload or navigate the page, wait durationMs (default 3000), end the trace, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics,target}. Holds one connection open for the whole window, so it is immune to the cross-process limitation of start/stop
|
|
3872
|
+
description: "PRIMARY one-shot trace: start tracing, optionally reload or navigate the page, wait durationMs (default 3000), end the trace, write the trace JSON under /tmp/cdp-toolkit, and return {path,bytes,events,metrics,target}. Holds one connection open for the whole window, so it is immune to the cross-process limitation of start/stop, and it is the recommended entry point.",
|
|
3019
3873
|
inputSchema: {
|
|
3020
3874
|
type: "object",
|
|
3021
3875
|
properties: {
|
|
3876
|
+
lease: {
|
|
3877
|
+
type: "string",
|
|
3878
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3879
|
+
},
|
|
3022
3880
|
target: {
|
|
3023
3881
|
type: "string",
|
|
3024
3882
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -3053,6 +3911,10 @@ var MANIFEST = [
|
|
|
3053
3911
|
inputSchema: {
|
|
3054
3912
|
type: "object",
|
|
3055
3913
|
properties: {
|
|
3914
|
+
lease: {
|
|
3915
|
+
type: "string",
|
|
3916
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3917
|
+
},
|
|
3056
3918
|
target: {
|
|
3057
3919
|
type: "string",
|
|
3058
3920
|
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
@@ -3072,9 +3934,13 @@ var MANIFEST = [
|
|
|
3072
3934
|
inputSchema: {
|
|
3073
3935
|
type: "object",
|
|
3074
3936
|
properties: {
|
|
3937
|
+
lease: {
|
|
3938
|
+
type: "string",
|
|
3939
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3940
|
+
},
|
|
3075
3941
|
url: {
|
|
3076
3942
|
type: "string",
|
|
3077
|
-
description: "The URL to audit. Required
|
|
3943
|
+
description: "The URL to audit. Required, and never points at a user tab implicitly."
|
|
3078
3944
|
},
|
|
3079
3945
|
categories: {
|
|
3080
3946
|
type: "array",
|
|
@@ -3108,10 +3974,14 @@ var MANIFEST = [
|
|
|
3108
3974
|
},
|
|
3109
3975
|
{
|
|
3110
3976
|
name: "mock_request",
|
|
3111
|
-
description: "Register a mock rule on a target's persistent fake-backend session (CDP Fetch domain): intercept requests whose URL matches urlPattern and fulfill them with a canned response, fail (abort) them, or continue them
|
|
3977
|
+
description: "Register a mock rule on a target's persistent fake-backend session (CDP Fetch domain): intercept requests whose URL matches urlPattern and fulfill them with a canned response, fail (abort) them, or continue them, optionally with fault injection (delayMs/failRate). The session survives reloads and navigations and lives until clear_mocks. Call repeatedly to mock several endpoints on the same target. Pass reload:true to apply immediately. Persistent across calls only via the MCP server (not the one-shot CLI).",
|
|
3112
3978
|
inputSchema: {
|
|
3113
3979
|
type: "object",
|
|
3114
3980
|
properties: {
|
|
3981
|
+
lease: {
|
|
3982
|
+
type: "string",
|
|
3983
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
3984
|
+
},
|
|
3115
3985
|
target: {
|
|
3116
3986
|
type: "string",
|
|
3117
3987
|
description: "Target selector: active | index:N | url:<substr> | title:<substr> | <targetId>. Defaults to the active page."
|
|
@@ -3178,6 +4048,10 @@ var MANIFEST = [
|
|
|
3178
4048
|
inputSchema: {
|
|
3179
4049
|
type: "object",
|
|
3180
4050
|
properties: {
|
|
4051
|
+
lease: {
|
|
4052
|
+
type: "string",
|
|
4053
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
4054
|
+
},
|
|
3181
4055
|
target: {
|
|
3182
4056
|
type: "string",
|
|
3183
4057
|
description: "Unused filter placeholder; list_mocks returns all active sessions."
|
|
@@ -3193,6 +4067,10 @@ var MANIFEST = [
|
|
|
3193
4067
|
inputSchema: {
|
|
3194
4068
|
type: "object",
|
|
3195
4069
|
properties: {
|
|
4070
|
+
lease: {
|
|
4071
|
+
type: "string",
|
|
4072
|
+
description: "Opaque lease token from claim_page. Required only when the tab this call resolves to is leased by someone; omit it for unleased tabs, which behave exactly as before."
|
|
4073
|
+
},
|
|
3196
4074
|
target: {
|
|
3197
4075
|
type: "string",
|
|
3198
4076
|
description: "Target selector whose mock session to clear. Defaults to the active page. Ignored when all:true."
|
|
@@ -3205,6 +4083,60 @@ var MANIFEST = [
|
|
|
3205
4083
|
required: [],
|
|
3206
4084
|
additionalProperties: false
|
|
3207
4085
|
}
|
|
4086
|
+
},
|
|
4087
|
+
{
|
|
4088
|
+
name: "claim_page",
|
|
4089
|
+
description: "Take exclusive ownership of a browser tab and return an opaque lease token. With no targetId, opens a fresh tab (optionally at url) and claims it, so 'give me my own tab' is one call. 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
|
+
inputSchema: {
|
|
4091
|
+
type: "object",
|
|
4092
|
+
properties: {
|
|
4093
|
+
targetId: {
|
|
4094
|
+
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."
|
|
4096
|
+
},
|
|
4097
|
+
url: {
|
|
4098
|
+
type: "string",
|
|
4099
|
+
description: "When opening a fresh tab, navigate it here. Ignored when targetId is given."
|
|
4100
|
+
},
|
|
4101
|
+
label: {
|
|
4102
|
+
type: "string",
|
|
4103
|
+
description: "Agent label recorded on the lease and surfaced in conflict errors and list_leases. Defaults to pid-<pid>."
|
|
4104
|
+
},
|
|
4105
|
+
ttlMs: {
|
|
4106
|
+
type: "number",
|
|
4107
|
+
description: "How long the lease survives without use before it is reclaimable. Defaults to CDP_LEASE_TTL_MS, else 900000 (15 minutes). Every checked call refreshes it, so an active agent never expires."
|
|
4108
|
+
}
|
|
4109
|
+
},
|
|
4110
|
+
required: [],
|
|
4111
|
+
additionalProperties: false
|
|
4112
|
+
}
|
|
4113
|
+
},
|
|
4114
|
+
{
|
|
4115
|
+
name: "release_page",
|
|
4116
|
+
description: "Release a held lease, given its token. Idempotent: releasing an already-released, reclaimed, or expired lease reports released:false rather than erroring. Does not close the tab.",
|
|
4117
|
+
inputSchema: {
|
|
4118
|
+
type: "object",
|
|
4119
|
+
properties: {
|
|
4120
|
+
lease: {
|
|
4121
|
+
type: "string",
|
|
4122
|
+
description: "The opaque token claim_page (or new_page with claim:true) returned."
|
|
4123
|
+
}
|
|
4124
|
+
},
|
|
4125
|
+
required: [
|
|
4126
|
+
"lease"
|
|
4127
|
+
],
|
|
4128
|
+
additionalProperties: false
|
|
4129
|
+
}
|
|
4130
|
+
},
|
|
4131
|
+
{
|
|
4132
|
+
name: "list_leases",
|
|
4133
|
+
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.',
|
|
4134
|
+
inputSchema: {
|
|
4135
|
+
type: "object",
|
|
4136
|
+
properties: {},
|
|
4137
|
+
required: [],
|
|
4138
|
+
additionalProperties: false
|
|
4139
|
+
}
|
|
3208
4140
|
}
|
|
3209
4141
|
];
|
|
3210
4142
|
|
|
@@ -3712,6 +4644,37 @@ function buildSnapshotFunctionSource() {
|
|
|
3712
4644
|
var MAX_NAME_LENGTH = 120;
|
|
3713
4645
|
|
|
3714
4646
|
// src/bidi/driver.ts
|
|
4647
|
+
function normalizeBidiCookie(c) {
|
|
4648
|
+
const value = c.value.type === "base64" ? Buffer.from(c.value.value, "base64").toString("utf8") : c.value.value;
|
|
4649
|
+
const hasExpiry = typeof c.expiry === "number";
|
|
4650
|
+
return {
|
|
4651
|
+
name: c.name,
|
|
4652
|
+
value,
|
|
4653
|
+
domain: c.domain,
|
|
4654
|
+
path: c.path,
|
|
4655
|
+
expires: hasExpiry ? c.expiry : -1,
|
|
4656
|
+
size: typeof c.size === "number" ? c.size : Buffer.byteLength(`${c.name}${value}`, "utf8"),
|
|
4657
|
+
httpOnly: c.httpOnly === true,
|
|
4658
|
+
secure: c.secure === true,
|
|
4659
|
+
sameSite: c.sameSite ?? "default",
|
|
4660
|
+
session: !hasExpiry
|
|
4661
|
+
};
|
|
4662
|
+
}
|
|
4663
|
+
function hostFromUrl(url, toolName) {
|
|
4664
|
+
if (url === undefined || url === "") {
|
|
4665
|
+
throw driverError2("unsupported", `${toolName} on Firefox needs a 'domain': BiDi's storage cookie calls have no 'url' parameter, and no url was given to derive one from.`);
|
|
4666
|
+
}
|
|
4667
|
+
let host = "";
|
|
4668
|
+
try {
|
|
4669
|
+
host = new URL(url).hostname;
|
|
4670
|
+
} catch {
|
|
4671
|
+
throw driverError2("unsupported", `${toolName} could not parse the url '${url}' to derive the 'domain' BiDi requires. Pass 'domain' explicitly.`);
|
|
4672
|
+
}
|
|
4673
|
+
if (host === "") {
|
|
4674
|
+
throw driverError2("unsupported", `${toolName} cannot derive a 'domain' from '${url}': that url has no host. Pass 'domain' explicitly.`);
|
|
4675
|
+
}
|
|
4676
|
+
return host;
|
|
4677
|
+
}
|
|
3715
4678
|
function driverError2(code, message, data) {
|
|
3716
4679
|
return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
|
|
3717
4680
|
}
|
|
@@ -3963,12 +4926,17 @@ async function safeTitle(conn, contextId) {
|
|
|
3963
4926
|
async function pageInfoOf(conn, ctx) {
|
|
3964
4927
|
return { id: ctx.context, url: ctx.url, title: await safeTitle(conn, ctx.context) };
|
|
3965
4928
|
}
|
|
3966
|
-
async function resolveContext(conn, selector) {
|
|
4929
|
+
async function resolveContext(conn, selector, opts = {}) {
|
|
3967
4930
|
const { contexts } = await conn.send("browsingContext.getTree", {}).catch((e) => {
|
|
3968
4931
|
throw mapBidiError(e);
|
|
3969
4932
|
});
|
|
3970
4933
|
if (!contexts.length)
|
|
3971
4934
|
throw driverError2("no-such-target", "no browsing contexts available");
|
|
4935
|
+
const hit = await pickContext(conn, contexts, selector);
|
|
4936
|
+
await assertLeaseOk("firefox", hit.context, { lease: opts.lease, url: hit.url });
|
|
4937
|
+
return hit;
|
|
4938
|
+
}
|
|
4939
|
+
async function pickContext(conn, contexts, selector) {
|
|
3972
4940
|
if (selector === undefined || selector === "active")
|
|
3973
4941
|
return contexts[0];
|
|
3974
4942
|
if (selector.startsWith("index:")) {
|
|
@@ -4315,6 +5283,49 @@ class BidiPageDriver {
|
|
|
4315
5283
|
}
|
|
4316
5284
|
return this.handleOneDialog(accept, promptText, opts?.timeoutMs ?? 15000);
|
|
4317
5285
|
}
|
|
5286
|
+
async getCookies() {
|
|
5287
|
+
try {
|
|
5288
|
+
const { cookies } = await this.conn.send("storage.getCookies", { partition: { type: "context", context: this.contextId } });
|
|
5289
|
+
return (cookies ?? []).map(normalizeBidiCookie);
|
|
5290
|
+
} catch (e) {
|
|
5291
|
+
throw mapBidiError(e);
|
|
5292
|
+
}
|
|
5293
|
+
}
|
|
5294
|
+
async setCookie(params) {
|
|
5295
|
+
const domain = params.domain ?? hostFromUrl(params.url, "set_cookie");
|
|
5296
|
+
try {
|
|
5297
|
+
await this.conn.send("storage.setCookie", {
|
|
5298
|
+
cookie: {
|
|
5299
|
+
name: params.name,
|
|
5300
|
+
value: { type: "string", value: params.value },
|
|
5301
|
+
domain,
|
|
5302
|
+
...params.path !== undefined ? { path: params.path } : {},
|
|
5303
|
+
...params.expires !== undefined ? { expiry: params.expires } : {},
|
|
5304
|
+
...params.httpOnly !== undefined ? { httpOnly: params.httpOnly } : {},
|
|
5305
|
+
...params.secure !== undefined ? { secure: params.secure } : {},
|
|
5306
|
+
...params.sameSite !== undefined ? { sameSite: params.sameSite } : {}
|
|
5307
|
+
},
|
|
5308
|
+
partition: { type: "context", context: this.contextId }
|
|
5309
|
+
});
|
|
5310
|
+
} catch (e) {
|
|
5311
|
+
throw mapBidiError(e);
|
|
5312
|
+
}
|
|
5313
|
+
}
|
|
5314
|
+
async deleteCookies(filter) {
|
|
5315
|
+
const domain = filter.domain ?? hostFromUrl(filter.url, "delete_cookies");
|
|
5316
|
+
try {
|
|
5317
|
+
await this.conn.send("storage.deleteCookies", {
|
|
5318
|
+
filter: {
|
|
5319
|
+
name: filter.name,
|
|
5320
|
+
domain,
|
|
5321
|
+
...filter.path !== undefined ? { path: filter.path } : {}
|
|
5322
|
+
},
|
|
5323
|
+
partition: { type: "context", context: this.contextId }
|
|
5324
|
+
});
|
|
5325
|
+
} catch (e) {
|
|
5326
|
+
throw mapBidiError(e);
|
|
5327
|
+
}
|
|
5328
|
+
}
|
|
4318
5329
|
async startBodyCapture() {
|
|
4319
5330
|
let collector;
|
|
4320
5331
|
try {
|
|
@@ -4489,7 +5500,7 @@ import { spawn as spawn2 } from "node:child_process";
|
|
|
4489
5500
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
4490
5501
|
import { createConnection, createServer } from "node:net";
|
|
4491
5502
|
import { tmpdir } from "node:os";
|
|
4492
|
-
import { join as
|
|
5503
|
+
import { join as join6 } from "node:path";
|
|
4493
5504
|
function resolveBinary() {
|
|
4494
5505
|
const candidates = [
|
|
4495
5506
|
process.env.FIREFOX_BIN,
|
|
@@ -4563,7 +5574,7 @@ async function launchFirefox(opts = {}) {
|
|
|
4563
5574
|
const binary = resolveBinary();
|
|
4564
5575
|
const port = await pickPort(opts.port);
|
|
4565
5576
|
const isThrowaway = opts.profilePath === undefined;
|
|
4566
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
5577
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join6(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
4567
5578
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
4568
5579
|
if (opts.headless)
|
|
4569
5580
|
args.push("--headless");
|
|
@@ -4828,11 +5839,12 @@ var BIDI_ONLY_TOOLS = {
|
|
|
4828
5839
|
// src/firefox-tools.ts
|
|
4829
5840
|
var FIREFOX_TOOLS = {
|
|
4830
5841
|
...SHARED_TOOLS,
|
|
4831
|
-
...BIDI_ONLY_TOOLS
|
|
5842
|
+
...BIDI_ONLY_TOOLS,
|
|
5843
|
+
...LEASE_TOOLS
|
|
4832
5844
|
};
|
|
4833
5845
|
|
|
4834
5846
|
// src/mcp.ts
|
|
4835
|
-
var VERSION = "1.
|
|
5847
|
+
var VERSION = "1.4.0";
|
|
4836
5848
|
var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
|
|
4837
5849
|
var AVAILABILITY = toolAvailability(BROWSER);
|
|
4838
5850
|
var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
|
|
@@ -4862,29 +5874,31 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
4862
5874
|
isError: true
|
|
4863
5875
|
};
|
|
4864
5876
|
}
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
5877
|
+
return withLeaseScope(leaseFromArgs(args), async () => {
|
|
5878
|
+
if (BROWSER === "chrome") {
|
|
5879
|
+
const fn = dispatch[name];
|
|
5880
|
+
if (!fn)
|
|
5881
|
+
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
|
|
5882
|
+
try {
|
|
5883
|
+
const result = await fn(args);
|
|
5884
|
+
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
5885
|
+
} catch (err) {
|
|
5886
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
5887
|
+
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
5888
|
+
}
|
|
5889
|
+
}
|
|
5890
|
+
const neutralFn = neutralDispatch[name];
|
|
5891
|
+
if (!neutralFn)
|
|
4868
5892
|
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
|
|
4869
5893
|
try {
|
|
4870
|
-
const
|
|
5894
|
+
const session = await getOrCreateFirefoxSession();
|
|
5895
|
+
const result = await neutralFn(session.driver, args);
|
|
4871
5896
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
4872
5897
|
} catch (err) {
|
|
4873
5898
|
const message = err instanceof Error ? err.message : String(err);
|
|
4874
5899
|
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
4875
5900
|
}
|
|
4876
|
-
}
|
|
4877
|
-
const neutralFn = neutralDispatch[name];
|
|
4878
|
-
if (!neutralFn)
|
|
4879
|
-
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
|
|
4880
|
-
try {
|
|
4881
|
-
const session = await getOrCreateFirefoxSession();
|
|
4882
|
-
const result = await neutralFn(session.driver, args);
|
|
4883
|
-
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
4884
|
-
} catch (err) {
|
|
4885
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4886
|
-
return { content: [{ type: "text", text: `Error: ${message}` }], isError: true };
|
|
4887
|
-
}
|
|
5901
|
+
});
|
|
4888
5902
|
});
|
|
4889
5903
|
auditCoverage();
|
|
4890
5904
|
await server.connect(new StdioServerTransport);
|