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/mcp.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)` : ``));
|
|
@@ -4157,7 +4414,7 @@ var MANIFEST = [
|
|
|
4157
4414
|
},
|
|
4158
4415
|
target: {
|
|
4159
4416
|
type: "string",
|
|
4160
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4417
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4161
4418
|
}
|
|
4162
4419
|
},
|
|
4163
4420
|
required: [
|
|
@@ -4178,7 +4435,7 @@ var MANIFEST = [
|
|
|
4178
4435
|
},
|
|
4179
4436
|
target: {
|
|
4180
4437
|
type: "string",
|
|
4181
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4438
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4182
4439
|
}
|
|
4183
4440
|
},
|
|
4184
4441
|
required: [
|
|
@@ -4199,7 +4456,7 @@ var MANIFEST = [
|
|
|
4199
4456
|
},
|
|
4200
4457
|
target: {
|
|
4201
4458
|
type: "string",
|
|
4202
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4459
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4203
4460
|
},
|
|
4204
4461
|
url: {
|
|
4205
4462
|
type: "string",
|
|
@@ -4249,7 +4506,7 @@ var MANIFEST = [
|
|
|
4249
4506
|
},
|
|
4250
4507
|
target: {
|
|
4251
4508
|
type: "string",
|
|
4252
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4509
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4253
4510
|
},
|
|
4254
4511
|
text: {
|
|
4255
4512
|
type: "string",
|
|
@@ -4272,7 +4529,7 @@ var MANIFEST = [
|
|
|
4272
4529
|
},
|
|
4273
4530
|
{
|
|
4274
4531
|
name: "evaluate_script",
|
|
4275
|
-
description: "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string. Pass 'savePath' to write the evaluated value to a JSON file instead: the response then carries only {path,bytes,type,target} and the value itself never appears in it, in any form. That is the way to read a credential (a JWT or session token out of localStorage, for example) without putting the secret into the caller's transcript.",
|
|
4532
|
+
description: "Run arbitrary JavaScript in the target page's main-world context over raw CDP and return the evaluated value (returnByValue). With no 'args' the 'expression' is evaluated as a raw expression; when 'args' is provided 'expression' must be a function literal (arrow or classic) invoked on globalThis with the args passed positionally. A thrown exception surfaces as an error; non-serializable returns (DOM nodes, functions) come back as their CDP description string. Pass 'savePath' to write the evaluated value to a JSON file instead: the response then carries only {path,bytes,type,target} and the value itself never appears in it, in any form. That is the way to read a credential (a JWT or session token out of localStorage, for example) without putting the secret into the caller's transcript. This is the one tool that can EVALUATE inside an MV3 extension's background SERVICE WORKER rather than a page (list_network_requests/get_network_request/list_console_messages OBSERVE one): pass target 'worker:<extension-id>' (Chrome only) and read chrome.storage.local, chrome.runtime and the worker's own globals directly, with an idle-evicted worker started for you first (see 'wake').",
|
|
4276
4533
|
inputSchema: {
|
|
4277
4534
|
type: "object",
|
|
4278
4535
|
properties: {
|
|
@@ -4282,11 +4539,15 @@ var MANIFEST = [
|
|
|
4282
4539
|
},
|
|
4283
4540
|
target: {
|
|
4284
4541
|
type: "string",
|
|
4285
|
-
description: "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring."
|
|
4542
|
+
description: "Page selector: 'active' (or omit) -> first page-type target | '<32-hex targetId>' -> exact target by id | 'index:N' -> Nth page-type target (0-based) | 'url:<substring>' -> first page whose url contains substring | 'title:<substring>' -> first page whose title contains substring | 'label:<name>' -> the page-type target with exactly this label (checked against the origin ledger and live leases) | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js) and read chrome.storage.local from it. The worker: arm is CHROME-ONLY (Capability 'worker.targets') and is accepted by four tools only — evaluate_script, list_network_requests, get_network_request and list_console_messages — never as universal grammar; an MV3 worker that has been idle-evicted is started automatically first, see 'wake'."
|
|
4543
|
+
},
|
|
4544
|
+
wake: {
|
|
4545
|
+
type: "boolean",
|
|
4546
|
+
description: "Only valid with a 'worker:<substring>' target, and rejected on a page target rather than ignored. Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so a wake asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything; the error then says the worker may simply be asleep."
|
|
4286
4547
|
},
|
|
4287
4548
|
expression: {
|
|
4288
4549
|
type: "string",
|
|
4289
|
-
description: "JavaScript to run. Evaluated as an expression when 'args' is omitted; must be a function literal (arrow or classic) whose parameters receive 'args' when 'args' is provided."
|
|
4550
|
+
description: "JavaScript to run. Evaluated as an expression when 'args' is omitted; must be a function literal (arrow or classic) whose parameters receive 'args' when 'args' is provided. REQUIRED — the code always goes in this key, never in 'function', 'code', 'js', 'script', 'fn' or 'body'; a call using one of those instead is rejected with an error naming the wrong key."
|
|
4290
4551
|
},
|
|
4291
4552
|
awaitPromise: {
|
|
4292
4553
|
type: "boolean",
|
|
@@ -4319,7 +4580,7 @@ var MANIFEST = [
|
|
|
4319
4580
|
},
|
|
4320
4581
|
target: {
|
|
4321
4582
|
type: "string",
|
|
4322
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
4583
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
4323
4584
|
},
|
|
4324
4585
|
domain: {
|
|
4325
4586
|
type: "string",
|
|
@@ -4350,7 +4611,7 @@ var MANIFEST = [
|
|
|
4350
4611
|
},
|
|
4351
4612
|
target: {
|
|
4352
4613
|
type: "string",
|
|
4353
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
4614
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
4354
4615
|
},
|
|
4355
4616
|
name: {
|
|
4356
4617
|
type: "string",
|
|
@@ -4406,7 +4667,7 @@ var MANIFEST = [
|
|
|
4406
4667
|
},
|
|
4407
4668
|
target: {
|
|
4408
4669
|
type: "string",
|
|
4409
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
4670
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
4410
4671
|
},
|
|
4411
4672
|
name: {
|
|
4412
4673
|
type: "string",
|
|
@@ -4441,7 +4702,7 @@ var MANIFEST = [
|
|
|
4441
4702
|
},
|
|
4442
4703
|
target: {
|
|
4443
4704
|
type: "string",
|
|
4444
|
-
description: "Target page selector: 'active' (default, = first page-type target) | 'index:N' (Nth page-type target, 0-based) | 'url:<substring>' | 'title:<substring>' | '<32-hex targetId>' (exact target by id)."
|
|
4705
|
+
description: "Target page selector: 'active' (default, = first page-type target) | 'index:N' (Nth page-type target, 0-based) | 'url:<substring>' | 'title:<substring>' | 'label:<name>' (exact label match, origin ledger or live lease) | '<32-hex targetId>' (exact target by id)."
|
|
4445
4706
|
},
|
|
4446
4707
|
interactiveOnly: {
|
|
4447
4708
|
type: "boolean",
|
|
@@ -4464,7 +4725,7 @@ var MANIFEST = [
|
|
|
4464
4725
|
},
|
|
4465
4726
|
target: {
|
|
4466
4727
|
type: "string",
|
|
4467
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4728
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4468
4729
|
},
|
|
4469
4730
|
uid: {
|
|
4470
4731
|
type: "number",
|
|
@@ -4517,7 +4778,7 @@ var MANIFEST = [
|
|
|
4517
4778
|
},
|
|
4518
4779
|
target: {
|
|
4519
4780
|
type: "string",
|
|
4520
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4781
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4521
4782
|
},
|
|
4522
4783
|
uid: {
|
|
4523
4784
|
type: "number",
|
|
@@ -4544,7 +4805,7 @@ var MANIFEST = [
|
|
|
4544
4805
|
},
|
|
4545
4806
|
target: {
|
|
4546
4807
|
type: "string",
|
|
4547
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4808
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4548
4809
|
},
|
|
4549
4810
|
from: {
|
|
4550
4811
|
type: "object",
|
|
@@ -4630,7 +4891,7 @@ var MANIFEST = [
|
|
|
4630
4891
|
},
|
|
4631
4892
|
target: {
|
|
4632
4893
|
type: "string",
|
|
4633
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4894
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4634
4895
|
},
|
|
4635
4896
|
uid: {
|
|
4636
4897
|
type: "number",
|
|
@@ -4673,7 +4934,7 @@ var MANIFEST = [
|
|
|
4673
4934
|
},
|
|
4674
4935
|
target: {
|
|
4675
4936
|
type: "string",
|
|
4676
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4937
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4677
4938
|
},
|
|
4678
4939
|
action: {
|
|
4679
4940
|
type: "string",
|
|
@@ -4739,7 +5000,7 @@ var MANIFEST = [
|
|
|
4739
5000
|
},
|
|
4740
5001
|
target: {
|
|
4741
5002
|
type: "string",
|
|
4742
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'. A download is browser-scoped, so this names the tab for the lease check and for the echoed target, not which download is returned."
|
|
5003
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'. A download is browser-scoped, so this names the tab for the lease check and for the echoed target, not which download is returned."
|
|
4743
5004
|
},
|
|
4744
5005
|
arm: {
|
|
4745
5006
|
type: "boolean",
|
|
@@ -4766,7 +5027,7 @@ var MANIFEST = [
|
|
|
4766
5027
|
},
|
|
4767
5028
|
target: {
|
|
4768
5029
|
type: "string",
|
|
4769
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'. Used for the lease check and, when 'origin' is omitted, as the source of the origin to grant for."
|
|
5030
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'. Used for the lease check and, when 'origin' is omitted, as the source of the origin to grant for."
|
|
4770
5031
|
},
|
|
4771
5032
|
permissions: {
|
|
4772
5033
|
type: "array",
|
|
@@ -4800,7 +5061,7 @@ var MANIFEST = [
|
|
|
4800
5061
|
},
|
|
4801
5062
|
target: {
|
|
4802
5063
|
type: "string",
|
|
4803
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5064
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4804
5065
|
},
|
|
4805
5066
|
uid: {
|
|
4806
5067
|
type: "number",
|
|
@@ -4833,7 +5094,7 @@ var MANIFEST = [
|
|
|
4833
5094
|
},
|
|
4834
5095
|
target: {
|
|
4835
5096
|
type: "string",
|
|
4836
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5097
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4837
5098
|
},
|
|
4838
5099
|
fields: {
|
|
4839
5100
|
type: "array",
|
|
@@ -4879,7 +5140,7 @@ var MANIFEST = [
|
|
|
4879
5140
|
},
|
|
4880
5141
|
target: {
|
|
4881
5142
|
type: "string",
|
|
4882
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5143
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4883
5144
|
},
|
|
4884
5145
|
uid: {
|
|
4885
5146
|
type: "number",
|
|
@@ -4912,7 +5173,7 @@ var MANIFEST = [
|
|
|
4912
5173
|
},
|
|
4913
5174
|
target: {
|
|
4914
5175
|
type: "string",
|
|
4915
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5176
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4916
5177
|
},
|
|
4917
5178
|
key: {
|
|
4918
5179
|
type: "string",
|
|
@@ -4944,7 +5205,7 @@ var MANIFEST = [
|
|
|
4944
5205
|
},
|
|
4945
5206
|
target: {
|
|
4946
5207
|
type: "string",
|
|
4947
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5208
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4948
5209
|
},
|
|
4949
5210
|
uid: {
|
|
4950
5211
|
type: "number",
|
|
@@ -4983,7 +5244,7 @@ var MANIFEST = [
|
|
|
4983
5244
|
},
|
|
4984
5245
|
target: {
|
|
4985
5246
|
type: "string",
|
|
4986
|
-
description: "Target page selector. undefined or 'active' -> first page target; '<32-hex targetId>' -> exact target by id; 'index:N' -> Nth page target (0-based); 'url:<substring>' -> first page whose url contains substring; 'title:<substring>' -> first page whose title contains substring."
|
|
5247
|
+
description: "Target page selector. undefined or 'active' -> first page target; '<32-hex targetId>' -> exact target by id; 'index:N' -> Nth page target (0-based); 'url:<substring>' -> first page whose url contains substring; 'title:<substring>' -> first page whose title contains substring; 'label:<name>' -> the page with exactly this label (origin ledger or live lease)."
|
|
4987
5248
|
},
|
|
4988
5249
|
format: {
|
|
4989
5250
|
type: "string",
|
|
@@ -5034,7 +5295,7 @@ var MANIFEST = [
|
|
|
5034
5295
|
},
|
|
5035
5296
|
target: {
|
|
5036
5297
|
type: "string",
|
|
5037
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5298
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5038
5299
|
},
|
|
5039
5300
|
width: {
|
|
5040
5301
|
type: "number",
|
|
@@ -5134,7 +5395,7 @@ var MANIFEST = [
|
|
|
5134
5395
|
},
|
|
5135
5396
|
target: {
|
|
5136
5397
|
type: "string",
|
|
5137
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5398
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5138
5399
|
},
|
|
5139
5400
|
width: {
|
|
5140
5401
|
type: "number",
|
|
@@ -5172,7 +5433,7 @@ var MANIFEST = [
|
|
|
5172
5433
|
},
|
|
5173
5434
|
target: {
|
|
5174
5435
|
type: "string",
|
|
5175
|
-
description: "Target page selector: 'active' (or omitted) for the first page-type target | 'index:N' (0-based) | 'url:<substring>' | 'title:<substring>' | a 32-hex <targetId>."
|
|
5436
|
+
description: "Target page selector: 'active' (or omitted) for the first page-type target | 'index:N' (0-based) | 'url:<substring>' | 'title:<substring>' | 'label:<name>' (exact label match, origin ledger or live lease) | a 32-hex <targetId>."
|
|
5176
5437
|
},
|
|
5177
5438
|
accept: {
|
|
5178
5439
|
type: "boolean",
|
|
@@ -5199,7 +5460,7 @@ var MANIFEST = [
|
|
|
5199
5460
|
},
|
|
5200
5461
|
{
|
|
5201
5462
|
name: "list_console_messages",
|
|
5202
|
-
description: "Read console output (logs, warnings, exceptions) captured for the target page. By default reads the target's existing shared buffer and returns parsed console entries (empty if no capture has run); with reload:true it reloads the page and records a fresh capture window (both console+network) so a network reload never wipes console history.",
|
|
5463
|
+
description: "Read console output (logs, warnings, exceptions) captured for the target page. By default reads the target's existing shared buffer and returns parsed console entries (empty if no capture has run); with reload:true it reloads the page and records a fresh capture window (both console+network) so a network reload never wipes console history. SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker's console.log arrives as Runtime.consoleAPICalled on its own session. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
5203
5464
|
inputSchema: {
|
|
5204
5465
|
type: "object",
|
|
5205
5466
|
properties: {
|
|
@@ -5209,15 +5470,19 @@ var MANIFEST = [
|
|
|
5209
5470
|
},
|
|
5210
5471
|
target: {
|
|
5211
5472
|
type: "string",
|
|
5212
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
5473
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
5213
5474
|
},
|
|
5214
5475
|
reload: {
|
|
5215
5476
|
type: "boolean",
|
|
5216
|
-
description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer)."
|
|
5477
|
+
description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer). With a worker: target there is nothing to reload: the window records for durationMs while the worker runs, and the session it holds keeps that worker from being idle-evicted until the capture stops."
|
|
5217
5478
|
},
|
|
5218
5479
|
durationMs: {
|
|
5219
5480
|
type: "number",
|
|
5220
5481
|
description: "Capture window for reload mode, in milliseconds. Default 2500."
|
|
5482
|
+
},
|
|
5483
|
+
wake: {
|
|
5484
|
+
type: "boolean",
|
|
5485
|
+
description: "Worker targets only, and only when a capture is being started (reload:true). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
5221
5486
|
}
|
|
5222
5487
|
},
|
|
5223
5488
|
required: [],
|
|
@@ -5236,7 +5501,7 @@ var MANIFEST = [
|
|
|
5236
5501
|
},
|
|
5237
5502
|
target: {
|
|
5238
5503
|
type: "string",
|
|
5239
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
5504
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
5240
5505
|
},
|
|
5241
5506
|
index: {
|
|
5242
5507
|
type: "number",
|
|
@@ -5249,7 +5514,7 @@ var MANIFEST = [
|
|
|
5249
5514
|
},
|
|
5250
5515
|
{
|
|
5251
5516
|
name: "list_network_requests",
|
|
5252
|
-
description: "Return correlated network request rows (one per requestId, with status/headers/state) for the target page. By default reads the target's existing buffer; with reload:true it reloads and records a fresh both-domains capture window. Use filterUrl to keep only requests whose URL contains a substring.",
|
|
5517
|
+
description: "Return correlated network request rows (one per requestId, with status/headers/state) for the target page. By default reads the target's existing buffer; with reload:true it reloads and records a fresh both-domains capture window. Use filterUrl to keep only requests whose URL contains a substring. SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
5253
5518
|
inputSchema: {
|
|
5254
5519
|
type: "object",
|
|
5255
5520
|
properties: {
|
|
@@ -5259,11 +5524,11 @@ var MANIFEST = [
|
|
|
5259
5524
|
},
|
|
5260
5525
|
target: {
|
|
5261
5526
|
type: "string",
|
|
5262
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
5527
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
5263
5528
|
},
|
|
5264
5529
|
reload: {
|
|
5265
5530
|
type: "boolean",
|
|
5266
|
-
description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer)."
|
|
5531
|
+
description: "Record fresh by reloading the page and capturing for a window. Default false (read the existing buffer). With a worker: target there is nothing to reload: the window records for durationMs while the worker runs, and the session it holds keeps that worker from being idle-evicted until the capture stops."
|
|
5267
5532
|
},
|
|
5268
5533
|
durationMs: {
|
|
5269
5534
|
type: "number",
|
|
@@ -5272,6 +5537,10 @@ var MANIFEST = [
|
|
|
5272
5537
|
filterUrl: {
|
|
5273
5538
|
type: "string",
|
|
5274
5539
|
description: "Only return requests whose URL contains this substring."
|
|
5540
|
+
},
|
|
5541
|
+
wake: {
|
|
5542
|
+
type: "boolean",
|
|
5543
|
+
description: "Worker targets only, and only when a capture is being started (reload:true). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
5275
5544
|
}
|
|
5276
5545
|
},
|
|
5277
5546
|
required: [],
|
|
@@ -5280,7 +5549,7 @@ var MANIFEST = [
|
|
|
5280
5549
|
},
|
|
5281
5550
|
{
|
|
5282
5551
|
name: "get_network_request",
|
|
5283
|
-
description: "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh reload
|
|
5552
|
+
description: "Return one network request (matched by exact requestId, else by url substring) including status/headers. Requires at least one of requestId or url (throws otherwise). With includeBody:true the body fetch drives a fresh capture (a reload for a page, a listen window for a worker) and is matched by url ONLY (a fresh capture re-mints requestIds, so a carried-over requestId cannot fetch a body, so it returns metadata plus bodyUnavailableReason). SERVICE WORKERS (Chrome only, Capability 'worker.targets'): pass target 'worker:<extension-id>' to watch an MV3 extension's BACKGROUND SERVICE WORKER. The outbound request that worker makes to your backend is recorded from CDP's Network domain on the worker's own session, which observes what the code ACTUALLY SENT and so needs neither of the two tricks a caller reaches for first. Both of those fail for ordinary JavaScript reasons rather than service-worker ones: a value bound at module top level is not visible from evaluate_script's global scope (plain lexical scoping, exactly the same in a page), and assigning self.fetch cannot rebind a fetch reference the module already captured. A worker capture has no reload to drive it: reload:true means 'listen for durationMs while the worker runs', so trigger the worker's work (another tool call, an extension message, an alarm) during the window; a worker woken by the call has already run its top-level code by the time the recorder attaches, so a request it makes at startup can be missed. SIDE EFFECT, deliberate and documented: the recording holds a CDP session on the worker, and that KEEPS THE WORKER ALIVE for as long as the capture runs — an MV3 worker is otherwise idle-evicted within seconds — and Chrome resumes evicting it once the capture stops.",
|
|
5284
5553
|
inputSchema: {
|
|
5285
5554
|
type: "object",
|
|
5286
5555
|
properties: {
|
|
@@ -5290,7 +5559,7 @@ var MANIFEST = [
|
|
|
5290
5559
|
},
|
|
5291
5560
|
target: {
|
|
5292
5561
|
type: "string",
|
|
5293
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
5562
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>' | 'worker:<substring>' -> a service/shared worker whose url contains the substring, which is how you reach an MV3 extension's background service worker (worker:<extension-id> or worker:background.js). The worker: arm is CHROME-ONLY (Capability 'worker.targets'), is refused on Firefox, and a bare worker target id works too."
|
|
5294
5563
|
},
|
|
5295
5564
|
requestId: {
|
|
5296
5565
|
type: "string",
|
|
@@ -5307,6 +5576,10 @@ var MANIFEST = [
|
|
|
5307
5576
|
durationMs: {
|
|
5308
5577
|
type: "number",
|
|
5309
5578
|
description: "Capture window for the reload-driven body fetch, in milliseconds. Default 2500."
|
|
5579
|
+
},
|
|
5580
|
+
wake: {
|
|
5581
|
+
type: "boolean",
|
|
5582
|
+
description: "Worker targets only, and only when a capture is being started (includeBody together with url). Default true: an MV3 extension service worker is idle-evicted after seconds of inactivity and then exists in NO target listing, so the capture asks Chrome to start it (ServiceWorker.startWorker over a page session) and then re-reads the target list to confirm it actually came up. Pass false to fail fast instead of starting anything. Rejected rather than ignored on a page target, on a bare target id (Chrome re-mints a worker's target id when it restarts, so only the url-substring form can name a worker that is not running), and on a read-only call (the buffer is keyed by target id, so a woken worker's buffer would be empty)."
|
|
5310
5583
|
}
|
|
5311
5584
|
},
|
|
5312
5585
|
required: [],
|
|
@@ -5325,7 +5598,7 @@ var MANIFEST = [
|
|
|
5325
5598
|
},
|
|
5326
5599
|
target: {
|
|
5327
5600
|
type: "string",
|
|
5328
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5601
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5329
5602
|
},
|
|
5330
5603
|
categories: {
|
|
5331
5604
|
type: "array",
|
|
@@ -5351,7 +5624,7 @@ var MANIFEST = [
|
|
|
5351
5624
|
},
|
|
5352
5625
|
target: {
|
|
5353
5626
|
type: "string",
|
|
5354
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5627
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5355
5628
|
}
|
|
5356
5629
|
},
|
|
5357
5630
|
required: [],
|
|
@@ -5389,7 +5662,7 @@ var MANIFEST = [
|
|
|
5389
5662
|
},
|
|
5390
5663
|
target: {
|
|
5391
5664
|
type: "string",
|
|
5392
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5665
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5393
5666
|
},
|
|
5394
5667
|
durationMs: {
|
|
5395
5668
|
type: "number",
|
|
@@ -5427,7 +5700,7 @@ var MANIFEST = [
|
|
|
5427
5700
|
},
|
|
5428
5701
|
target: {
|
|
5429
5702
|
type: "string",
|
|
5430
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5703
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5431
5704
|
},
|
|
5432
5705
|
format: {
|
|
5433
5706
|
type: "string",
|
|
@@ -5474,7 +5747,7 @@ var MANIFEST = [
|
|
|
5474
5747
|
},
|
|
5475
5748
|
target: {
|
|
5476
5749
|
type: "string",
|
|
5477
|
-
description: "Which recording to stop: 'active' (default, valid only when exactly one recording is in flight) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5750
|
+
description: "Which recording to stop: 'active' (default, valid only when exactly one recording is in flight) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5478
5751
|
},
|
|
5479
5752
|
savePath: {
|
|
5480
5753
|
type: "string",
|
|
@@ -5497,7 +5770,7 @@ var MANIFEST = [
|
|
|
5497
5770
|
},
|
|
5498
5771
|
target: {
|
|
5499
5772
|
type: "string",
|
|
5500
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5773
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5501
5774
|
},
|
|
5502
5775
|
savePath: {
|
|
5503
5776
|
type: "string",
|
|
@@ -5564,7 +5837,7 @@ var MANIFEST = [
|
|
|
5564
5837
|
},
|
|
5565
5838
|
target: {
|
|
5566
5839
|
type: "string",
|
|
5567
|
-
description: "Target selector: active | index:N | url:<substr> | title:<substr> | <targetId>. Defaults to the active page."
|
|
5840
|
+
description: "Target selector: active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>. Defaults to the active page."
|
|
5568
5841
|
},
|
|
5569
5842
|
urlPattern: {
|
|
5570
5843
|
type: "string",
|
|
@@ -5672,7 +5945,7 @@ var MANIFEST = [
|
|
|
5672
5945
|
properties: {
|
|
5673
5946
|
target: {
|
|
5674
5947
|
type: "string",
|
|
5675
|
-
description: "Take over an already-open tab, e.g. one the human has open, when asked to: pass any target selector — active | index:N | url:<substr> | title:<substr> | <targetId>. Resolved against the live page list only, so it NEVER opens a tab: a selector that matches nothing is an error, not a new blank tab. The tab is left open on release because the toolkit did not create it. Refused if another live process holds it, including a lease the gate acquired for that process automatically under CDP_REQUIRE_LEASE: this takes over unleased (human) tabs and never steals one from a live agent. Mutually exclusive with targetId."
|
|
5948
|
+
description: "Take over an already-open tab, e.g. one the human has open, when asked to: pass any target selector — active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>. Resolved against the live page list only, so it NEVER opens a tab: a selector that matches nothing is an error, not a new blank tab. The tab is left open on release because the toolkit did not create it. Refused if another live process holds it, including a lease the gate acquired for that process automatically under CDP_REQUIRE_LEASE: this takes over unleased (human) tabs and never steals one from a live agent. Mutually exclusive with targetId."
|
|
5676
5949
|
},
|
|
5677
5950
|
targetId: {
|
|
5678
5951
|
type: "string",
|
|
@@ -5707,7 +5980,7 @@ var MANIFEST = [
|
|
|
5707
5980
|
},
|
|
5708
5981
|
target: {
|
|
5709
5982
|
type: "string",
|
|
5710
|
-
description: "Target selector for a tab this process holds: active | index:N | url:<substr> | title:<substr> | <targetId>. Mutually exclusive with 'lease'. Refused if another process holds the tab."
|
|
5983
|
+
description: "Target selector for a tab this process holds: active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>. Mutually exclusive with 'lease'. Refused if another process holds the tab."
|
|
5711
5984
|
},
|
|
5712
5985
|
close: {
|
|
5713
5986
|
type: "boolean",
|
|
@@ -6552,6 +6825,22 @@ async function pickContext(conn, contexts, selector) {
|
|
|
6552
6825
|
return c;
|
|
6553
6826
|
throw driverError2("no-such-target", `no context with title containing '${needle}'`);
|
|
6554
6827
|
}
|
|
6828
|
+
if (selector.startsWith("label:")) {
|
|
6829
|
+
const label = selector.slice(6);
|
|
6830
|
+
const { matchIds, knownLabels } = await resolveLiveLabel("firefox", label, contexts.map((c) => c.context));
|
|
6831
|
+
if (matchIds.length > 1) {
|
|
6832
|
+
throw driverError2("no-such-target", `label 'label:${label}' matches more than one live target: ${matchIds.join(", ")}. Resolve by target id instead.`);
|
|
6833
|
+
}
|
|
6834
|
+
if (matchIds.length === 1) {
|
|
6835
|
+
const hit = contexts.find((c) => c.context === matchIds[0]);
|
|
6836
|
+
if (hit)
|
|
6837
|
+
return hit;
|
|
6838
|
+
}
|
|
6839
|
+
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)`);
|
|
6840
|
+
}
|
|
6841
|
+
if (isWorkerSelector(selector)) {
|
|
6842
|
+
throw driverError2("unsupported", WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
6843
|
+
}
|
|
6555
6844
|
const exact = contexts.find((c) => c.context === selector);
|
|
6556
6845
|
if (exact)
|
|
6557
6846
|
return exact;
|
|
@@ -7414,6 +7703,11 @@ async function withPage3(driver, target, fn) {
|
|
|
7414
7703
|
}
|
|
7415
7704
|
var consoleBuffers = new Map;
|
|
7416
7705
|
var networkBuffers = new Map;
|
|
7706
|
+
function refuseWorkerSelector(driver, target) {
|
|
7707
|
+
if (isWorkerSelector(target) && !driver.capabilities.has("worker.targets")) {
|
|
7708
|
+
throw new BidiToolError(WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
7709
|
+
}
|
|
7710
|
+
}
|
|
7417
7711
|
function bufferFor(map, id) {
|
|
7418
7712
|
let b = map.get(id);
|
|
7419
7713
|
if (!b) {
|
|
@@ -7442,6 +7736,7 @@ async function captureFresh(page, durationMs) {
|
|
|
7442
7736
|
}
|
|
7443
7737
|
}
|
|
7444
7738
|
async function listConsoleMessages2(driver, args = {}) {
|
|
7739
|
+
refuseWorkerSelector(driver, args.target);
|
|
7445
7740
|
return withPage3(driver, args.target, async (page) => {
|
|
7446
7741
|
if (args.reload)
|
|
7447
7742
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
@@ -7450,6 +7745,7 @@ async function listConsoleMessages2(driver, args = {}) {
|
|
|
7450
7745
|
});
|
|
7451
7746
|
}
|
|
7452
7747
|
async function getConsoleMessage2(driver, args = {}) {
|
|
7748
|
+
refuseWorkerSelector(driver, args.target);
|
|
7453
7749
|
return withPage3(driver, args.target, async (page) => {
|
|
7454
7750
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
7455
7751
|
const idx = args.index ?? 0;
|
|
@@ -7460,6 +7756,7 @@ async function getConsoleMessage2(driver, args = {}) {
|
|
|
7460
7756
|
});
|
|
7461
7757
|
}
|
|
7462
7758
|
async function listNetworkRequests2(driver, args = {}) {
|
|
7759
|
+
refuseWorkerSelector(driver, args.target);
|
|
7463
7760
|
return withPage3(driver, args.target, async (page) => {
|
|
7464
7761
|
if (args.reload)
|
|
7465
7762
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
@@ -7470,6 +7767,7 @@ async function listNetworkRequests2(driver, args = {}) {
|
|
|
7470
7767
|
});
|
|
7471
7768
|
}
|
|
7472
7769
|
async function getNetworkRequest2(driver, args = {}) {
|
|
7770
|
+
refuseWorkerSelector(driver, args.target);
|
|
7473
7771
|
if (!args.requestId && !args.url)
|
|
7474
7772
|
throw new BidiToolError("get_network_request requires requestId or url");
|
|
7475
7773
|
return withPage3(driver, args.target, async (page) => {
|
|
@@ -7561,7 +7859,7 @@ var FIREFOX_TOOLS = {
|
|
|
7561
7859
|
};
|
|
7562
7860
|
|
|
7563
7861
|
// src/mcp.ts
|
|
7564
|
-
var VERSION = "1.
|
|
7862
|
+
var VERSION = "1.9.1";
|
|
7565
7863
|
var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
|
|
7566
7864
|
var AVAILABILITY = toolAvailability(BROWSER);
|
|
7567
7865
|
var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
|