cdp-toolkit 1.8.0 → 1.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +237 -0
- package/CONTRACT.md +5 -1
- package/README.md +41 -8
- package/dist/cli.js +407 -121
- package/dist/index.js +377 -120
- package/dist/mcp.js +464 -166
- package/package.json +3 -2
- package/src/bidi/driver.ts +28 -0
- package/src/bidi-tools.ts +21 -0
- package/src/cdp/driver.ts +16 -1
- package/src/cdp/workers.ts +177 -0
- package/src/cli.ts +5 -1
- package/src/client.ts +52 -2
- package/src/driver.ts +27 -1
- package/src/leases-tools.ts +2 -2
- package/src/manifest.ts +61 -45
- package/src/mcp.ts +1 -1
- package/src/origins.ts +41 -1
- package/src/shared-tools.ts +104 -3
- package/src/tools/console.ts +24 -7
- package/src/tools/network.ts +21 -7
- package/src/tools/recorder.ts +110 -10
- package/src/types.ts +22 -0
- package/src/workers.ts +221 -0
package/dist/cli.js
CHANGED
|
@@ -278,6 +278,185 @@ async function listLeases(opts = {}) {
|
|
|
278
278
|
return out;
|
|
279
279
|
}
|
|
280
280
|
|
|
281
|
+
// src/origins.ts
|
|
282
|
+
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
283
|
+
import { join as join2 } from "node:path";
|
|
284
|
+
function safeId2(id) {
|
|
285
|
+
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
286
|
+
}
|
|
287
|
+
function originFile(backend, targetId) {
|
|
288
|
+
return join2(leaseDir(), `origin-${backend}-${safeId2(targetId)}.json`);
|
|
289
|
+
}
|
|
290
|
+
var ORIGIN_FILE_RE = /^origin-(chrome|firefox)-(.+)\.json$/;
|
|
291
|
+
async function recordOrigin(backend, targetId, opts = {}) {
|
|
292
|
+
const record = {
|
|
293
|
+
backend,
|
|
294
|
+
targetId,
|
|
295
|
+
label: typeof opts.label === "string" && opts.label.length ? opts.label : defaultLabel(),
|
|
296
|
+
pid: process.pid,
|
|
297
|
+
createdAt: opts.now ?? Date.now()
|
|
298
|
+
};
|
|
299
|
+
const file = originFile(backend, targetId);
|
|
300
|
+
try {
|
|
301
|
+
await mkdir2(leaseDir(), { recursive: true });
|
|
302
|
+
try {
|
|
303
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
304
|
+
return;
|
|
305
|
+
} catch (err) {
|
|
306
|
+
if (err.code !== "EEXIST")
|
|
307
|
+
throw err;
|
|
308
|
+
}
|
|
309
|
+
await unlink2(file).catch(() => {
|
|
310
|
+
return;
|
|
311
|
+
});
|
|
312
|
+
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
313
|
+
} catch {}
|
|
314
|
+
}
|
|
315
|
+
async function readOrigin(backend, targetId) {
|
|
316
|
+
let raw;
|
|
317
|
+
try {
|
|
318
|
+
raw = await readFile2(originFile(backend, targetId), "utf8");
|
|
319
|
+
} catch {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
try {
|
|
323
|
+
const rec = JSON.parse(raw);
|
|
324
|
+
if (typeof rec?.targetId === "string" && typeof rec?.label === "string" && typeof rec?.createdAt === "number")
|
|
325
|
+
return rec;
|
|
326
|
+
} catch {}
|
|
327
|
+
return;
|
|
328
|
+
}
|
|
329
|
+
async function listOrigins(opts = {}) {
|
|
330
|
+
let names = [];
|
|
331
|
+
try {
|
|
332
|
+
names = await readdir2(leaseDir());
|
|
333
|
+
} catch {
|
|
334
|
+
return [];
|
|
335
|
+
}
|
|
336
|
+
const live = opts.liveIds === undefined ? undefined : new Set(opts.liveIds.map((id) => safeId2(id)));
|
|
337
|
+
const out = [];
|
|
338
|
+
for (const name of names) {
|
|
339
|
+
const parts = ORIGIN_FILE_RE.exec(name);
|
|
340
|
+
if (!parts)
|
|
341
|
+
continue;
|
|
342
|
+
const backend = parts[1];
|
|
343
|
+
const nameId = parts[2];
|
|
344
|
+
const scoped = live !== undefined && (opts.liveBackend === undefined || opts.liveBackend === backend);
|
|
345
|
+
if (scoped && !live.has(nameId)) {
|
|
346
|
+
await unlink2(join2(leaseDir(), name)).catch(() => {
|
|
347
|
+
return;
|
|
348
|
+
});
|
|
349
|
+
continue;
|
|
350
|
+
}
|
|
351
|
+
const fromName = { backend, targetId: nameId, label: "", pid: 0, createdAt: 0 };
|
|
352
|
+
let rec;
|
|
353
|
+
try {
|
|
354
|
+
rec = JSON.parse(await readFile2(join2(leaseDir(), name), "utf8"));
|
|
355
|
+
} catch (err) {
|
|
356
|
+
const code = err.code;
|
|
357
|
+
if (code === "ENOENT")
|
|
358
|
+
continue;
|
|
359
|
+
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (typeof rec?.targetId !== "string" || typeof rec?.label !== "string" || typeof rec?.createdAt !== "number") {
|
|
363
|
+
out.push({ ...fromName, unreadable: "unparseable" });
|
|
364
|
+
continue;
|
|
365
|
+
}
|
|
366
|
+
out.push({ backend: rec.backend, targetId: rec.targetId, label: rec.label, pid: rec.pid, createdAt: rec.createdAt });
|
|
367
|
+
}
|
|
368
|
+
return out;
|
|
369
|
+
}
|
|
370
|
+
async function originIndex(backend, liveIds) {
|
|
371
|
+
const rows = await listOrigins({ liveIds, liveBackend: backend }).catch(() => []);
|
|
372
|
+
const index = new Map;
|
|
373
|
+
for (const row of rows) {
|
|
374
|
+
if (row.backend !== backend)
|
|
375
|
+
continue;
|
|
376
|
+
index.set(row.targetId, row);
|
|
377
|
+
}
|
|
378
|
+
return index;
|
|
379
|
+
}
|
|
380
|
+
async function newTrackedPage(driver, backend, opts = {}) {
|
|
381
|
+
const page = await driver.newPage(opts.url);
|
|
382
|
+
await recordOrigin(backend, page.id, { label: opts.label });
|
|
383
|
+
return page;
|
|
384
|
+
}
|
|
385
|
+
async function resolveLiveLabel(backend, label, liveIds) {
|
|
386
|
+
const live = new Set(liveIds);
|
|
387
|
+
const [origins, leases] = await Promise.all([originIndex(backend), listLeases({ liveBackend: backend })]);
|
|
388
|
+
const knownLabels = new Set;
|
|
389
|
+
const matchIds = new Set;
|
|
390
|
+
for (const rec of origins.values()) {
|
|
391
|
+
if (rec.unreadable !== undefined || !live.has(rec.targetId))
|
|
392
|
+
continue;
|
|
393
|
+
knownLabels.add(rec.label);
|
|
394
|
+
if (rec.label === label)
|
|
395
|
+
matchIds.add(rec.targetId);
|
|
396
|
+
}
|
|
397
|
+
for (const rec of leases) {
|
|
398
|
+
if (rec.backend !== backend || rec.unreadable !== undefined || !live.has(rec.targetId))
|
|
399
|
+
continue;
|
|
400
|
+
knownLabels.add(rec.label);
|
|
401
|
+
if (rec.label === label)
|
|
402
|
+
matchIds.add(rec.targetId);
|
|
403
|
+
}
|
|
404
|
+
return { matchIds: [...matchIds], knownLabels: [...knownLabels].sort() };
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// src/workers.ts
|
|
408
|
+
var WORKER_SELECTOR_PREFIX = "worker:";
|
|
409
|
+
var WORKER_CAPABLE_TOOLS = [
|
|
410
|
+
"evaluate_script",
|
|
411
|
+
"list_network_requests",
|
|
412
|
+
"get_network_request",
|
|
413
|
+
"list_console_messages"
|
|
414
|
+
];
|
|
415
|
+
var WORKER_TARGET_TYPES = new Set(["service_worker", "shared_worker"]);
|
|
416
|
+
function isWorkerSelector(selector) {
|
|
417
|
+
return typeof selector === "string" && selector.startsWith(WORKER_SELECTOR_PREFIX);
|
|
418
|
+
}
|
|
419
|
+
function workerNeedle(selector) {
|
|
420
|
+
return selector.slice(WORKER_SELECTOR_PREFIX.length);
|
|
421
|
+
}
|
|
422
|
+
function isWorkerTargetType(type) {
|
|
423
|
+
return type !== undefined && WORKER_TARGET_TYPES.has(type);
|
|
424
|
+
}
|
|
425
|
+
function resolveWorkerTargets(targets, needle) {
|
|
426
|
+
const liveWorkers = targets.filter((t) => isWorkerTargetType(t.type));
|
|
427
|
+
return {
|
|
428
|
+
matches: needle === "" ? [] : liveWorkers.filter((t) => t.url.includes(needle)),
|
|
429
|
+
liveWorkers
|
|
430
|
+
};
|
|
431
|
+
}
|
|
432
|
+
var WORKER_EMPTY_NEEDLE_MESSAGE = "'worker:' needs a substring of the worker's url, e.g. worker:<extension-id> or worker:background.js";
|
|
433
|
+
var WORKER_SELECTOR_UNSUPPORTED_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, which this backend cannot address " + "(WebDriver BiDi has no extension service-worker target; Capability 'worker.targets' is Chrome-only). " + "Use --browser chrome to evaluate in, or record console/network from, an extension's background worker.";
|
|
434
|
+
var WORKER_SELECTOR_PAGE_ONLY_MESSAGE = "target 'worker:<substring>' selects a service/shared worker, and this tool is page-only " + "(closing, selecting or releasing a worker is meaningless). The tools that accept a worker: target are " + `${WORKER_CAPABLE_TOOLS.join(", ")}.`;
|
|
435
|
+
function workerWakeMisuseMessage(kind) {
|
|
436
|
+
if (kind === "not-a-worker-selector") {
|
|
437
|
+
return "'wake' only applies to a target of the form 'worker:<substring>'. A page is never asleep, and a bare " + "worker target id cannot be woken either: Chrome destroys the target when an MV3 worker is evicted and " + "mints a NEW id when it restarts, so only the url-substring form can name a worker that is not running.";
|
|
438
|
+
}
|
|
439
|
+
return "'wake' only applies when a capture is being started (reload:true, or includeBody with url). Reading an " + "existing buffer cannot use a woken worker: a restarted MV3 worker gets a NEW target id, and the buffer is " + "keyed by id, so the wake would hand back an empty capture. Re-run with reload:true to record a fresh one.";
|
|
440
|
+
}
|
|
441
|
+
function workerBufferReadMissMessage(needle, liveWorkers) {
|
|
442
|
+
const running = liveWorkers.length ? ` (running workers: ${urlList(liveWorkers)})` : " (no workers are running)";
|
|
443
|
+
return `no running worker whose url contains '${needle}'${running}. ` + "An MV3 extension service worker is idle-evicted after seconds of inactivity and then appears in NO target " + "listing, so this is usually a sleeping worker rather than a wrong substring. This call only READS a buffer " + "recorded earlier, and that buffer is keyed by the worker's target id, which Chrome re-mints on restart — so " + "re-run with reload:true, which starts the worker and records a fresh capture window.";
|
|
444
|
+
}
|
|
445
|
+
function urlList(workers) {
|
|
446
|
+
return workers.map((w) => w.url).join(", ");
|
|
447
|
+
}
|
|
448
|
+
function workerAmbiguityMessage(needle, matches) {
|
|
449
|
+
return `'worker:${needle}' matches more than one running worker: ` + matches.map((m) => `${m.url} (${m.id})`).join(", ") + ". Narrow the substring, or pass the worker's target id directly.";
|
|
450
|
+
}
|
|
451
|
+
function workerMissMessage(needle, opts) {
|
|
452
|
+
const running = opts.liveWorkers?.length ? ` (running workers: ${urlList(opts.liveWorkers)})` : " (no workers are running)";
|
|
453
|
+
if (!opts.wakeAttempted) {
|
|
454
|
+
return `no running worker whose url contains '${needle}'${running}. ` + "An MV3 extension service worker is idle-evicted after seconds of inactivity and then appears in NO target " + "listing, so this is usually a sleeping worker rather than a wrong substring. You passed wake:false; " + "wake:true (the default) asks Chrome to start it first.";
|
|
455
|
+
}
|
|
456
|
+
const scopes = opts.registeredScopes?.length ? ` Registered service-worker scopes in this browser: ${opts.registeredScopes.join(", ")}.` : " No service-worker registrations were reported by this browser at all.";
|
|
457
|
+
return `no worker whose url contains '${needle}' could be reached${running}. ` + `A start was requested (wake:true) and no matching worker appeared.${scopes} ` + "If the extension is installed but its worker never starts, check the worker script for a top-level throw.";
|
|
458
|
+
}
|
|
459
|
+
|
|
281
460
|
// src/client.ts
|
|
282
461
|
var BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
283
462
|
var DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
|
|
@@ -433,11 +612,13 @@ async function browserWsUrl() {
|
|
|
433
612
|
}
|
|
434
613
|
async function resolveTarget(selector, opts = {}) {
|
|
435
614
|
const targets = await listTargets();
|
|
436
|
-
const hit = pickTarget(targets, selector);
|
|
615
|
+
const hit = await pickTarget(targets, selector);
|
|
616
|
+
if (isWorkerTargetType(hit.type))
|
|
617
|
+
return hit;
|
|
437
618
|
await assertLeaseOk("chrome", hit.id, { lease: opts.lease, url: hit.url, title: hit.title });
|
|
438
619
|
return hit;
|
|
439
620
|
}
|
|
440
|
-
function pickTarget(targets, selector) {
|
|
621
|
+
async function pickTarget(targets, selector) {
|
|
441
622
|
const pages = targets.filter((t) => t.type === "page");
|
|
442
623
|
if (!selector || selector === "active") {
|
|
443
624
|
if (!pages.length)
|
|
@@ -464,6 +645,30 @@ function pickTarget(targets, selector) {
|
|
|
464
645
|
throw new CdpError(`no page title containing '${needle}'`);
|
|
465
646
|
return hit;
|
|
466
647
|
}
|
|
648
|
+
if (selector.startsWith("label:")) {
|
|
649
|
+
const label = selector.slice(6);
|
|
650
|
+
const { matchIds, knownLabels } = await resolveLiveLabel("chrome", label, pages.map((t) => t.id));
|
|
651
|
+
if (matchIds.length > 1) {
|
|
652
|
+
throw new CdpError(`label 'label:${label}' matches more than one live target: ${matchIds.join(", ")}. Resolve by target id instead.`);
|
|
653
|
+
}
|
|
654
|
+
if (matchIds.length === 1) {
|
|
655
|
+
const hit = pages.find((t) => t.id === matchIds[0]);
|
|
656
|
+
if (hit)
|
|
657
|
+
return hit;
|
|
658
|
+
}
|
|
659
|
+
throw new CdpError(knownLabels.length ? `no live target with label '${label}' (labels currently in use: ${knownLabels.join(", ")})` : `no live target with label '${label}' (no labels are currently assigned to any open target)`);
|
|
660
|
+
}
|
|
661
|
+
if (isWorkerSelector(selector)) {
|
|
662
|
+
const needle = workerNeedle(selector);
|
|
663
|
+
if (needle === "")
|
|
664
|
+
throw new CdpError(WORKER_EMPTY_NEEDLE_MESSAGE);
|
|
665
|
+
const { matches, liveWorkers } = resolveWorkerTargets(targets, needle);
|
|
666
|
+
if (matches.length > 1)
|
|
667
|
+
throw new CdpError(workerAmbiguityMessage(needle, matches));
|
|
668
|
+
if (matches.length === 1)
|
|
669
|
+
return matches[0];
|
|
670
|
+
throw new CdpError(workerMissMessage(needle, { wakeAttempted: false, liveWorkers }));
|
|
671
|
+
}
|
|
467
672
|
const byId = targets.find((t) => t.id === selector);
|
|
468
673
|
if (!byId)
|
|
469
674
|
throw new CdpError(`no target with id '${selector}'`);
|
|
@@ -653,6 +858,91 @@ async function resolveUid(conn, uid) {
|
|
|
653
858
|
return { objectId: object.objectId };
|
|
654
859
|
}
|
|
655
860
|
|
|
861
|
+
// src/cdp/workers.ts
|
|
862
|
+
var INVENTORY_FIRST_EVENT_MS = 1500;
|
|
863
|
+
var INVENTORY_SETTLE_MS = 250;
|
|
864
|
+
var WAKE_POLL_MS = 300;
|
|
865
|
+
var WAKE_POLL_ATTEMPTS = 30;
|
|
866
|
+
var NO_HOST_PAGE_MESSAGE = "cannot start a service worker: the ServiceWorker CDP domain is only reachable over a page session, " + "and this browser has no page target open. Open any tab (new_page) and retry — the tab does not have " + "to belong to the extension.";
|
|
867
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
868
|
+
async function requestWorkerStart(needle) {
|
|
869
|
+
const conn = await openBrowser();
|
|
870
|
+
try {
|
|
871
|
+
const host = (await listTargets()).find((t) => t.type === "page");
|
|
872
|
+
if (!host)
|
|
873
|
+
throw new CdpError(NO_HOST_PAGE_MESSAGE);
|
|
874
|
+
const { sessionId } = await conn.send("Target.attachToTarget", {
|
|
875
|
+
targetId: host.id,
|
|
876
|
+
flatten: true
|
|
877
|
+
});
|
|
878
|
+
if (!sessionId)
|
|
879
|
+
throw new CdpError("could not attach a session to host the ServiceWorker domain");
|
|
880
|
+
const scopeById = new Map;
|
|
881
|
+
const versions = [];
|
|
882
|
+
conn.on("ServiceWorker.workerRegistrationUpdated", (params) => {
|
|
883
|
+
for (const r of params.registrations ?? []) {
|
|
884
|
+
if (r.isDeleted === true)
|
|
885
|
+
continue;
|
|
886
|
+
scopeById.set(String(r.registrationId), String(r.scopeURL ?? ""));
|
|
887
|
+
}
|
|
888
|
+
});
|
|
889
|
+
conn.on("ServiceWorker.workerVersionUpdated", (params) => {
|
|
890
|
+
for (const v of params.versions ?? []) {
|
|
891
|
+
versions.push({ registrationId: String(v.registrationId), scriptURL: String(v.scriptURL ?? "") });
|
|
892
|
+
}
|
|
893
|
+
});
|
|
894
|
+
await conn.send("ServiceWorker.enable", {}, { sessionId });
|
|
895
|
+
await conn.waitFor("ServiceWorker.workerVersionUpdated", undefined, INVENTORY_FIRST_EVENT_MS).catch(() => {
|
|
896
|
+
return;
|
|
897
|
+
});
|
|
898
|
+
await sleep(INVENTORY_SETTLE_MS);
|
|
899
|
+
const scopes = [...new Set(scopeById.values())].filter((s) => s.length > 0).sort();
|
|
900
|
+
const matched = new Set;
|
|
901
|
+
for (const v of versions) {
|
|
902
|
+
const scope = scopeById.get(v.registrationId);
|
|
903
|
+
if (scope === undefined)
|
|
904
|
+
continue;
|
|
905
|
+
if (v.scriptURL.includes(needle) || scope.includes(needle))
|
|
906
|
+
matched.add(scope);
|
|
907
|
+
}
|
|
908
|
+
if (matched.size === 0)
|
|
909
|
+
return { startRequested: false, scopes };
|
|
910
|
+
if (matched.size > 1) {
|
|
911
|
+
throw new CdpError(`'worker:${needle}' matches more than one registered service-worker scope: ${[...matched].sort().join(", ")}. ` + "Narrow the substring.");
|
|
912
|
+
}
|
|
913
|
+
await conn.send("ServiceWorker.startWorker", { scopeURL: [...matched][0] }, { sessionId });
|
|
914
|
+
return { startRequested: true, scopes };
|
|
915
|
+
} finally {
|
|
916
|
+
conn.close();
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
async function resolveWorkerSelector(selector, opts) {
|
|
920
|
+
const needle = workerNeedle(selector);
|
|
921
|
+
if (needle === "")
|
|
922
|
+
throw new CdpError(WORKER_EMPTY_NEEDLE_MESSAGE);
|
|
923
|
+
const first = resolveWorkerTargets(await listTargets(), needle);
|
|
924
|
+
if (first.matches.length > 1)
|
|
925
|
+
throw new CdpError(workerAmbiguityMessage(needle, first.matches));
|
|
926
|
+
if (first.matches.length === 1)
|
|
927
|
+
return first.matches[0];
|
|
928
|
+
if (!opts.wake) {
|
|
929
|
+
throw new CdpError(workerMissMessage(needle, { wakeAttempted: false, liveWorkers: first.liveWorkers }));
|
|
930
|
+
}
|
|
931
|
+
const { startRequested, scopes } = await requestWorkerStart(needle);
|
|
932
|
+
if (startRequested) {
|
|
933
|
+
for (let i = 0;i < WAKE_POLL_ATTEMPTS; i++) {
|
|
934
|
+
await sleep(WAKE_POLL_MS);
|
|
935
|
+
const again = resolveWorkerTargets(await listTargets(), needle);
|
|
936
|
+
if (again.matches.length > 1)
|
|
937
|
+
throw new CdpError(workerAmbiguityMessage(needle, again.matches));
|
|
938
|
+
if (again.matches.length === 1)
|
|
939
|
+
return again.matches[0];
|
|
940
|
+
}
|
|
941
|
+
}
|
|
942
|
+
const final = resolveWorkerTargets(await listTargets(), needle);
|
|
943
|
+
throw new CdpError(workerMissMessage(needle, { wakeAttempted: true, liveWorkers: final.liveWorkers, registeredScopes: scopes }));
|
|
944
|
+
}
|
|
945
|
+
|
|
656
946
|
// src/tools/network_mock.ts
|
|
657
947
|
function escapeRegExpChar(ch) {
|
|
658
948
|
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
@@ -1134,7 +1424,8 @@ var CDP_CAPABILITIES = new Set([
|
|
|
1134
1424
|
"input.raw",
|
|
1135
1425
|
"input.html5Drag",
|
|
1136
1426
|
"browser.downloads",
|
|
1137
|
-
"browser.permissions"
|
|
1427
|
+
"browser.permissions",
|
|
1428
|
+
"worker.targets"
|
|
1138
1429
|
]);
|
|
1139
1430
|
|
|
1140
1431
|
class CdpPageDriver {
|
|
@@ -1702,6 +1993,10 @@ class CdpBrowserDriver {
|
|
|
1702
1993
|
await withBrowserConn((conn) => conn.send("Target.activateTarget", { targetId: target.id }));
|
|
1703
1994
|
return { id: target.id, url: target.url, title: target.title };
|
|
1704
1995
|
}
|
|
1996
|
+
async resolveWorkerTarget(selector, opts) {
|
|
1997
|
+
const target = await resolveWorkerSelector(selector, opts).catch(noSuchTarget);
|
|
1998
|
+
return { id: target.id, url: target.url, title: target.title, type: target.type };
|
|
1999
|
+
}
|
|
1705
2000
|
async page(selector) {
|
|
1706
2001
|
const { conn, target } = await openPage(selector).catch(noSuchTarget);
|
|
1707
2002
|
return new CdpPageDriver(conn, target, this);
|
|
@@ -1788,111 +2083,6 @@ function createCdpDriver() {
|
|
|
1788
2083
|
return new CdpBrowserDriver;
|
|
1789
2084
|
}
|
|
1790
2085
|
|
|
1791
|
-
// src/origins.ts
|
|
1792
|
-
import { mkdir as mkdir2, readFile as readFile2, readdir as readdir2, unlink as unlink2, writeFile as writeFile2 } from "node:fs/promises";
|
|
1793
|
-
import { join as join2 } from "node:path";
|
|
1794
|
-
function safeId2(id) {
|
|
1795
|
-
return id.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1796
|
-
}
|
|
1797
|
-
function originFile(backend, targetId) {
|
|
1798
|
-
return join2(leaseDir(), `origin-${backend}-${safeId2(targetId)}.json`);
|
|
1799
|
-
}
|
|
1800
|
-
var ORIGIN_FILE_RE = /^origin-(chrome|firefox)-(.+)\.json$/;
|
|
1801
|
-
async function recordOrigin(backend, targetId, opts = {}) {
|
|
1802
|
-
const record = {
|
|
1803
|
-
backend,
|
|
1804
|
-
targetId,
|
|
1805
|
-
label: typeof opts.label === "string" && opts.label.length ? opts.label : defaultLabel(),
|
|
1806
|
-
pid: process.pid,
|
|
1807
|
-
createdAt: opts.now ?? Date.now()
|
|
1808
|
-
};
|
|
1809
|
-
const file = originFile(backend, targetId);
|
|
1810
|
-
try {
|
|
1811
|
-
await mkdir2(leaseDir(), { recursive: true });
|
|
1812
|
-
try {
|
|
1813
|
-
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1814
|
-
return;
|
|
1815
|
-
} catch (err) {
|
|
1816
|
-
if (err.code !== "EEXIST")
|
|
1817
|
-
throw err;
|
|
1818
|
-
}
|
|
1819
|
-
await unlink2(file).catch(() => {
|
|
1820
|
-
return;
|
|
1821
|
-
});
|
|
1822
|
-
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1823
|
-
} catch {}
|
|
1824
|
-
}
|
|
1825
|
-
async function readOrigin(backend, targetId) {
|
|
1826
|
-
let raw;
|
|
1827
|
-
try {
|
|
1828
|
-
raw = await readFile2(originFile(backend, targetId), "utf8");
|
|
1829
|
-
} catch {
|
|
1830
|
-
return;
|
|
1831
|
-
}
|
|
1832
|
-
try {
|
|
1833
|
-
const rec = JSON.parse(raw);
|
|
1834
|
-
if (typeof rec?.targetId === "string" && typeof rec?.label === "string" && typeof rec?.createdAt === "number")
|
|
1835
|
-
return rec;
|
|
1836
|
-
} catch {}
|
|
1837
|
-
return;
|
|
1838
|
-
}
|
|
1839
|
-
async function listOrigins(opts = {}) {
|
|
1840
|
-
let names = [];
|
|
1841
|
-
try {
|
|
1842
|
-
names = await readdir2(leaseDir());
|
|
1843
|
-
} catch {
|
|
1844
|
-
return [];
|
|
1845
|
-
}
|
|
1846
|
-
const live = opts.liveIds === undefined ? undefined : new Set(opts.liveIds.map((id) => safeId2(id)));
|
|
1847
|
-
const out = [];
|
|
1848
|
-
for (const name of names) {
|
|
1849
|
-
const parts = ORIGIN_FILE_RE.exec(name);
|
|
1850
|
-
if (!parts)
|
|
1851
|
-
continue;
|
|
1852
|
-
const backend = parts[1];
|
|
1853
|
-
const nameId = parts[2];
|
|
1854
|
-
const scoped = live !== undefined && (opts.liveBackend === undefined || opts.liveBackend === backend);
|
|
1855
|
-
if (scoped && !live.has(nameId)) {
|
|
1856
|
-
await unlink2(join2(leaseDir(), name)).catch(() => {
|
|
1857
|
-
return;
|
|
1858
|
-
});
|
|
1859
|
-
continue;
|
|
1860
|
-
}
|
|
1861
|
-
const fromName = { backend, targetId: nameId, label: "", pid: 0, createdAt: 0 };
|
|
1862
|
-
let rec;
|
|
1863
|
-
try {
|
|
1864
|
-
rec = JSON.parse(await readFile2(join2(leaseDir(), name), "utf8"));
|
|
1865
|
-
} catch (err) {
|
|
1866
|
-
const code = err.code;
|
|
1867
|
-
if (code === "ENOENT")
|
|
1868
|
-
continue;
|
|
1869
|
-
out.push({ ...fromName, unreadable: code ?? "unparseable" });
|
|
1870
|
-
continue;
|
|
1871
|
-
}
|
|
1872
|
-
if (typeof rec?.targetId !== "string" || typeof rec?.label !== "string" || typeof rec?.createdAt !== "number") {
|
|
1873
|
-
out.push({ ...fromName, unreadable: "unparseable" });
|
|
1874
|
-
continue;
|
|
1875
|
-
}
|
|
1876
|
-
out.push({ backend: rec.backend, targetId: rec.targetId, label: rec.label, pid: rec.pid, createdAt: rec.createdAt });
|
|
1877
|
-
}
|
|
1878
|
-
return out;
|
|
1879
|
-
}
|
|
1880
|
-
async function originIndex(backend, liveIds) {
|
|
1881
|
-
const rows = await listOrigins({ liveIds, liveBackend: backend }).catch(() => []);
|
|
1882
|
-
const index = new Map;
|
|
1883
|
-
for (const row of rows) {
|
|
1884
|
-
if (row.backend !== backend)
|
|
1885
|
-
continue;
|
|
1886
|
-
index.set(row.targetId, row);
|
|
1887
|
-
}
|
|
1888
|
-
return index;
|
|
1889
|
-
}
|
|
1890
|
-
async function newTrackedPage(driver, backend, opts = {}) {
|
|
1891
|
-
const page = await driver.newPage(opts.url);
|
|
1892
|
-
await recordOrigin(backend, page.id, { label: opts.label });
|
|
1893
|
-
return page;
|
|
1894
|
-
}
|
|
1895
|
-
|
|
1896
2086
|
// src/reap.ts
|
|
1897
2087
|
function staleAgentTabs(input) {
|
|
1898
2088
|
const live = new Set(input.livePageIds);
|
|
@@ -1993,6 +2183,11 @@ async function pickPage(driver, pages, selector) {
|
|
|
1993
2183
|
throw new SharedToolError(`no page title containing '${needle}'`);
|
|
1994
2184
|
return p;
|
|
1995
2185
|
}
|
|
2186
|
+
if (selector.startsWith("label:"))
|
|
2187
|
+
return pickByLabel(driver, pages, selector.slice(6));
|
|
2188
|
+
if (isWorkerSelector(selector)) {
|
|
2189
|
+
throw new SharedToolError(backendOf(driver) === "firefox" ? WORKER_SELECTOR_UNSUPPORTED_MESSAGE : WORKER_SELECTOR_PAGE_ONLY_MESSAGE);
|
|
2190
|
+
}
|
|
1996
2191
|
const exactPage = pages.find((x) => x.id === selector);
|
|
1997
2192
|
if (exactPage)
|
|
1998
2193
|
return exactPage;
|
|
@@ -2002,6 +2197,18 @@ async function pickPage(driver, pages, selector) {
|
|
|
2002
2197
|
return exact;
|
|
2003
2198
|
throw new SharedToolError(`no target with id '${selector}'`);
|
|
2004
2199
|
}
|
|
2200
|
+
async function pickByLabel(driver, pages, label) {
|
|
2201
|
+
const { matchIds, knownLabels } = await resolveLiveLabel(backendOf(driver), label, pages.map((x) => x.id));
|
|
2202
|
+
if (matchIds.length > 1) {
|
|
2203
|
+
throw new SharedToolError(`label 'label:${label}' matches more than one live target: ${matchIds.join(", ")}. Resolve by target id instead.`);
|
|
2204
|
+
}
|
|
2205
|
+
if (matchIds.length === 1) {
|
|
2206
|
+
const p = pages.find((x) => x.id === matchIds[0]);
|
|
2207
|
+
if (p)
|
|
2208
|
+
return p;
|
|
2209
|
+
}
|
|
2210
|
+
throw new SharedToolError(knownLabels.length ? `no live target with label '${label}' (labels currently in use: ${knownLabels.join(", ")})` : `no live target with label '${label}' (no labels are currently assigned to any open target)`);
|
|
2211
|
+
}
|
|
2005
2212
|
function locatorOf(args) {
|
|
2006
2213
|
const hasUid = args.uid !== undefined && args.uid !== null && args.uid !== "";
|
|
2007
2214
|
const hasSelector = typeof args.selector === "string" && args.selector.length > 0;
|
|
@@ -2143,11 +2350,29 @@ async function writeJsonSink(savePath, value) {
|
|
|
2143
2350
|
function sinkRequested(savePath) {
|
|
2144
2351
|
return savePath !== undefined && savePath !== "";
|
|
2145
2352
|
}
|
|
2353
|
+
var EXPRESSION_ALIAS_KEYS = ["function", "code", "js", "script", "fn", "body"];
|
|
2354
|
+
async function resolveWorkerSelectorFor(driver, selector, wake) {
|
|
2355
|
+
if (!driver.capabilities.has("worker.targets") || typeof driver.resolveWorkerTarget !== "function") {
|
|
2356
|
+
throw new SharedToolError(`evaluateScript: ${WORKER_SELECTOR_UNSUPPORTED_MESSAGE}`);
|
|
2357
|
+
}
|
|
2358
|
+
return driver.resolveWorkerTarget(selector, { wake });
|
|
2359
|
+
}
|
|
2146
2360
|
async function evaluateScript(driver, args) {
|
|
2147
2361
|
if (typeof args.expression !== "string" || args.expression.length === 0) {
|
|
2362
|
+
const wrongKey = EXPRESSION_ALIAS_KEYS.find((k) => args[k] !== undefined);
|
|
2363
|
+
if (wrongKey !== undefined) {
|
|
2364
|
+
throw new SharedToolError(`evaluateScript: you passed '${wrongKey}'; the key is 'expression' — function literals go in 'expression', invoked with 'args'.`);
|
|
2365
|
+
}
|
|
2148
2366
|
throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
|
|
2149
2367
|
}
|
|
2150
|
-
|
|
2368
|
+
if (args.wake !== undefined && typeof args.wake !== "boolean") {
|
|
2369
|
+
throw new SharedToolError("evaluateScript: 'wake' must be a boolean");
|
|
2370
|
+
}
|
|
2371
|
+
if (args.wake !== undefined && !isWorkerSelector(args.target)) {
|
|
2372
|
+
throw new SharedToolError("evaluateScript: 'wake' only applies to a target of the form 'worker:<substring>'; a page target is never asleep.");
|
|
2373
|
+
}
|
|
2374
|
+
const target = isWorkerSelector(args.target) ? (await resolveWorkerSelectorFor(driver, args.target, args.wake ?? true)).id : args.target;
|
|
2375
|
+
return withPage2(driver, target, async (page) => {
|
|
2151
2376
|
const value = await page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true });
|
|
2152
2377
|
if (!sinkRequested(args.savePath))
|
|
2153
2378
|
return value;
|
|
@@ -2511,7 +2736,7 @@ async function claimPage(driver, args = {}) {
|
|
|
2511
2736
|
const hasTarget = typeof args.target === "string" && args.target.length > 0;
|
|
2512
2737
|
const hasTargetId = typeof args.targetId === "string" && args.targetId.length > 0;
|
|
2513
2738
|
if (hasTarget && hasTargetId) {
|
|
2514
|
-
throw new LeaseToolError("claim_page takes at most one of 'target' (any selector: active | index:N | url:<substr> | title:<substr> | <targetId>) or 'targetId' (an exact target id, kept for back-compat). Pass 'target' alone to claim an open tab, or neither to open a fresh one.");
|
|
2739
|
+
throw new LeaseToolError("claim_page takes at most one of 'target' (any selector: active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>) or 'targetId' (an exact target id, kept for back-compat). Pass 'target' alone to claim an open tab, or neither to open a fresh one.");
|
|
2515
2740
|
}
|
|
2516
2741
|
const backend = backendOf2(driver);
|
|
2517
2742
|
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
@@ -2631,6 +2856,31 @@ function captureFile(targetId, captureId) {
|
|
|
2631
2856
|
function delay2(ms) {
|
|
2632
2857
|
return new Promise((r) => setTimeout(r, ms));
|
|
2633
2858
|
}
|
|
2859
|
+
async function resolveRecorderTarget(target, opts) {
|
|
2860
|
+
if (!isWorkerSelector(target))
|
|
2861
|
+
return resolveTarget(target);
|
|
2862
|
+
if (opts.capture)
|
|
2863
|
+
return resolveWorkerSelector(target, { wake: opts.wake ?? true });
|
|
2864
|
+
const needle = workerNeedle(target);
|
|
2865
|
+
if (needle === "")
|
|
2866
|
+
throw new CdpError(WORKER_EMPTY_NEEDLE_MESSAGE);
|
|
2867
|
+
const { matches, liveWorkers } = resolveWorkerTargets(await listTargets(), needle);
|
|
2868
|
+
if (matches.length > 1)
|
|
2869
|
+
throw new CdpError(workerAmbiguityMessage(needle, matches));
|
|
2870
|
+
if (matches.length === 1)
|
|
2871
|
+
return matches[0];
|
|
2872
|
+
throw new CdpError(workerBufferReadMissMessage(needle, liveWorkers));
|
|
2873
|
+
}
|
|
2874
|
+
function assertWakeApplies(target, wake, capture) {
|
|
2875
|
+
if (wake === undefined)
|
|
2876
|
+
return;
|
|
2877
|
+
if (typeof wake !== "boolean")
|
|
2878
|
+
throw new CdpError("'wake' must be a boolean");
|
|
2879
|
+
if (!isWorkerSelector(target))
|
|
2880
|
+
throw new CdpError(workerWakeMisuseMessage("not-a-worker-selector"));
|
|
2881
|
+
if (!capture)
|
|
2882
|
+
throw new CdpError(workerWakeMisuseMessage("read-only-call"));
|
|
2883
|
+
}
|
|
2634
2884
|
var NETWORK_EVENTS = [
|
|
2635
2885
|
"Network.requestWillBeSent",
|
|
2636
2886
|
"Network.responseReceived",
|
|
@@ -2696,7 +2946,8 @@ async function startRecorder(target, opts = {}) {
|
|
|
2696
2946
|
};
|
|
2697
2947
|
}
|
|
2698
2948
|
async function captureWindow(target, durationMs, opts = {}) {
|
|
2699
|
-
const resolved = await
|
|
2949
|
+
const resolved = await resolveRecorderTarget(target, { capture: true, wake: opts.wake });
|
|
2950
|
+
const isWorker = isWorkerTargetType(resolved.type);
|
|
2700
2951
|
const captureId = `${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
2701
2952
|
const file = captureFile(resolved.id, captureId);
|
|
2702
2953
|
const rec = await startRecorder(resolved.id, {
|
|
@@ -2706,13 +2957,16 @@ async function captureWindow(target, durationMs, opts = {}) {
|
|
|
2706
2957
|
file,
|
|
2707
2958
|
timeoutMs: opts.timeoutMs
|
|
2708
2959
|
});
|
|
2709
|
-
|
|
2710
|
-
|
|
2960
|
+
if (!isWorker) {
|
|
2961
|
+
await rec.conn.send("Page.enable");
|
|
2962
|
+
await rec.conn.send("Page.reload", { ignoreCache: false });
|
|
2963
|
+
}
|
|
2711
2964
|
await delay2(durationMs);
|
|
2712
2965
|
return {
|
|
2713
2966
|
file: rec.file,
|
|
2714
2967
|
conn: rec.conn,
|
|
2715
2968
|
resolved: { id: rec.target.id, url: rec.target.url, title: rec.target.title },
|
|
2969
|
+
mode: isWorker ? "listen" : "reload",
|
|
2716
2970
|
droppedWrites: rec.droppedWrites,
|
|
2717
2971
|
flush: () => rec.flush(),
|
|
2718
2972
|
async stop() {
|
|
@@ -2812,8 +3066,9 @@ async function readConsoleEntries(file) {
|
|
|
2812
3066
|
return out;
|
|
2813
3067
|
}
|
|
2814
3068
|
async function listConsoleMessages(args = {}) {
|
|
3069
|
+
assertWakeApplies(args.target, args.wake, args.reload === true);
|
|
2815
3070
|
if (args.reload) {
|
|
2816
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS);
|
|
3071
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS, { wake: args.wake });
|
|
2817
3072
|
await cap.stop();
|
|
2818
3073
|
const messages2 = await readConsoleEntries(cap.file);
|
|
2819
3074
|
return {
|
|
@@ -2823,7 +3078,7 @@ async function listConsoleMessages(args = {}) {
|
|
|
2823
3078
|
droppedWrites: cap.droppedWrites()
|
|
2824
3079
|
};
|
|
2825
3080
|
}
|
|
2826
|
-
const target = await
|
|
3081
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
2827
3082
|
const messages = await readConsoleEntries(recFile(target.id));
|
|
2828
3083
|
return {
|
|
2829
3084
|
target: { id: target.id, url: target.url, title: target.title },
|
|
@@ -2832,7 +3087,7 @@ async function listConsoleMessages(args = {}) {
|
|
|
2832
3087
|
};
|
|
2833
3088
|
}
|
|
2834
3089
|
async function getConsoleMessage(args = {}) {
|
|
2835
|
-
const target = await
|
|
3090
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
2836
3091
|
const messages = await readConsoleEntries(recFile(target.id));
|
|
2837
3092
|
const index = args.index ?? 0;
|
|
2838
3093
|
const entry = messages[index];
|
|
@@ -2930,14 +3185,15 @@ async function listNetworkRequests(args = {}) {
|
|
|
2930
3185
|
let file;
|
|
2931
3186
|
let resolved;
|
|
2932
3187
|
let droppedWrites;
|
|
3188
|
+
assertWakeApplies(args.target, args.wake, args.reload === true);
|
|
2933
3189
|
if (args.reload) {
|
|
2934
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
|
|
3190
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
|
|
2935
3191
|
await cap.stop();
|
|
2936
3192
|
file = cap.file;
|
|
2937
3193
|
resolved = cap.resolved;
|
|
2938
3194
|
droppedWrites = cap.droppedWrites();
|
|
2939
3195
|
} else {
|
|
2940
|
-
const target = await
|
|
3196
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
2941
3197
|
file = recFile(target.id);
|
|
2942
3198
|
resolved = { id: target.id, url: target.url, title: target.title };
|
|
2943
3199
|
}
|
|
@@ -2958,8 +3214,9 @@ async function getNetworkRequest(args = {}) {
|
|
|
2958
3214
|
return requests.find((r) => r.url.includes(args.url));
|
|
2959
3215
|
};
|
|
2960
3216
|
const describeSelector = () => args.requestId ? `requestId ${args.requestId}` : `url ~ '${args.url}'`;
|
|
3217
|
+
assertWakeApplies(args.target, args.wake, Boolean(args.includeBody && args.url));
|
|
2961
3218
|
if (args.includeBody && args.url) {
|
|
2962
|
-
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2);
|
|
3219
|
+
const cap = await captureWindow(args.target, args.durationMs ?? DEFAULT_CAPTURE_MS2, { wake: args.wake });
|
|
2963
3220
|
try {
|
|
2964
3221
|
await cap.flush();
|
|
2965
3222
|
const match2 = (await readNetworkRequests(cap.file)).find((r) => r.url.includes(args.url));
|
|
@@ -2976,7 +3233,7 @@ async function getNetworkRequest(args = {}) {
|
|
|
2976
3233
|
await cap.stop();
|
|
2977
3234
|
}
|
|
2978
3235
|
}
|
|
2979
|
-
const target = await
|
|
3236
|
+
const target = await resolveRecorderTarget(args.target, { capture: false });
|
|
2980
3237
|
const match = pick(await readNetworkRequests(recFile(target.id)));
|
|
2981
3238
|
if (!match) {
|
|
2982
3239
|
throw new CdpError(`no network request matched ${describeSelector()}; run list_network_requests with reload:true first` + (args.includeBody ? `, then fetch the body with the url selector (reload re-mints requestIds)` : ``));
|
|
@@ -4906,6 +5163,22 @@ async function pickContext(conn, contexts, selector) {
|
|
|
4906
5163
|
return c;
|
|
4907
5164
|
throw driverError2("no-such-target", `no context with title containing '${needle}'`);
|
|
4908
5165
|
}
|
|
5166
|
+
if (selector.startsWith("label:")) {
|
|
5167
|
+
const label = selector.slice(6);
|
|
5168
|
+
const { matchIds, knownLabels } = await resolveLiveLabel("firefox", label, contexts.map((c) => c.context));
|
|
5169
|
+
if (matchIds.length > 1) {
|
|
5170
|
+
throw driverError2("no-such-target", `label 'label:${label}' matches more than one live target: ${matchIds.join(", ")}. Resolve by target id instead.`);
|
|
5171
|
+
}
|
|
5172
|
+
if (matchIds.length === 1) {
|
|
5173
|
+
const hit = contexts.find((c) => c.context === matchIds[0]);
|
|
5174
|
+
if (hit)
|
|
5175
|
+
return hit;
|
|
5176
|
+
}
|
|
5177
|
+
throw driverError2("no-such-target", knownLabels.length ? `no live target with label '${label}' (labels currently in use: ${knownLabels.join(", ")})` : `no live target with label '${label}' (no labels are currently assigned to any open target)`);
|
|
5178
|
+
}
|
|
5179
|
+
if (isWorkerSelector(selector)) {
|
|
5180
|
+
throw driverError2("unsupported", WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
5181
|
+
}
|
|
4909
5182
|
const exact = contexts.find((c) => c.context === selector);
|
|
4910
5183
|
if (exact)
|
|
4911
5184
|
return exact;
|
|
@@ -5768,6 +6041,11 @@ async function withPage3(driver, target, fn) {
|
|
|
5768
6041
|
}
|
|
5769
6042
|
var consoleBuffers = new Map;
|
|
5770
6043
|
var networkBuffers = new Map;
|
|
6044
|
+
function refuseWorkerSelector(driver, target) {
|
|
6045
|
+
if (isWorkerSelector(target) && !driver.capabilities.has("worker.targets")) {
|
|
6046
|
+
throw new BidiToolError(WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
6047
|
+
}
|
|
6048
|
+
}
|
|
5771
6049
|
function bufferFor(map, id) {
|
|
5772
6050
|
let b = map.get(id);
|
|
5773
6051
|
if (!b) {
|
|
@@ -5796,6 +6074,7 @@ async function captureFresh(page, durationMs) {
|
|
|
5796
6074
|
}
|
|
5797
6075
|
}
|
|
5798
6076
|
async function listConsoleMessages2(driver, args = {}) {
|
|
6077
|
+
refuseWorkerSelector(driver, args.target);
|
|
5799
6078
|
return withPage3(driver, args.target, async (page) => {
|
|
5800
6079
|
if (args.reload)
|
|
5801
6080
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
@@ -5804,6 +6083,7 @@ async function listConsoleMessages2(driver, args = {}) {
|
|
|
5804
6083
|
});
|
|
5805
6084
|
}
|
|
5806
6085
|
async function getConsoleMessage2(driver, args = {}) {
|
|
6086
|
+
refuseWorkerSelector(driver, args.target);
|
|
5807
6087
|
return withPage3(driver, args.target, async (page) => {
|
|
5808
6088
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
5809
6089
|
const idx = args.index ?? 0;
|
|
@@ -5814,6 +6094,7 @@ async function getConsoleMessage2(driver, args = {}) {
|
|
|
5814
6094
|
});
|
|
5815
6095
|
}
|
|
5816
6096
|
async function listNetworkRequests2(driver, args = {}) {
|
|
6097
|
+
refuseWorkerSelector(driver, args.target);
|
|
5817
6098
|
return withPage3(driver, args.target, async (page) => {
|
|
5818
6099
|
if (args.reload)
|
|
5819
6100
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
@@ -5824,6 +6105,7 @@ async function listNetworkRequests2(driver, args = {}) {
|
|
|
5824
6105
|
});
|
|
5825
6106
|
}
|
|
5826
6107
|
async function getNetworkRequest2(driver, args = {}) {
|
|
6108
|
+
refuseWorkerSelector(driver, args.target);
|
|
5827
6109
|
if (!args.requestId && !args.url)
|
|
5828
6110
|
throw new BidiToolError("get_network_request requires requestId or url");
|
|
5829
6111
|
return withPage3(driver, args.target, async (page) => {
|
|
@@ -5935,7 +6217,11 @@ Backend selection: --browser chrome|firefox, else the CDP_BROWSER env var, else
|
|
|
5935
6217
|
zero behavior change for existing users). Firefox is LAUNCHED per invocation (it cannot be attached
|
|
5936
6218
|
to), used for exactly one tool call, then disposed before the process exits.
|
|
5937
6219
|
|
|
5938
|
-
Target selector grammar: active | index:N | url:<substr> | title:<substr> | <targetId>
|
|
6220
|
+
Target selector grammar: active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>
|
|
6221
|
+
plus worker:<substr> (Chrome only) for evaluate_script, list_network_requests, get_network_request
|
|
6222
|
+
and list_console_messages ONLY: an MV3 extension's background service worker, e.g.
|
|
6223
|
+
worker:<extension-id>. An idle-evicted worker is started first unless wake:false. Recording a
|
|
6224
|
+
worker keeps it alive for the length of the capture.
|
|
5939
6225
|
Leases: pass --lease <token> to act on a tab another agent claimed with claim_page (MCP only).
|
|
5940
6226
|
Run list_leases to see who holds what; release_page --lease <token> gives a lease back.
|
|
5941
6227
|
Run with --list to print every tool name available under the selected backend.
|