cdp-toolkit 1.8.0 → 1.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +119 -0
- package/CONTRACT.md +4 -0
- package/README.md +23 -4
- package/dist/cli.js +338 -111
- package/dist/index.js +319 -110
- package/dist/mcp.js +380 -151
- package/package.json +3 -2
- package/src/bidi/driver.ts +28 -0
- package/src/cdp/driver.ts +16 -1
- package/src/cdp/workers.ts +177 -0
- package/src/cli.ts +3 -1
- package/src/client.ts +52 -2
- package/src/driver.ts +27 -1
- package/src/leases-tools.ts +2 -2
- package/src/manifest.ts +44 -40
- package/src/mcp.ts +1 -1
- package/src/origins.ts +41 -1
- package/src/shared-tools.ts +104 -3
- package/src/types.ts +20 -0
- package/src/workers.ts +159 -0
package/dist/mcp.js
CHANGED
|
@@ -278,6 +278,169 @@ 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_TARGET_TYPES = new Set(["service_worker", "shared_worker"]);
|
|
410
|
+
function isWorkerSelector(selector) {
|
|
411
|
+
return typeof selector === "string" && selector.startsWith(WORKER_SELECTOR_PREFIX);
|
|
412
|
+
}
|
|
413
|
+
function workerNeedle(selector) {
|
|
414
|
+
return selector.slice(WORKER_SELECTOR_PREFIX.length);
|
|
415
|
+
}
|
|
416
|
+
function isWorkerTargetType(type) {
|
|
417
|
+
return type !== undefined && WORKER_TARGET_TYPES.has(type);
|
|
418
|
+
}
|
|
419
|
+
function resolveWorkerTargets(targets, needle) {
|
|
420
|
+
const liveWorkers = targets.filter((t) => isWorkerTargetType(t.type));
|
|
421
|
+
return {
|
|
422
|
+
matches: needle === "" ? [] : liveWorkers.filter((t) => t.url.includes(needle)),
|
|
423
|
+
liveWorkers
|
|
424
|
+
};
|
|
425
|
+
}
|
|
426
|
+
var WORKER_EMPTY_NEEDLE_MESSAGE = "'worker:' needs a substring of the worker's url, e.g. worker:<extension-id> or worker:background.js";
|
|
427
|
+
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 an extension's background worker.";
|
|
428
|
+
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). evaluate_script is the one tool that " + "accepts a worker: target.";
|
|
429
|
+
function urlList(workers) {
|
|
430
|
+
return workers.map((w) => w.url).join(", ");
|
|
431
|
+
}
|
|
432
|
+
function workerAmbiguityMessage(needle, matches) {
|
|
433
|
+
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.";
|
|
434
|
+
}
|
|
435
|
+
function workerMissMessage(needle, opts) {
|
|
436
|
+
const running = opts.liveWorkers?.length ? ` (running workers: ${urlList(opts.liveWorkers)})` : " (no workers are running)";
|
|
437
|
+
if (!opts.wakeAttempted) {
|
|
438
|
+
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.";
|
|
439
|
+
}
|
|
440
|
+
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.";
|
|
441
|
+
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.";
|
|
442
|
+
}
|
|
443
|
+
|
|
281
444
|
// src/client.ts
|
|
282
445
|
var BASE = process.env.CDP_BASE ?? "http://127.0.0.1:9222";
|
|
283
446
|
var DEFAULT_TIMEOUT_MS = Number(process.env.CDP_TIMEOUT_MS ?? 15000);
|
|
@@ -433,11 +596,13 @@ async function browserWsUrl() {
|
|
|
433
596
|
}
|
|
434
597
|
async function resolveTarget(selector, opts = {}) {
|
|
435
598
|
const targets = await listTargets();
|
|
436
|
-
const hit = pickTarget(targets, selector);
|
|
599
|
+
const hit = await pickTarget(targets, selector);
|
|
600
|
+
if (isWorkerTargetType(hit.type))
|
|
601
|
+
return hit;
|
|
437
602
|
await assertLeaseOk("chrome", hit.id, { lease: opts.lease, url: hit.url, title: hit.title });
|
|
438
603
|
return hit;
|
|
439
604
|
}
|
|
440
|
-
function pickTarget(targets, selector) {
|
|
605
|
+
async function pickTarget(targets, selector) {
|
|
441
606
|
const pages = targets.filter((t) => t.type === "page");
|
|
442
607
|
if (!selector || selector === "active") {
|
|
443
608
|
if (!pages.length)
|
|
@@ -464,6 +629,30 @@ function pickTarget(targets, selector) {
|
|
|
464
629
|
throw new CdpError(`no page title containing '${needle}'`);
|
|
465
630
|
return hit;
|
|
466
631
|
}
|
|
632
|
+
if (selector.startsWith("label:")) {
|
|
633
|
+
const label = selector.slice(6);
|
|
634
|
+
const { matchIds, knownLabels } = await resolveLiveLabel("chrome", label, pages.map((t) => t.id));
|
|
635
|
+
if (matchIds.length > 1) {
|
|
636
|
+
throw new CdpError(`label 'label:${label}' matches more than one live target: ${matchIds.join(", ")}. Resolve by target id instead.`);
|
|
637
|
+
}
|
|
638
|
+
if (matchIds.length === 1) {
|
|
639
|
+
const hit = pages.find((t) => t.id === matchIds[0]);
|
|
640
|
+
if (hit)
|
|
641
|
+
return hit;
|
|
642
|
+
}
|
|
643
|
+
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)`);
|
|
644
|
+
}
|
|
645
|
+
if (isWorkerSelector(selector)) {
|
|
646
|
+
const needle = workerNeedle(selector);
|
|
647
|
+
if (needle === "")
|
|
648
|
+
throw new CdpError(WORKER_EMPTY_NEEDLE_MESSAGE);
|
|
649
|
+
const { matches, liveWorkers } = resolveWorkerTargets(targets, needle);
|
|
650
|
+
if (matches.length > 1)
|
|
651
|
+
throw new CdpError(workerAmbiguityMessage(needle, matches));
|
|
652
|
+
if (matches.length === 1)
|
|
653
|
+
return matches[0];
|
|
654
|
+
throw new CdpError(workerMissMessage(needle, { wakeAttempted: false, liveWorkers }));
|
|
655
|
+
}
|
|
467
656
|
const byId = targets.find((t) => t.id === selector);
|
|
468
657
|
if (!byId)
|
|
469
658
|
throw new CdpError(`no target with id '${selector}'`);
|
|
@@ -653,6 +842,91 @@ async function resolveUid(conn, uid) {
|
|
|
653
842
|
return { objectId: object.objectId };
|
|
654
843
|
}
|
|
655
844
|
|
|
845
|
+
// src/cdp/workers.ts
|
|
846
|
+
var INVENTORY_FIRST_EVENT_MS = 1500;
|
|
847
|
+
var INVENTORY_SETTLE_MS = 250;
|
|
848
|
+
var WAKE_POLL_MS = 300;
|
|
849
|
+
var WAKE_POLL_ATTEMPTS = 30;
|
|
850
|
+
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.";
|
|
851
|
+
var sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
852
|
+
async function requestWorkerStart(needle) {
|
|
853
|
+
const conn = await openBrowser();
|
|
854
|
+
try {
|
|
855
|
+
const host = (await listTargets()).find((t) => t.type === "page");
|
|
856
|
+
if (!host)
|
|
857
|
+
throw new CdpError(NO_HOST_PAGE_MESSAGE);
|
|
858
|
+
const { sessionId } = await conn.send("Target.attachToTarget", {
|
|
859
|
+
targetId: host.id,
|
|
860
|
+
flatten: true
|
|
861
|
+
});
|
|
862
|
+
if (!sessionId)
|
|
863
|
+
throw new CdpError("could not attach a session to host the ServiceWorker domain");
|
|
864
|
+
const scopeById = new Map;
|
|
865
|
+
const versions = [];
|
|
866
|
+
conn.on("ServiceWorker.workerRegistrationUpdated", (params) => {
|
|
867
|
+
for (const r of params.registrations ?? []) {
|
|
868
|
+
if (r.isDeleted === true)
|
|
869
|
+
continue;
|
|
870
|
+
scopeById.set(String(r.registrationId), String(r.scopeURL ?? ""));
|
|
871
|
+
}
|
|
872
|
+
});
|
|
873
|
+
conn.on("ServiceWorker.workerVersionUpdated", (params) => {
|
|
874
|
+
for (const v of params.versions ?? []) {
|
|
875
|
+
versions.push({ registrationId: String(v.registrationId), scriptURL: String(v.scriptURL ?? "") });
|
|
876
|
+
}
|
|
877
|
+
});
|
|
878
|
+
await conn.send("ServiceWorker.enable", {}, { sessionId });
|
|
879
|
+
await conn.waitFor("ServiceWorker.workerVersionUpdated", undefined, INVENTORY_FIRST_EVENT_MS).catch(() => {
|
|
880
|
+
return;
|
|
881
|
+
});
|
|
882
|
+
await sleep(INVENTORY_SETTLE_MS);
|
|
883
|
+
const scopes = [...new Set(scopeById.values())].filter((s) => s.length > 0).sort();
|
|
884
|
+
const matched = new Set;
|
|
885
|
+
for (const v of versions) {
|
|
886
|
+
const scope = scopeById.get(v.registrationId);
|
|
887
|
+
if (scope === undefined)
|
|
888
|
+
continue;
|
|
889
|
+
if (v.scriptURL.includes(needle) || scope.includes(needle))
|
|
890
|
+
matched.add(scope);
|
|
891
|
+
}
|
|
892
|
+
if (matched.size === 0)
|
|
893
|
+
return { startRequested: false, scopes };
|
|
894
|
+
if (matched.size > 1) {
|
|
895
|
+
throw new CdpError(`'worker:${needle}' matches more than one registered service-worker scope: ${[...matched].sort().join(", ")}. ` + "Narrow the substring.");
|
|
896
|
+
}
|
|
897
|
+
await conn.send("ServiceWorker.startWorker", { scopeURL: [...matched][0] }, { sessionId });
|
|
898
|
+
return { startRequested: true, scopes };
|
|
899
|
+
} finally {
|
|
900
|
+
conn.close();
|
|
901
|
+
}
|
|
902
|
+
}
|
|
903
|
+
async function resolveWorkerSelector(selector, opts) {
|
|
904
|
+
const needle = workerNeedle(selector);
|
|
905
|
+
if (needle === "")
|
|
906
|
+
throw new CdpError(WORKER_EMPTY_NEEDLE_MESSAGE);
|
|
907
|
+
const first = resolveWorkerTargets(await listTargets(), needle);
|
|
908
|
+
if (first.matches.length > 1)
|
|
909
|
+
throw new CdpError(workerAmbiguityMessage(needle, first.matches));
|
|
910
|
+
if (first.matches.length === 1)
|
|
911
|
+
return first.matches[0];
|
|
912
|
+
if (!opts.wake) {
|
|
913
|
+
throw new CdpError(workerMissMessage(needle, { wakeAttempted: false, liveWorkers: first.liveWorkers }));
|
|
914
|
+
}
|
|
915
|
+
const { startRequested, scopes } = await requestWorkerStart(needle);
|
|
916
|
+
if (startRequested) {
|
|
917
|
+
for (let i = 0;i < WAKE_POLL_ATTEMPTS; i++) {
|
|
918
|
+
await sleep(WAKE_POLL_MS);
|
|
919
|
+
const again = resolveWorkerTargets(await listTargets(), needle);
|
|
920
|
+
if (again.matches.length > 1)
|
|
921
|
+
throw new CdpError(workerAmbiguityMessage(needle, again.matches));
|
|
922
|
+
if (again.matches.length === 1)
|
|
923
|
+
return again.matches[0];
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
const final = resolveWorkerTargets(await listTargets(), needle);
|
|
927
|
+
throw new CdpError(workerMissMessage(needle, { wakeAttempted: true, liveWorkers: final.liveWorkers, registeredScopes: scopes }));
|
|
928
|
+
}
|
|
929
|
+
|
|
656
930
|
// src/tools/network_mock.ts
|
|
657
931
|
function escapeRegExpChar(ch) {
|
|
658
932
|
return /[.*+?^${}()|[\]\\]/.test(ch) ? `\\${ch}` : ch;
|
|
@@ -1134,7 +1408,8 @@ var CDP_CAPABILITIES = new Set([
|
|
|
1134
1408
|
"input.raw",
|
|
1135
1409
|
"input.html5Drag",
|
|
1136
1410
|
"browser.downloads",
|
|
1137
|
-
"browser.permissions"
|
|
1411
|
+
"browser.permissions",
|
|
1412
|
+
"worker.targets"
|
|
1138
1413
|
]);
|
|
1139
1414
|
|
|
1140
1415
|
class CdpPageDriver {
|
|
@@ -1702,6 +1977,10 @@ class CdpBrowserDriver {
|
|
|
1702
1977
|
await withBrowserConn((conn) => conn.send("Target.activateTarget", { targetId: target.id }));
|
|
1703
1978
|
return { id: target.id, url: target.url, title: target.title };
|
|
1704
1979
|
}
|
|
1980
|
+
async resolveWorkerTarget(selector, opts) {
|
|
1981
|
+
const target = await resolveWorkerSelector(selector, opts).catch(noSuchTarget);
|
|
1982
|
+
return { id: target.id, url: target.url, title: target.title, type: target.type };
|
|
1983
|
+
}
|
|
1705
1984
|
async page(selector) {
|
|
1706
1985
|
const { conn, target } = await openPage(selector).catch(noSuchTarget);
|
|
1707
1986
|
return new CdpPageDriver(conn, target, this);
|
|
@@ -1788,111 +2067,6 @@ function createCdpDriver() {
|
|
|
1788
2067
|
return new CdpBrowserDriver;
|
|
1789
2068
|
}
|
|
1790
2069
|
|
|
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
2070
|
// src/reap.ts
|
|
1897
2071
|
function staleAgentTabs(input) {
|
|
1898
2072
|
const live = new Set(input.livePageIds);
|
|
@@ -1993,6 +2167,11 @@ async function pickPage(driver, pages, selector) {
|
|
|
1993
2167
|
throw new SharedToolError(`no page title containing '${needle}'`);
|
|
1994
2168
|
return p;
|
|
1995
2169
|
}
|
|
2170
|
+
if (selector.startsWith("label:"))
|
|
2171
|
+
return pickByLabel(driver, pages, selector.slice(6));
|
|
2172
|
+
if (isWorkerSelector(selector)) {
|
|
2173
|
+
throw new SharedToolError(backendOf(driver) === "firefox" ? WORKER_SELECTOR_UNSUPPORTED_MESSAGE : WORKER_SELECTOR_PAGE_ONLY_MESSAGE);
|
|
2174
|
+
}
|
|
1996
2175
|
const exactPage = pages.find((x) => x.id === selector);
|
|
1997
2176
|
if (exactPage)
|
|
1998
2177
|
return exactPage;
|
|
@@ -2002,6 +2181,18 @@ async function pickPage(driver, pages, selector) {
|
|
|
2002
2181
|
return exact;
|
|
2003
2182
|
throw new SharedToolError(`no target with id '${selector}'`);
|
|
2004
2183
|
}
|
|
2184
|
+
async function pickByLabel(driver, pages, label) {
|
|
2185
|
+
const { matchIds, knownLabels } = await resolveLiveLabel(backendOf(driver), label, pages.map((x) => x.id));
|
|
2186
|
+
if (matchIds.length > 1) {
|
|
2187
|
+
throw new SharedToolError(`label 'label:${label}' matches more than one live target: ${matchIds.join(", ")}. Resolve by target id instead.`);
|
|
2188
|
+
}
|
|
2189
|
+
if (matchIds.length === 1) {
|
|
2190
|
+
const p = pages.find((x) => x.id === matchIds[0]);
|
|
2191
|
+
if (p)
|
|
2192
|
+
return p;
|
|
2193
|
+
}
|
|
2194
|
+
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)`);
|
|
2195
|
+
}
|
|
2005
2196
|
function locatorOf(args) {
|
|
2006
2197
|
const hasUid = args.uid !== undefined && args.uid !== null && args.uid !== "";
|
|
2007
2198
|
const hasSelector = typeof args.selector === "string" && args.selector.length > 0;
|
|
@@ -2143,11 +2334,29 @@ async function writeJsonSink(savePath, value) {
|
|
|
2143
2334
|
function sinkRequested(savePath) {
|
|
2144
2335
|
return savePath !== undefined && savePath !== "";
|
|
2145
2336
|
}
|
|
2337
|
+
var EXPRESSION_ALIAS_KEYS = ["function", "code", "js", "script", "fn", "body"];
|
|
2338
|
+
async function resolveWorkerSelectorFor(driver, selector, wake) {
|
|
2339
|
+
if (!driver.capabilities.has("worker.targets") || typeof driver.resolveWorkerTarget !== "function") {
|
|
2340
|
+
throw new SharedToolError(`evaluateScript: ${WORKER_SELECTOR_UNSUPPORTED_MESSAGE}`);
|
|
2341
|
+
}
|
|
2342
|
+
return driver.resolveWorkerTarget(selector, { wake });
|
|
2343
|
+
}
|
|
2146
2344
|
async function evaluateScript(driver, args) {
|
|
2147
2345
|
if (typeof args.expression !== "string" || args.expression.length === 0) {
|
|
2346
|
+
const wrongKey = EXPRESSION_ALIAS_KEYS.find((k) => args[k] !== undefined);
|
|
2347
|
+
if (wrongKey !== undefined) {
|
|
2348
|
+
throw new SharedToolError(`evaluateScript: you passed '${wrongKey}'; the key is 'expression' — function literals go in 'expression', invoked with 'args'.`);
|
|
2349
|
+
}
|
|
2148
2350
|
throw new SharedToolError("evaluateScript: 'expression' must be a non-empty string");
|
|
2149
2351
|
}
|
|
2150
|
-
|
|
2352
|
+
if (args.wake !== undefined && typeof args.wake !== "boolean") {
|
|
2353
|
+
throw new SharedToolError("evaluateScript: 'wake' must be a boolean");
|
|
2354
|
+
}
|
|
2355
|
+
if (args.wake !== undefined && !isWorkerSelector(args.target)) {
|
|
2356
|
+
throw new SharedToolError("evaluateScript: 'wake' only applies to a target of the form 'worker:<substring>'; a page target is never asleep.");
|
|
2357
|
+
}
|
|
2358
|
+
const target = isWorkerSelector(args.target) ? (await resolveWorkerSelectorFor(driver, args.target, args.wake ?? true)).id : args.target;
|
|
2359
|
+
return withPage2(driver, target, async (page) => {
|
|
2151
2360
|
const value = await page.evaluate(args.expression, { args: args.args, awaitPromise: args.awaitPromise ?? true });
|
|
2152
2361
|
if (!sinkRequested(args.savePath))
|
|
2153
2362
|
return value;
|
|
@@ -2511,7 +2720,7 @@ async function claimPage(driver, args = {}) {
|
|
|
2511
2720
|
const hasTarget = typeof args.target === "string" && args.target.length > 0;
|
|
2512
2721
|
const hasTargetId = typeof args.targetId === "string" && args.targetId.length > 0;
|
|
2513
2722
|
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.");
|
|
2723
|
+
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
2724
|
}
|
|
2516
2725
|
const backend = backendOf2(driver);
|
|
2517
2726
|
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
@@ -4157,7 +4366,7 @@ var MANIFEST = [
|
|
|
4157
4366
|
},
|
|
4158
4367
|
target: {
|
|
4159
4368
|
type: "string",
|
|
4160
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4369
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4161
4370
|
}
|
|
4162
4371
|
},
|
|
4163
4372
|
required: [
|
|
@@ -4178,7 +4387,7 @@ var MANIFEST = [
|
|
|
4178
4387
|
},
|
|
4179
4388
|
target: {
|
|
4180
4389
|
type: "string",
|
|
4181
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4390
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4182
4391
|
}
|
|
4183
4392
|
},
|
|
4184
4393
|
required: [
|
|
@@ -4199,7 +4408,7 @@ var MANIFEST = [
|
|
|
4199
4408
|
},
|
|
4200
4409
|
target: {
|
|
4201
4410
|
type: "string",
|
|
4202
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4411
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4203
4412
|
},
|
|
4204
4413
|
url: {
|
|
4205
4414
|
type: "string",
|
|
@@ -4249,7 +4458,7 @@ var MANIFEST = [
|
|
|
4249
4458
|
},
|
|
4250
4459
|
target: {
|
|
4251
4460
|
type: "string",
|
|
4252
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4461
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4253
4462
|
},
|
|
4254
4463
|
text: {
|
|
4255
4464
|
type: "string",
|
|
@@ -4272,7 +4481,7 @@ var MANIFEST = [
|
|
|
4272
4481
|
},
|
|
4273
4482
|
{
|
|
4274
4483
|
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.",
|
|
4484
|
+
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 also the one tool that can evaluate inside an MV3 extension's background SERVICE WORKER rather than a page: 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
4485
|
inputSchema: {
|
|
4277
4486
|
type: "object",
|
|
4278
4487
|
properties: {
|
|
@@ -4282,11 +4491,15 @@ var MANIFEST = [
|
|
|
4282
4491
|
},
|
|
4283
4492
|
target: {
|
|
4284
4493
|
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."
|
|
4494
|
+
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 this tool only; an MV3 worker that has been idle-evicted is started automatically first, see 'wake'."
|
|
4495
|
+
},
|
|
4496
|
+
wake: {
|
|
4497
|
+
type: "boolean",
|
|
4498
|
+
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
4499
|
},
|
|
4287
4500
|
expression: {
|
|
4288
4501
|
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."
|
|
4502
|
+
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
4503
|
},
|
|
4291
4504
|
awaitPromise: {
|
|
4292
4505
|
type: "boolean",
|
|
@@ -4319,7 +4532,7 @@ var MANIFEST = [
|
|
|
4319
4532
|
},
|
|
4320
4533
|
target: {
|
|
4321
4534
|
type: "string",
|
|
4322
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
4535
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
4323
4536
|
},
|
|
4324
4537
|
domain: {
|
|
4325
4538
|
type: "string",
|
|
@@ -4350,7 +4563,7 @@ var MANIFEST = [
|
|
|
4350
4563
|
},
|
|
4351
4564
|
target: {
|
|
4352
4565
|
type: "string",
|
|
4353
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
4566
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
4354
4567
|
},
|
|
4355
4568
|
name: {
|
|
4356
4569
|
type: "string",
|
|
@@ -4406,7 +4619,7 @@ var MANIFEST = [
|
|
|
4406
4619
|
},
|
|
4407
4620
|
target: {
|
|
4408
4621
|
type: "string",
|
|
4409
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
4622
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
4410
4623
|
},
|
|
4411
4624
|
name: {
|
|
4412
4625
|
type: "string",
|
|
@@ -4441,7 +4654,7 @@ var MANIFEST = [
|
|
|
4441
4654
|
},
|
|
4442
4655
|
target: {
|
|
4443
4656
|
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)."
|
|
4657
|
+
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
4658
|
},
|
|
4446
4659
|
interactiveOnly: {
|
|
4447
4660
|
type: "boolean",
|
|
@@ -4464,7 +4677,7 @@ var MANIFEST = [
|
|
|
4464
4677
|
},
|
|
4465
4678
|
target: {
|
|
4466
4679
|
type: "string",
|
|
4467
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4680
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4468
4681
|
},
|
|
4469
4682
|
uid: {
|
|
4470
4683
|
type: "number",
|
|
@@ -4517,7 +4730,7 @@ var MANIFEST = [
|
|
|
4517
4730
|
},
|
|
4518
4731
|
target: {
|
|
4519
4732
|
type: "string",
|
|
4520
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4733
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4521
4734
|
},
|
|
4522
4735
|
uid: {
|
|
4523
4736
|
type: "number",
|
|
@@ -4544,7 +4757,7 @@ var MANIFEST = [
|
|
|
4544
4757
|
},
|
|
4545
4758
|
target: {
|
|
4546
4759
|
type: "string",
|
|
4547
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4760
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4548
4761
|
},
|
|
4549
4762
|
from: {
|
|
4550
4763
|
type: "object",
|
|
@@ -4630,7 +4843,7 @@ var MANIFEST = [
|
|
|
4630
4843
|
},
|
|
4631
4844
|
target: {
|
|
4632
4845
|
type: "string",
|
|
4633
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4846
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4634
4847
|
},
|
|
4635
4848
|
uid: {
|
|
4636
4849
|
type: "number",
|
|
@@ -4673,7 +4886,7 @@ var MANIFEST = [
|
|
|
4673
4886
|
},
|
|
4674
4887
|
target: {
|
|
4675
4888
|
type: "string",
|
|
4676
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4889
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4677
4890
|
},
|
|
4678
4891
|
action: {
|
|
4679
4892
|
type: "string",
|
|
@@ -4739,7 +4952,7 @@ var MANIFEST = [
|
|
|
4739
4952
|
},
|
|
4740
4953
|
target: {
|
|
4741
4954
|
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."
|
|
4955
|
+
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
4956
|
},
|
|
4744
4957
|
arm: {
|
|
4745
4958
|
type: "boolean",
|
|
@@ -4766,7 +4979,7 @@ var MANIFEST = [
|
|
|
4766
4979
|
},
|
|
4767
4980
|
target: {
|
|
4768
4981
|
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."
|
|
4982
|
+
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
4983
|
},
|
|
4771
4984
|
permissions: {
|
|
4772
4985
|
type: "array",
|
|
@@ -4800,7 +5013,7 @@ var MANIFEST = [
|
|
|
4800
5013
|
},
|
|
4801
5014
|
target: {
|
|
4802
5015
|
type: "string",
|
|
4803
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5016
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4804
5017
|
},
|
|
4805
5018
|
uid: {
|
|
4806
5019
|
type: "number",
|
|
@@ -4833,7 +5046,7 @@ var MANIFEST = [
|
|
|
4833
5046
|
},
|
|
4834
5047
|
target: {
|
|
4835
5048
|
type: "string",
|
|
4836
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5049
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4837
5050
|
},
|
|
4838
5051
|
fields: {
|
|
4839
5052
|
type: "array",
|
|
@@ -4879,7 +5092,7 @@ var MANIFEST = [
|
|
|
4879
5092
|
},
|
|
4880
5093
|
target: {
|
|
4881
5094
|
type: "string",
|
|
4882
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5095
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4883
5096
|
},
|
|
4884
5097
|
uid: {
|
|
4885
5098
|
type: "number",
|
|
@@ -4912,7 +5125,7 @@ var MANIFEST = [
|
|
|
4912
5125
|
},
|
|
4913
5126
|
target: {
|
|
4914
5127
|
type: "string",
|
|
4915
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5128
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4916
5129
|
},
|
|
4917
5130
|
key: {
|
|
4918
5131
|
type: "string",
|
|
@@ -4944,7 +5157,7 @@ var MANIFEST = [
|
|
|
4944
5157
|
},
|
|
4945
5158
|
target: {
|
|
4946
5159
|
type: "string",
|
|
4947
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5160
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
4948
5161
|
},
|
|
4949
5162
|
uid: {
|
|
4950
5163
|
type: "number",
|
|
@@ -4983,7 +5196,7 @@ var MANIFEST = [
|
|
|
4983
5196
|
},
|
|
4984
5197
|
target: {
|
|
4985
5198
|
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."
|
|
5199
|
+
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
5200
|
},
|
|
4988
5201
|
format: {
|
|
4989
5202
|
type: "string",
|
|
@@ -5034,7 +5247,7 @@ var MANIFEST = [
|
|
|
5034
5247
|
},
|
|
5035
5248
|
target: {
|
|
5036
5249
|
type: "string",
|
|
5037
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5250
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5038
5251
|
},
|
|
5039
5252
|
width: {
|
|
5040
5253
|
type: "number",
|
|
@@ -5134,7 +5347,7 @@ var MANIFEST = [
|
|
|
5134
5347
|
},
|
|
5135
5348
|
target: {
|
|
5136
5349
|
type: "string",
|
|
5137
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5350
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5138
5351
|
},
|
|
5139
5352
|
width: {
|
|
5140
5353
|
type: "number",
|
|
@@ -5172,7 +5385,7 @@ var MANIFEST = [
|
|
|
5172
5385
|
},
|
|
5173
5386
|
target: {
|
|
5174
5387
|
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>."
|
|
5388
|
+
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
5389
|
},
|
|
5177
5390
|
accept: {
|
|
5178
5391
|
type: "boolean",
|
|
@@ -5209,7 +5422,7 @@ var MANIFEST = [
|
|
|
5209
5422
|
},
|
|
5210
5423
|
target: {
|
|
5211
5424
|
type: "string",
|
|
5212
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
5425
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
5213
5426
|
},
|
|
5214
5427
|
reload: {
|
|
5215
5428
|
type: "boolean",
|
|
@@ -5236,7 +5449,7 @@ var MANIFEST = [
|
|
|
5236
5449
|
},
|
|
5237
5450
|
target: {
|
|
5238
5451
|
type: "string",
|
|
5239
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
5452
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
5240
5453
|
},
|
|
5241
5454
|
index: {
|
|
5242
5455
|
type: "number",
|
|
@@ -5259,7 +5472,7 @@ var MANIFEST = [
|
|
|
5259
5472
|
},
|
|
5260
5473
|
target: {
|
|
5261
5474
|
type: "string",
|
|
5262
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
5475
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
5263
5476
|
},
|
|
5264
5477
|
reload: {
|
|
5265
5478
|
type: "boolean",
|
|
@@ -5290,7 +5503,7 @@ var MANIFEST = [
|
|
|
5290
5503
|
},
|
|
5291
5504
|
target: {
|
|
5292
5505
|
type: "string",
|
|
5293
|
-
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>'."
|
|
5506
|
+
description: "Target page selector: 'active' (default) | '<targetId>' | 'index:N' | 'url:<substring>' | 'title:<substring>' | 'label:<name>'."
|
|
5294
5507
|
},
|
|
5295
5508
|
requestId: {
|
|
5296
5509
|
type: "string",
|
|
@@ -5325,7 +5538,7 @@ var MANIFEST = [
|
|
|
5325
5538
|
},
|
|
5326
5539
|
target: {
|
|
5327
5540
|
type: "string",
|
|
5328
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5541
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5329
5542
|
},
|
|
5330
5543
|
categories: {
|
|
5331
5544
|
type: "array",
|
|
@@ -5351,7 +5564,7 @@ var MANIFEST = [
|
|
|
5351
5564
|
},
|
|
5352
5565
|
target: {
|
|
5353
5566
|
type: "string",
|
|
5354
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5567
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5355
5568
|
}
|
|
5356
5569
|
},
|
|
5357
5570
|
required: [],
|
|
@@ -5389,7 +5602,7 @@ var MANIFEST = [
|
|
|
5389
5602
|
},
|
|
5390
5603
|
target: {
|
|
5391
5604
|
type: "string",
|
|
5392
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5605
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5393
5606
|
},
|
|
5394
5607
|
durationMs: {
|
|
5395
5608
|
type: "number",
|
|
@@ -5427,7 +5640,7 @@ var MANIFEST = [
|
|
|
5427
5640
|
},
|
|
5428
5641
|
target: {
|
|
5429
5642
|
type: "string",
|
|
5430
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5643
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5431
5644
|
},
|
|
5432
5645
|
format: {
|
|
5433
5646
|
type: "string",
|
|
@@ -5474,7 +5687,7 @@ var MANIFEST = [
|
|
|
5474
5687
|
},
|
|
5475
5688
|
target: {
|
|
5476
5689
|
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>'."
|
|
5690
|
+
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
5691
|
},
|
|
5479
5692
|
savePath: {
|
|
5480
5693
|
type: "string",
|
|
@@ -5497,7 +5710,7 @@ var MANIFEST = [
|
|
|
5497
5710
|
},
|
|
5498
5711
|
target: {
|
|
5499
5712
|
type: "string",
|
|
5500
|
-
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
5713
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | 'label:<name>' | '<targetId>'."
|
|
5501
5714
|
},
|
|
5502
5715
|
savePath: {
|
|
5503
5716
|
type: "string",
|
|
@@ -5564,7 +5777,7 @@ var MANIFEST = [
|
|
|
5564
5777
|
},
|
|
5565
5778
|
target: {
|
|
5566
5779
|
type: "string",
|
|
5567
|
-
description: "Target selector: active | index:N | url:<substr> | title:<substr> | <targetId>. Defaults to the active page."
|
|
5780
|
+
description: "Target selector: active | index:N | url:<substr> | title:<substr> | label:<name> | <targetId>. Defaults to the active page."
|
|
5568
5781
|
},
|
|
5569
5782
|
urlPattern: {
|
|
5570
5783
|
type: "string",
|
|
@@ -5672,7 +5885,7 @@ var MANIFEST = [
|
|
|
5672
5885
|
properties: {
|
|
5673
5886
|
target: {
|
|
5674
5887
|
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."
|
|
5888
|
+
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
5889
|
},
|
|
5677
5890
|
targetId: {
|
|
5678
5891
|
type: "string",
|
|
@@ -5707,7 +5920,7 @@ var MANIFEST = [
|
|
|
5707
5920
|
},
|
|
5708
5921
|
target: {
|
|
5709
5922
|
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."
|
|
5923
|
+
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
5924
|
},
|
|
5712
5925
|
close: {
|
|
5713
5926
|
type: "boolean",
|
|
@@ -6552,6 +6765,22 @@ async function pickContext(conn, contexts, selector) {
|
|
|
6552
6765
|
return c;
|
|
6553
6766
|
throw driverError2("no-such-target", `no context with title containing '${needle}'`);
|
|
6554
6767
|
}
|
|
6768
|
+
if (selector.startsWith("label:")) {
|
|
6769
|
+
const label = selector.slice(6);
|
|
6770
|
+
const { matchIds, knownLabels } = await resolveLiveLabel("firefox", label, contexts.map((c) => c.context));
|
|
6771
|
+
if (matchIds.length > 1) {
|
|
6772
|
+
throw driverError2("no-such-target", `label 'label:${label}' matches more than one live target: ${matchIds.join(", ")}. Resolve by target id instead.`);
|
|
6773
|
+
}
|
|
6774
|
+
if (matchIds.length === 1) {
|
|
6775
|
+
const hit = contexts.find((c) => c.context === matchIds[0]);
|
|
6776
|
+
if (hit)
|
|
6777
|
+
return hit;
|
|
6778
|
+
}
|
|
6779
|
+
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)`);
|
|
6780
|
+
}
|
|
6781
|
+
if (isWorkerSelector(selector)) {
|
|
6782
|
+
throw driverError2("unsupported", WORKER_SELECTOR_UNSUPPORTED_MESSAGE);
|
|
6783
|
+
}
|
|
6555
6784
|
const exact = contexts.find((c) => c.context === selector);
|
|
6556
6785
|
if (exact)
|
|
6557
6786
|
return exact;
|
|
@@ -7561,7 +7790,7 @@ var FIREFOX_TOOLS = {
|
|
|
7561
7790
|
};
|
|
7562
7791
|
|
|
7563
7792
|
// src/mcp.ts
|
|
7564
|
-
var VERSION = "1.
|
|
7793
|
+
var VERSION = "1.9.0";
|
|
7565
7794
|
var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
|
|
7566
7795
|
var AVAILABILITY = toolAvailability(BROWSER);
|
|
7567
7796
|
var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
|