cdp-toolkit 1.0.0 → 1.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/cli.js CHANGED
@@ -1,8 +1,256 @@
1
1
  #!/usr/bin/env node
2
2
 
3
- // src/tools/pages.ts
4
- import { mkdir, writeFile } from "node:fs/promises";
5
- import { dirname } from "node:path";
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 safeId(id) {
17
+ return id.replace(/[^A-Za-z0-9._-]/g, "_");
18
+ }
19
+ function leaseFile(backend, targetId) {
20
+ return join(leaseDir(), `lease-${backend}-${safeId(targetId)}.json`);
21
+ }
22
+ function mintToken(backend, targetId, nonce) {
23
+ return `${backend}:${targetId}:${nonce}`;
24
+ }
25
+ function tokenParts(token) {
26
+ if (typeof token !== "string")
27
+ return;
28
+ const first = token.indexOf(":");
29
+ const last = token.lastIndexOf(":");
30
+ if (first <= 0 || last <= first)
31
+ return;
32
+ const backend = token.slice(0, first);
33
+ if (backend !== "chrome" && backend !== "firefox")
34
+ return;
35
+ const targetId = token.slice(first + 1, last);
36
+ const nonce = token.slice(last + 1);
37
+ if (!targetId.length || !/^[0-9a-f]{24}$/.test(nonce))
38
+ return;
39
+ return { backend, targetId, nonce };
40
+ }
41
+
42
+ class LeaseConflictError extends Error {
43
+ targetId;
44
+ holder;
45
+ constructor(message, targetId, holder) {
46
+ super(message);
47
+ this.targetId = targetId;
48
+ this.holder = holder;
49
+ this.name = "LeaseConflictError";
50
+ }
51
+ }
52
+ async function readLease(backend, targetId) {
53
+ let raw;
54
+ try {
55
+ raw = await readFile(leaseFile(backend, targetId), "utf8");
56
+ } catch (err) {
57
+ if (err.code === "ENOENT")
58
+ return;
59
+ throw err;
60
+ }
61
+ try {
62
+ const rec = JSON.parse(raw);
63
+ if (typeof rec?.nonce === "string" && typeof rec?.pid === "number")
64
+ return rec;
65
+ } catch {}
66
+ return;
67
+ }
68
+ function isPidAlive(pid) {
69
+ if (!Number.isInteger(pid) || pid <= 0)
70
+ return false;
71
+ try {
72
+ process.kill(pid, 0);
73
+ return true;
74
+ } catch (err) {
75
+ return err.code === "EPERM";
76
+ }
77
+ }
78
+ function staleReason(rec, opts) {
79
+ if (!isPidAlive(rec.pid))
80
+ return "dead-pid";
81
+ if (opts.now - rec.lastUsedAt > rec.ttlMs)
82
+ return "expired";
83
+ if (opts.liveIds && !opts.liveIds.includes(rec.targetId))
84
+ return "target-gone";
85
+ return false;
86
+ }
87
+ async function claimLease(backend, targetId, opts) {
88
+ const now = opts.now ?? Date.now();
89
+ await mkdir(leaseDir(), { recursive: true });
90
+ for (let attempt = 0;attempt < 3; attempt++) {
91
+ const record = {
92
+ backend,
93
+ targetId,
94
+ nonce: randomBytes(12).toString("hex"),
95
+ pid: process.pid,
96
+ label: opts.label,
97
+ createdAt: now,
98
+ lastUsedAt: now,
99
+ ttlMs: opts.ttlMs ?? leaseTtlMs()
100
+ };
101
+ try {
102
+ await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
103
+ return { token: mintToken(backend, targetId, record.nonce), record };
104
+ } catch (err) {
105
+ if (err.code !== "EEXIST")
106
+ throw err;
107
+ }
108
+ const existing = await readLease(backend, targetId);
109
+ if (!existing)
110
+ continue;
111
+ const stale = staleReason(existing, { now, liveIds: opts.liveIds });
112
+ if (!stale) {
113
+ 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);
114
+ }
115
+ await unlink(leaseFile(backend, targetId)).catch(() => {
116
+ return;
117
+ });
118
+ }
119
+ throw new LeaseConflictError(`target '${targetId}' could not be claimed after 3 attempts: another process is claiming and releasing it concurrently`, targetId);
120
+ }
121
+ async function releaseLease(token) {
122
+ const parts = tokenParts(token);
123
+ if (!parts)
124
+ return { released: false };
125
+ const rec = await readLease(parts.backend, parts.targetId);
126
+ if (!rec || rec.nonce !== parts.nonce)
127
+ return { released: false };
128
+ await unlink(leaseFile(parts.backend, parts.targetId)).catch(() => {
129
+ return;
130
+ });
131
+ return { released: true };
132
+ }
133
+ async function releaseLeaseFor(backend, targetId) {
134
+ if (!await readLease(backend, targetId))
135
+ return { released: false };
136
+ await unlink(leaseFile(backend, targetId)).catch(() => {
137
+ return;
138
+ });
139
+ return { released: true };
140
+ }
141
+ var leaseScope = new AsyncLocalStorage;
142
+ function withLeaseScope(lease, fn) {
143
+ return leaseScope.run(lease, fn);
144
+ }
145
+ function currentLease() {
146
+ return leaseScope.getStore();
147
+ }
148
+ function leaseFromArgs(args) {
149
+ if (!args || typeof args !== "object")
150
+ return;
151
+ const lease = args.lease;
152
+ return typeof lease === "string" && lease.length > 0 ? lease : undefined;
153
+ }
154
+ async function touchLease(rec, now = Date.now()) {
155
+ const current = await readLease(rec.backend, rec.targetId).catch(() => {
156
+ return;
157
+ });
158
+ if (!current || current.nonce !== rec.nonce)
159
+ return;
160
+ await writeFile(leaseFile(current.backend, current.targetId), JSON.stringify({ ...current, lastUsedAt: now }), "utf8").catch(() => {
161
+ return;
162
+ });
163
+ }
164
+ function describeTarget(targetId, ctx) {
165
+ const url = ctx.url ? ` (url ${ctx.url})` : ctx.title ? ` (title ${JSON.stringify(ctx.title)})` : "";
166
+ return `target '${targetId}'${url}`;
167
+ }
168
+ async function assertLeaseOk(backend, targetId, ctx = {}) {
169
+ const token = ctx.lease ?? currentLease();
170
+ const now = ctx.now ?? Date.now();
171
+ const rec = await readLease(backend, targetId);
172
+ const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
173
+ const where = describeTarget(targetId, ctx);
174
+ if (token === undefined) {
175
+ if (!held)
176
+ return;
177
+ 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);
178
+ }
179
+ const parts = tokenParts(token);
180
+ if (!parts) {
181
+ throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
182
+ }
183
+ if (parts.backend !== backend || parts.targetId !== targetId) {
184
+ if (!held)
185
+ return;
186
+ 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);
187
+ }
188
+ if (!held) {
189
+ 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);
190
+ }
191
+ if (parts.nonce !== held.nonce) {
192
+ 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);
193
+ }
194
+ await touchLease(held, now);
195
+ }
196
+ var LEASE_FILE_RE = /^lease-(chrome|firefox)-(.+)\.json$/;
197
+ async function listLeases(opts = {}) {
198
+ const now = opts.now ?? Date.now();
199
+ let names = [];
200
+ try {
201
+ names = await readdir(leaseDir());
202
+ } catch (err) {
203
+ if (err.code === "ENOENT")
204
+ return [];
205
+ throw err;
206
+ }
207
+ const out = [];
208
+ for (const name of names) {
209
+ const parts = LEASE_FILE_RE.exec(name);
210
+ if (!parts)
211
+ continue;
212
+ const fromName = {
213
+ backend: parts[1],
214
+ targetId: parts[2],
215
+ label: "",
216
+ pid: 0,
217
+ createdAt: 0,
218
+ lastUsedAt: 0,
219
+ ttlMs: 0,
220
+ pidAlive: false,
221
+ stale: false
222
+ };
223
+ let rec;
224
+ try {
225
+ rec = JSON.parse(await readFile(join(leaseDir(), name), "utf8"));
226
+ } catch (err) {
227
+ const code = err.code;
228
+ if (code === "ENOENT")
229
+ continue;
230
+ out.push({ ...fromName, unreadable: code ?? "unparseable" });
231
+ continue;
232
+ }
233
+ if (typeof rec?.nonce !== "string" || typeof rec?.pid !== "number") {
234
+ out.push({ ...fromName, unreadable: "unparseable" });
235
+ continue;
236
+ }
237
+ out.push({
238
+ backend: rec.backend,
239
+ targetId: rec.targetId,
240
+ label: rec.label,
241
+ pid: rec.pid,
242
+ createdAt: rec.createdAt,
243
+ lastUsedAt: rec.lastUsedAt,
244
+ ttlMs: rec.ttlMs,
245
+ pidAlive: isPidAlive(rec.pid),
246
+ stale: staleReason(rec, {
247
+ now,
248
+ liveIds: opts.liveBackend === undefined || opts.liveBackend === rec.backend ? opts.liveIds : undefined
249
+ })
250
+ });
251
+ }
252
+ return out;
253
+ }
6
254
 
7
255
  // src/client.ts
8
256
  var BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
@@ -157,8 +405,13 @@ async function browserWsUrl() {
157
405
  const v = await httpJson("/json/version");
158
406
  return v.webSocketDebuggerUrl;
159
407
  }
160
- async function resolveTarget(selector) {
408
+ async function resolveTarget(selector, opts = {}) {
161
409
  const targets = await listTargets();
410
+ const hit = pickTarget(targets, selector);
411
+ await assertLeaseOk("chrome", hit.id, { lease: opts.lease, url: hit.url, title: hit.title });
412
+ return hit;
413
+ }
414
+ function pickTarget(targets, selector) {
162
415
  const pages = targets.filter((t) => t.type === "page");
163
416
  if (!selector || selector === "active") {
164
417
  if (!pages.length)
@@ -194,7 +447,7 @@ async function openBrowser(opts = {}) {
194
447
  return new CdpConnection(await browserWsUrl(), opts).connect();
195
448
  }
196
449
  async function openPage(selector, opts = {}) {
197
- const target = await resolveTarget(selector);
450
+ const target = await resolveTarget(selector, { lease: opts.lease });
198
451
  const conn = await new CdpConnection(target.webSocketDebuggerUrl, opts).connect();
199
452
  return { conn, target };
200
453
  }
@@ -207,191 +460,6 @@ async function withPage(selector, fn, opts = {}) {
207
460
  }
208
461
  }
209
462
 
210
- // src/tools/pages.ts
211
- var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
212
- var SELECTED_FILE = `${STATE_DIR}/selected`;
213
- function toPageInfo(t) {
214
- return { id: t.id, url: t.url, title: t.title, type: t.type };
215
- }
216
- async function listPages(args = {}) {
217
- const targets = await listTargets();
218
- const filtered = args.all ? targets : targets.filter((t) => t.type === "page");
219
- const pages = filtered.map(toPageInfo);
220
- return { pages, count: pages.length };
221
- }
222
- async function newPage(args = {}) {
223
- const url = args.url ?? "about:blank";
224
- const conn = await openBrowser();
225
- try {
226
- const { targetId } = await conn.send("Target.createTarget", { url });
227
- if (!targetId)
228
- throw new CdpError("Target.createTarget returned no targetId");
229
- return { targetId, url };
230
- } finally {
231
- conn.close();
232
- }
233
- }
234
- async function closePage(args) {
235
- if (args.target === undefined || args.target === "") {
236
- throw new CdpError("close_page requires an explicit target; refusing to guess which page to close");
237
- }
238
- const target = await resolveTarget(args.target);
239
- const conn = await openBrowser();
240
- try {
241
- const res = await conn.send("Target.closeTarget", { targetId: target.id });
242
- return { closed: target.id, success: res.success ?? true };
243
- } finally {
244
- conn.close();
245
- }
246
- }
247
- async function selectPage(args) {
248
- if (args.target === undefined || args.target === "") {
249
- throw new CdpError("select_page requires an explicit target");
250
- }
251
- const target = await resolveTarget(args.target);
252
- const conn = await openBrowser();
253
- try {
254
- await conn.send("Target.activateTarget", { targetId: target.id });
255
- } finally {
256
- conn.close();
257
- }
258
- await mkdir(dirname(SELECTED_FILE), { recursive: true });
259
- await writeFile(SELECTED_FILE, target.id, "utf8");
260
- return { selected: target.id };
261
- }
262
-
263
- // src/tools/navigation.ts
264
- async function navigatePage(args) {
265
- const reload = args.reload === true;
266
- if (!reload && (!args.url || typeof args.url !== "string")) {
267
- throw new CdpError("navigate_page: 'url' is required (or pass reload:true to reload the current page)");
268
- }
269
- const waitUntil = args.waitUntil ?? "load";
270
- return withPage(args.target, async (conn) => {
271
- await conn.send("Page.enable");
272
- const milestoneMethod = waitUntil === "load" ? "Page.loadEventFired" : "Page.domContentEventFired";
273
- const milestone = conn.waitFor(milestoneMethod, undefined, args.timeoutMs).then(() => waitUntil).catch(() => {
274
- return;
275
- });
276
- const stopped = conn.waitFor("Page.frameStoppedLoading", undefined, args.timeoutMs).then(() => "frameStoppedLoading").catch(() => {
277
- return;
278
- });
279
- let resolvedUrl;
280
- let frameId;
281
- if (reload) {
282
- await conn.send("Page.reload", { ignoreCache: args.ignoreCache === true }, args.timeoutMs ? { timeoutMs: args.timeoutMs } : {});
283
- const tree = await conn.send("Page.getFrameTree").catch(() => {
284
- return;
285
- });
286
- frameId = tree?.frameTree?.frame?.id ?? "";
287
- resolvedUrl = tree?.frameTree?.frame?.url ?? args.url ?? "";
288
- } else {
289
- const nav = await conn.send("Page.navigate", { url: args.url }, args.timeoutMs ? { timeoutMs: args.timeoutMs } : {});
290
- if (nav.errorText) {
291
- throw new CdpError(`navigate_page: ${nav.errorText} (${args.url})`);
292
- }
293
- frameId = nav.frameId;
294
- resolvedUrl = args.url;
295
- }
296
- const settled = await Promise.race([milestone, stopped]);
297
- const waitedFor = settled ?? "navigate-only";
298
- return reload ? { url: resolvedUrl, frameId, reloaded: true, waitedFor } : { url: resolvedUrl, frameId, waitedFor };
299
- }, args.timeoutMs ? { timeoutMs: args.timeoutMs } : {});
300
- }
301
- var DEFAULT_WAIT_TIMEOUT_MS = 15000;
302
- var DEFAULT_POLL_MS = 250;
303
- async function waitForText(args) {
304
- if (typeof args.text !== "string" || args.text.length === 0) {
305
- throw new CdpError("wait_for: 'text' is required");
306
- }
307
- const timeoutMs = args.timeoutMs ?? DEFAULT_WAIT_TIMEOUT_MS;
308
- const pollMs = args.pollMs ?? DEFAULT_POLL_MS;
309
- const start = Date.now();
310
- return withPage(args.target, async (conn, _target) => {
311
- await conn.send("Runtime.enable");
312
- const expr = `(() => { const b = document.body; return !!b && typeof b.innerText === 'string' && b.innerText.includes(${JSON.stringify(args.text)}); })()`;
313
- for (;; ) {
314
- const { result } = await conn.send("Runtime.evaluate", {
315
- expression: expr,
316
- returnByValue: true
317
- });
318
- if (result?.value === true) {
319
- return { found: true, waitedMs: Date.now() - start };
320
- }
321
- if (Date.now() - start >= timeoutMs) {
322
- throw new CdpError(`wait_for: text not found within ${timeoutMs}ms: ${JSON.stringify(args.text)}`);
323
- }
324
- await delay(Math.min(pollMs, Math.max(0, timeoutMs - (Date.now() - start))));
325
- }
326
- }, { timeoutMs: Math.max(timeoutMs, DEFAULT_WAIT_TIMEOUT_MS) });
327
- }
328
- function delay(ms) {
329
- return new Promise((resolve) => setTimeout(resolve, ms));
330
- }
331
-
332
- // src/tools/evaluate.ts
333
- function exceptionMessage(details) {
334
- const ex = details.exception;
335
- const fromObject = ex?.description ?? (typeof ex?.value === "string" ? ex.value : ex?.value !== undefined ? JSON.stringify(ex.value) : undefined);
336
- return fromObject ?? details.text ?? "evaluation threw";
337
- }
338
- function unwrap(obj) {
339
- if (Object.prototype.hasOwnProperty.call(obj, "value"))
340
- return obj.value;
341
- if (obj.unserializableValue !== undefined)
342
- return obj.unserializableValue;
343
- if (obj.type === "undefined")
344
- return;
345
- return obj.description ?? null;
346
- }
347
- async function evaluateExpression(conn, expression, awaitPromise) {
348
- const res = await conn.send("Runtime.evaluate", {
349
- expression,
350
- returnByValue: true,
351
- awaitPromise,
352
- replMode: false,
353
- silent: false
354
- });
355
- if (res.exceptionDetails)
356
- throw new CdpError(exceptionMessage(res.exceptionDetails));
357
- return unwrap(res.result);
358
- }
359
- async function evaluateFunction(conn, expression, awaitPromise, args) {
360
- const globalObj = await conn.send("Runtime.evaluate", {
361
- expression: "globalThis",
362
- returnByValue: false
363
- });
364
- const objectId = globalObj.result.objectId;
365
- if (!objectId)
366
- throw new CdpError("could not resolve global object for function call");
367
- const res = await conn.send("Runtime.callFunctionOn", {
368
- objectId,
369
- functionDeclaration: expression,
370
- arguments: args.map((value) => ({ value })),
371
- returnByValue: true,
372
- awaitPromise,
373
- silent: false
374
- });
375
- if (res.exceptionDetails)
376
- throw new CdpError(exceptionMessage(res.exceptionDetails));
377
- return unwrap(res.result);
378
- }
379
- async function evaluateScript(args) {
380
- const { expression } = args;
381
- if (typeof expression !== "string" || expression.length === 0) {
382
- throw new CdpError("evaluateScript: 'expression' must be a non-empty string");
383
- }
384
- const awaitPromise = args.awaitPromise ?? true;
385
- const callArgs = args.args;
386
- return withPage(args.target, async (conn) => {
387
- await conn.send("Runtime.enable");
388
- if (callArgs && callArgs.length > 0) {
389
- return evaluateFunction(conn, expression, awaitPromise, callArgs);
390
- }
391
- return evaluateExpression(conn, expression, awaitPromise);
392
- });
393
- }
394
-
395
463
  // src/tools/snapshot.ts
396
464
  var INTERACTIVE_ROLES = new Set([
397
465
  "button",
@@ -414,92 +482,6 @@ var INTERACTIVE_ROLES = new Set([
414
482
  "MenuItem",
415
483
  "Button"
416
484
  ]);
417
- function axString(v) {
418
- if (!v)
419
- return;
420
- const raw = v.value;
421
- if (raw === undefined || raw === null)
422
- return;
423
- const s = String(raw).trim();
424
- return s.length ? s : undefined;
425
- }
426
- function propValue(node, name) {
427
- const p = node.properties?.find((x) => x.name === name);
428
- return axString(p?.value);
429
- }
430
- function isInteractive(role) {
431
- if (!role)
432
- return false;
433
- return INTERACTIVE_ROLES.has(role) || INTERACTIVE_ROLES.has(role.toLowerCase());
434
- }
435
- function formatNode(node, interactiveOnly) {
436
- const role = axString(node.role);
437
- const name = axString(node.name);
438
- const uid = node.backendDOMNodeId;
439
- if (uid === undefined)
440
- return;
441
- if (interactiveOnly && !isInteractive(role))
442
- return;
443
- if (!interactiveOnly && !role && !name)
444
- return;
445
- const extras = [];
446
- const value = axString(node.value);
447
- if (value)
448
- extras.push(`value=${JSON.stringify(value)}`);
449
- const checked = propValue(node, "checked");
450
- if (checked && checked !== "false")
451
- extras.push(`checked=${checked}`);
452
- const expanded = propValue(node, "expanded");
453
- if (expanded)
454
- extras.push(`expanded=${expanded}`);
455
- const disabled = propValue(node, "disabled");
456
- if (disabled === "true")
457
- extras.push("disabled");
458
- const url = propValue(node, "url");
459
- if (url)
460
- extras.push(`url=${url}`);
461
- const focused = propValue(node, "focused");
462
- if (focused === "true")
463
- extras.push("focused");
464
- const label = name ? ` ${JSON.stringify(name)}` : "";
465
- const extra = extras.length ? ` [${extras.join(" ")}]` : "";
466
- return `[${uid}] ${role ?? "generic"}${label}${extra}`;
467
- }
468
- async function takeSnapshot(args = {}) {
469
- const interactiveOnly = args.interactiveOnly ?? false;
470
- return withPage(args.target, async (conn, target) => {
471
- await conn.send("Accessibility.enable");
472
- const { nodes } = await conn.send("Accessibility.getFullAXTree");
473
- const byId = new Map;
474
- for (const n of nodes)
475
- byId.set(n.nodeId, n);
476
- const roots = nodes.filter((n) => !n.parentId || !byId.has(n.parentId));
477
- const lines = [];
478
- let nodeCount = 0;
479
- const walk = (node, depth) => {
480
- const line = node.ignored ? undefined : formatNode(node, interactiveOnly);
481
- if (line !== undefined) {
482
- const indent = interactiveOnly ? "" : " ".repeat(depth);
483
- lines.push(`${indent}${line}`);
484
- nodeCount++;
485
- }
486
- const childDepth = line !== undefined && !interactiveOnly ? depth + 1 : depth;
487
- for (const childId of node.childIds ?? []) {
488
- const child = byId.get(childId);
489
- if (child)
490
- walk(child, childDepth);
491
- }
492
- };
493
- for (const root of roots)
494
- walk(root, 0);
495
- return {
496
- snapshot: lines.join(`
497
- `),
498
- target: { id: target.id, url: target.url, title: target.title },
499
- nodeCount
500
- };
501
- });
502
- }
503
485
  async function resolveUid(conn, uid) {
504
486
  const { object } = await conn.send("DOM.resolveNode", {
505
487
  backendNodeId: uid
@@ -510,149 +492,236 @@ async function resolveUid(conn, uid) {
510
492
  return { objectId: object.objectId };
511
493
  }
512
494
 
513
- // src/tools/input.ts
514
- async function resolveElement(conn, t) {
515
- const hasUid = t.uid !== undefined && t.uid !== null;
516
- const hasSelector = typeof t.selector === "string" && t.selector.length > 0;
517
- if (hasUid === hasSelector) {
518
- throw new CdpError("provide exactly one of { uid } or { selector }");
519
- }
520
- if (hasUid) {
521
- return resolveUid(conn, t.uid);
522
- }
523
- const { result, exceptionDetails } = await conn.send("Runtime.evaluate", {
524
- expression: `document.querySelector(${JSON.stringify(t.selector)})`,
525
- returnByValue: false
526
- });
527
- if (exceptionDetails) {
528
- throw new CdpError(`selector '${t.selector}' evaluation failed: ${exceptionDetails.text ?? "error"}`);
529
- }
530
- if (!result.objectId || result.subtype === "null") {
531
- throw new CdpError(`selector '${t.selector}' matched no element`);
532
- }
533
- return { objectId: result.objectId };
495
+ // src/tools/network_mock.ts
496
+ function escapeRegExpChar(ch) {
497
+ return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
534
498
  }
535
- async function centerOf(conn, objectId) {
536
- const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", {
537
- objectId,
538
- functionDeclaration: "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};}",
539
- returnByValue: true
540
- });
541
- if (exceptionDetails) {
542
- throw new CdpError(`could not measure element: ${exceptionDetails.text ?? "error"}`);
543
- }
544
- if (!result.value) {
545
- throw new CdpError("element has zero size / is not visible; cannot compute a click point");
499
+ function globToRegExp(pattern) {
500
+ let re = "";
501
+ for (let i = 0;i < pattern.length; i++) {
502
+ const ch = pattern[i];
503
+ if (ch === "\\") {
504
+ const next = pattern[i + 1];
505
+ if (next !== undefined) {
506
+ re += escapeRegExpChar(next);
507
+ i++;
508
+ } else {
509
+ re += "\\\\";
510
+ }
511
+ } else if (ch === "*") {
512
+ re += ".*";
513
+ } else if (ch === "?") {
514
+ re += ".";
515
+ } else {
516
+ re += escapeRegExpChar(ch);
517
+ }
546
518
  }
547
- return result.value;
548
- }
549
- async function focusElement(conn, objectId) {
550
- await conn.send("Runtime.callFunctionOn", {
551
- objectId,
552
- functionDeclaration: "function(){this.focus&&this.focus();}",
553
- returnByValue: true
554
- });
555
- }
556
- async function click(args) {
557
- const button = args.button ?? "left";
558
- const clickCount = args.clickCount ?? 1;
559
- return withPage(args.target, async (conn) => {
560
- const { objectId } = await resolveElement(conn, args);
561
- const { x, y } = await centerOf(conn, objectId);
562
- await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
563
- await conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button, clickCount });
564
- await conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button, clickCount });
565
- return { clicked: true, x, y };
566
- });
519
+ return new RegExp(`^${re}$`);
567
520
  }
568
- async function hover(args) {
569
- return withPage(args.target, async (conn) => {
570
- const { objectId } = await resolveElement(conn, args);
571
- const { x, y } = await centerOf(conn, objectId);
572
- await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
573
- return { hovered: true, x, y };
574
- });
521
+ function urlMatches(pattern, url) {
522
+ return globToRegExp(pattern).test(url);
575
523
  }
576
- async function drag(args) {
577
- if (!args.from || !args.to)
578
- throw new CdpError("drag requires { from } and { to }");
579
- return withPage(args.target, async (conn) => {
580
- const { objectId: srcId } = await resolveElement(conn, args.from);
581
- const from = await centerOf(conn, srcId);
582
- const { objectId: dstId } = await resolveElement(conn, args.to);
583
- const to = await centerOf(conn, dstId);
584
- await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: from.x, y: from.y });
585
- await conn.send("Input.dispatchMouseEvent", {
586
- type: "mousePressed",
587
- x: from.x,
588
- y: from.y,
589
- button: "left",
590
- clickCount: 1
591
- });
592
- await conn.send("Input.dispatchMouseEvent", {
593
- type: "mouseMoved",
594
- x: (from.x + to.x) / 2,
595
- y: (from.y + to.y) / 2,
596
- button: "left"
597
- });
598
- await conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: to.x, y: to.y, button: "left" });
599
- await conn.send("Input.dispatchMouseEvent", {
600
- type: "mouseReleased",
601
- x: to.x,
602
- y: to.y,
603
- button: "left",
604
- clickCount: 1
605
- });
606
- return { dragged: true, from, to };
607
- });
524
+ function selectRule(rules, url, method) {
525
+ return rules.find((r) => urlMatches(r.urlPattern, url) && (!r.method || r.method.toUpperCase() === method.toUpperCase()));
608
526
  }
609
- async function setValue(conn, objectId, value) {
610
- await focusElement(conn, objectId);
611
- await conn.send("Runtime.callFunctionOn", {
612
- objectId,
613
- functionDeclaration: "function(){if('value'in this){this.value='';this.dispatchEvent(new Event('input',{bubbles:true}));}else if(this.isContentEditable){this.textContent='';}}",
614
- returnByValue: true
615
- });
616
- if (value.length) {
617
- await conn.send("Input.insertText", { text: value });
527
+ function buildFulfillParams(requestId, rule) {
528
+ const merged = new Map;
529
+ merged.set("content-type", { name: "Content-Type", value: rule.contentType ?? "application/json" });
530
+ for (const [name, value] of Object.entries(rule.headers ?? {})) {
531
+ merged.set(name.toLowerCase(), { name, value });
618
532
  }
533
+ return {
534
+ requestId,
535
+ responseCode: rule.status ?? 200,
536
+ responseHeaders: [...merged.values()],
537
+ body: Buffer.from(rule.body ?? "", "utf8").toString("base64")
538
+ };
619
539
  }
620
- async function fill(args) {
621
- if (typeof args.value !== "string")
622
- throw new CdpError("fill requires a string { value }");
623
- return withPage(args.target, async (conn) => {
624
- const { objectId } = await resolveElement(conn, args);
625
- await setValue(conn, objectId, args.value);
626
- return { filled: true, value: args.value };
627
- });
628
- }
629
- async function typeText(args) {
630
- if (typeof args.text !== "string")
631
- throw new CdpError("type_text requires a string { text }");
632
- return withPage(args.target, async (conn) => {
633
- const { objectId } = await resolveElement(conn, args);
634
- await focusElement(conn, objectId);
635
- if (args.text.length)
636
- await conn.send("Input.insertText", { text: args.text });
637
- return { typed: true, text: args.text };
540
+ function effectiveAction(rule, rnd) {
541
+ if (rule.failRate !== undefined && rule.failRate > 0 && rnd < rule.failRate)
542
+ return "fail";
543
+ return rule.action;
544
+ }
545
+ var sessions = new Map;
546
+ function delay(ms) {
547
+ return new Promise((r) => setTimeout(r, ms));
548
+ }
549
+ function registerHandler(session) {
550
+ session.conn.on("Fetch.requestPaused", (params) => {
551
+ const p = params;
552
+ session.chain = session.chain.then(async () => {
553
+ const rule = selectRule(session.rules, p.request.url, p.request.method);
554
+ if (!rule) {
555
+ await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
556
+ return;
557
+ }
558
+ if (rule.delayMs)
559
+ await delay(rule.delayMs);
560
+ const act = effectiveAction(rule, Math.random());
561
+ if (act === "fulfill") {
562
+ await session.conn.send("Fetch.fulfillRequest", { ...buildFulfillParams(p.requestId, rule) });
563
+ } else if (act === "fail") {
564
+ await session.conn.send("Fetch.failRequest", { requestId: p.requestId, errorReason: rule.errorReason ?? "Failed" });
565
+ } else {
566
+ await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
567
+ }
568
+ rule.hits++;
569
+ session.intercepted.push({ url: p.request.url, method: p.request.method, action: act });
570
+ }).catch((e) => {
571
+ if (/closed|connection|Target|Inspected/i.test(String(e)))
572
+ session.dead = true;
573
+ });
638
574
  });
639
575
  }
640
- async function fillForm(args) {
641
- if (!Array.isArray(args.fields) || args.fields.length === 0) {
642
- throw new CdpError("fill_form requires a non-empty { fields } array");
576
+ async function mockRequest(args) {
577
+ if (!args.urlPattern)
578
+ throw new CdpError("mock_request requires a { urlPattern }");
579
+ const rule = {
580
+ urlPattern: args.urlPattern,
581
+ action: args.action ?? "fulfill",
582
+ status: args.status,
583
+ body: args.body,
584
+ contentType: args.contentType,
585
+ headers: args.headers,
586
+ errorReason: args.errorReason,
587
+ method: args.method,
588
+ delayMs: args.delayMs,
589
+ failRate: args.failRate,
590
+ hits: 0
591
+ };
592
+ const resolved = await resolveTarget(args.target);
593
+ let session = sessions.get(resolved.id);
594
+ if (session && !session.dead) {
595
+ const idx = session.rules.findIndex((r) => r.urlPattern === rule.urlPattern && (r.method ?? "") === (rule.method ?? ""));
596
+ if (idx >= 0)
597
+ session.rules[idx] = rule;
598
+ else
599
+ session.rules.push(rule);
600
+ await session.conn.send("Fetch.enable", { patterns: session.rules.map((r) => ({ urlPattern: r.urlPattern })) });
601
+ } else {
602
+ const { conn, target } = await openPage(resolved.id);
603
+ session = { conn, target, rules: [rule], intercepted: [], chain: Promise.resolve(), dead: false };
604
+ registerHandler(session);
605
+ await conn.send("Fetch.enable", { patterns: [{ urlPattern: rule.urlPattern }] });
606
+ sessions.set(target.id, session);
643
607
  }
644
- return withPage(args.target, async (conn) => {
645
- let filled = 0;
646
- for (const field of args.fields) {
647
- if (typeof field.value !== "string") {
648
- throw new CdpError("each fill_form field requires a string value");
649
- }
650
- const { objectId } = await resolveElement(conn, field);
651
- await setValue(conn, objectId, field.value);
652
- filled++;
608
+ let reloaded = false;
609
+ if (args.reload) {
610
+ await session.conn.send("Page.enable");
611
+ await session.conn.send("Page.reload", { ignoreCache: true });
612
+ await delay(800);
613
+ reloaded = true;
614
+ }
615
+ return {
616
+ target: { id: session.target.id, url: session.target.url, title: session.target.title },
617
+ pattern: rule.urlPattern,
618
+ ruleCount: session.rules.length,
619
+ reloaded
620
+ };
621
+ }
622
+ async function listMocks(_args = {}) {
623
+ for (const [id, s] of sessions) {
624
+ if (s.dead) {
625
+ try {
626
+ s.conn.close();
627
+ } catch {}
628
+ sessions.delete(id);
629
+ continue;
653
630
  }
654
- return { filled, fields: args.fields.length };
655
- });
631
+ try {
632
+ await s.conn.send("Runtime.evaluate", { expression: "1", returnByValue: true }, { timeoutMs: 3000 });
633
+ } catch {
634
+ try {
635
+ s.conn.close();
636
+ } catch {}
637
+ sessions.delete(id);
638
+ }
639
+ }
640
+ const mocks = [...sessions.values()].map((s) => ({
641
+ target: { id: s.target.id, url: s.target.url, title: s.target.title },
642
+ rules: s.rules.map((r) => ({ urlPattern: r.urlPattern, action: r.action, method: r.method, hits: r.hits })),
643
+ hits: s.intercepted.length
644
+ }));
645
+ return { count: mocks.length, mocks };
646
+ }
647
+ async function clearMocks(args = {}) {
648
+ let ids;
649
+ if (args.all) {
650
+ ids = [...sessions.keys()];
651
+ } else {
652
+ const resolved = await resolveTarget(args.target);
653
+ ids = sessions.has(resolved.id) ? [resolved.id] : [];
654
+ }
655
+ let cleared = 0;
656
+ for (const id of ids) {
657
+ const s = sessions.get(id);
658
+ if (!s)
659
+ continue;
660
+ try {
661
+ await s.conn.send("Fetch.disable");
662
+ } catch {}
663
+ try {
664
+ s.conn.close();
665
+ } catch {}
666
+ sessions.delete(id);
667
+ cleared++;
668
+ }
669
+ return { cleared };
670
+ }
671
+
672
+ // src/driver.ts
673
+ var UID_STAMP_ATTR = "data-cdp-uid";
674
+ var LEGACY_NUMERIC_UID = /^[0-9]+$/;
675
+ var REQUIRED_CAPABILITIES = {
676
+ performance_start_trace: ["trace.performance"],
677
+ performance_stop_trace: ["trace.performance"],
678
+ performance_analyze_insight: ["trace.performance"],
679
+ performance_trace: ["trace.performance"],
680
+ take_heapsnapshot: ["heap.snapshot"],
681
+ lighthouse_audit: ["audit.lighthouse"],
682
+ mock_request: ["network.intercept"],
683
+ list_mocks: ["network.intercept"],
684
+ clear_mocks: ["network.intercept"]
685
+ };
686
+ function isDriverError(e) {
687
+ return e instanceof Error && typeof e.code === "string";
688
+ }
689
+
690
+ // src/cdp/driver.ts
691
+ function driverError(code, message, data) {
692
+ return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
693
+ }
694
+ function encodeUid(backendNodeId) {
695
+ return `cdp:${backendNodeId}`;
696
+ }
697
+ function decodeUid(uid) {
698
+ if (LEGACY_NUMERIC_UID.test(uid))
699
+ return Number(uid);
700
+ if (!uid.startsWith("cdp:"))
701
+ throw driverError("foreign-uid", `uid is not owned by the cdp driver: ${uid}`);
702
+ const n = Number(uid.slice(4));
703
+ if (!Number.isSafeInteger(n) || n <= 0)
704
+ throw driverError("foreign-uid", `malformed cdp uid payload: ${uid}`);
705
+ return n;
706
+ }
707
+ async function centerOf(conn, objectId) {
708
+ const fn = "function(){this.scrollIntoView({block:'center',inline:'center'});const r=this.getBoundingClientRect();if(r.width===0&&r.height===0)return null;return {x:r.left+r.width/2,y:r.top+r.height/2};}";
709
+ const { result, exceptionDetails } = await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
710
+ if (exceptionDetails)
711
+ throw driverError("page-error", `could not measure element: ${exceptionDetails.text ?? "error"}`);
712
+ if (!result.value)
713
+ throw driverError("page-error", "element has zero size / is not visible; cannot compute a click point");
714
+ return result.value;
715
+ }
716
+ async function focusElement(conn, objectId) {
717
+ await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: "function(){this.focus&&this.focus();}", returnByValue: true });
718
+ }
719
+ async function setValueOnObject(conn, objectId, value) {
720
+ await focusElement(conn, objectId);
721
+ const fn = "function(){if('value'in this){this.value='';this.dispatchEvent(new Event('input',{bubbles:true}));}else if(this.isContentEditable){this.textContent='';}}";
722
+ await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
723
+ if (value.length)
724
+ await conn.send("Input.insertText", { text: value });
656
725
  }
657
726
  var NAMED_KEYS = {
658
727
  enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
@@ -676,15 +745,7 @@ var NAMED_KEYS = {
676
745
  pageup: { key: "PageUp", code: "PageUp", keyCode: 33 },
677
746
  pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 }
678
747
  };
679
- var MODIFIER_BITS = {
680
- alt: 1,
681
- control: 2,
682
- ctrl: 2,
683
- meta: 4,
684
- cmd: 4,
685
- command: 4,
686
- shift: 8
687
- };
748
+ var MODIFIER_BITS = { alt: 1, control: 2, ctrl: 2, meta: 4, cmd: 4, command: 4, shift: 8 };
688
749
  function resolveKey(key) {
689
750
  const named = NAMED_KEYS[key.toLowerCase()];
690
751
  if (named)
@@ -694,225 +755,407 @@ function resolveKey(key) {
694
755
  const isLetter = upper >= "A" && upper <= "Z";
695
756
  const isDigit = key >= "0" && key <= "9";
696
757
  const code = isLetter ? `Key${upper}` : isDigit ? `Digit${key}` : `Key${upper}`;
697
- const keyCode = upper.charCodeAt(0);
698
- return { key, code, keyCode, text: key };
699
- }
700
- throw new CdpError(`unknown key '${key}' (use a named key like Enter/Tab/ArrowDown or a single character)`);
701
- }
702
- async function pressKey(args) {
703
- if (typeof args.key !== "string" || !args.key.length) {
704
- throw new CdpError("press_key requires a non-empty { key }");
705
- }
706
- const spec = resolveKey(args.key);
707
- let modBits = 0;
708
- const mods = args.modifiers ?? [];
709
- for (const m of mods) {
710
- const bit = MODIFIER_BITS[m.toLowerCase()];
711
- if (bit === undefined)
712
- throw new CdpError(`unknown modifier '${m}'`);
713
- modBits |= bit;
714
- }
715
- const suppressText = (modBits & ~MODIFIER_BITS.shift) !== 0;
716
- return withPage(args.target, async (conn) => {
717
- const down = {
718
- type: "keyDown",
719
- key: spec.key,
720
- code: spec.code,
721
- windowsVirtualKeyCode: spec.keyCode,
722
- nativeVirtualKeyCode: spec.keyCode,
723
- modifiers: modBits
724
- };
725
- if (spec.text && !suppressText) {
726
- down.text = spec.text;
727
- }
728
- await conn.send("Input.dispatchKeyEvent", down);
729
- await conn.send("Input.dispatchKeyEvent", {
730
- type: "keyUp",
731
- key: spec.key,
732
- code: spec.code,
733
- windowsVirtualKeyCode: spec.keyCode,
734
- nativeVirtualKeyCode: spec.keyCode,
735
- modifiers: modBits
736
- });
737
- return { pressed: spec.key, modifiers: mods };
738
- });
758
+ return { key, code, keyCode: upper.charCodeAt(0), text: key };
759
+ }
760
+ throw driverError("page-error", `unknown key '${key}' (use a named key like Enter/Tab/ArrowDown or a single character)`);
739
761
  }
740
- async function uploadFile(args) {
741
- const files = Array.isArray(args.files) ? args.files : [args.files];
742
- if (!files.length || files.some((f) => typeof f !== "string" || !f.length)) {
743
- throw new CdpError("upload_file requires { files } as a non-empty path or array of paths");
762
+ function unwrap(obj) {
763
+ if (Object.prototype.hasOwnProperty.call(obj, "value"))
764
+ return obj.value;
765
+ if (obj.unserializableValue !== undefined)
766
+ return obj.unserializableValue;
767
+ return obj.type === "undefined" ? undefined : obj.description ?? null;
768
+ }
769
+ function exceptionMessage(details) {
770
+ const ex = details.exception;
771
+ const fromObject = ex?.description ?? (typeof ex?.value === "string" ? ex.value : ex?.value !== undefined ? JSON.stringify(ex.value) : undefined);
772
+ return fromObject ?? details.text ?? "evaluation threw";
773
+ }
774
+ var INTERACTIVE_ROLES2 = new Set([
775
+ "button",
776
+ "link",
777
+ "textbox",
778
+ "searchbox",
779
+ "checkbox",
780
+ "radio",
781
+ "combobox",
782
+ "listbox",
783
+ "option",
784
+ "menuitem",
785
+ "menuitemcheckbox",
786
+ "menuitemradio",
787
+ "tab",
788
+ "switch",
789
+ "slider",
790
+ "spinbutton",
791
+ "textfield"
792
+ ]);
793
+ function axString(v) {
794
+ if (!v || v.value === undefined || v.value === null)
795
+ return;
796
+ const s = String(v.value).trim();
797
+ return s.length ? s : undefined;
798
+ }
799
+ function propValue(node, name) {
800
+ return axString(node.properties?.find((p) => p.name === name)?.value);
801
+ }
802
+ function isInteractive(role) {
803
+ return !!role && (INTERACTIVE_ROLES2.has(role) || INTERACTIVE_ROLES2.has(role.toLowerCase()));
804
+ }
805
+ function axExtras(node) {
806
+ const extras = {};
807
+ const value = axString(node.value);
808
+ if (value)
809
+ extras.value = value;
810
+ const checked = propValue(node, "checked");
811
+ if (checked && checked !== "false")
812
+ extras.checked = checked;
813
+ const expanded = propValue(node, "expanded");
814
+ if (expanded)
815
+ extras.expanded = expanded;
816
+ if (propValue(node, "disabled") === "true")
817
+ extras.disabled = "true";
818
+ const url = propValue(node, "url");
819
+ if (url)
820
+ extras.url = url;
821
+ if (propValue(node, "focused") === "true")
822
+ extras.focused = "true";
823
+ return extras;
824
+ }
825
+ async function resolveElementLocator(conn, loc) {
826
+ if ("uid" in loc) {
827
+ return resolveUid(conn, decodeUid(loc.uid)).catch(() => {
828
+ throw driverError("stale-uid", `uid does not resolve: ${loc.uid}`);
829
+ });
744
830
  }
745
- return withPage(args.target, async (conn) => {
746
- const { objectId } = await resolveElement(conn, args);
747
- await conn.send("DOM.setFileInputFiles", { files, objectId });
748
- return { uploaded: files };
831
+ if ("css" in loc) {
832
+ const { result, exceptionDetails } = await conn.send("Runtime.evaluate", { expression: `document.querySelector(${JSON.stringify(loc.css)})`, returnByValue: false });
833
+ if (exceptionDetails)
834
+ throw driverError("page-error", `css '${loc.css}' evaluation failed: ${exceptionDetails.text ?? "error"}`);
835
+ if (!result.objectId || result.subtype === "null")
836
+ throw driverError("no-such-element", `css '${loc.css}' matched no element`);
837
+ return { objectId: result.objectId };
838
+ }
839
+ const backendNodeId = await searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
840
+ return resolveUid(conn, backendNodeId).catch(() => {
841
+ throw driverError("stale-uid", "text/xpath match no longer resolves");
749
842
  });
750
843
  }
751
-
752
- // src/tools/screenshot.ts
753
- import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
754
- import { join } from "node:path";
755
- var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
756
- function shortId(id) {
757
- return id.slice(0, 8);
844
+ async function searchLocate(conn, query) {
845
+ await conn.send("DOM.getDocument", { depth: 0 }).catch(() => {
846
+ return;
847
+ });
848
+ const { searchId, resultCount } = await conn.send("DOM.performSearch", { query });
849
+ try {
850
+ if (!resultCount)
851
+ throw driverError("no-such-element", `locator matched nothing: ${query}`);
852
+ const { nodeIds } = await conn.send("DOM.getSearchResults", { searchId, fromIndex: 0, toIndex: 1 });
853
+ const nodeId = nodeIds[0];
854
+ if (nodeId === undefined)
855
+ throw driverError("no-such-element", `locator matched nothing: ${query}`);
856
+ const { node } = await conn.send("DOM.describeNode", { nodeId });
857
+ return node.backendNodeId;
858
+ } finally {
859
+ await conn.send("DOM.discardSearchResults", { searchId }).catch(() => {
860
+ return;
861
+ });
862
+ }
758
863
  }
759
- function stamp() {
760
- return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
864
+ async function backendNodeIdOf(conn, loc) {
865
+ if ("uid" in loc)
866
+ return decodeUid(loc.uid);
867
+ if ("css" in loc) {
868
+ const { objectId } = await resolveElementLocator(conn, loc);
869
+ const { node } = await conn.send("DOM.describeNode", { objectId });
870
+ return node.backendNodeId;
871
+ }
872
+ return searchLocate(conn, "xpath" in loc ? loc.xpath : loc.text);
761
873
  }
762
874
  async function fullPageClip(conn) {
763
875
  const m = await conn.send("Page.getLayoutMetrics");
764
876
  const size = m.cssContentSize ?? m.contentSize;
765
877
  if (!size)
766
- throw new CdpError("Page.getLayoutMetrics returned no content size");
767
- return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height), scale: 1 };
768
- }
769
- async function elementClip(conn, uid, selector) {
770
- let backendNodeId;
771
- if (uid != null) {
772
- backendNodeId = uid;
773
- } else if (selector) {
774
- const { root } = await conn.send("DOM.getDocument", { depth: 0 });
775
- const { nodeId } = await conn.send("DOM.querySelector", {
776
- nodeId: root.nodeId,
777
- selector
778
- });
779
- if (!nodeId)
780
- throw new CdpError(`selector matched no element: ${selector}`);
781
- const desc = await conn.send("DOM.describeNode", { nodeId });
782
- backendNodeId = desc.node.backendNodeId;
783
- } else {
784
- throw new CdpError("elementClip requires uid or selector");
785
- }
786
- try {
787
- await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId });
788
- } catch {}
878
+ throw driverError("page-error", "Page.getLayoutMetrics returned no content size");
879
+ return { x: 0, y: 0, width: Math.ceil(size.width), height: Math.ceil(size.height) };
880
+ }
881
+ async function elementClip(conn, loc) {
882
+ const backendNodeId = await backendNodeIdOf(conn, loc);
883
+ await conn.send("DOM.scrollIntoViewIfNeeded", { backendNodeId }).catch(() => {
884
+ return;
885
+ });
789
886
  const box = await conn.send("DOM.getBoxModel", { backendNodeId });
790
887
  const q = box.model.content;
791
- const q0 = q[0];
792
- const q1 = q[1];
793
- if (q0 == null || q1 == null)
794
- throw new CdpError("DOM.getBoxModel returned an empty content quad");
795
888
  const xs = [q[0], q[2], q[4], q[6]].filter((n) => n != null);
796
889
  const ys = [q[1], q[3], q[5], q[7]].filter((n) => n != null);
797
- const minX = Math.min(...xs);
798
- const minY = Math.min(...ys);
799
- const maxX = Math.max(...xs);
800
- const maxY = Math.max(...ys);
801
- const width = Math.ceil(maxX - minX);
802
- const height = Math.ceil(maxY - minY);
890
+ if (!xs.length || !ys.length)
891
+ throw driverError("page-error", "DOM.getBoxModel returned an empty content quad");
892
+ const minX = Math.min(...xs), minY = Math.min(...ys);
893
+ const width = Math.ceil(Math.max(...xs) - minX), height = Math.ceil(Math.max(...ys) - minY);
803
894
  if (width <= 0 || height <= 0)
804
- throw new CdpError("resolved element has zero area");
805
- return { x: minX, y: minY, width, height, scale: 1 };
806
- }
807
- async function takeScreenshot(args = {}) {
808
- const { target, fullPage, uid, selector, savePath, returnBase64 } = args;
809
- if (uid != null && selector)
810
- throw new CdpError("provide exactly one of uid or selector, not both");
811
- const format = args.format ?? "png";
812
- const quality = format === "jpeg" ? args.quality ?? 80 : undefined;
813
- return withPage(target, async (conn, t) => {
814
- await conn.send("Page.enable");
815
- const params = { format, captureBeyondViewport: true };
816
- if (quality != null)
817
- params.quality = quality;
818
- if (uid != null || selector) {
819
- params.clip = await elementClip(conn, uid, selector);
820
- } else if (fullPage) {
821
- params.clip = await fullPageClip(conn);
822
- }
823
- const { data } = await conn.send("Page.captureScreenshot", params);
824
- const buf = Buffer.from(data, "base64");
825
- if (buf.byteLength === 0)
826
- throw new CdpError("Page.captureScreenshot returned empty data");
827
- await mkdir2(ARTIFACT_DIR, { recursive: true });
828
- const ext = format === "jpeg" ? "jpg" : "png";
829
- const path = savePath ?? join(ARTIFACT_DIR, `screenshot-${shortId(t.id)}-${stamp()}.${ext}`);
830
- await writeFile2(path, buf);
831
- const result = {
832
- path,
833
- bytes: buf.byteLength,
834
- format,
835
- target: { id: t.id, url: t.url, title: t.title }
836
- };
837
- if (returnBase64)
838
- result.base64 = data;
839
- return result;
840
- });
895
+ throw driverError("page-error", "resolved element has zero area");
896
+ return { x: minX, y: minY, width, height };
841
897
  }
898
+ var CDP_CAPABILITIES = new Set([
899
+ "trace.performance",
900
+ "heap.snapshot",
901
+ "audit.lighthouse",
902
+ "emulate.cpuThrottling",
903
+ "emulate.mediaFeatures",
904
+ "emulate.deviceMetrics",
905
+ "emulate.networkConditions",
906
+ "screenshot.fullPage",
907
+ "screenshot.element",
908
+ "network.intercept",
909
+ "snapshot.accessibilityTree",
910
+ "input.insertTextAtomic",
911
+ "locate.text",
912
+ "locate.xpath"
913
+ ]);
842
914
 
843
- // src/tools/emulation.ts
844
- async function clearAll(conn) {
845
- await conn.send("Emulation.clearDeviceMetricsOverride").catch(() => {
846
- return;
847
- });
848
- await conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(() => {
849
- return;
850
- });
851
- await conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(() => {
852
- return;
853
- });
854
- await conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(() => {
855
- return;
856
- });
857
- await conn.send("Network.enable").catch(() => {
858
- return;
859
- });
860
- await conn.send("Network.emulateNetworkConditions", {
861
- offline: false,
862
- latency: 0,
863
- downloadThroughput: -1,
864
- uploadThroughput: -1
865
- }).catch(() => {
866
- return;
867
- });
868
- }
869
- async function emulate(args = {}) {
870
- const { target } = args;
871
- return withPage(target, async (conn, t) => {
872
- if (args.clearOverrides) {
873
- await clearAll(conn);
874
- return {
875
- target: { id: t.id, url: t.url, title: t.title },
876
- applied: [],
877
- cleared: true
915
+ class CdpPageDriver {
916
+ conn;
917
+ browser;
918
+ info;
919
+ enabledDomains = new Set;
920
+ released = false;
921
+ constructor(conn, target, browser) {
922
+ this.conn = conn;
923
+ this.browser = browser;
924
+ this.info = { id: target.id, url: target.url, title: target.title, type: target.type };
925
+ }
926
+ async ensureDomain(name) {
927
+ if (this.enabledDomains.has(name))
928
+ return;
929
+ this.enabledDomains.add(name);
930
+ await this.conn.send(`${name}.enable`);
931
+ }
932
+ async ensureDomainSoft(name) {
933
+ if (this.enabledDomains.has(name))
934
+ return;
935
+ this.enabledDomains.add(name);
936
+ await this.conn.send(`${name}.enable`).catch(() => {
937
+ return;
938
+ });
939
+ }
940
+ async navigate(opts) {
941
+ await this.ensureDomain("Page");
942
+ const reload = opts.reload === true;
943
+ if (!reload && !opts.url)
944
+ throw driverError("page-error", "navigate: 'url' is required unless reload:true");
945
+ const waitUntil = opts.waitUntil ?? "load";
946
+ const milestoneMethod = waitUntil === "load" ? "Page.loadEventFired" : "Page.domContentEventFired";
947
+ const milestone = this.conn.waitFor(milestoneMethod, undefined, opts.timeoutMs).then(() => waitUntil).catch(() => {
948
+ return;
949
+ });
950
+ const stopped = this.conn.waitFor("Page.frameStoppedLoading", undefined, opts.timeoutMs).then(() => "committed").catch(() => {
951
+ return;
952
+ });
953
+ let url, contextId, reloaded;
954
+ if (reload) {
955
+ await this.conn.send("Page.reload", { ignoreCache: opts.ignoreCache === true }, opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {});
956
+ const tree = await this.conn.send("Page.getFrameTree").catch(() => {
957
+ return;
958
+ });
959
+ contextId = tree?.frameTree?.frame?.id ?? "";
960
+ url = tree?.frameTree?.frame?.url ?? opts.url ?? "";
961
+ reloaded = true;
962
+ } else {
963
+ const nav = await this.conn.send("Page.navigate", { url: opts.url }, opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {});
964
+ if (nav.errorText)
965
+ throw driverError("page-error", `navigate: ${nav.errorText} (${opts.url})`);
966
+ contextId = nav.frameId;
967
+ url = opts.url;
968
+ }
969
+ const settled = await Promise.race([milestone, stopped]);
970
+ return { url, contextId, ...reloaded ? { reloaded } : {}, waitedFor: settled ?? "timeout" };
971
+ }
972
+ async waitForText(text, timeoutMs = 15000, pollMs = 250) {
973
+ await this.ensureDomain("Runtime");
974
+ const start = Date.now();
975
+ const expr = `(() => { const b = document.body; return !!b && typeof b.innerText === 'string' && b.innerText.includes(${JSON.stringify(text)}); })()`;
976
+ for (;; ) {
977
+ const { result } = await this.conn.send("Runtime.evaluate", { expression: expr, returnByValue: true });
978
+ if (result?.value === true)
979
+ return { found: true, elapsedMs: Date.now() - start };
980
+ if (Date.now() - start >= timeoutMs)
981
+ throw driverError("timeout", `waitForText: '${text}' not found within ${timeoutMs}ms`);
982
+ await new Promise((r) => setTimeout(r, Math.min(pollMs, Math.max(0, timeoutMs - (Date.now() - start)))));
983
+ }
984
+ }
985
+ async callFn(objectId, functionDeclaration, args, awaitPromise) {
986
+ const res = await this.conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration, arguments: args.map((value) => ({ value })), returnByValue: true, awaitPromise });
987
+ if (res.exceptionDetails)
988
+ throw driverError("page-error", exceptionMessage(res.exceptionDetails));
989
+ return unwrap(res.result);
990
+ }
991
+ async evaluate(expression, opts) {
992
+ await this.ensureDomain("Runtime");
993
+ const awaitPromise = opts?.awaitPromise ?? true;
994
+ if (opts?.args && opts.args.length > 0) {
995
+ const globalObj = await this.conn.send("Runtime.evaluate", { expression: "globalThis", returnByValue: false });
996
+ const objectId = globalObj.result.objectId;
997
+ if (!objectId)
998
+ throw driverError("page-error", "could not resolve global object for function call");
999
+ return this.callFn(objectId, expression, opts.args, awaitPromise);
1000
+ }
1001
+ const res = await this.conn.send("Runtime.evaluate", { expression, returnByValue: true, awaitPromise });
1002
+ if (res.exceptionDetails)
1003
+ throw driverError("page-error", exceptionMessage(res.exceptionDetails));
1004
+ return unwrap(res.result);
1005
+ }
1006
+ async callOnElement(loc, functionDeclaration, args = []) {
1007
+ const { objectId } = await resolveElementLocator(this.conn, loc);
1008
+ return this.callFn(objectId, functionDeclaration, args, true);
1009
+ }
1010
+ async snapshot(opts) {
1011
+ const interactiveOnly = opts?.interactiveOnly ?? false;
1012
+ await this.ensureDomain("Accessibility");
1013
+ const { nodes } = await this.conn.send("Accessibility.getFullAXTree");
1014
+ const byId = new Map;
1015
+ for (const n of nodes)
1016
+ byId.set(n.nodeId, n);
1017
+ const roots = nodes.filter((n) => !n.parentId || !byId.has(n.parentId));
1018
+ const out = [];
1019
+ const walk = (node, depth) => {
1020
+ const role = axString(node.role);
1021
+ const name = axString(node.name);
1022
+ const backendNodeId = node.backendDOMNodeId;
1023
+ const emitted = !node.ignored && backendNodeId !== undefined && (interactiveOnly ? isInteractive(role) : !!(role || name));
1024
+ if (emitted) {
1025
+ const extras = axExtras(node);
1026
+ out.push({ uid: encodeUid(backendNodeId), role: role ?? "generic", depth, ...name ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} });
1027
+ }
1028
+ const childDepth = emitted && !interactiveOnly ? depth + 1 : depth;
1029
+ for (const childId of node.childIds ?? []) {
1030
+ const child = byId.get(childId);
1031
+ if (child)
1032
+ walk(child, childDepth);
1033
+ }
1034
+ };
1035
+ for (const root of roots)
1036
+ walk(root, 0);
1037
+ return out;
1038
+ }
1039
+ async locate(loc) {
1040
+ return encodeUid(await backendNodeIdOf(this.conn, loc));
1041
+ }
1042
+ async resolve(uid) {
1043
+ await resolveUid(this.conn, decodeUid(uid)).catch(() => {
1044
+ throw driverError("stale-uid", `uid does not resolve: ${uid}`);
1045
+ });
1046
+ }
1047
+ async hover(loc) {
1048
+ const { objectId } = await resolveElementLocator(this.conn, loc);
1049
+ const { x, y } = await centerOf(this.conn, objectId);
1050
+ await this.conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
1051
+ return { x, y };
1052
+ }
1053
+ async click(loc, opts) {
1054
+ const button = opts?.button ?? "left";
1055
+ const clickCount = opts?.clickCount ?? 1;
1056
+ const { x, y } = await this.hover(loc);
1057
+ await this.conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button, clickCount });
1058
+ await this.conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button, clickCount });
1059
+ return { x, y };
1060
+ }
1061
+ async drag(from, to) {
1062
+ const fromPt = await this.hover(from);
1063
+ const { objectId: dstId } = await resolveElementLocator(this.conn, to);
1064
+ const toPt = await centerOf(this.conn, dstId);
1065
+ await this.conn.send("Input.dispatchMouseEvent", { type: "mousePressed", x: fromPt.x, y: fromPt.y, button: "left", clickCount: 1 });
1066
+ await this.conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: (fromPt.x + toPt.x) / 2, y: (fromPt.y + toPt.y) / 2, button: "left" });
1067
+ await this.conn.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: toPt.x, y: toPt.y, button: "left" });
1068
+ await this.conn.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: toPt.x, y: toPt.y, button: "left", clickCount: 1 });
1069
+ return { from: fromPt, to: toPt };
1070
+ }
1071
+ async setValue(loc, value) {
1072
+ const { objectId } = await resolveElementLocator(this.conn, loc);
1073
+ await setValueOnObject(this.conn, objectId, value);
1074
+ }
1075
+ async typeText(loc, text) {
1076
+ const { objectId } = await resolveElementLocator(this.conn, loc);
1077
+ await focusElement(this.conn, objectId);
1078
+ if (text.length)
1079
+ await this.conn.send("Input.insertText", { text });
1080
+ }
1081
+ async pressKey(press) {
1082
+ const spec = resolveKey(press.key);
1083
+ let modBits = 0;
1084
+ for (const m of press.modifiers ?? []) {
1085
+ const bit = MODIFIER_BITS[m.toLowerCase()];
1086
+ if (bit === undefined)
1087
+ throw driverError("page-error", `unknown modifier '${m}'`);
1088
+ modBits |= bit;
1089
+ }
1090
+ const suppressText = (modBits & ~MODIFIER_BITS.shift) !== 0;
1091
+ const down = { type: "keyDown", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode, modifiers: modBits };
1092
+ if (spec.text && !suppressText)
1093
+ down.text = spec.text;
1094
+ await this.conn.send("Input.dispatchKeyEvent", down);
1095
+ await this.conn.send("Input.dispatchKeyEvent", { type: "keyUp", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode, modifiers: modBits });
1096
+ }
1097
+ async setFiles(loc, files) {
1098
+ const { objectId } = await resolveElementLocator(this.conn, loc);
1099
+ await this.conn.send("DOM.setFileInputFiles", { files: [...files], objectId });
1100
+ }
1101
+ async screenshot(opts) {
1102
+ await this.ensureDomain("Page");
1103
+ const format = opts?.format ?? "png";
1104
+ const quality = format === "jpeg" ? opts?.quality ?? 80 : undefined;
1105
+ const params = { format, captureBeyondViewport: true };
1106
+ if (quality != null)
1107
+ params.quality = quality;
1108
+ if (opts?.clip)
1109
+ params.clip = { ...await elementClip(this.conn, opts.clip), scale: 1 };
1110
+ else if (opts?.fullPage)
1111
+ params.clip = { ...await fullPageClip(this.conn), scale: 1 };
1112
+ const { data } = await this.conn.send("Page.captureScreenshot", params);
1113
+ return { data: new Uint8Array(Buffer.from(data, "base64")), format };
1114
+ }
1115
+ async emulate(opts) {
1116
+ if (opts.clearOverrides) {
1117
+ const c = () => {
1118
+ return;
878
1119
  };
1120
+ await this.conn.send("Emulation.clearDeviceMetricsOverride").catch(c);
1121
+ await this.conn.send("Emulation.setUserAgentOverride", { userAgent: "" }).catch(c);
1122
+ await this.conn.send("Emulation.setCPUThrottlingRate", { rate: 1 }).catch(c);
1123
+ await this.conn.send("Emulation.setEmulatedMedia", { media: "", features: [] }).catch(c);
1124
+ await this.ensureDomainSoft("Network");
1125
+ await this.conn.send("Network.emulateNetworkConditions", { offline: false, latency: 0, downloadThroughput: -1, uploadThroughput: -1 }).catch(c);
1126
+ return { applied: [] };
879
1127
  }
880
1128
  const applied = [];
881
- if (args.width != null || args.height != null) {
882
- if (args.width == null || args.height == null) {
883
- throw new CdpError("emulate device metrics require both width and height");
1129
+ if (opts.width != null || opts.height != null) {
1130
+ if (opts.width == null || opts.height == null) {
1131
+ throw driverError("page-error", "emulate device metrics require both width and height");
884
1132
  }
885
- await conn.send("Emulation.setDeviceMetricsOverride", {
886
- width: args.width,
887
- height: args.height,
888
- deviceScaleFactor: args.deviceScaleFactor ?? 0,
889
- mobile: args.mobile ?? false
1133
+ await this.conn.send("Emulation.setDeviceMetricsOverride", {
1134
+ width: opts.width,
1135
+ height: opts.height,
1136
+ deviceScaleFactor: opts.deviceScaleFactor ?? 0,
1137
+ mobile: opts.mobile ?? false
890
1138
  });
891
1139
  applied.push("deviceMetrics");
892
1140
  }
893
- if (args.userAgent != null) {
894
- await conn.send("Emulation.setUserAgentOverride", { userAgent: args.userAgent });
1141
+ if (opts.userAgent != null) {
1142
+ await this.conn.send("Emulation.setUserAgentOverride", { userAgent: opts.userAgent });
895
1143
  applied.push("userAgent");
896
1144
  }
897
- if (args.cpuThrottlingRate != null) {
898
- if (args.cpuThrottlingRate < 1)
899
- throw new CdpError("cpuThrottlingRate must be >= 1");
900
- await conn.send("Emulation.setCPUThrottlingRate", { rate: args.cpuThrottlingRate });
1145
+ if (opts.cpuThrottlingRate != null) {
1146
+ if (opts.cpuThrottlingRate < 1)
1147
+ throw driverError("page-error", "cpuThrottlingRate must be >= 1");
1148
+ await this.conn.send("Emulation.setCPUThrottlingRate", { rate: opts.cpuThrottlingRate });
901
1149
  applied.push("cpuThrottlingRate");
902
1150
  }
903
- if (args.media != null || args.mediaFeatures != null) {
904
- await conn.send("Emulation.setEmulatedMedia", {
905
- media: args.media ?? "",
906
- features: args.mediaFeatures ?? []
907
- });
1151
+ if (opts.media != null || opts.mediaFeatures != null) {
1152
+ await this.conn.send("Emulation.setEmulatedMedia", { media: opts.media ?? "", features: opts.mediaFeatures ?? [] });
908
1153
  applied.push("emulatedMedia");
909
1154
  }
910
- if (args.networkConditions != null) {
911
- const n = args.networkConditions;
912
- await conn.send("Network.enable").catch(() => {
913
- return;
914
- });
915
- await conn.send("Network.emulateNetworkConditions", {
1155
+ if (opts.networkConditions != null) {
1156
+ const n = opts.networkConditions;
1157
+ await this.ensureDomainSoft("Network");
1158
+ await this.conn.send("Network.emulateNetworkConditions", {
916
1159
  offline: n.offline ?? false,
917
1160
  latency: n.latency ?? 0,
918
1161
  downloadThroughput: n.downloadThroughput ?? -1,
@@ -921,227 +1164,674 @@ async function emulate(args = {}) {
921
1164
  });
922
1165
  applied.push("networkConditions");
923
1166
  }
924
- if (applied.length === 0) {
925
- throw new CdpError("emulate: no overrides specified (pass clearOverrides:true to reset)");
1167
+ return { applied };
1168
+ }
1169
+ static EVENT_MAP = {
1170
+ navigated: { method: "Page.frameNavigated" },
1171
+ console: { method: "Runtime.consoleAPICalled" },
1172
+ "network.request": { method: "Network.requestWillBeSent", domain: "Network" },
1173
+ "network.response": { method: "Network.responseReceived", domain: "Network" },
1174
+ dialog: { method: "Page.javascriptDialogOpening" },
1175
+ "page.closed": { method: "Inspector.detached", domain: "Inspector" }
1176
+ };
1177
+ on(event, handler) {
1178
+ const spec = CdpPageDriver.EVENT_MAP[event];
1179
+ if (spec.domain && !this.enabledDomains.has(spec.domain)) {
1180
+ this.enabledDomains.add(spec.domain);
1181
+ this.conn.send(`${spec.domain}.enable`);
926
1182
  }
927
- return { target: { id: t.id, url: t.url, title: t.title }, applied };
928
- });
929
- }
930
- async function resizePage(args) {
931
- const { target, width, height } = args;
932
- if (!Number.isFinite(width) || !Number.isFinite(height) || width <= 0 || height <= 0) {
933
- throw new CdpError("resize_page requires positive numeric width and height");
934
- }
935
- return withPage(target, async (conn, t) => {
936
- await conn.send("Emulation.setDeviceMetricsOverride", {
937
- width,
938
- height,
939
- deviceScaleFactor: args.deviceScaleFactor ?? 0,
940
- mobile: args.mobile ?? false
941
- });
942
- await conn.send("Runtime.enable").catch(() => {
943
- return;
944
- });
945
- const { result } = await conn.send("Runtime.evaluate", {
946
- expression: "({ w: window.innerWidth, h: window.innerHeight })",
947
- returnByValue: true
1183
+ return this.conn.on(spec.method, (params) => handler(params));
1184
+ }
1185
+ async waitForEvent(event, predicate, timeoutMs) {
1186
+ const spec = CdpPageDriver.EVENT_MAP[event];
1187
+ if (spec.domain)
1188
+ await this.ensureDomain(spec.domain);
1189
+ return this.conn.waitFor(spec.method, predicate, timeoutMs);
1190
+ }
1191
+ armDialog(accept, promptText, onHandled) {
1192
+ return this.conn.on("Page.javascriptDialogOpening", (params) => {
1193
+ const p = params;
1194
+ this.conn.send("Page.handleJavaScriptDialog", { accept, ...promptText !== undefined ? { promptText } : {} }).then(() => onHandled({
1195
+ type: p.type,
1196
+ message: p.message,
1197
+ url: p.url,
1198
+ ...p.defaultPrompt !== undefined ? { defaultPrompt: p.defaultPrompt } : {},
1199
+ accept,
1200
+ ...promptText !== undefined ? { promptText } : {},
1201
+ handled: true
1202
+ })).catch(() => {
1203
+ return;
1204
+ });
948
1205
  });
949
- const v = result.value;
950
- return {
951
- target: { id: t.id, url: t.url, title: t.title },
952
- width,
953
- height,
954
- innerWidth: v?.w ?? width,
955
- innerHeight: v?.h ?? height
956
- };
957
- });
958
- }
959
-
960
- // src/tools/dialogs.ts
961
- function armDialogHandler(conn, accept, promptText, onHandled) {
962
- return conn.on("Page.javascriptDialogOpening", (params) => {
963
- const ev = params;
964
- (async () => {
965
- try {
966
- await conn.send("Page.handleJavaScriptDialog", {
967
- accept,
968
- ...promptText !== undefined ? { promptText } : {}
969
- });
970
- onHandled({
971
- type: ev.type,
972
- message: ev.message,
973
- url: ev.url,
974
- ...ev.defaultPrompt !== undefined ? { defaultPrompt: ev.defaultPrompt } : {},
975
- accept,
976
- ...promptText !== undefined ? { promptText } : {},
977
- handled: true
978
- });
979
- } catch {}
980
- })();
981
- });
982
- }
983
- async function handleDialog(args) {
984
- const { target, accept, promptText, timeoutMs = 15000, autoMs } = args;
985
- const { conn } = await openPage(target);
986
- try {
987
- await conn.send("Page.enable");
988
- if (autoMs !== undefined) {
1206
+ }
1207
+ async handleDialog(accept, promptText, opts) {
1208
+ await this.ensureDomain("Page");
1209
+ if (opts?.autoMs !== undefined) {
989
1210
  const handled = [];
990
- const disarm = armDialogHandler(conn, accept, promptText, (d) => handled.push(d));
991
- await new Promise((resolve) => setTimeout(resolve, autoMs));
992
- disarm();
1211
+ const off = this.armDialog(accept, promptText, (d) => handled.push(d));
1212
+ await new Promise((resolve) => setTimeout(resolve, opts.autoMs));
1213
+ off();
993
1214
  return { handled, count: handled.length };
994
1215
  }
995
- return await new Promise((resolve, reject) => {
1216
+ const timeoutMs = opts?.timeoutMs ?? 15000;
1217
+ return new Promise((resolve, reject) => {
996
1218
  const timer = setTimeout(() => {
997
- disarm();
998
- reject(new CdpError(`handle_dialog: no dialog opened within ${timeoutMs}ms`));
1219
+ off();
1220
+ reject(driverError("timeout", `handleDialog: no dialog opened within ${timeoutMs}ms`));
999
1221
  }, timeoutMs);
1000
- const disarm = armDialogHandler(conn, accept, promptText, (d) => {
1222
+ const off = this.armDialog(accept, promptText, (d) => {
1001
1223
  clearTimeout(timer);
1002
- disarm();
1224
+ off();
1003
1225
  resolve(d);
1004
1226
  });
1005
1227
  });
1228
+ }
1229
+ async startBodyCapture() {
1230
+ await this.ensureDomain("Network");
1231
+ return async () => {
1232
+ await this.conn.send("Network.disable").catch(() => {
1233
+ return;
1234
+ });
1235
+ this.enabledDomains.delete("Network");
1236
+ };
1237
+ }
1238
+ async getResponseBody(requestId) {
1239
+ return this.conn.send("Network.getResponseBody", { requestId });
1240
+ }
1241
+ async intercept(rules) {
1242
+ await this.conn.send("Fetch.enable", { patterns: rules.map((r) => ({ urlPattern: r.urlPattern })) });
1243
+ let chain = Promise.resolve();
1244
+ const off = this.conn.on("Fetch.requestPaused", (params) => {
1245
+ const p = params;
1246
+ chain = chain.then(async () => {
1247
+ const rule = selectRule(rules, p.request.url, p.request.method);
1248
+ if (!rule) {
1249
+ await this.conn.send("Fetch.continueRequest", { requestId: p.requestId });
1250
+ return;
1251
+ }
1252
+ if (rule.delayMs)
1253
+ await new Promise((r) => setTimeout(r, rule.delayMs));
1254
+ const act = effectiveAction(rule, Math.random());
1255
+ if (act === "fulfill")
1256
+ await this.conn.send("Fetch.fulfillRequest", { ...buildFulfillParams(p.requestId, rule) });
1257
+ else if (act === "fail")
1258
+ await this.conn.send("Fetch.failRequest", { requestId: p.requestId, errorReason: rule.errorReason ?? "Failed" });
1259
+ else
1260
+ await this.conn.send("Fetch.continueRequest", { requestId: p.requestId });
1261
+ }).catch(() => {
1262
+ return;
1263
+ });
1264
+ });
1265
+ return async () => {
1266
+ off();
1267
+ await this.conn.send("Fetch.disable").catch(() => {
1268
+ return;
1269
+ });
1270
+ };
1271
+ }
1272
+ async release() {
1273
+ if (this.released)
1274
+ return;
1275
+ this.released = true;
1276
+ this.conn.close();
1277
+ }
1278
+ }
1279
+ async function withBrowserConn(fn) {
1280
+ const conn = await openBrowser();
1281
+ try {
1282
+ return await fn(conn);
1006
1283
  } finally {
1007
1284
  conn.close();
1008
1285
  }
1009
1286
  }
1010
-
1011
- // src/tools/console.ts
1012
- import { readFile } from "node:fs/promises";
1013
-
1014
- // src/tools/recorder.ts
1015
- import { mkdir as mkdir3, appendFile, writeFile as writeFile3, copyFile } from "node:fs/promises";
1016
- var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1017
- function recFile(targetId) {
1018
- return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
1019
- }
1020
- function captureFile(targetId, captureId) {
1021
- return `${ARTIFACT_DIR2}/rec-${targetId}-${captureId}.jsonl`;
1022
- }
1023
- function delay2(ms) {
1024
- return new Promise((r) => setTimeout(r, ms));
1287
+ function noSuchTarget(e) {
1288
+ throw driverError("no-such-target", e instanceof CdpError ? e.message : String(e));
1025
1289
  }
1026
- var NETWORK_EVENTS = [
1027
- "Network.requestWillBeSent",
1028
- "Network.responseReceived",
1029
- "Network.loadingFinished",
1030
- "Network.loadingFailed"
1031
- ];
1032
- var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Log.entryAdded"];
1033
- async function startRecorder(target, opts = {}) {
1034
- const network = opts.network ?? true;
1035
- const consoleCap = opts.console ?? true;
1036
- await mkdir3(ARTIFACT_DIR2, { recursive: true });
1037
- const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
1038
- const file = opts.file ?? recFile(resolved.id);
1039
- if (opts.truncate) {
1040
- await writeFile3(file, "");
1290
+
1291
+ class CdpBrowserDriver {
1292
+ scheme = "cdp";
1293
+ lifetime = "per-call";
1294
+ capabilities = CDP_CAPABILITIES;
1295
+ uidStability = "browser-node";
1296
+ snapshotFidelity = "accessibility-tree";
1297
+ async listPages(opts) {
1298
+ const targets = await listTargets();
1299
+ const filtered = opts?.all ? targets : targets.filter((t) => t.type === "page");
1300
+ return filtered.map((t) => ({ id: t.id, url: t.url, title: t.title, type: t.type }));
1041
1301
  }
1042
- let chain = Promise.resolve();
1043
- let dropped = 0;
1044
- const persist = (kind, method, params) => {
1045
- const line = { kind, method, ts: Date.now(), params };
1046
- const text = `${JSON.stringify(line)}
1047
- `;
1048
- chain = chain.then(() => appendFile(file, text)).catch(() => {
1049
- dropped += 1;
1302
+ async newPage(url) {
1303
+ return withBrowserConn(async (conn) => {
1304
+ const { targetId } = await conn.send("Target.createTarget", { url: url ?? "about:blank" });
1305
+ if (!targetId)
1306
+ throw driverError("page-error", "Target.createTarget returned no targetId");
1307
+ return { id: targetId, url: url ?? "about:blank", title: "" };
1050
1308
  });
1051
- };
1052
- const unsubs = [];
1053
- try {
1054
- if (network) {
1055
- await conn.send("Network.enable");
1056
- for (const ev of NETWORK_EVENTS) {
1057
- unsubs.push(conn.on(ev, (params) => persist("network", ev, params)));
1058
- }
1059
- }
1060
- if (consoleCap) {
1061
- await conn.send("Runtime.enable");
1062
- try {
1063
- await conn.send("Log.enable");
1064
- } catch {}
1065
- for (const ev of CONSOLE_EVENTS) {
1066
- unsubs.push(conn.on(ev, (params) => persist("console", ev, params)));
1067
- }
1068
- }
1069
- } catch (err) {
1070
- conn.close();
1071
- throw err;
1072
1309
  }
1073
- const flush = async () => {
1074
- for (const off of unsubs)
1075
- off();
1076
- await chain;
1077
- };
1078
- return {
1079
- file,
1080
- target: resolved,
1081
- conn,
1082
- droppedWrites: () => dropped,
1083
- flush,
1084
- async stop() {
1085
- await flush();
1086
- conn.close();
1087
- }
1088
- };
1310
+ async closePage(id) {
1311
+ return withBrowserConn(async (conn) => {
1312
+ const res = await conn.send("Target.closeTarget", { targetId: id });
1313
+ return { success: res.success ?? true };
1314
+ });
1315
+ }
1316
+ async activatePage(id) {
1317
+ const target = await resolveTarget(id).catch(noSuchTarget);
1318
+ await withBrowserConn((conn) => conn.send("Target.activateTarget", { targetId: target.id }));
1319
+ return { id: target.id, url: target.url, title: target.title };
1320
+ }
1321
+ async page(selector) {
1322
+ const { conn, target } = await openPage(selector).catch(noSuchTarget);
1323
+ return new CdpPageDriver(conn, target, this);
1324
+ }
1325
+ async dispose() {}
1089
1326
  }
1090
- async function captureWindow(target, durationMs, opts = {}) {
1091
- const resolved = await resolveTarget(target);
1092
- const captureId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1093
- const file = captureFile(resolved.id, captureId);
1094
- const rec = await startRecorder(resolved.id, {
1095
- network: true,
1096
- console: true,
1097
- truncate: true,
1098
- file,
1099
- timeoutMs: opts.timeoutMs
1327
+ function createCdpDriver() {
1328
+ return new CdpBrowserDriver;
1329
+ }
1330
+
1331
+ // src/leases-tools.ts
1332
+ class LeaseToolError extends Error {
1333
+ }
1334
+ function backendOf(driver) {
1335
+ return driver.scheme === "bidi" ? "firefox" : "chrome";
1336
+ }
1337
+ async function claimPage(driver, args = {}) {
1338
+ const backend = backendOf(driver);
1339
+ const label = typeof args.label === "string" && args.label.length ? args.label : `pid-${process.pid}`;
1340
+ const pages = await driver.listPages();
1341
+ let targetId = args.targetId;
1342
+ let url = args.url ?? "about:blank";
1343
+ if (targetId === undefined || targetId === "") {
1344
+ const page = await driver.newPage(args.url);
1345
+ targetId = page.id;
1346
+ url = page.url;
1347
+ } else {
1348
+ const existing = pages.find((p) => p.id === targetId);
1349
+ if (!existing)
1350
+ throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
1351
+ url = existing.url;
1352
+ }
1353
+ const liveIds = [...pages.map((p) => p.id), targetId];
1354
+ const { record, token } = await claimLease(backend, targetId, {
1355
+ label,
1356
+ ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
1357
+ liveIds
1100
1358
  });
1101
- await rec.conn.send("Page.enable");
1102
- await rec.conn.send("Page.reload", { ignoreCache: false });
1103
- await delay2(durationMs);
1104
1359
  return {
1105
- file: rec.file,
1106
- conn: rec.conn,
1107
- resolved: { id: rec.target.id, url: rec.target.url, title: rec.target.title },
1108
- droppedWrites: rec.droppedWrites,
1109
- flush: () => rec.flush(),
1110
- async stop() {
1111
- await rec.stop();
1112
- try {
1113
- await copyFile(rec.file, recFile(rec.target.id));
1114
- } catch {}
1115
- }
1360
+ lease: token,
1361
+ targetId,
1362
+ url,
1363
+ label: record.label,
1364
+ ttlMs: record.ttlMs,
1365
+ expiresAt: record.lastUsedAt + record.ttlMs
1116
1366
  };
1117
1367
  }
1368
+ async function releasePage(_driver, args) {
1369
+ if (typeof args.lease !== "string" || !args.lease.length) {
1370
+ throw new LeaseToolError("release_page requires the 'lease' token returned by claim_page");
1371
+ }
1372
+ return releaseLease(args.lease);
1373
+ }
1374
+ async function listLeasesTool(driver, _args = {}) {
1375
+ const liveIds = (await driver.listPages()).map((p) => p.id);
1376
+ const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
1377
+ return { leases, count: leases.length };
1378
+ }
1379
+ var LEASE_TOOLS = {
1380
+ claim_page: claimPage,
1381
+ release_page: releasePage,
1382
+ list_leases: listLeasesTool
1383
+ };
1118
1384
 
1119
- // src/tools/console.ts
1120
- var DEFAULT_CAPTURE_MS = 2500;
1121
- function remoteObjectToText(arg) {
1122
- if (!arg)
1123
- return "";
1124
- if (arg.value !== undefined && arg.value !== null)
1125
- return String(arg.value);
1126
- if (typeof arg.description === "string")
1127
- return arg.description;
1128
- if (typeof arg.unserializableValue === "string")
1129
- return arg.unserializableValue;
1130
- if (arg.type === "undefined")
1131
- return "undefined";
1132
- if (arg.type === "object" && arg.subtype === "null")
1133
- return "null";
1134
- if (typeof arg.type === "string")
1135
- return `[${arg.type}]`;
1136
- return "";
1385
+ // src/shared-tools.ts
1386
+ import { mkdir as mkdir2, writeFile as writeFile2 } from "node:fs/promises";
1387
+ import { join as join2 } from "node:path";
1388
+ var ARTIFACT_DIR = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1389
+ var STATE_DIR = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
1390
+
1391
+ class SharedToolError extends Error {
1137
1392
  }
1138
- function lineToEntry(line, index) {
1139
- const p = line.params;
1140
- if (line.method === "Runtime.consoleAPICalled") {
1141
- const args = Array.isArray(p.args) ? p.args : [];
1142
- return {
1143
- index,
1144
- source: "console",
1393
+ function target3(p) {
1394
+ return { id: p.id, url: p.url, title: p.title };
1395
+ }
1396
+ function backendOf2(driver) {
1397
+ return driver.scheme === "bidi" ? "firefox" : "chrome";
1398
+ }
1399
+ async function withPage2(driver, target, fn) {
1400
+ const page = await driver.page(target);
1401
+ try {
1402
+ return await fn(page);
1403
+ } finally {
1404
+ await page.release();
1405
+ }
1406
+ }
1407
+ async function resolvePage(driver, selector) {
1408
+ const pages = await driver.listPages();
1409
+ const hit = await pickPage(driver, pages, selector);
1410
+ await assertLeaseOk(backendOf2(driver), hit.id, { url: hit.url, title: hit.title });
1411
+ return hit;
1412
+ }
1413
+ async function pickPage(driver, pages, selector) {
1414
+ if (selector === undefined || selector === "active") {
1415
+ if (!pages.length)
1416
+ throw new SharedToolError("no page targets open");
1417
+ return pages[0];
1418
+ }
1419
+ if (selector.startsWith("index:")) {
1420
+ const p = pages[Number(selector.slice(6))];
1421
+ if (!p)
1422
+ throw new SharedToolError(`no page target at index ${selector.slice(6)} (have ${pages.length})`);
1423
+ return p;
1424
+ }
1425
+ if (selector.startsWith("url:")) {
1426
+ const needle = selector.slice(4);
1427
+ const p = pages.find((x) => x.url.includes(needle));
1428
+ if (!p)
1429
+ throw new SharedToolError(`no page url containing '${needle}'`);
1430
+ return p;
1431
+ }
1432
+ if (selector.startsWith("title:")) {
1433
+ const needle = selector.slice(6);
1434
+ const p = pages.find((x) => x.title.includes(needle));
1435
+ if (!p)
1436
+ throw new SharedToolError(`no page title containing '${needle}'`);
1437
+ return p;
1438
+ }
1439
+ const exactPage = pages.find((x) => x.id === selector);
1440
+ if (exactPage)
1441
+ return exactPage;
1442
+ const all = await driver.listPages({ all: true });
1443
+ const exact = all.find((x) => x.id === selector);
1444
+ if (exact)
1445
+ return exact;
1446
+ throw new SharedToolError(`no target with id '${selector}'`);
1447
+ }
1448
+ function locatorOf(args) {
1449
+ const hasUid = args.uid !== undefined && args.uid !== null && args.uid !== "";
1450
+ const hasSelector = typeof args.selector === "string" && args.selector.length > 0;
1451
+ if (hasUid === hasSelector)
1452
+ throw new SharedToolError("provide exactly one of { uid } or { selector }");
1453
+ return hasUid ? { uid: args.uid } : { css: args.selector };
1454
+ }
1455
+ async function listPages(driver, args = {}) {
1456
+ const pages = await driver.listPages({ all: args.all });
1457
+ return { pages, count: pages.length };
1458
+ }
1459
+ async function newPage(driver, args = {}) {
1460
+ const p = await driver.newPage(args.url);
1461
+ if (args.claim !== true)
1462
+ return { targetId: p.id, url: p.url };
1463
+ const { record, token } = await claimLease(backendOf2(driver), p.id, {
1464
+ label: typeof args.label === "string" && args.label.length ? args.label : `pid-${process.pid}`,
1465
+ ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
1466
+ });
1467
+ return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
1468
+ }
1469
+ async function closePage(driver, args) {
1470
+ if (args.target === undefined || args.target === "")
1471
+ throw new SharedToolError("close_page requires an explicit target; refusing to guess which page to close");
1472
+ const p = await resolvePage(driver, args.target);
1473
+ const res = await driver.closePage(p.id);
1474
+ if (!res.success)
1475
+ return { closed: p.id, success: false, leaseReleased: false };
1476
+ const released = await releaseLeaseFor(backendOf2(driver), p.id);
1477
+ return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
1478
+ }
1479
+ async function writeSelectedFile(id) {
1480
+ await mkdir2(STATE_DIR, { recursive: true });
1481
+ await writeFile2(join2(STATE_DIR, "selected"), id, "utf8");
1482
+ }
1483
+ async function selectPage(driver, args) {
1484
+ if (args.target === undefined || args.target === "")
1485
+ throw new SharedToolError("select_page requires an explicit target");
1486
+ const p = await resolvePage(driver, args.target);
1487
+ const activated = await driver.activatePage(p.id);
1488
+ await writeSelectedFile(activated.id);
1489
+ return { selected: activated.id };
1490
+ }
1491
+ function toLegacyWaitedFor(w) {
1492
+ if (w === "committed")
1493
+ return "frameStoppedLoading";
1494
+ if (w === "timeout")
1495
+ return "navigate-only";
1496
+ return w;
1497
+ }
1498
+ async function navigatePage(driver, args) {
1499
+ const reload = args.reload === true;
1500
+ if (!reload && (!args.url || typeof args.url !== "string")) {
1501
+ throw new SharedToolError("navigate_page: 'url' is required (or pass reload:true to reload the current page)");
1502
+ }
1503
+ return withPage2(driver, args.target, async (page) => {
1504
+ const r = await page.navigate(args);
1505
+ return {
1506
+ url: r.url,
1507
+ frameId: r.contextId,
1508
+ ...r.reloaded ? { reloaded: r.reloaded } : {},
1509
+ waitedFor: toLegacyWaitedFor(r.waitedFor)
1510
+ };
1511
+ });
1512
+ }
1513
+ async function waitForText(driver, args) {
1514
+ if (typeof args.text !== "string" || args.text.length === 0)
1515
+ throw new SharedToolError("wait_for: 'text' is required");
1516
+ return withPage2(driver, args.target, async (page) => {
1517
+ const r = await page.waitForText(args.text, args.timeoutMs ?? 15000, args.pollMs ?? 250);
1518
+ return { found: true, waitedMs: r.elapsedMs };
1519
+ });
1520
+ }
1521
+ async function evaluateScript(driver, args) {
1522
+ if (typeof args.expression !== "string" || args.expression.length === 0) {
1523
+ throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
1524
+ }
1525
+ return withPage2(driver, args.target, (page) => page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true }));
1526
+ }
1527
+ var BARE_TOKEN_EXTRAS = new Set(["disabled", "focused"]);
1528
+ var UNQUOTED_EXTRAS = new Set(["checked", "expanded", "url"]);
1529
+ function renderSnapshotLine(n, scheme) {
1530
+ const indent = " ".repeat(n.depth);
1531
+ const uidText = scheme === "cdp" && n.uid.startsWith("cdp:") ? n.uid.slice(4) : n.uid;
1532
+ const name = n.name !== undefined ? ` ${JSON.stringify(n.name)}` : "";
1533
+ const extrasToks = [];
1534
+ if (n.extras) {
1535
+ for (const [k, v] of Object.entries(n.extras)) {
1536
+ if (BARE_TOKEN_EXTRAS.has(k))
1537
+ extrasToks.push(k);
1538
+ else if (UNQUOTED_EXTRAS.has(k))
1539
+ extrasToks.push(`${k}=${v}`);
1540
+ else
1541
+ extrasToks.push(`${k}=${JSON.stringify(v)}`);
1542
+ }
1543
+ }
1544
+ const extra = extrasToks.length ? ` [${extrasToks.join(" ")}]` : "";
1545
+ return `${indent}[${uidText}] ${n.role}${name}${extra}`;
1546
+ }
1547
+ async function takeSnapshot(driver, args = {}) {
1548
+ return withPage2(driver, args.target, async (page) => {
1549
+ const nodes = await page.snapshot({ interactiveOnly: args.interactiveOnly ?? false });
1550
+ const scheme = page.browser.scheme;
1551
+ return { snapshot: nodes.map((n) => renderSnapshotLine(n, scheme)).join(`
1552
+ `), target: target3(page.info), nodeCount: nodes.length };
1553
+ });
1554
+ }
1555
+ async function click(driver, args) {
1556
+ return withPage2(driver, args.target, async (page) => {
1557
+ const { x, y } = await page.click(locatorOf(args), { button: args.button ?? "left", clickCount: args.clickCount ?? 1 });
1558
+ return { clicked: true, x, y };
1559
+ });
1560
+ }
1561
+ async function hover(driver, args) {
1562
+ return withPage2(driver, args.target, async (page) => {
1563
+ const { x, y } = await page.hover(locatorOf(args));
1564
+ return { hovered: true, x, y };
1565
+ });
1566
+ }
1567
+ async function drag(driver, args) {
1568
+ if (!args.from || !args.to)
1569
+ throw new SharedToolError("drag requires { from } and { to }");
1570
+ return withPage2(driver, args.target, async (page) => {
1571
+ const { from, to } = await page.drag(locatorOf(args.from), locatorOf(args.to));
1572
+ return { dragged: true, from, to };
1573
+ });
1574
+ }
1575
+ async function fill(driver, args) {
1576
+ if (typeof args.value !== "string")
1577
+ throw new SharedToolError("fill requires a string { value }");
1578
+ return withPage2(driver, args.target, async (page) => {
1579
+ await page.setValue(locatorOf(args), args.value);
1580
+ return { filled: true, value: args.value };
1581
+ });
1582
+ }
1583
+ async function fillForm(driver, args) {
1584
+ if (!Array.isArray(args.fields) || args.fields.length === 0)
1585
+ throw new SharedToolError("fill_form requires a non-empty { fields } array");
1586
+ return withPage2(driver, args.target, async (page) => {
1587
+ let filled = 0;
1588
+ for (const f of args.fields) {
1589
+ if (typeof f.value !== "string")
1590
+ throw new SharedToolError("each fill_form field requires a string value");
1591
+ await page.setValue(locatorOf(f), f.value);
1592
+ filled++;
1593
+ }
1594
+ return { filled, fields: args.fields.length };
1595
+ });
1596
+ }
1597
+ async function typeText(driver, args) {
1598
+ if (typeof args.text !== "string")
1599
+ throw new SharedToolError("type_text requires a string { text }");
1600
+ return withPage2(driver, args.target, async (page) => {
1601
+ await page.typeText(locatorOf(args), args.text);
1602
+ return { typed: true, text: args.text };
1603
+ });
1604
+ }
1605
+ async function pressKey(driver, args) {
1606
+ if (typeof args.key !== "string" || !args.key.length)
1607
+ throw new SharedToolError("press_key requires a non-empty { key }");
1608
+ const modifiers = args.modifiers ?? [];
1609
+ return withPage2(driver, args.target, async (page) => {
1610
+ await page.pressKey({ key: args.key, modifiers });
1611
+ return { pressed: args.key, modifiers };
1612
+ });
1613
+ }
1614
+ async function uploadFile(driver, args) {
1615
+ const files = Array.isArray(args.files) ? args.files : [args.files];
1616
+ if (!files.length || files.some((f) => typeof f !== "string" || !f.length)) {
1617
+ throw new SharedToolError("upload_file requires { files } as a non-empty path or array of paths");
1618
+ }
1619
+ return withPage2(driver, args.target, async (page) => {
1620
+ await page.setFiles(locatorOf(args), files);
1621
+ return { uploaded: files };
1622
+ });
1623
+ }
1624
+ function stamp() {
1625
+ return new Date(Date.now()).toISOString().replace(/[:.]/g, "-");
1626
+ }
1627
+ async function takeScreenshot(driver, args = {}) {
1628
+ if (args.uid !== undefined && args.uid !== "" && args.selector)
1629
+ throw new SharedToolError("provide exactly one of uid or selector, not both");
1630
+ const format = args.format ?? "png";
1631
+ return withPage2(driver, args.target, async (page) => {
1632
+ const clip = args.uid !== undefined || args.selector ? locatorOf(args) : undefined;
1633
+ const { data, format: outFormat } = await page.screenshot({ format, quality: args.quality, fullPage: args.fullPage, ...clip ? { clip } : {} });
1634
+ if (data.byteLength === 0)
1635
+ throw new SharedToolError("captureScreenshot returned empty data");
1636
+ await mkdir2(ARTIFACT_DIR, { recursive: true });
1637
+ const ext = outFormat === "jpeg" ? "jpg" : "png";
1638
+ const path = args.savePath ?? join2(ARTIFACT_DIR, `screenshot-${page.info.id.slice(0, 8)}-${stamp()}.${ext}`);
1639
+ await writeFile2(path, Buffer.from(data));
1640
+ const result = {
1641
+ path,
1642
+ bytes: data.byteLength,
1643
+ format: outFormat,
1644
+ target: target3(page.info)
1645
+ };
1646
+ if (args.returnBase64)
1647
+ result.base64 = Buffer.from(data).toString("base64");
1648
+ return result;
1649
+ });
1650
+ }
1651
+ async function emulate(driver, args = {}) {
1652
+ return withPage2(driver, args.target, async (page) => {
1653
+ if (args.clearOverrides) {
1654
+ await page.emulate({ clearOverrides: true });
1655
+ return { target: target3(page.info), applied: [], cleared: true };
1656
+ }
1657
+ const { applied } = await page.emulate(args);
1658
+ if (applied.length === 0)
1659
+ throw new SharedToolError("emulate: no overrides specified (pass clearOverrides:true to reset)");
1660
+ return { target: target3(page.info), applied };
1661
+ });
1662
+ }
1663
+ async function resizePage(driver, args) {
1664
+ if (!Number.isFinite(args.width) || !Number.isFinite(args.height) || args.width <= 0 || args.height <= 0) {
1665
+ throw new SharedToolError("resize_page requires positive numeric width and height");
1666
+ }
1667
+ return withPage2(driver, args.target, async (page) => {
1668
+ await page.emulate({ width: args.width, height: args.height, deviceScaleFactor: args.deviceScaleFactor, mobile: args.mobile });
1669
+ const v = await page.evaluate("({ w: window.innerWidth, h: window.innerHeight })");
1670
+ return { target: target3(page.info), width: args.width, height: args.height, innerWidth: v?.w ?? args.width, innerHeight: v?.h ?? args.height };
1671
+ });
1672
+ }
1673
+ async function handleDialog(driver, args) {
1674
+ if (args.accept === undefined)
1675
+ throw new SharedToolError("handle_dialog requires { accept }");
1676
+ return withPage2(driver, args.target, (page) => page.handleDialog(args.accept, args.promptText, { timeoutMs: args.timeoutMs, autoMs: args.autoMs }));
1677
+ }
1678
+ var SHARED_TOOLS = {
1679
+ list_pages: listPages,
1680
+ new_page: newPage,
1681
+ close_page: closePage,
1682
+ select_page: selectPage,
1683
+ navigate_page: navigatePage,
1684
+ wait_for: waitForText,
1685
+ evaluate_script: evaluateScript,
1686
+ take_snapshot: takeSnapshot,
1687
+ click,
1688
+ hover,
1689
+ drag,
1690
+ fill,
1691
+ fill_form: fillForm,
1692
+ type_text: typeText,
1693
+ press_key: pressKey,
1694
+ upload_file: uploadFile,
1695
+ take_screenshot: takeScreenshot,
1696
+ emulate,
1697
+ resize_page: resizePage,
1698
+ handle_dialog: handleDialog
1699
+ };
1700
+
1701
+ // src/tools/console.ts
1702
+ import { readFile as readFile2 } from "node:fs/promises";
1703
+
1704
+ // src/tools/recorder.ts
1705
+ import { mkdir as mkdir3, appendFile, writeFile as writeFile3, copyFile } from "node:fs/promises";
1706
+ var ARTIFACT_DIR2 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1707
+ function recFile(targetId) {
1708
+ return `${ARTIFACT_DIR2}/rec-${targetId}.jsonl`;
1709
+ }
1710
+ function captureFile(targetId, captureId) {
1711
+ return `${ARTIFACT_DIR2}/rec-${targetId}-${captureId}.jsonl`;
1712
+ }
1713
+ function delay2(ms) {
1714
+ return new Promise((r) => setTimeout(r, ms));
1715
+ }
1716
+ var NETWORK_EVENTS = [
1717
+ "Network.requestWillBeSent",
1718
+ "Network.responseReceived",
1719
+ "Network.loadingFinished",
1720
+ "Network.loadingFailed"
1721
+ ];
1722
+ var CONSOLE_EVENTS = ["Runtime.consoleAPICalled", "Runtime.exceptionThrown", "Log.entryAdded"];
1723
+ async function startRecorder(target, opts = {}) {
1724
+ const network = opts.network ?? true;
1725
+ const consoleCap = opts.console ?? true;
1726
+ await mkdir3(ARTIFACT_DIR2, { recursive: true });
1727
+ const { conn, target: resolved } = await openPage(target, { timeoutMs: opts.timeoutMs });
1728
+ const file = opts.file ?? recFile(resolved.id);
1729
+ if (opts.truncate) {
1730
+ await writeFile3(file, "");
1731
+ }
1732
+ let chain = Promise.resolve();
1733
+ let dropped = 0;
1734
+ const persist = (kind, method, params) => {
1735
+ const line = { kind, method, ts: Date.now(), params };
1736
+ const text = `${JSON.stringify(line)}
1737
+ `;
1738
+ chain = chain.then(() => appendFile(file, text)).catch(() => {
1739
+ dropped += 1;
1740
+ });
1741
+ };
1742
+ const unsubs = [];
1743
+ try {
1744
+ if (network) {
1745
+ await conn.send("Network.enable");
1746
+ for (const ev of NETWORK_EVENTS) {
1747
+ unsubs.push(conn.on(ev, (params) => persist("network", ev, params)));
1748
+ }
1749
+ }
1750
+ if (consoleCap) {
1751
+ await conn.send("Runtime.enable");
1752
+ try {
1753
+ await conn.send("Log.enable");
1754
+ } catch {}
1755
+ for (const ev of CONSOLE_EVENTS) {
1756
+ unsubs.push(conn.on(ev, (params) => persist("console", ev, params)));
1757
+ }
1758
+ }
1759
+ } catch (err) {
1760
+ conn.close();
1761
+ throw err;
1762
+ }
1763
+ const flush = async () => {
1764
+ for (const off of unsubs)
1765
+ off();
1766
+ await chain;
1767
+ };
1768
+ return {
1769
+ file,
1770
+ target: resolved,
1771
+ conn,
1772
+ droppedWrites: () => dropped,
1773
+ flush,
1774
+ async stop() {
1775
+ await flush();
1776
+ conn.close();
1777
+ }
1778
+ };
1779
+ }
1780
+ async function captureWindow(target, durationMs, opts = {}) {
1781
+ const resolved = await resolveTarget(target);
1782
+ const captureId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
1783
+ const file = captureFile(resolved.id, captureId);
1784
+ const rec = await startRecorder(resolved.id, {
1785
+ network: true,
1786
+ console: true,
1787
+ truncate: true,
1788
+ file,
1789
+ timeoutMs: opts.timeoutMs
1790
+ });
1791
+ await rec.conn.send("Page.enable");
1792
+ await rec.conn.send("Page.reload", { ignoreCache: false });
1793
+ await delay2(durationMs);
1794
+ return {
1795
+ file: rec.file,
1796
+ conn: rec.conn,
1797
+ resolved: { id: rec.target.id, url: rec.target.url, title: rec.target.title },
1798
+ droppedWrites: rec.droppedWrites,
1799
+ flush: () => rec.flush(),
1800
+ async stop() {
1801
+ await rec.stop();
1802
+ try {
1803
+ await copyFile(rec.file, recFile(rec.target.id));
1804
+ } catch {}
1805
+ }
1806
+ };
1807
+ }
1808
+
1809
+ // src/tools/console.ts
1810
+ var DEFAULT_CAPTURE_MS = 2500;
1811
+ function remoteObjectToText(arg) {
1812
+ if (!arg)
1813
+ return "";
1814
+ if (arg.value !== undefined && arg.value !== null)
1815
+ return String(arg.value);
1816
+ if (typeof arg.description === "string")
1817
+ return arg.description;
1818
+ if (typeof arg.unserializableValue === "string")
1819
+ return arg.unserializableValue;
1820
+ if (arg.type === "undefined")
1821
+ return "undefined";
1822
+ if (arg.type === "object" && arg.subtype === "null")
1823
+ return "null";
1824
+ if (typeof arg.type === "string")
1825
+ return `[${arg.type}]`;
1826
+ return "";
1827
+ }
1828
+ function lineToEntry(line, index) {
1829
+ const p = line.params;
1830
+ if (line.method === "Runtime.consoleAPICalled") {
1831
+ const args = Array.isArray(p.args) ? p.args : [];
1832
+ return {
1833
+ index,
1834
+ source: "console",
1145
1835
  level: typeof p.type === "string" ? p.type : "log",
1146
1836
  text: args.map(remoteObjectToText).join(" "),
1147
1837
  url: undefined,
@@ -1179,7 +1869,7 @@ function lineToEntry(line, index) {
1179
1869
  async function readConsoleEntries(file) {
1180
1870
  let raw;
1181
1871
  try {
1182
- raw = await readFile(file, "utf8");
1872
+ raw = await readFile2(file, "utf8");
1183
1873
  } catch {
1184
1874
  return [];
1185
1875
  }
@@ -1235,7 +1925,7 @@ async function getConsoleMessage(args = {}) {
1235
1925
  }
1236
1926
 
1237
1927
  // src/tools/network.ts
1238
- import { readFile as readFile2 } from "node:fs/promises";
1928
+ import { readFile as readFile3 } from "node:fs/promises";
1239
1929
  var DEFAULT_CAPTURE_MS2 = 2500;
1240
1930
  function asHeaders(v) {
1241
1931
  if (!v || typeof v !== "object")
@@ -1302,7 +1992,7 @@ function correlate(lines) {
1302
1992
  async function readNetworkRequests(file) {
1303
1993
  let raw;
1304
1994
  try {
1305
- raw = await readFile2(file, "utf8");
1995
+ raw = await readFile3(file, "utf8");
1306
1996
  } catch {
1307
1997
  return [];
1308
1998
  }
@@ -1384,8 +2074,8 @@ async function getNetworkRequest(args = {}) {
1384
2074
  }
1385
2075
 
1386
2076
  // src/tools/performance.ts
1387
- import { mkdir as mkdir4, readFile as readFile3, writeFile as writeFile4, stat, unlink } from "node:fs/promises";
1388
- import { join as join2 } from "node:path";
2077
+ import { mkdir as mkdir4, readFile as readFile4, writeFile as writeFile4, stat, unlink as unlink2 } from "node:fs/promises";
2078
+ import { join as join3 } from "node:path";
1389
2079
  var ARTIFACT_DIR3 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1390
2080
  var STATE_DIR2 = process.env.CDP_STATE_DIR ?? "/tmp/cdp-toolkit";
1391
2081
  var DEFAULT_CATEGORIES = [
@@ -1399,11 +2089,11 @@ var liveTraces = new Map;
1399
2089
  function stamp2() {
1400
2090
  return new Date().toISOString().replace(/[:.]/g, "-");
1401
2091
  }
1402
- function shortId2(id) {
2092
+ function shortId(id) {
1403
2093
  return id.slice(0, 8);
1404
2094
  }
1405
2095
  function stateFilePath() {
1406
- return join2(STATE_DIR2, "trace-state.json");
2096
+ return join3(STATE_DIR2, "trace-state.json");
1407
2097
  }
1408
2098
  function bufferTraceData(conn, sink) {
1409
2099
  return conn.on("Tracing.dataCollected", (params) => {
@@ -1426,7 +2116,7 @@ async function endTrace(conn, sink, timeoutMs = 30000) {
1426
2116
  }
1427
2117
  async function writeTrace(events, targetId) {
1428
2118
  await mkdir4(ARTIFACT_DIR3, { recursive: true });
1429
- const path = join2(ARTIFACT_DIR3, `trace-${shortId2(targetId)}-${stamp2()}.json`);
2119
+ const path = join3(ARTIFACT_DIR3, `trace-${shortId(targetId)}-${stamp2()}.json`);
1430
2120
  const body = JSON.stringify({ traceEvents: events, metadata: { source: "cdp-toolkit" } });
1431
2121
  await writeFile4(path, body, "utf8");
1432
2122
  const { size } = await stat(path);
@@ -1437,7 +2127,7 @@ async function performanceStartTrace(args = {}) {
1437
2127
  const { conn, target } = await openPage(args.target);
1438
2128
  if (liveTraces.has(target.id)) {
1439
2129
  conn.close();
1440
- throw new CdpError(`a trace is already in progress for target ${shortId2(target.id)}; call performanceStopTrace first`);
2130
+ throw new CdpError(`a trace is already in progress for target ${shortId(target.id)}; call performanceStopTrace first`);
1441
2131
  }
1442
2132
  const events = [];
1443
2133
  bufferTraceData(conn, events);
@@ -1469,9 +2159,9 @@ async function performanceStopTrace(args = {}) {
1469
2159
  if (!entry) {
1470
2160
  let hint = "";
1471
2161
  try {
1472
- const raw = await readFile3(stateFilePath(), "utf8");
2162
+ const raw = await readFile4(stateFilePath(), "utf8");
1473
2163
  const state = JSON.parse(raw);
1474
- hint = state.pid === process.pid ? ` (state file lists target ${shortId2(state.targetId)} but its live connection is gone)` : ` (trace was started by a DIFFERENT process pid=${state.pid}; live recordings cannot cross process boundaries)`;
2164
+ 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)`;
1475
2165
  } catch {}
1476
2166
  throw new CdpError(`no in-process trace to stop${hint}. Use performanceTrace for a robust single-call trace.`);
1477
2167
  }
@@ -1481,7 +2171,7 @@ async function performanceStopTrace(args = {}) {
1481
2171
  } finally {
1482
2172
  conn.close();
1483
2173
  liveTraces.delete(target.id);
1484
- await unlink(stateFilePath()).catch(() => {});
2174
+ await unlink2(stateFilePath()).catch(() => {});
1485
2175
  }
1486
2176
  const written = await writeTrace(events, target.id);
1487
2177
  const metrics = analyzeEvents(events);
@@ -1599,7 +2289,7 @@ async function performanceAnalyzeInsight(args = {}) {
1599
2289
  }
1600
2290
  let raw;
1601
2291
  try {
1602
- raw = await readFile3(args.tracePath, "utf8");
2292
+ raw = await readFile4(args.tracePath, "utf8");
1603
2293
  } catch (err) {
1604
2294
  throw new CdpError(`cannot read trace file '${args.tracePath}': ${err.message}`);
1605
2295
  }
@@ -1617,7 +2307,7 @@ async function performanceAnalyzeInsight(args = {}) {
1617
2307
 
1618
2308
  // src/tools/heap.ts
1619
2309
  import { mkdir as mkdir5, writeFile as writeFile5 } from "node:fs/promises";
1620
- import { join as join3 } from "node:path";
2310
+ import { join as join4 } from "node:path";
1621
2311
  var ARTIFACT_DIR4 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1622
2312
  function stamp3(targetId) {
1623
2313
  const short = targetId.slice(0, 8);
@@ -1643,7 +2333,7 @@ async function takeHeapsnapshot(args = {}) {
1643
2333
  throw new Error("HeapProfiler.takeHeapSnapshot produced no chunks");
1644
2334
  }
1645
2335
  await mkdir5(ARTIFACT_DIR4, { recursive: true });
1646
- const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join3(ARTIFACT_DIR4, args.savePath) : join3(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
2336
+ const path = args.savePath ? args.savePath.startsWith("/") ? args.savePath : join4(ARTIFACT_DIR4, args.savePath) : join4(ARTIFACT_DIR4, `take_heapsnapshot-${stamp3(target.id)}.heapsnapshot`);
1647
2337
  await writeFile5(path, snapshot, "utf8");
1648
2338
  const bytes = Buffer.byteLength(snapshot, "utf8");
1649
2339
  return {
@@ -1657,7 +2347,7 @@ async function takeHeapsnapshot(args = {}) {
1657
2347
 
1658
2348
  // src/tools/lighthouse.ts
1659
2349
  import { spawn } from "node:child_process";
1660
- import { mkdir as mkdir6, readFile as readFile4, stat as stat2 } from "node:fs/promises";
2350
+ import { mkdir as mkdir6, readFile as readFile5, stat as stat2 } from "node:fs/promises";
1661
2351
  var ARTIFACT_DIR5 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
1662
2352
  function cdpPort() {
1663
2353
  const base = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
@@ -1747,7 +2437,7 @@ async function lighthouseAudit(args) {
1747
2437
  }
1748
2438
  let raw;
1749
2439
  try {
1750
- raw = await readFile4(outPath, "utf8");
2440
+ raw = await readFile5(outPath, "utf8");
1751
2441
  } catch {
1752
2442
  raw = undefined;
1753
2443
  }
@@ -1755,7 +2445,7 @@ async function lighthouseAudit(args) {
1755
2445
  if (res.signal === "SIGKILL") {
1756
2446
  throw new CdpError(`lighthouse_audit timed out after ${timeoutMs}ms (no report written). stderr: ${truncate(res.stderr)}`);
1757
2447
  }
1758
- 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." : "";
2448
+ 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." : "";
1759
2449
  throw new CdpError(`lighthouse_audit produced no report (exit code ${res.code}).${hint} stderr: ${truncate(res.stderr)}`);
1760
2450
  }
1761
2451
  let report;
@@ -1776,242 +2466,1702 @@ async function lighthouseAudit(args) {
1776
2466
  }
1777
2467
  const { size } = await stat2(outPath);
1778
2468
  return {
1779
- path: outPath,
1780
- bytes: size,
1781
- url,
1782
- finalUrl: report.finalDisplayedUrl ?? report.finalUrl,
1783
- lighthouseVersion: report.lighthouseVersion,
1784
- fetchTime: report.fetchTime,
1785
- categories,
1786
- warnings: Array.isArray(report.runWarnings) && report.runWarnings.length > 0 ? report.runWarnings : undefined
2469
+ path: outPath,
2470
+ bytes: size,
2471
+ url,
2472
+ finalUrl: report.finalDisplayedUrl ?? report.finalUrl,
2473
+ lighthouseVersion: report.lighthouseVersion,
2474
+ fetchTime: report.fetchTime,
2475
+ categories,
2476
+ warnings: Array.isArray(report.runWarnings) && report.runWarnings.length > 0 ? report.runWarnings : undefined
2477
+ };
2478
+ }
2479
+ function truncate(s, max = 600) {
2480
+ const t = s.trim();
2481
+ return t.length > max ? `${t.slice(0, max)}…` : t;
2482
+ }
2483
+
2484
+ // src/index.ts
2485
+ var cdpDriver = createCdpDriver();
2486
+ function onCdp(fn) {
2487
+ return (args) => fn(cdpDriver, args);
2488
+ }
2489
+ var TOOLS = {
2490
+ list_pages: onCdp(SHARED_TOOLS.list_pages),
2491
+ new_page: onCdp(SHARED_TOOLS.new_page),
2492
+ close_page: onCdp(SHARED_TOOLS.close_page),
2493
+ select_page: onCdp(SHARED_TOOLS.select_page),
2494
+ navigate_page: onCdp(SHARED_TOOLS.navigate_page),
2495
+ wait_for: onCdp(SHARED_TOOLS.wait_for),
2496
+ evaluate_script: onCdp(SHARED_TOOLS.evaluate_script),
2497
+ take_snapshot: onCdp(SHARED_TOOLS.take_snapshot),
2498
+ click: onCdp(SHARED_TOOLS.click),
2499
+ hover: onCdp(SHARED_TOOLS.hover),
2500
+ drag: onCdp(SHARED_TOOLS.drag),
2501
+ fill: onCdp(SHARED_TOOLS.fill),
2502
+ fill_form: onCdp(SHARED_TOOLS.fill_form),
2503
+ type_text: onCdp(SHARED_TOOLS.type_text),
2504
+ press_key: onCdp(SHARED_TOOLS.press_key),
2505
+ upload_file: onCdp(SHARED_TOOLS.upload_file),
2506
+ take_screenshot: onCdp(SHARED_TOOLS.take_screenshot),
2507
+ emulate: onCdp(SHARED_TOOLS.emulate),
2508
+ resize_page: onCdp(SHARED_TOOLS.resize_page),
2509
+ handle_dialog: onCdp(SHARED_TOOLS.handle_dialog),
2510
+ list_console_messages: listConsoleMessages,
2511
+ get_console_message: getConsoleMessage,
2512
+ list_network_requests: listNetworkRequests,
2513
+ get_network_request: getNetworkRequest,
2514
+ performance_start_trace: performanceStartTrace,
2515
+ performance_stop_trace: performanceStopTrace,
2516
+ performance_analyze_insight: performanceAnalyzeInsight,
2517
+ performance_trace: performanceTrace,
2518
+ take_heapsnapshot: takeHeapsnapshot,
2519
+ lighthouse_audit: lighthouseAudit,
2520
+ mock_request: mockRequest,
2521
+ list_mocks: listMocks,
2522
+ clear_mocks: clearMocks,
2523
+ claim_page: onCdp(LEASE_TOOLS.claim_page),
2524
+ release_page: onCdp(LEASE_TOOLS.release_page),
2525
+ list_leases: onCdp(LEASE_TOOLS.list_leases)
2526
+ };
2527
+ var TOOL_NAMES = Object.keys(TOOLS);
2528
+
2529
+ // src/bidi/client.ts
2530
+ var DEFAULT_TIMEOUT_MS2 = Number(process.env.BIDI_TIMEOUT_MS ?? 15000);
2531
+
2532
+ class BidiError extends Error {
2533
+ code;
2534
+ data;
2535
+ constructor(message, code, data) {
2536
+ super(message);
2537
+ this.code = code;
2538
+ this.data = data;
2539
+ this.name = "BidiError";
2540
+ }
2541
+ }
2542
+
2543
+ class BidiConnection {
2544
+ wsUrl;
2545
+ opts;
2546
+ ws;
2547
+ nextId = 0;
2548
+ sessId;
2549
+ caps;
2550
+ closed = false;
2551
+ refCount = 0;
2552
+ pending = new Map;
2553
+ listeners = new Map;
2554
+ constructor(wsUrl, opts = {}) {
2555
+ this.wsUrl = wsUrl;
2556
+ this.opts = opts;
2557
+ }
2558
+ get sessionId() {
2559
+ return this.sessId;
2560
+ }
2561
+ get capabilities() {
2562
+ return this.caps;
2563
+ }
2564
+ get holders() {
2565
+ return this.refCount;
2566
+ }
2567
+ connect() {
2568
+ return new Promise((resolve, reject) => {
2569
+ const ws = new WebSocket(this.wsUrl);
2570
+ this.ws = ws;
2571
+ const openTimer = setTimeout(() => reject(new BidiError(`connect timeout: ${this.wsUrl}`)), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2);
2572
+ ws.onopen = () => {
2573
+ clearTimeout(openTimer);
2574
+ resolve(this);
2575
+ };
2576
+ ws.onerror = (e) => {
2577
+ clearTimeout(openTimer);
2578
+ const msg = e?.message ?? "websocket error";
2579
+ if (this.pending.size === 0)
2580
+ reject(new BidiError(msg));
2581
+ this.rejectAll(new BidiError(msg));
2582
+ };
2583
+ ws.onclose = () => {
2584
+ this.closed = true;
2585
+ this.rejectAll(new BidiError("connection closed"));
2586
+ };
2587
+ ws.onmessage = (ev) => this.onMessage(String(ev.data));
2588
+ });
2589
+ }
2590
+ async newSession(capabilities = {}) {
2591
+ const result = await this.send("session.new", { capabilities });
2592
+ this.sessId = result.sessionId;
2593
+ this.caps = result.capabilities;
2594
+ return result.sessionId;
2595
+ }
2596
+ onMessage(raw) {
2597
+ let msg;
2598
+ try {
2599
+ msg = JSON.parse(raw);
2600
+ } catch {
2601
+ return;
2602
+ }
2603
+ if (msg.type === "success" || msg.type === "error") {
2604
+ const id = msg.id;
2605
+ if (typeof id !== "number" || !this.pending.has(id))
2606
+ return;
2607
+ const p = this.pending.get(id);
2608
+ clearTimeout(p.timer);
2609
+ this.pending.delete(id);
2610
+ p.resolve(msg);
2611
+ return;
2612
+ }
2613
+ if (msg.type === "event") {
2614
+ const set = this.listeners.get(msg.method);
2615
+ if (set)
2616
+ for (const h of set)
2617
+ h(msg.params);
2618
+ }
2619
+ }
2620
+ rejectAll(err) {
2621
+ for (const [, p] of this.pending) {
2622
+ clearTimeout(p.timer);
2623
+ p.reject(err);
2624
+ }
2625
+ this.pending.clear();
2626
+ }
2627
+ send(method, params, opts = {}) {
2628
+ if (this.closed || !this.ws)
2629
+ return Promise.reject(new BidiError("connection not open"));
2630
+ const id = ++this.nextId;
2631
+ const timeoutMs = opts.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2;
2632
+ const payload = { id, method, params };
2633
+ return new Promise((resolve, reject) => {
2634
+ const start = Date.now();
2635
+ const timer = setTimeout(() => {
2636
+ this.pending.delete(id);
2637
+ const elapsed = Date.now() - start;
2638
+ reject(new BidiError(`'${method}' timed out after ${elapsed}ms`));
2639
+ }, timeoutMs);
2640
+ this.pending.set(id, {
2641
+ method,
2642
+ start,
2643
+ timer,
2644
+ resolve: (raw) => {
2645
+ if (raw.type === "error")
2646
+ reject(new BidiError(`${method}: ${raw.message}`, raw.error));
2647
+ else
2648
+ resolve(raw.result);
2649
+ },
2650
+ reject
2651
+ });
2652
+ this.ws.send(JSON.stringify(payload));
2653
+ });
2654
+ }
2655
+ on(event, handler) {
2656
+ let set = this.listeners.get(event);
2657
+ if (!set) {
2658
+ set = new Set;
2659
+ this.listeners.set(event, set);
2660
+ }
2661
+ const first = set.size === 0;
2662
+ set.add(handler);
2663
+ if (first) {
2664
+ this.send("session.subscribe", { events: [event] }).catch(() => {});
2665
+ }
2666
+ let removed = false;
2667
+ return () => {
2668
+ if (removed)
2669
+ return;
2670
+ removed = true;
2671
+ const s = this.listeners.get(event);
2672
+ s?.delete(handler);
2673
+ if (s && s.size === 0) {
2674
+ this.send("session.unsubscribe", { events: [event] }).catch(() => {});
2675
+ }
2676
+ };
2677
+ }
2678
+ waitFor(event, predicate, timeoutMs = this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS2) {
2679
+ return new Promise((resolve, reject) => {
2680
+ const timer = setTimeout(() => {
2681
+ off();
2682
+ reject(new BidiError(`waitFor('${event}') timed out after ${timeoutMs}ms`));
2683
+ }, timeoutMs);
2684
+ const off = this.on(event, (params) => {
2685
+ if (!predicate || predicate(params)) {
2686
+ clearTimeout(timer);
2687
+ off();
2688
+ resolve(params);
2689
+ }
2690
+ });
2691
+ });
2692
+ }
2693
+ retain() {
2694
+ this.refCount++;
2695
+ }
2696
+ release() {
2697
+ if (this.refCount > 0)
2698
+ this.refCount--;
2699
+ }
2700
+ dispose() {
2701
+ if (this.closed)
2702
+ return;
2703
+ this.closed = true;
2704
+ this.rejectAll(new BidiError("connection disposed"));
2705
+ try {
2706
+ this.ws?.close();
2707
+ } catch {}
2708
+ }
2709
+ }
2710
+ async function connectBidiSession(port, opts = {}) {
2711
+ const conn = new BidiConnection(`ws://127.0.0.1:${port}/session`, { timeoutMs: opts.timeoutMs });
2712
+ await conn.connect();
2713
+ await conn.newSession(opts.capabilities ?? {});
2714
+ conn.retain();
2715
+ return conn;
2716
+ }
2717
+
2718
+ // src/bidi/snapshot.ts
2719
+ var STAMP_ATTR = "data-cdp-uid";
2720
+ async function takeStampedSnapshot(evaluate) {
2721
+ const raw = await evaluate(buildSnapshotFunctionSource());
2722
+ return coerceSnapshotResult(raw);
2723
+ }
2724
+ function coerceSnapshotResult(raw) {
2725
+ if (typeof raw !== "object" || raw === null || typeof raw.snapshot !== "string" || typeof raw.nodeCount !== "number") {
2726
+ throw new Error("takeStampedSnapshot: evaluate() returned an unexpected shape");
2727
+ }
2728
+ return { snapshot: raw.snapshot, nodeCount: raw.nodeCount };
2729
+ }
2730
+ function buildSnapshotFunctionSource() {
2731
+ return `function() {
2732
+ var STAMP_ATTR = ${JSON.stringify(STAMP_ATTR)};
2733
+ var MAX_NAME_LENGTH = ${MAX_NAME_LENGTH};
2734
+ var lines = [];
2735
+ var nodeCount = 0;
2736
+
2737
+ // THE UID CODEC (driver.ts) documents a bidi payload as exactly 12 lowercase hex chars,
2738
+ // /^[0-9a-f]{12}$/. Build it from two padded random blocks so the length is exact regardless
2739
+ // of the random value drawn, never from string concatenation that could come up short.
2740
+ function nextStamp() {
2741
+ var hi = Math.floor(Math.random() * 0x100000000).toString(16).padStart(8, "0");
2742
+ var lo = Math.floor(Math.random() * 0x10000).toString(16).padStart(4, "0");
2743
+ return hi + lo;
2744
+ }
2745
+
2746
+ function stampOf(el) {
2747
+ var existing = el.getAttribute(STAMP_ATTR);
2748
+ if (existing) return existing;
2749
+ var stamp = nextStamp();
2750
+ el.setAttribute(STAMP_ATTR, stamp);
2751
+ return stamp;
2752
+ }
2753
+
2754
+ // skipSizeCheck exempts <option>/<optgroup> from the zero-size prune: a
2755
+ // closed select's popup is unpainted (0x0) though its options are still
2756
+ // present and pickable. Other visibility rules still apply to them.
2757
+ function isVisible(el, skipSizeCheck) {
2758
+ if (el.hidden) return false;
2759
+ if (el.getAttribute("aria-hidden") === "true") return false;
2760
+ var style;
2761
+ try {
2762
+ style = el.ownerDocument.defaultView.getComputedStyle(el);
2763
+ } catch (e) {
2764
+ style = null;
2765
+ }
2766
+ if (style) {
2767
+ if (style.display === "none" || style.visibility === "hidden") return false;
2768
+ }
2769
+ if (!skipSizeCheck) {
2770
+ var rect = el.getBoundingClientRect();
2771
+ if (rect.width === 0 && rect.height === 0) return false;
2772
+ }
2773
+ return true;
2774
+ }
2775
+
2776
+ function truncate(s) {
2777
+ s = s.trim().replace(/\\s+/g, " ");
2778
+ if (s.length > MAX_NAME_LENGTH) return s.slice(0, MAX_NAME_LENGTH - 1) + "\\u2026";
2779
+ return s;
2780
+ }
2781
+
2782
+ function textOf(el) {
2783
+ return el.textContent || "";
2784
+ }
2785
+
2786
+ function idRefsText(doc, ids) {
2787
+ var parts = [];
2788
+ var list = ids.trim().split(/\\s+/);
2789
+ for (var i = 0; i < list.length; i++) {
2790
+ var ref = doc.getElementById(list[i]);
2791
+ if (ref) parts.push(textOf(ref));
2792
+ }
2793
+ return parts.join(" ").trim();
2794
+ }
2795
+
2796
+ // Text of a <label>, minus any nested form control's own content: a
2797
+ // label that WRAPS its control ("<label>Color <select>...</select></label>")
2798
+ // would otherwise include every option's text in the control's own name.
2799
+ function labelOwnText(label) {
2800
+ var clone = label.cloneNode(true);
2801
+ var controls = clone.querySelectorAll("input, select, textarea, button");
2802
+ for (var i = 0; i < controls.length; i++) {
2803
+ controls[i].parentNode.removeChild(controls[i]);
2804
+ }
2805
+ return textOf(clone);
2806
+ }
2807
+
2808
+ // Guard: without this, an <option> inside a labelled <select> would
2809
+ // inherit the select's own label via closest("label"), not its text.
2810
+ var LABELABLE_TAGS = { INPUT: 1, TEXTAREA: 1, SELECT: 1, BUTTON: 1, METER: 1, OUTPUT: 1, PROGRESS: 1 };
2811
+
2812
+ function nativeLabelText(el) {
2813
+ if (!LABELABLE_TAGS[el.tagName]) return "";
2814
+ var doc = el.ownerDocument;
2815
+ if (el.id) {
2816
+ var forLabel = doc.querySelector('label[for="' + el.id + '"]');
2817
+ if (forLabel) return labelOwnText(forLabel);
2818
+ }
2819
+ var ancestor = el.closest ? el.closest("label") : null;
2820
+ if (ancestor) return labelOwnText(ancestor);
2821
+ return "";
2822
+ }
2823
+
2824
+ // Reduced accname: aria-labelledby, then aria-label, then native label,
2825
+ // then alt/title/placeholder, then trimmed own text for content-named
2826
+ // elements. Not the full W3C accname algorithm (no recursive traversal
2827
+ // rules for nested presentational content) but covers the common cases.
2828
+ function accessibleName(el, contentNamed) {
2829
+ var doc = el.ownerDocument;
2830
+ var labelledby = el.getAttribute("aria-labelledby");
2831
+ if (labelledby) {
2832
+ var viaIds = idRefsText(doc, labelledby);
2833
+ if (viaIds) return truncate(viaIds);
2834
+ }
2835
+ var ariaLabel = el.getAttribute("aria-label");
2836
+ if (ariaLabel && ariaLabel.trim()) return truncate(ariaLabel);
2837
+ var native = nativeLabelText(el);
2838
+ if (native && native.trim()) return truncate(native);
2839
+ var alt = el.getAttribute("alt");
2840
+ if (alt && alt.trim()) return truncate(alt);
2841
+ var title = el.getAttribute("title");
2842
+ if (title && title.trim()) return truncate(title);
2843
+ var placeholder = el.getAttribute("placeholder");
2844
+ if (placeholder && placeholder.trim()) return truncate(placeholder);
2845
+ if (contentNamed) {
2846
+ var text = textOf(el);
2847
+ if (text && text.trim()) return truncate(text);
2848
+ }
2849
+ return "";
2850
+ }
2851
+
2852
+ var IMPLICIT_ROLE_BY_TAG = {
2853
+ A: "link", BUTTON: "button", NAV: "navigation", MAIN: "main", FORM: "form", DIALOG: "dialog",
2854
+ UL: "list", OL: "list", LI: "listitem", TABLE: "table", TR: "row", TD: "cell", TH: "columnheader",
2855
+ SELECT: "combobox", OPTION: "option", TEXTAREA: "textbox", IMG: "img",
2856
+ H1: "heading", H2: "heading", H3: "heading", H4: "heading", H5: "heading", H6: "heading",
2857
+ PROGRESS: "progressbar", METER: "meter"
2858
+ };
2859
+
2860
+ var INPUT_TYPE_ROLE = {
2861
+ button: "button", submit: "button", reset: "button", checkbox: "checkbox", radio: "radio",
2862
+ range: "slider", number: "spinbutton", search: "searchbox", email: "textbox", tel: "textbox",
2863
+ url: "textbox", password: "textbox", text: "textbox", image: "button"
2864
+ };
2865
+
2866
+ var CONTENT_NAMED_ROLES = {
2867
+ button: true, link: true, heading: true, option: true, cell: true,
2868
+ columnheader: true, listitem: true, menuitem: true, tab: true
2869
+ };
2870
+
2871
+ function implicitRole(el) {
2872
+ var explicit = el.getAttribute("role");
2873
+ if (explicit) return explicit;
2874
+ var tag = el.tagName;
2875
+ if (tag === "INPUT") {
2876
+ var type = (el.getAttribute("type") || "text").toLowerCase();
2877
+ return INPUT_TYPE_ROLE[type] || "textbox";
2878
+ }
2879
+ if (tag === "A" && !el.getAttribute("href")) return "";
2880
+ if (Object.prototype.hasOwnProperty.call(IMPLICIT_ROLE_BY_TAG, tag)) {
2881
+ return IMPLICIT_ROLE_BY_TAG[tag];
2882
+ }
2883
+ // Deliberate divergence: Chrome's real AX tree says "generic" here,
2884
+ // but "textbox" is the useful signal an agent needs. Chrome is out of
2885
+ // step, do not "fix" this toward parity.
2886
+ if (el.isContentEditable) return "textbox";
2887
+ return "";
2888
+ }
2889
+
2890
+ function headingLevel(el) {
2891
+ var m = /^H([1-6])$/.exec(el.tagName);
2892
+ return m ? m[1] : null;
2893
+ }
2894
+
2895
+ function currentValue(el) {
2896
+ var tag = el.tagName;
2897
+ if (tag === "INPUT" || tag === "TEXTAREA" || tag === "SELECT") {
2898
+ return el.value;
2899
+ }
2900
+ if (el.isContentEditable) return textOf(el);
2901
+ return undefined;
2902
+ }
2903
+
2904
+ function isChecked(el) {
2905
+ var tag = el.tagName;
2906
+ if (tag === "INPUT") {
2907
+ var type = (el.getAttribute("type") || "").toLowerCase();
2908
+ if (type === "checkbox" || type === "radio") return !!el.checked;
2909
+ }
2910
+ var aria = el.getAttribute("aria-checked");
2911
+ if (aria === "true") return true;
2912
+ return false;
2913
+ }
2914
+
2915
+ function describeNode(el, role) {
2916
+ var contentNamed = !!CONTENT_NAMED_ROLES[role];
2917
+ var name = accessibleName(el, contentNamed);
2918
+ var extras = [];
2919
+ var value = currentValue(el);
2920
+ if (value !== undefined && value !== "") extras.push("value=" + JSON.stringify(String(value)));
2921
+ if (isChecked(el)) extras.push("checked=true");
2922
+ if (el.tagName === "OPTION" && el.selected) extras.push("selected");
2923
+ var href = el.getAttribute("href");
2924
+ if (href) extras.push("url=" + href);
2925
+ if (el.disabled || el.getAttribute("aria-disabled") === "true") extras.push("disabled");
2926
+ var level = headingLevel(el);
2927
+ if (level) extras.push("level=" + level);
2928
+ var label = name ? " " + JSON.stringify(name) : "";
2929
+ var extra = extras.length ? " [" + extras.join(" ") + "]" : "";
2930
+ return { role: role, label: label, extra: extra, name: name };
2931
+ }
2932
+
2933
+ // Traverses light DOM, open shadow roots, and same-origin iframe documents.
2934
+ // A cross-origin iframe throws on contentDocument access; caught and
2935
+ // skipped rather than aborting the whole walk, and noted in a comment
2936
+ // here rather than silently vanishing from the tree without a trace.
2937
+ function walk(root, depth) {
2938
+ var children = root.children ? Array.prototype.slice.call(root.children) : [];
2939
+ for (var i = 0; i < children.length; i++) {
2940
+ var el = children[i];
2941
+ var isSelectChild = el.tagName === "OPTION" || el.tagName === "OPTGROUP";
2942
+ if (!isVisible(el, isSelectChild)) continue;
2943
+
2944
+ var role = implicitRole(el);
2945
+ var isLandmarkOrControl = !!role;
2946
+ var described = null;
2947
+ if (isLandmarkOrControl) {
2948
+ described = describeNode(el, role);
2949
+ }
2950
+
2951
+ var hasContent = described && (described.name || described.extra);
2952
+ var isStructural = isLandmarkOrControl && (hasContent || isInteractiveRole(role) || isLandmarkRole(role));
2953
+
2954
+ // Role-less visible leaf text (measured vs Chrome's take_snapshot,
2955
+ // which keeps a StaticText under a generic wrapper for this case,
2956
+ // e.g. a plain status <div>). Only a leaf (no element children, so
2957
+ // its text is not already captured by a nested control/label).
2958
+ var hasElementChildren = !!(el.children && el.children.length > 0);
2959
+ var leafName = !isStructural && !hasElementChildren ? truncate(textOf(el)) : "";
2960
+ var isLeafText = !!leafName;
2961
+
2962
+ var nextDepth = depth;
2963
+ if (isStructural || isLeafText) {
2964
+ var stamp = stampOf(el);
2965
+ var indent = new Array(depth + 1).join(" ");
2966
+ var lineTail = isStructural
2967
+ ? described.role + described.label + described.extra
2968
+ : "text " + JSON.stringify(leafName);
2969
+ lines.push(indent + "[" + stamp + "] " + lineTail);
2970
+ nodeCount += 1;
2971
+ nextDepth = depth + 1;
2972
+ }
2973
+
2974
+ if (el.shadowRoot) {
2975
+ walk(el.shadowRoot, nextDepth);
2976
+ } else if (el.tagName === "IFRAME") {
2977
+ try {
2978
+ var doc = el.contentDocument;
2979
+ if (doc && doc.body) walk(doc.body, nextDepth);
2980
+ } catch (e) {
2981
+ // cross-origin iframe: intentionally skipped, not silently merged
2982
+ // into the parent tree as though it were empty.
2983
+ }
2984
+ } else {
2985
+ walk(el, nextDepth);
2986
+ }
2987
+ }
2988
+ }
2989
+
2990
+ function isInteractiveRole(role) {
2991
+ return (
2992
+ role === "button" ||
2993
+ role === "link" ||
2994
+ role === "textbox" ||
2995
+ role === "searchbox" ||
2996
+ role === "checkbox" ||
2997
+ role === "radio" ||
2998
+ role === "combobox" ||
2999
+ role === "listbox" ||
3000
+ role === "option" ||
3001
+ role === "slider" ||
3002
+ role === "spinbutton" ||
3003
+ role === "progressbar" ||
3004
+ role === "meter"
3005
+ );
3006
+ }
3007
+
3008
+ function isLandmarkRole(role) {
3009
+ return (
3010
+ role === "navigation" ||
3011
+ role === "main" ||
3012
+ role === "form" ||
3013
+ role === "dialog" ||
3014
+ role === "region" ||
3015
+ role === "heading" ||
3016
+ role === "list" ||
3017
+ role === "listitem" ||
3018
+ role === "table" ||
3019
+ role === "row" ||
3020
+ role === "cell" ||
3021
+ role === "columnheader" ||
3022
+ role === "img"
3023
+ );
3024
+ }
3025
+
3026
+ walk(document.body, 0);
3027
+ return { snapshot: lines.join("\\n"), nodeCount: nodeCount };
3028
+ }`;
3029
+ }
3030
+ var MAX_NAME_LENGTH = 120;
3031
+
3032
+ // src/bidi/driver.ts
3033
+ function driverError2(code, message, data) {
3034
+ return Object.assign(new Error(message), { code, ...data !== undefined ? { data } : {} });
3035
+ }
3036
+ function encodeUid2(stamp5) {
3037
+ return `bidi:${stamp5}`;
3038
+ }
3039
+ function decodeStamp(uid) {
3040
+ if (!uid.startsWith("bidi:"))
3041
+ throw driverError2("foreign-uid", `uid is not owned by the bidi driver: ${uid}`);
3042
+ const payload = uid.slice(5);
3043
+ if (!payload.length)
3044
+ throw driverError2("foreign-uid", `malformed bidi uid payload: ${uid}`);
3045
+ return payload;
3046
+ }
3047
+ function cssEscape(s) {
3048
+ return s.replace(/["\\]/g, (c) => `\\${c}`);
3049
+ }
3050
+ function mapBidiError(e) {
3051
+ if (e instanceof BidiError) {
3052
+ const code = e.code;
3053
+ if (code === "no such node" || code === "no such element")
3054
+ return driverError2("no-such-element", e.message);
3055
+ if (code === "no such frame")
3056
+ return driverError2("no-such-target", e.message);
3057
+ if (code === "invalid session id")
3058
+ return driverError2("disconnected", e.message);
3059
+ if (code === "unsupported operation")
3060
+ return driverError2("unsupported", e.message);
3061
+ if (!code && /timed out/i.test(e.message))
3062
+ return driverError2("timeout", e.message);
3063
+ if (!code && /(disposed|not open|connection closed)/i.test(e.message))
3064
+ return driverError2("disconnected", e.message);
3065
+ return driverError2("page-error", e.message);
3066
+ }
3067
+ return driverError2("page-error", e instanceof Error ? e.message : String(e));
3068
+ }
3069
+ function serializeArg(v) {
3070
+ if (v === undefined)
3071
+ return { type: "undefined" };
3072
+ if (v === null)
3073
+ return { type: "null" };
3074
+ if (typeof v === "string")
3075
+ return { type: "string", value: v };
3076
+ if (typeof v === "number")
3077
+ return { type: "number", value: Number.isNaN(v) ? "NaN" : v };
3078
+ if (typeof v === "boolean")
3079
+ return { type: "boolean", value: v };
3080
+ if (Array.isArray(v))
3081
+ return { type: "array", value: v.map(serializeArg) };
3082
+ if (typeof v === "object")
3083
+ return { type: "object", value: Object.entries(v).map(([k, val]) => [k, serializeArg(val)]) };
3084
+ return { type: "string", value: String(v) };
3085
+ }
3086
+ function deserializeRemote(rv) {
3087
+ switch (rv.type) {
3088
+ case "undefined":
3089
+ return;
3090
+ case "null":
3091
+ return null;
3092
+ case "string":
3093
+ return rv.value;
3094
+ case "boolean":
3095
+ return rv.value;
3096
+ case "bigint":
3097
+ return rv.value;
3098
+ case "number":
3099
+ return typeof rv.value === "number" ? rv.value : rv.value;
3100
+ case "array":
3101
+ return (rv.value ?? []).map(deserializeRemote);
3102
+ case "object": {
3103
+ const out = {};
3104
+ for (const [k, v] of rv.value ?? [])
3105
+ out[typeof k === "string" ? k : String(deserializeRemote(k))] = deserializeRemote(v);
3106
+ return out;
3107
+ }
3108
+ case "node":
3109
+ return { bidiSharedId: rv.sharedId };
3110
+ default:
3111
+ return;
3112
+ }
3113
+ }
3114
+ function exceptionMessage2(d) {
3115
+ if (d.text)
3116
+ return d.text;
3117
+ const ex = deserializeRemote(d.exception);
3118
+ return typeof ex === "string" ? ex : "evaluation threw";
3119
+ }
3120
+ var LINE_RE = /^((?: {2})*)\[([0-9a-f]+)\] ([a-zA-Z_][\w-]*)(?: ("(?:[^"\\]|\\.)*"))?(?: \[(.*)\])?$/;
3121
+ function splitExtras(s) {
3122
+ const out = [];
3123
+ let cur = "";
3124
+ let inQuotes = false;
3125
+ for (let i = 0;i < s.length; i++) {
3126
+ const ch = s[i];
3127
+ if (ch === '"' && s[i - 1] !== "\\")
3128
+ inQuotes = !inQuotes;
3129
+ if (ch === " " && !inQuotes) {
3130
+ if (cur)
3131
+ out.push(cur);
3132
+ cur = "";
3133
+ } else
3134
+ cur += ch;
3135
+ }
3136
+ if (cur)
3137
+ out.push(cur);
3138
+ return out;
3139
+ }
3140
+ function parseSnapshotLine(line) {
3141
+ const m = LINE_RE.exec(line);
3142
+ if (!m)
3143
+ return;
3144
+ const indent = m[1] ?? "", stamp5 = m[2], role = m[3], nameLit = m[4], extrasRaw = m[5];
3145
+ const depth = indent.length / 2;
3146
+ let name;
3147
+ if (nameLit) {
3148
+ try {
3149
+ name = JSON.parse(nameLit);
3150
+ } catch {
3151
+ name = nameLit;
3152
+ }
3153
+ }
3154
+ const extras = {};
3155
+ if (extrasRaw) {
3156
+ for (const tok of splitExtras(extrasRaw)) {
3157
+ const eq = tok.indexOf("=");
3158
+ if (eq === -1) {
3159
+ extras[tok] = "true";
3160
+ continue;
3161
+ }
3162
+ const key = tok.slice(0, eq);
3163
+ let val = tok.slice(eq + 1);
3164
+ if (val.startsWith('"')) {
3165
+ try {
3166
+ val = JSON.parse(val);
3167
+ } catch {}
3168
+ }
3169
+ extras[key] = val;
3170
+ }
3171
+ }
3172
+ return { uid: encodeUid2(stamp5), role, depth, ...name !== undefined ? { name } : {}, ...Object.keys(extras).length ? { extras } : {} };
3173
+ }
3174
+ var INTERACTIVE_ROLES3 = new Set([
3175
+ "button",
3176
+ "link",
3177
+ "textbox",
3178
+ "searchbox",
3179
+ "checkbox",
3180
+ "radio",
3181
+ "combobox",
3182
+ "listbox",
3183
+ "option",
3184
+ "slider",
3185
+ "spinbutton",
3186
+ "progressbar",
3187
+ "meter"
3188
+ ]);
3189
+ var STAMP_ONE_SOURCE = `function(){var a=${JSON.stringify(UID_STAMP_ATTR)};var e=this.getAttribute(a);if(e)return e;var s=Math.floor(Math.random()*0x100000000).toString(16).padStart(8,"0")+Math.floor(Math.random()*0x10000).toString(16).padStart(4,"0");this.setAttribute(a,s);return s;}`;
3190
+ async function locateNodes(conn, contextId, locator) {
3191
+ try {
3192
+ const { nodes } = await conn.send("browsingContext.locateNodes", { context: contextId, locator, maxNodeCount: 1 });
3193
+ return nodes;
3194
+ } catch (e) {
3195
+ throw mapBidiError(e);
3196
+ }
3197
+ }
3198
+ function sharedRefOf(node, describe) {
3199
+ if (!node.sharedId)
3200
+ throw driverError2("page-error", `located node has no sharedId (${describe})`);
3201
+ return { sharedId: node.sharedId };
3202
+ }
3203
+ async function resolveElementLocator2(conn, contextId, loc) {
3204
+ if ("uid" in loc) {
3205
+ const stamp5 = decodeStamp(loc.uid);
3206
+ const nodes2 = await locateNodes(conn, contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp5)}"]` });
3207
+ if (!nodes2.length)
3208
+ throw driverError2("stale-uid", `uid does not resolve: ${loc.uid}`);
3209
+ return sharedRefOf(nodes2[0], loc.uid);
3210
+ }
3211
+ if ("css" in loc) {
3212
+ const nodes2 = await locateNodes(conn, contextId, { type: "css", value: loc.css });
3213
+ if (!nodes2.length)
3214
+ throw driverError2("no-such-element", `css '${loc.css}' matched no element`);
3215
+ return sharedRefOf(nodes2[0], `css '${loc.css}'`);
3216
+ }
3217
+ if ("xpath" in loc) {
3218
+ const nodes2 = await locateNodes(conn, contextId, { type: "xpath", value: loc.xpath });
3219
+ if (!nodes2.length)
3220
+ throw driverError2("no-such-element", `xpath '${loc.xpath}' matched no element`);
3221
+ return sharedRefOf(nodes2[0], `xpath '${loc.xpath}'`);
3222
+ }
3223
+ const nodes = await locateNodes(conn, contextId, { type: "innerText", value: loc.text, matchType: "partial" });
3224
+ if (!nodes.length)
3225
+ throw driverError2("no-such-element", `text '${loc.text}' matched no element`);
3226
+ return sharedRefOf(nodes[0], `text '${loc.text}'`);
3227
+ }
3228
+ var KEY_VALUES = {
3229
+ enter: "",
3230
+ return: "",
3231
+ tab: "",
3232
+ escape: "",
3233
+ esc: "",
3234
+ backspace: "",
3235
+ delete: "",
3236
+ space: " ",
3237
+ arrowup: "",
3238
+ arrowdown: "",
3239
+ arrowleft: "",
3240
+ arrowright: "",
3241
+ up: "",
3242
+ down: "",
3243
+ left: "",
3244
+ right: "",
3245
+ home: "",
3246
+ end: "",
3247
+ pageup: "",
3248
+ pagedown: ""
3249
+ };
3250
+ var MODIFIER_VALUES = { shift: "", control: "", ctrl: "", alt: "", meta: "", cmd: "", command: "" };
3251
+ var BUTTON_CODES = { left: 0, middle: 1, right: 2 };
3252
+ var BIDI_CAPABILITIES = new Set([
3253
+ "emulate.deviceMetrics",
3254
+ "screenshot.fullPage",
3255
+ "screenshot.element",
3256
+ "network.intercept",
3257
+ "locate.xpath"
3258
+ ]);
3259
+ var connections = new Map;
3260
+ async function getConnection(port, timeoutMs) {
3261
+ const existing = connections.get(port);
3262
+ if (existing) {
3263
+ existing.retain();
3264
+ return existing;
3265
+ }
3266
+ const conn = await connectBidiSession(port, {
3267
+ ...timeoutMs !== undefined ? { timeoutMs } : {},
3268
+ capabilities: { alwaysMatch: { unhandledPromptBehavior: { default: "ignore" } } }
3269
+ });
3270
+ connections.set(port, conn);
3271
+ return conn;
3272
+ }
3273
+ async function safeTitle(conn, contextId) {
3274
+ try {
3275
+ const res = await conn.send("script.evaluate", { expression: "document.title", target: { context: contextId }, awaitPromise: false });
3276
+ return res.type === "success" && res.result.type === "string" ? res.result.value : "";
3277
+ } catch {
3278
+ return "";
3279
+ }
3280
+ }
3281
+ async function pageInfoOf(conn, ctx) {
3282
+ return { id: ctx.context, url: ctx.url, title: await safeTitle(conn, ctx.context) };
3283
+ }
3284
+ async function resolveContext(conn, selector, opts = {}) {
3285
+ const { contexts } = await conn.send("browsingContext.getTree", {}).catch((e) => {
3286
+ throw mapBidiError(e);
3287
+ });
3288
+ if (!contexts.length)
3289
+ throw driverError2("no-such-target", "no browsing contexts available");
3290
+ const hit = await pickContext(conn, contexts, selector);
3291
+ await assertLeaseOk("firefox", hit.context, { lease: opts.lease, url: hit.url });
3292
+ return hit;
3293
+ }
3294
+ async function pickContext(conn, contexts, selector) {
3295
+ if (selector === undefined || selector === "active")
3296
+ return contexts[0];
3297
+ if (selector.startsWith("index:")) {
3298
+ const t = contexts[Number(selector.slice(6))];
3299
+ if (!t)
3300
+ throw driverError2("no-such-target", `no context at ${selector}`);
3301
+ return t;
3302
+ }
3303
+ if (selector.startsWith("url:")) {
3304
+ const needle = selector.slice(4);
3305
+ const t = contexts.find((c) => c.url.includes(needle));
3306
+ if (!t)
3307
+ throw driverError2("no-such-target", `no context with url containing '${needle}'`);
3308
+ return t;
3309
+ }
3310
+ if (selector.startsWith("title:")) {
3311
+ const needle = selector.slice(6);
3312
+ for (const c of contexts)
3313
+ if ((await safeTitle(conn, c.context)).includes(needle))
3314
+ return c;
3315
+ throw driverError2("no-such-target", `no context with title containing '${needle}'`);
3316
+ }
3317
+ const exact = contexts.find((c) => c.context === selector);
3318
+ if (exact)
3319
+ return exact;
3320
+ throw driverError2("no-such-target", `no context matching '${selector}'`);
3321
+ }
3322
+
3323
+ class BidiPageDriver {
3324
+ conn;
3325
+ contextId;
3326
+ info;
3327
+ browser;
3328
+ released = false;
3329
+ bodyCollector;
3330
+ constructor(conn, contextId, info, browser) {
3331
+ this.conn = conn;
3332
+ this.contextId = contextId;
3333
+ this.info = info;
3334
+ this.browser = browser;
3335
+ }
3336
+ async navigate(opts) {
3337
+ const reload = opts.reload === true;
3338
+ if (!reload && !opts.url)
3339
+ throw driverError2("page-error", "navigate: 'url' is required unless reload:true");
3340
+ const waitUntil = opts.waitUntil ?? "load";
3341
+ const wait = waitUntil === "domcontentloaded" ? "interactive" : "complete";
3342
+ const sendOpts = opts.timeoutMs !== undefined ? { timeoutMs: opts.timeoutMs } : {};
3343
+ try {
3344
+ if (reload) {
3345
+ const r2 = await this.conn.send("browsingContext.reload", { context: this.contextId, ignoreCache: opts.ignoreCache === true, wait }, sendOpts);
3346
+ return { url: r2.url, contextId: this.contextId, reloaded: true, waitedFor: waitUntil };
3347
+ }
3348
+ const r = await this.conn.send("browsingContext.navigate", { context: this.contextId, url: opts.url, wait }, sendOpts);
3349
+ return { url: r.url, contextId: this.contextId, waitedFor: waitUntil };
3350
+ } catch (e) {
3351
+ throw mapBidiError(e);
3352
+ }
3353
+ }
3354
+ async waitForText(text, timeoutMs = 15000, pollMs = 250) {
3355
+ const start = Date.now();
3356
+ const expr = `(() => { const b = document.body; return !!b && typeof b.innerText === 'string' && b.innerText.includes(${JSON.stringify(text)}); })()`;
3357
+ for (;; ) {
3358
+ const found = await this.conn.send("script.evaluate", { expression: expr, target: { context: this.contextId }, awaitPromise: true }).then((r) => r.type === "success" && deserializeRemote(r.result) === true).catch(() => false);
3359
+ if (found)
3360
+ return { found: true, elapsedMs: Date.now() - start };
3361
+ if (Date.now() - start >= timeoutMs)
3362
+ throw driverError2("timeout", `waitForText: '${text}' not found within ${timeoutMs}ms`);
3363
+ await new Promise((r) => setTimeout(r, Math.min(pollMs, Math.max(0, timeoutMs - (Date.now() - start)))));
3364
+ }
3365
+ }
3366
+ async callFunctionValue(functionDeclaration, args, thisRef) {
3367
+ try {
3368
+ const res = await this.conn.send("script.callFunction", {
3369
+ functionDeclaration,
3370
+ awaitPromise: true,
3371
+ target: { context: this.contextId },
3372
+ arguments: args.map(serializeArg),
3373
+ ...thisRef ? { this: thisRef } : {}
3374
+ });
3375
+ if (res.type === "exception")
3376
+ throw driverError2("page-error", exceptionMessage2(res.exceptionDetails));
3377
+ return deserializeRemote(res.result);
3378
+ } catch (e) {
3379
+ throw isDriverError(e) ? e : mapBidiError(e);
3380
+ }
3381
+ }
3382
+ async evaluate(expression, opts) {
3383
+ const awaitPromise = opts?.awaitPromise ?? true;
3384
+ if (opts?.args && opts.args.length > 0)
3385
+ return this.callFunctionValue(expression, [...opts.args]);
3386
+ try {
3387
+ const res = await this.conn.send("script.evaluate", { expression, awaitPromise, target: { context: this.contextId } });
3388
+ if (res.type === "exception")
3389
+ throw driverError2("page-error", exceptionMessage2(res.exceptionDetails));
3390
+ return deserializeRemote(res.result);
3391
+ } catch (e) {
3392
+ throw isDriverError(e) ? e : mapBidiError(e);
3393
+ }
3394
+ }
3395
+ async callOnElement(loc, functionDeclaration, args = []) {
3396
+ const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
3397
+ return this.callFunctionValue(functionDeclaration, [...args], ref);
3398
+ }
3399
+ async snapshot(opts) {
3400
+ const { snapshot } = await takeStampedSnapshot((src) => this.callFunctionValue(src, []));
3401
+ const nodes = [];
3402
+ for (const line of snapshot.split(`
3403
+ `)) {
3404
+ if (!line)
3405
+ continue;
3406
+ const n = parseSnapshotLine(line);
3407
+ if (n)
3408
+ nodes.push(n);
3409
+ }
3410
+ return opts?.interactiveOnly ? nodes.filter((n) => INTERACTIVE_ROLES3.has(n.role)) : nodes;
3411
+ }
3412
+ async locate(loc) {
3413
+ if ("uid" in loc) {
3414
+ await this.resolve(loc.uid);
3415
+ return loc.uid;
3416
+ }
3417
+ const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
3418
+ const stamp5 = await this.callFunctionValue(STAMP_ONE_SOURCE, [], ref);
3419
+ if (typeof stamp5 !== "string")
3420
+ throw driverError2("page-error", "stamping the located element did not return a string");
3421
+ return encodeUid2(stamp5);
3422
+ }
3423
+ async resolve(uid) {
3424
+ const stamp5 = decodeStamp(uid);
3425
+ const nodes = await locateNodes(this.conn, this.contextId, { type: "css", value: `[${UID_STAMP_ATTR}="${cssEscape(stamp5)}"]` });
3426
+ if (!nodes.length)
3427
+ throw driverError2("stale-uid", `uid does not resolve: ${uid}`);
3428
+ }
3429
+ async performActions(actions) {
3430
+ try {
3431
+ await this.conn.send("input.performActions", { context: this.contextId, actions });
3432
+ } catch (e) {
3433
+ throw mapBidiError(e);
3434
+ }
3435
+ }
3436
+ async centerOf(ref) {
3437
+ 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};}";
3438
+ const val = await this.callFunctionValue(fn, [], ref);
3439
+ if (!val || typeof val !== "object")
3440
+ throw driverError2("page-error", "element has zero size / is not visible; cannot compute a click point");
3441
+ return val;
3442
+ }
3443
+ async hover(loc) {
3444
+ const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
3445
+ const { x, y } = await this.centerOf(ref);
3446
+ await this.performActions([{ type: "pointer", id: "cdp-mouse", actions: [{ type: "pointerMove", x, y, duration: 0 }] }]);
3447
+ return { x, y };
3448
+ }
3449
+ async click(loc, opts) {
3450
+ const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
3451
+ const { x, y } = await this.centerOf(ref);
3452
+ const button = BUTTON_CODES[opts?.button ?? "left"];
3453
+ const actions = [{ type: "pointerMove", x, y, duration: 0 }];
3454
+ for (let i = 0;i < (opts?.clickCount ?? 1); i++)
3455
+ actions.push({ type: "pointerDown", button }, { type: "pointerUp", button });
3456
+ await this.performActions([{ type: "pointer", id: "cdp-mouse", actions }]);
3457
+ return { x, y };
3458
+ }
3459
+ async drag(from, to) {
3460
+ const fromPt = await this.centerOf(await resolveElementLocator2(this.conn, this.contextId, from));
3461
+ const toPt = await this.centerOf(await resolveElementLocator2(this.conn, this.contextId, to));
3462
+ const actions = [
3463
+ { type: "pointerMove", x: fromPt.x, y: fromPt.y, duration: 0 },
3464
+ { type: "pointerDown", button: 0 },
3465
+ { type: "pointerMove", x: (fromPt.x + toPt.x) / 2, y: (fromPt.y + toPt.y) / 2, duration: 50 },
3466
+ { type: "pointerMove", x: toPt.x, y: toPt.y, duration: 50 },
3467
+ { type: "pointerUp", button: 0 }
3468
+ ];
3469
+ await this.performActions([{ type: "pointer", id: "cdp-mouse", actions }]);
3470
+ return { from: fromPt, to: toPt };
3471
+ }
3472
+ async sendKeys(text) {
3473
+ if (!text.length)
3474
+ return;
3475
+ const actions = [];
3476
+ for (const ch of text)
3477
+ actions.push({ type: "keyDown", value: ch }, { type: "keyUp", value: ch });
3478
+ await this.performActions([{ type: "key", id: "cdp-keyboard", actions }]);
3479
+ }
3480
+ static SELECT_SOURCE = "function(v){var found=false;for(var i=0;i<this.options.length;i++){if(this.options[i].value===v){this.selectedIndex=i;found=true;break;}}if(!found)this.value=v;this.dispatchEvent(new Event('input',{bubbles:true}));this.dispatchEvent(new Event('change',{bubbles:true}));}";
3481
+ static INSERT_TEXT_SOURCE = "function(v,collapseAtEnd){this.focus();var r=document.createRange();" + "r.selectNodeContents(this);if(collapseAtEnd)r.collapse(false);" + "var s=window.getSelection();s.removeAllRanges();s.addRange(r);" + "return document.execCommand('insertText', false, v);}";
3482
+ static IS_CONTENT_EDITABLE = "function(){return this.isContentEditable===true;}";
3483
+ async setValue(loc, value) {
3484
+ const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
3485
+ const isSelect = await this.callFunctionValue("function(){return this.tagName==='SELECT';}", [], ref);
3486
+ if (isSelect === true) {
3487
+ await this.callFunctionValue(BidiPageDriver.SELECT_SOURCE, [value], ref);
3488
+ return;
3489
+ }
3490
+ const isContentEditable = await this.callFunctionValue(BidiPageDriver.IS_CONTENT_EDITABLE, [], ref);
3491
+ if (isContentEditable === true) {
3492
+ await this.callFunctionValue(BidiPageDriver.INSERT_TEXT_SOURCE, [value, false], ref);
3493
+ return;
3494
+ }
3495
+ const clear = "function(){this.focus&&this.focus();if('value' in this){this.value='';this.dispatchEvent(new Event('input',{bubbles:true}));}}";
3496
+ await this.callFunctionValue(clear, [], ref);
3497
+ await this.sendKeys(value);
3498
+ }
3499
+ async typeText(loc, text) {
3500
+ const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
3501
+ const isContentEditable = await this.callFunctionValue(BidiPageDriver.IS_CONTENT_EDITABLE, [], ref);
3502
+ if (isContentEditable === true) {
3503
+ await this.callFunctionValue(BidiPageDriver.INSERT_TEXT_SOURCE, [text, true], ref);
3504
+ return;
3505
+ }
3506
+ await this.callFunctionValue("function(){this.focus&&this.focus();}", [], ref);
3507
+ await this.sendKeys(text);
3508
+ }
3509
+ async pressKey(press) {
3510
+ const value = KEY_VALUES[press.key.toLowerCase()] ?? (press.key.length === 1 ? press.key : undefined);
3511
+ if (value === undefined)
3512
+ throw driverError2("page-error", `unknown key '${press.key}' (use a named key like Enter/Tab/ArrowDown or a single character)`);
3513
+ const mods = [];
3514
+ for (const m of press.modifiers ?? []) {
3515
+ const v = MODIFIER_VALUES[m.toLowerCase()];
3516
+ if (v === undefined)
3517
+ throw driverError2("page-error", `unknown modifier '${m}'`);
3518
+ mods.push(v);
3519
+ }
3520
+ const actions = [...mods.map((v) => ({ type: "keyDown", value: v })), { type: "keyDown", value }, { type: "keyUp", value }, ...[...mods].reverse().map((v) => ({ type: "keyUp", value: v }))];
3521
+ await this.performActions([{ type: "key", id: "cdp-keyboard", actions }]);
3522
+ }
3523
+ async setFiles(loc, files) {
3524
+ const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
3525
+ try {
3526
+ await this.conn.send("input.setFiles", { context: this.contextId, element: ref, files: [...files] });
3527
+ } catch (e) {
3528
+ throw mapBidiError(e);
3529
+ }
3530
+ }
3531
+ async screenshot(opts) {
3532
+ const format = opts?.format ?? "png";
3533
+ const params = {
3534
+ context: this.contextId,
3535
+ format: { type: format === "jpeg" ? "image/jpeg" : "image/png", ...format === "jpeg" ? { quality: (opts?.quality ?? 80) / 100 } : {} }
3536
+ };
3537
+ if (opts?.clip)
3538
+ params.clip = { type: "element", element: await resolveElementLocator2(this.conn, this.contextId, opts.clip) };
3539
+ else if (opts?.fullPage)
3540
+ params.origin = "document";
3541
+ try {
3542
+ const { data } = await this.conn.send("browsingContext.captureScreenshot", params);
3543
+ return { data: new Uint8Array(Buffer.from(data, "base64")), format };
3544
+ } catch (e) {
3545
+ throw mapBidiError(e);
3546
+ }
3547
+ }
3548
+ async emulate(opts) {
3549
+ if (opts.clearOverrides) {
3550
+ await this.conn.send("emulation.setUserAgentOverride", { userAgent: null, contexts: [this.contextId] }).catch(() => {
3551
+ return;
3552
+ });
3553
+ await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: null, devicePixelRatio: null }).catch(() => {
3554
+ return;
3555
+ });
3556
+ return { applied: [] };
3557
+ }
3558
+ const applied = [];
3559
+ try {
3560
+ if (opts.width != null && opts.height != null) {
3561
+ await this.conn.send("browsingContext.setViewport", { context: this.contextId, viewport: { width: opts.width, height: opts.height }, ...opts.deviceScaleFactor != null ? { devicePixelRatio: opts.deviceScaleFactor } : {} });
3562
+ applied.push("deviceMetrics");
3563
+ }
3564
+ if (opts.userAgent != null) {
3565
+ await this.conn.send("emulation.setUserAgentOverride", { userAgent: opts.userAgent, contexts: [this.contextId] });
3566
+ applied.push("userAgent");
3567
+ }
3568
+ } catch (e) {
3569
+ throw mapBidiError(e);
3570
+ }
3571
+ return { applied };
3572
+ }
3573
+ static EVENT_MAP = {
3574
+ navigated: "browsingContext.navigationCommitted",
3575
+ console: "log.entryAdded",
3576
+ "network.request": "network.beforeRequestSent",
3577
+ "network.response": "network.responseCompleted",
3578
+ dialog: "browsingContext.userPromptOpened",
3579
+ "page.closed": "browsingContext.contextDestroyed"
3580
+ };
3581
+ contextMatches(event, payload) {
3582
+ if (event === "console")
3583
+ return true;
3584
+ return payload.context === undefined || payload.context === this.contextId;
3585
+ }
3586
+ on(event, handler) {
3587
+ return this.conn.on(BidiPageDriver.EVENT_MAP[event], (params) => {
3588
+ const p = params;
3589
+ if (this.contextMatches(event, p))
3590
+ handler(p);
3591
+ });
3592
+ }
3593
+ async waitForEvent(event, predicate, timeoutMs) {
3594
+ try {
3595
+ const p = await this.conn.waitFor(BidiPageDriver.EVENT_MAP[event], (params) => {
3596
+ const p2 = params;
3597
+ return this.contextMatches(event, p2) && (!predicate || predicate(p2));
3598
+ }, timeoutMs);
3599
+ return p;
3600
+ } catch (e) {
3601
+ throw mapBidiError(e);
3602
+ }
3603
+ }
3604
+ async handleOneDialog(accept, promptText, timeoutMs) {
3605
+ const opened = await this.conn.waitFor("browsingContext.userPromptOpened", (p) => p.context === this.contextId, timeoutMs).catch(() => {
3606
+ throw driverError2("timeout", `handleDialog: no dialog opened within ${timeoutMs}ms`);
3607
+ });
3608
+ try {
3609
+ await this.conn.send("browsingContext.handleUserPrompt", { context: this.contextId, accept, ...promptText !== undefined ? { userText: promptText } : {} });
3610
+ } catch (e) {
3611
+ throw mapBidiError(e);
3612
+ }
3613
+ return {
3614
+ type: opened.type,
3615
+ message: opened.message,
3616
+ url: this.info.url,
3617
+ ...opened.defaultValue !== undefined ? { defaultPrompt: opened.defaultValue } : {},
3618
+ accept,
3619
+ ...promptText !== undefined ? { promptText } : {},
3620
+ handled: true
3621
+ };
3622
+ }
3623
+ async handleDialog(accept, promptText, opts) {
3624
+ if (opts?.autoMs !== undefined) {
3625
+ const deadline = Date.now() + opts.autoMs;
3626
+ const handled = [];
3627
+ while (Date.now() < deadline) {
3628
+ const remaining = deadline - Date.now();
3629
+ if (remaining <= 0)
3630
+ break;
3631
+ const d = await this.handleOneDialog(accept, promptText, Math.min(250, remaining)).catch(() => {
3632
+ return;
3633
+ });
3634
+ if (d)
3635
+ handled.push(d);
3636
+ }
3637
+ return { handled, count: handled.length };
3638
+ }
3639
+ return this.handleOneDialog(accept, promptText, opts?.timeoutMs ?? 15000);
3640
+ }
3641
+ async startBodyCapture() {
3642
+ let collector;
3643
+ try {
3644
+ const res = await this.conn.send("network.addDataCollector", { dataTypes: ["response"], maxEncodedDataSize: 1e8, contexts: [this.contextId] });
3645
+ collector = res.collector;
3646
+ } catch (e) {
3647
+ throw mapBidiError(e);
3648
+ }
3649
+ this.bodyCollector = collector;
3650
+ return async () => {
3651
+ await this.conn.send("network.removeDataCollector", { collector }).catch(() => {
3652
+ return;
3653
+ });
3654
+ if (this.bodyCollector === collector)
3655
+ this.bodyCollector = undefined;
3656
+ };
3657
+ }
3658
+ async getResponseBody(requestId) {
3659
+ if (!this.bodyCollector)
3660
+ throw driverError2("unsupported", "getResponseBody: call startBodyCapture() first (network.responseBodyPostHoc is not supported)");
3661
+ try {
3662
+ const { bytes } = await this.conn.send("network.getData", { dataType: "response", collector: this.bodyCollector, request: requestId });
3663
+ return bytes.type === "base64" ? { body: bytes.value, base64Encoded: true } : { body: bytes.value, base64Encoded: false };
3664
+ } catch (e) {
3665
+ throw mapBidiError(e);
3666
+ }
3667
+ }
3668
+ async intercept(rules) {
3669
+ let intercept;
3670
+ try {
3671
+ const res = await this.conn.send("network.addIntercept", { phases: ["beforeRequestSent"], contexts: [this.contextId] });
3672
+ intercept = res.intercept;
3673
+ } catch (e) {
3674
+ throw mapBidiError(e);
3675
+ }
3676
+ const off = this.conn.on("network.beforeRequestSent", (params) => {
3677
+ (async () => {
3678
+ const req = params.request;
3679
+ const rule = selectRule(rules, req.url, req.method);
3680
+ if (!rule) {
3681
+ await this.conn.send("network.continueRequest", { request: req.request }).catch(() => {
3682
+ return;
3683
+ });
3684
+ return;
3685
+ }
3686
+ if (rule.delayMs)
3687
+ await new Promise((r) => setTimeout(r, rule.delayMs));
3688
+ const act = effectiveAction(rule, Math.random());
3689
+ if (act === "fulfill") {
3690
+ const built = buildFulfillParams(req.request, rule);
3691
+ await this.conn.send("network.provideResponse", {
3692
+ request: req.request,
3693
+ statusCode: built.responseCode,
3694
+ headers: built.responseHeaders.map((h) => ({ name: h.name, value: { type: "string", value: h.value } })),
3695
+ body: { type: "base64", value: built.body }
3696
+ }).catch(() => {
3697
+ return;
3698
+ });
3699
+ } else if (act === "fail") {
3700
+ await this.conn.send("network.failRequest", { request: req.request }).catch(() => {
3701
+ return;
3702
+ });
3703
+ } else {
3704
+ await this.conn.send("network.continueRequest", { request: req.request }).catch(() => {
3705
+ return;
3706
+ });
3707
+ }
3708
+ })();
3709
+ });
3710
+ return async () => {
3711
+ off();
3712
+ await this.conn.send("network.removeIntercept", { intercept }).catch(() => {
3713
+ return;
3714
+ });
3715
+ };
3716
+ }
3717
+ async release() {
3718
+ if (this.released)
3719
+ return;
3720
+ this.released = true;
3721
+ this.conn.release();
3722
+ }
3723
+ }
3724
+
3725
+ class BidiBrowserDriver {
3726
+ port;
3727
+ timeoutMs;
3728
+ scheme = "bidi";
3729
+ lifetime = "session";
3730
+ capabilities = BIDI_CAPABILITIES;
3731
+ uidStability = "document-stamp";
3732
+ snapshotFidelity = "dom-heuristic";
3733
+ constructor(port, timeoutMs) {
3734
+ this.port = port;
3735
+ this.timeoutMs = timeoutMs;
3736
+ }
3737
+ async listPages() {
3738
+ const conn = await getConnection(this.port, this.timeoutMs);
3739
+ try {
3740
+ const { contexts } = await conn.send("browsingContext.getTree", {}).catch((e) => {
3741
+ throw mapBidiError(e);
3742
+ });
3743
+ return Promise.all(contexts.map((c) => pageInfoOf(conn, c)));
3744
+ } finally {
3745
+ conn.release();
3746
+ }
3747
+ }
3748
+ async newPage(url) {
3749
+ const conn = await getConnection(this.port, this.timeoutMs);
3750
+ try {
3751
+ const { context } = await conn.send("browsingContext.create", { type: "tab" }).catch((e) => {
3752
+ throw mapBidiError(e);
3753
+ });
3754
+ if (url)
3755
+ await conn.send("browsingContext.navigate", { context, url, wait: "complete" }).catch((e) => {
3756
+ throw mapBidiError(e);
3757
+ });
3758
+ return { id: context, url: url ?? "about:blank", title: await safeTitle(conn, context) };
3759
+ } finally {
3760
+ conn.release();
3761
+ }
3762
+ }
3763
+ async closePage(id) {
3764
+ const conn = await getConnection(this.port, this.timeoutMs);
3765
+ try {
3766
+ await conn.send("browsingContext.close", { context: id }).catch((e) => {
3767
+ throw mapBidiError(e);
3768
+ });
3769
+ return { success: true };
3770
+ } finally {
3771
+ conn.release();
3772
+ }
3773
+ }
3774
+ async activatePage(id) {
3775
+ const conn = await getConnection(this.port, this.timeoutMs);
3776
+ try {
3777
+ await conn.send("browsingContext.activate", { context: id }).catch((e) => {
3778
+ throw mapBidiError(e);
3779
+ });
3780
+ const ctx = await resolveContext(conn, id);
3781
+ return await pageInfoOf(conn, ctx);
3782
+ } finally {
3783
+ conn.release();
3784
+ }
3785
+ }
3786
+ async page(selector) {
3787
+ const conn = await getConnection(this.port, this.timeoutMs);
3788
+ let ctx;
3789
+ try {
3790
+ ctx = await resolveContext(conn, selector);
3791
+ } catch (e) {
3792
+ conn.release();
3793
+ throw e;
3794
+ }
3795
+ const info = await pageInfoOf(conn, ctx);
3796
+ return new BidiPageDriver(conn, ctx.context, info, this);
3797
+ }
3798
+ async dispose() {
3799
+ const conn = connections.get(this.port);
3800
+ if (!conn)
3801
+ return;
3802
+ connections.delete(this.port);
3803
+ conn.dispose();
3804
+ }
3805
+ }
3806
+ function createFirefoxDriver(port, opts) {
3807
+ return new BidiBrowserDriver(port, opts?.timeoutMs);
3808
+ }
3809
+
3810
+ // src/bidi/launch.ts
3811
+ import { spawn as spawn2 } from "node:child_process";
3812
+ import { mkdtempSync, rmSync } from "node:fs";
3813
+ import { createConnection, createServer } from "node:net";
3814
+ import { tmpdir } from "node:os";
3815
+ import { join as join5 } from "node:path";
3816
+ function resolveBinary() {
3817
+ const candidates = [
3818
+ process.env.FIREFOX_BIN,
3819
+ "/Applications/Firefox.app/Contents/MacOS/firefox",
3820
+ "firefox"
3821
+ ];
3822
+ for (const c of candidates) {
3823
+ if (c)
3824
+ return c;
3825
+ }
3826
+ throw new Error("Firefox binary not found. Tried FIREFOX_BIN env var, " + "/Applications/Firefox.app/Contents/MacOS/firefox, and 'firefox' on PATH.");
3827
+ }
3828
+ function pickPort(requested) {
3829
+ return new Promise((resolve, reject) => {
3830
+ const server = createServer();
3831
+ server.once("error", (err) => {
3832
+ if (requested !== undefined && err.code === "EADDRINUSE") {
3833
+ reject(new Error(`port ${requested} is already in use`));
3834
+ return;
3835
+ }
3836
+ reject(err);
3837
+ });
3838
+ server.listen(requested ?? 0, "127.0.0.1", () => {
3839
+ const addr = server.address();
3840
+ if (addr === null || typeof addr === "string") {
3841
+ server.close();
3842
+ reject(new Error("failed to allocate a port"));
3843
+ return;
3844
+ }
3845
+ const { port } = addr;
3846
+ server.close(() => resolve(port));
3847
+ });
3848
+ });
3849
+ }
3850
+ function pollPort(port, deadlineMs) {
3851
+ const start = Date.now();
3852
+ return new Promise((resolve, reject) => {
3853
+ const attempt = () => {
3854
+ const sock = createConnection({ port, host: "127.0.0.1" });
3855
+ const onFail = () => {
3856
+ sock.destroy();
3857
+ if (Date.now() - start >= deadlineMs) {
3858
+ reject(new Error(`Firefox debug port ${port} did not accept a connection within ${deadlineMs}ms`));
3859
+ return;
3860
+ }
3861
+ setTimeout(attempt, 100);
3862
+ };
3863
+ sock.once("connect", () => {
3864
+ sock.destroy();
3865
+ resolve();
3866
+ });
3867
+ sock.once("error", onFail);
3868
+ };
3869
+ attempt();
3870
+ });
3871
+ }
3872
+ function waitForExit(proc, deadlineMs) {
3873
+ return new Promise((resolve) => {
3874
+ if (proc.exitCode !== null || proc.signalCode !== null) {
3875
+ resolve();
3876
+ return;
3877
+ }
3878
+ const timer = setTimeout(() => resolve(), deadlineMs);
3879
+ proc.once("exit", () => {
3880
+ clearTimeout(timer);
3881
+ resolve();
3882
+ });
3883
+ });
3884
+ }
3885
+ async function launchFirefox(opts = {}) {
3886
+ const binary = resolveBinary();
3887
+ const port = await pickPort(opts.port);
3888
+ const isThrowaway = opts.profilePath === undefined;
3889
+ const profilePath = opts.profilePath ?? mkdtempSync(join5(tmpdir(), "cdp-toolkit-ff-profile-"));
3890
+ const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
3891
+ if (opts.headless)
3892
+ args.push("--headless");
3893
+ const proc = spawn2(binary, args, { stdio: "ignore", detached: false });
3894
+ let closed = false;
3895
+ const close = async () => {
3896
+ if (closed)
3897
+ return;
3898
+ closed = true;
3899
+ if (proc.exitCode === null && proc.signalCode === null) {
3900
+ proc.kill("SIGTERM");
3901
+ await waitForExit(proc, 5000);
3902
+ if (proc.exitCode === null && proc.signalCode === null) {
3903
+ proc.kill("SIGKILL");
3904
+ await waitForExit(proc, 5000);
3905
+ }
3906
+ }
3907
+ if (isThrowaway) {
3908
+ rmSync(profilePath, { recursive: true, force: true });
3909
+ }
3910
+ };
3911
+ const pid = proc.pid;
3912
+ if (pid === undefined) {
3913
+ await close();
3914
+ throw new Error(`failed to spawn Firefox binary at ${binary}`);
3915
+ }
3916
+ const timeoutMs = opts.timeoutMs ?? 15000;
3917
+ try {
3918
+ await pollPort(port, timeoutMs);
3919
+ } catch (err) {
3920
+ await close();
3921
+ throw err;
3922
+ }
3923
+ return { port, pid, profilePath, close };
3924
+ }
3925
+
3926
+ // src/backend.ts
3927
+ function resolveBrowserKind(argv) {
3928
+ const flagIdx = argv.indexOf("--browser");
3929
+ const flagVal = flagIdx !== -1 ? argv[flagIdx + 1] : undefined;
3930
+ const raw = flagVal ?? process.env.CDP_BROWSER ?? "chrome";
3931
+ if (raw !== "chrome" && raw !== "firefox") {
3932
+ throw new Error(`unknown --browser '${raw}': expected 'chrome' or 'firefox' (or set CDP_BROWSER)`);
3933
+ }
3934
+ return raw;
3935
+ }
3936
+ function stripBrowserFlag(argv) {
3937
+ const out = [];
3938
+ for (let i = 0;i < argv.length; i++) {
3939
+ if (argv[i] === "--browser") {
3940
+ i++;
3941
+ continue;
3942
+ }
3943
+ out.push(argv[i]);
3944
+ }
3945
+ return out;
3946
+ }
3947
+ async function startFirefoxSession(opts) {
3948
+ const launched = await launchFirefox({ headless: opts?.headless });
3949
+ const driver = createFirefoxDriver(launched.port);
3950
+ let disposed = false;
3951
+ return {
3952
+ driver,
3953
+ async dispose() {
3954
+ if (disposed)
3955
+ return;
3956
+ disposed = true;
3957
+ await driver.dispose().catch(() => {
3958
+ return;
3959
+ });
3960
+ await launched.close();
3961
+ }
1787
3962
  };
1788
3963
  }
1789
- function truncate(s, max = 600) {
1790
- const t = s.trim();
1791
- return t.length > max ? `${t.slice(0, max)}…` : t;
3964
+ var sessionPromise;
3965
+ function getOrCreateFirefoxSession() {
3966
+ if (!sessionPromise)
3967
+ sessionPromise = startFirefoxSession();
3968
+ return sessionPromise;
3969
+ }
3970
+ async function disposeFirefoxSession() {
3971
+ if (!sessionPromise)
3972
+ return;
3973
+ const p = sessionPromise;
3974
+ sessionPromise = undefined;
3975
+ await p.then((s) => s.dispose()).catch(() => {
3976
+ return;
3977
+ });
1792
3978
  }
1793
3979
 
1794
- // src/tools/network_mock.ts
1795
- function escapeRegExpChar(ch) {
1796
- return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
3980
+ // src/capabilities.ts
3981
+ function capabilitiesFor(kind) {
3982
+ return kind === "chrome" ? createCdpDriver().capabilities : createFirefoxDriver(0).capabilities;
1797
3983
  }
1798
- function globToRegExp(pattern) {
1799
- let re = "";
1800
- for (let i = 0;i < pattern.length; i++) {
1801
- const ch = pattern[i];
1802
- if (ch === "\\") {
1803
- const next = pattern[i + 1];
1804
- if (next !== undefined) {
1805
- re += escapeRegExpChar(next);
1806
- i++;
1807
- } else {
1808
- re += "\\\\";
1809
- }
1810
- } else if (ch === "*") {
1811
- re += ".*";
1812
- } else if (ch === "?") {
1813
- re += ".";
3984
+ function toolAvailability(kind) {
3985
+ const caps = capabilitiesFor(kind);
3986
+ const available = [];
3987
+ const unavailable = [];
3988
+ for (const name of TOOL_NAMES) {
3989
+ const required = REQUIRED_CAPABILITIES[name];
3990
+ if (!required || required.every((c) => caps.has(c))) {
3991
+ available.push(name);
1814
3992
  } else {
1815
- re += escapeRegExpChar(ch);
3993
+ unavailable.push({ name, missing: required.filter((c) => !caps.has(c)) });
1816
3994
  }
1817
3995
  }
1818
- return new RegExp(`^${re}$`);
3996
+ return { kind, available, unavailable };
1819
3997
  }
1820
- function urlMatches(pattern, url) {
1821
- return globToRegExp(pattern).test(url);
3998
+
3999
+ // src/bidi-tools.ts
4000
+ class BidiToolError extends Error {
1822
4001
  }
1823
- function selectRule(rules, url, method) {
1824
- return rules.find((r) => urlMatches(r.urlPattern, url) && (!r.method || r.method.toUpperCase() === method.toUpperCase()));
4002
+ async function withPage3(driver, target, fn) {
4003
+ const page = await driver.page(target);
4004
+ try {
4005
+ return await fn(page);
4006
+ } finally {
4007
+ await page.release();
4008
+ }
1825
4009
  }
1826
- function buildFulfillParams(requestId, rule) {
1827
- const merged = new Map;
1828
- merged.set("content-type", { name: "Content-Type", value: rule.contentType ?? "application/json" });
1829
- for (const [name, value] of Object.entries(rule.headers ?? {})) {
1830
- merged.set(name.toLowerCase(), { name, value });
4010
+ var consoleBuffers = new Map;
4011
+ var networkBuffers = new Map;
4012
+ function bufferFor(map, id) {
4013
+ let b = map.get(id);
4014
+ if (!b) {
4015
+ b = [];
4016
+ map.set(id, b);
1831
4017
  }
1832
- return {
1833
- requestId,
1834
- responseCode: rule.status ?? 200,
1835
- responseHeaders: [...merged.values()],
1836
- body: Buffer.from(rule.body ?? "", "utf8").toString("base64")
1837
- };
4018
+ return b;
1838
4019
  }
1839
- function effectiveAction(rule, rnd) {
1840
- if (rule.failRate !== undefined && rule.failRate > 0 && rnd < rule.failRate)
1841
- return "fail";
1842
- return rule.action;
4020
+ async function captureFresh(page, durationMs) {
4021
+ const cBuf = bufferFor(consoleBuffers, page.info.id);
4022
+ const nBuf = bufferFor(networkBuffers, page.info.id);
4023
+ cBuf.length = 0;
4024
+ nBuf.length = 0;
4025
+ const offC = page.on("console", (p) => cBuf.push(p));
4026
+ const offReq = page.on("network.request", (p) => nBuf.push({ ...p, phase: "request" }));
4027
+ const offRes = page.on("network.response", (p) => nBuf.push({ ...p, phase: "response" }));
4028
+ try {
4029
+ await page.navigate({ reload: true }).catch(() => {
4030
+ return;
4031
+ });
4032
+ await new Promise((r) => setTimeout(r, durationMs));
4033
+ } finally {
4034
+ offC();
4035
+ offReq();
4036
+ offRes();
4037
+ }
1843
4038
  }
1844
- var sessions = new Map;
1845
- function delay3(ms) {
1846
- return new Promise((r) => setTimeout(r, ms));
4039
+ async function listConsoleMessages2(driver, args = {}) {
4040
+ return withPage3(driver, args.target, async (page) => {
4041
+ if (args.reload)
4042
+ await captureFresh(page, args.durationMs ?? 2500);
4043
+ const buf = bufferFor(consoleBuffers, page.info.id);
4044
+ return { messages: [...buf], count: buf.length };
4045
+ });
1847
4046
  }
1848
- function registerHandler(session) {
1849
- session.conn.on("Fetch.requestPaused", (params) => {
1850
- const p = params;
1851
- session.chain = session.chain.then(async () => {
1852
- const rule = selectRule(session.rules, p.request.url, p.request.method);
1853
- if (!rule) {
1854
- await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
1855
- return;
1856
- }
1857
- if (rule.delayMs)
1858
- await delay3(rule.delayMs);
1859
- const act = effectiveAction(rule, Math.random());
1860
- if (act === "fulfill") {
1861
- await session.conn.send("Fetch.fulfillRequest", { ...buildFulfillParams(p.requestId, rule) });
1862
- } else if (act === "fail") {
1863
- await session.conn.send("Fetch.failRequest", { requestId: p.requestId, errorReason: rule.errorReason ?? "Failed" });
1864
- } else {
1865
- await session.conn.send("Fetch.continueRequest", { requestId: p.requestId });
1866
- }
1867
- rule.hits++;
1868
- session.intercepted.push({ url: p.request.url, method: p.request.method, action: act });
1869
- }).catch((e) => {
1870
- if (/closed|connection|Target|Inspected/i.test(String(e)))
1871
- session.dead = true;
1872
- });
4047
+ async function getConsoleMessage2(driver, args = {}) {
4048
+ return withPage3(driver, args.target, async (page) => {
4049
+ const buf = bufferFor(consoleBuffers, page.info.id);
4050
+ const idx = args.index ?? 0;
4051
+ const entry = buf[idx];
4052
+ if (!entry)
4053
+ throw new BidiToolError(`no console message at index ${idx}; run list_console_messages first`);
4054
+ return entry;
1873
4055
  });
1874
4056
  }
1875
- async function mockRequest(args) {
4057
+ async function listNetworkRequests2(driver, args = {}) {
4058
+ return withPage3(driver, args.target, async (page) => {
4059
+ if (args.reload)
4060
+ await captureFresh(page, args.durationMs ?? 2500);
4061
+ let buf = bufferFor(networkBuffers, page.info.id);
4062
+ if (args.filterUrl)
4063
+ buf = buf.filter((r) => typeof r.url === "string" && r.url.includes(args.filterUrl));
4064
+ return { requests: buf, count: buf.length };
4065
+ });
4066
+ }
4067
+ async function getNetworkRequest2(driver, args = {}) {
4068
+ if (!args.requestId && !args.url)
4069
+ throw new BidiToolError("get_network_request requires requestId or url");
4070
+ return withPage3(driver, args.target, async (page) => {
4071
+ const buf = bufferFor(networkBuffers, page.info.id);
4072
+ const found = args.requestId ? buf.find((r) => r.requestId === args.requestId || r.request === args.requestId) : buf.find((r) => typeof r.url === "string" && r.url.includes(args.url));
4073
+ if (!found)
4074
+ throw new BidiToolError("no matching network request in the current buffer; run list_network_requests first");
4075
+ return found;
4076
+ });
4077
+ }
4078
+ var mockSessions = new Map;
4079
+ async function mockRequest2(driver, args) {
1876
4080
  if (!args.urlPattern)
1877
- throw new CdpError("mock_request requires a { urlPattern }");
1878
- const rule = {
1879
- urlPattern: args.urlPattern,
1880
- action: args.action ?? "fulfill",
1881
- status: args.status,
1882
- body: args.body,
1883
- contentType: args.contentType,
1884
- headers: args.headers,
1885
- errorReason: args.errorReason,
1886
- method: args.method,
1887
- delayMs: args.delayMs,
1888
- failRate: args.failRate,
1889
- hits: 0
1890
- };
1891
- const resolved = await resolveTarget(args.target);
1892
- let session = sessions.get(resolved.id);
1893
- if (session && !session.dead) {
1894
- const idx = session.rules.findIndex((r) => r.urlPattern === rule.urlPattern && (r.method ?? "") === (rule.method ?? ""));
1895
- if (idx >= 0)
1896
- session.rules[idx] = rule;
1897
- else
1898
- session.rules.push(rule);
1899
- await session.conn.send("Fetch.enable", { patterns: session.rules.map((r) => ({ urlPattern: r.urlPattern })) });
1900
- } else {
1901
- const { conn, target } = await openPage(resolved.id);
1902
- session = { conn, target, rules: [rule], intercepted: [], chain: Promise.resolve(), dead: false };
1903
- registerHandler(session);
1904
- await conn.send("Fetch.enable", { patterns: [{ urlPattern: rule.urlPattern }] });
1905
- sessions.set(target.id, session);
1906
- }
1907
- let reloaded = false;
1908
- if (args.reload) {
1909
- await session.conn.send("Page.enable");
1910
- await session.conn.send("Page.reload", { ignoreCache: true });
1911
- await delay3(800);
1912
- reloaded = true;
1913
- }
1914
- return {
1915
- target: { id: session.target.id, url: session.target.url, title: session.target.title },
1916
- pattern: rule.urlPattern,
1917
- ruleCount: session.rules.length,
1918
- reloaded
1919
- };
4081
+ throw new BidiToolError("mock_request requires a { urlPattern }");
4082
+ return withPage3(driver, args.target, async (page) => {
4083
+ const existing = mockSessions.get(page.info.id);
4084
+ const rule = {
4085
+ urlPattern: args.urlPattern,
4086
+ method: args.method,
4087
+ action: args.action ?? "fulfill",
4088
+ status: args.status,
4089
+ body: args.body,
4090
+ contentType: args.contentType,
4091
+ headers: args.headers,
4092
+ errorReason: args.errorReason,
4093
+ delayMs: args.delayMs,
4094
+ failRate: args.failRate,
4095
+ hits: 0
4096
+ };
4097
+ const rules = existing ? [...existing.rules, rule] : [rule];
4098
+ if (existing)
4099
+ await existing.dispose().catch(() => {
4100
+ return;
4101
+ });
4102
+ const dispose = await page.intercept(rules);
4103
+ mockSessions.set(page.info.id, { rules, dispose });
4104
+ if (args.reload)
4105
+ await page.navigate({ reload: true }).catch(() => {
4106
+ return;
4107
+ });
4108
+ return { target: page.info, rule: { urlPattern: rule.urlPattern, action: rule.action }, ruleCount: rules.length };
4109
+ });
1920
4110
  }
1921
- async function listMocks(_args = {}) {
1922
- for (const [id, s] of sessions) {
1923
- if (s.dead) {
1924
- try {
1925
- s.conn.close();
1926
- } catch {}
1927
- sessions.delete(id);
1928
- continue;
1929
- }
1930
- try {
1931
- await s.conn.send("Runtime.evaluate", { expression: "1", returnByValue: true }, { timeoutMs: 3000 });
1932
- } catch {
1933
- try {
1934
- s.conn.close();
1935
- } catch {}
1936
- sessions.delete(id);
1937
- }
1938
- }
1939
- const mocks = [...sessions.values()].map((s) => ({
1940
- target: { id: s.target.id, url: s.target.url, title: s.target.title },
1941
- rules: s.rules.map((r) => ({ urlPattern: r.urlPattern, action: r.action, method: r.method, hits: r.hits })),
1942
- hits: s.intercepted.length
4111
+ async function listMocks2(_driver, _args = {}) {
4112
+ const sessions2 = [...mockSessions.entries()].map(([id, s]) => ({
4113
+ target: id,
4114
+ rules: s.rules.map((r) => ({ urlPattern: r.urlPattern, action: r.action, method: r.method }))
1943
4115
  }));
1944
- return { count: mocks.length, mocks };
4116
+ return { sessions: sessions2 };
1945
4117
  }
1946
- async function clearMocks(args = {}) {
1947
- let ids;
4118
+ async function clearMocks2(driver, args = {}) {
1948
4119
  if (args.all) {
1949
- ids = [...sessions.keys()];
1950
- } else {
1951
- const resolved = await resolveTarget(args.target);
1952
- ids = sessions.has(resolved.id) ? [resolved.id] : [];
1953
- }
1954
- let cleared = 0;
1955
- for (const id of ids) {
1956
- const s = sessions.get(id);
1957
- if (!s)
1958
- continue;
1959
- try {
1960
- await s.conn.send("Fetch.disable");
1961
- } catch {}
1962
- try {
1963
- s.conn.close();
1964
- } catch {}
1965
- sessions.delete(id);
1966
- cleared++;
4120
+ let n = 0;
4121
+ for (const [, s] of mockSessions) {
4122
+ await s.dispose().catch(() => {
4123
+ return;
4124
+ });
4125
+ n++;
4126
+ }
4127
+ mockSessions.clear();
4128
+ return { cleared: n };
1967
4129
  }
1968
- return { cleared };
4130
+ return withPage3(driver, args.target, async (page) => {
4131
+ const existing = mockSessions.get(page.info.id);
4132
+ if (!existing)
4133
+ return { cleared: 0 };
4134
+ await existing.dispose().catch(() => {
4135
+ return;
4136
+ });
4137
+ mockSessions.delete(page.info.id);
4138
+ return { cleared: 1 };
4139
+ });
1969
4140
  }
4141
+ var BIDI_ONLY_TOOLS = {
4142
+ list_console_messages: listConsoleMessages2,
4143
+ get_console_message: getConsoleMessage2,
4144
+ list_network_requests: listNetworkRequests2,
4145
+ get_network_request: getNetworkRequest2,
4146
+ mock_request: mockRequest2,
4147
+ list_mocks: listMocks2,
4148
+ clear_mocks: clearMocks2
4149
+ };
1970
4150
 
1971
- // src/index.ts
1972
- var TOOLS = {
1973
- list_pages: listPages,
1974
- new_page: newPage,
1975
- close_page: closePage,
1976
- select_page: selectPage,
1977
- navigate_page: navigatePage,
1978
- wait_for: waitForText,
1979
- evaluate_script: evaluateScript,
1980
- take_snapshot: takeSnapshot,
1981
- click,
1982
- hover,
1983
- drag,
1984
- fill,
1985
- fill_form: fillForm,
1986
- type_text: typeText,
1987
- press_key: pressKey,
1988
- upload_file: uploadFile,
1989
- take_screenshot: takeScreenshot,
1990
- emulate,
1991
- resize_page: resizePage,
1992
- handle_dialog: handleDialog,
1993
- list_console_messages: listConsoleMessages,
1994
- get_console_message: getConsoleMessage,
1995
- list_network_requests: listNetworkRequests,
1996
- get_network_request: getNetworkRequest,
1997
- performance_start_trace: performanceStartTrace,
1998
- performance_stop_trace: performanceStopTrace,
1999
- performance_analyze_insight: performanceAnalyzeInsight,
2000
- performance_trace: performanceTrace,
2001
- take_heapsnapshot: takeHeapsnapshot,
2002
- lighthouse_audit: lighthouseAudit,
2003
- mock_request: mockRequest,
2004
- list_mocks: listMocks,
2005
- clear_mocks: clearMocks
4151
+ // src/firefox-tools.ts
4152
+ var FIREFOX_TOOLS = {
4153
+ ...SHARED_TOOLS,
4154
+ ...BIDI_ONLY_TOOLS,
4155
+ ...LEASE_TOOLS
2006
4156
  };
2007
- var TOOL_NAMES = Object.keys(TOOLS);
2008
4157
 
2009
4158
  // src/cli.ts
2010
- var USAGE = `cdp-toolkit — raw single-target CDP, 29-tool chrome-devtools-mcp parity.
4159
+ var USAGE = `cdp-toolkit: raw single-target CDP, 29-tool chrome-devtools-mcp parity, plus a Firefox backend over WebDriver BiDi.
2011
4160
 
2012
4161
  Usage:
2013
- bun run src/cli.ts <tool> [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
2014
- bun run src/cli.ts --list
4162
+ bun run src/cli.ts <tool> [--browser chrome|firefox] [--target <sel>] [--json '<obj>'] [--<key> <value> ...]
4163
+ bun run src/cli.ts --list [--browser chrome|firefox]
4164
+ bun run src/cli.ts --capabilities [--browser chrome|firefox]
2015
4165
 
2016
4166
  Examples:
2017
4167
  bun run src/cli.ts list_pages
@@ -2019,9 +4169,19 @@ Examples:
2019
4169
  bun run src/cli.ts click --target index:0 --uid 42
2020
4170
  bun run src/cli.ts evaluate_script --json '{"expression":"1+2"}'
2021
4171
  bun run src/cli.ts take_screenshot --target url:example --fullPage true
4172
+ bun run src/cli.ts --browser firefox take_snapshot
4173
+ bun run src/cli.ts --capabilities --browser firefox
4174
+
4175
+ Backend selection: --browser chrome|firefox, else the CDP_BROWSER env var, else "chrome" (default,
4176
+ zero behavior change for existing users). Firefox is LAUNCHED per invocation (it cannot be attached
4177
+ to), used for exactly one tool call, then disposed before the process exits.
2022
4178
 
2023
4179
  Target selector grammar: active | index:N | url:<substr> | title:<substr> | <targetId>
2024
- Run with --list to print every available tool name.`;
4180
+ Leases: pass --lease <token> to act on a tab another agent claimed with claim_page (MCP only).
4181
+ Run list_leases to see who holds what; release_page --lease <token> gives a lease back.
4182
+ Run with --list to print every tool name available under the selected backend.
4183
+ Run with --capabilities to see, per tool, whether it is available and (if not) which capability
4184
+ the selected backend is missing.`;
2025
4185
  function coerce(raw) {
2026
4186
  if (raw === "true")
2027
4187
  return true;
@@ -2033,7 +4193,7 @@ function coerce(raw) {
2033
4193
  return raw;
2034
4194
  }
2035
4195
  function parseArgv(argv) {
2036
- const out = { list: false, args: {} };
4196
+ const out = { list: false, capabilities: false, args: {} };
2037
4197
  let jsonObj;
2038
4198
  const flagPairs = [];
2039
4199
  for (let i = 0;i < argv.length; i++) {
@@ -2042,6 +4202,10 @@ function parseArgv(argv) {
2042
4202
  out.list = true;
2043
4203
  continue;
2044
4204
  }
4205
+ if (token === "--capabilities") {
4206
+ out.capabilities = true;
4207
+ continue;
4208
+ }
2045
4209
  if (token.startsWith("--")) {
2046
4210
  const key = token.slice(2);
2047
4211
  const next = argv[i + 1];
@@ -2077,9 +4241,25 @@ function isToolName(name) {
2077
4241
  return Object.prototype.hasOwnProperty.call(TOOLS, name);
2078
4242
  }
2079
4243
  async function main() {
2080
- const parsed = parseArgv(process.argv.slice(2));
4244
+ const rawArgv = process.argv.slice(2);
4245
+ const browser = resolveBrowserKind(rawArgv);
4246
+ const parsed = parseArgv(stripBrowserFlag(rawArgv));
4247
+ const availability = toolAvailability(browser);
4248
+ const availableSet = new Set(availability.available);
4249
+ if (parsed.capabilities) {
4250
+ const lines = [`backend: ${browser}`, "", "available:"];
4251
+ for (const name of availability.available)
4252
+ lines.push(` ${name}`);
4253
+ lines.push("", "unavailable:");
4254
+ for (const u of availability.unavailable)
4255
+ lines.push(` ${u.name} (needs: ${u.missing.join(", ")})`);
4256
+ process.stdout.write(`${lines.join(`
4257
+ `)}
4258
+ `);
4259
+ return 0;
4260
+ }
2081
4261
  if (parsed.list) {
2082
- process.stdout.write(`${TOOL_NAMES.join(`
4262
+ process.stdout.write(`${availability.available.join(`
2083
4263
  `)}
2084
4264
  `);
2085
4265
  return 0;
@@ -2094,11 +4274,46 @@ async function main() {
2094
4274
  `);
2095
4275
  return 1;
2096
4276
  }
2097
- const fn = TOOLS[parsed.tool];
2098
- const result = await fn(parsed.args);
2099
- process.stdout.write(`${JSON.stringify(result, null, 2)}
4277
+ if (!availableSet.has(parsed.tool)) {
4278
+ const gap = availability.unavailable.find((u) => u.name === parsed.tool);
4279
+ process.stderr.write(`${JSON.stringify({
4280
+ error: `tool '${parsed.tool}' is not available under --browser ${browser}${gap ? ` (needs: ${gap.missing.join(", ")})` : ""}. Run --capabilities --browser ${browser} to see the full list.`
4281
+ })}
4282
+ `);
4283
+ return 1;
4284
+ }
4285
+ if (parsed.tool === "claim_page") {
4286
+ process.stderr.write(`${JSON.stringify({
4287
+ error: "claim_page is not available from the CLI: a CLI invocation is one process per call, so the lease it claimed would be reclaimable immediately by the dead-pid rule. Claim from the cdp-toolkit MCP server, which is long lived, then pass the token to CLI calls with --lease <token>."
4288
+ })}
4289
+ `);
4290
+ return 1;
4291
+ }
4292
+ const tool = parsed.tool;
4293
+ return withLeaseScope(leaseFromArgs(parsed.args), async () => {
4294
+ if (browser === "chrome") {
4295
+ const fn = TOOLS[tool];
4296
+ const result = await fn(parsed.args);
4297
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
2100
4298
  `);
2101
- return 0;
4299
+ return 0;
4300
+ }
4301
+ const neutralFn = FIREFOX_TOOLS[tool];
4302
+ if (!neutralFn) {
4303
+ process.stderr.write(`${JSON.stringify({ error: `tool '${tool}' has no Firefox implementation wired` })}
4304
+ `);
4305
+ return 1;
4306
+ }
4307
+ const session = await startFirefoxSession();
4308
+ try {
4309
+ const result = await neutralFn(session.driver, parsed.args);
4310
+ process.stdout.write(`${JSON.stringify(result, null, 2)}
4311
+ `);
4312
+ return 0;
4313
+ } finally {
4314
+ await session.dispose();
4315
+ }
4316
+ });
2102
4317
  }
2103
4318
  main().then((code) => process.exit(code)).catch((err) => {
2104
4319
  const message = err instanceof Error ? err.message : String(err);