cdp-toolkit 1.6.0 → 1.8.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 +439 -0
- package/CONTRACT.md +11 -2
- package/README.md +89 -23
- package/dist/cli.js +1150 -111
- package/dist/index.js +1011 -99
- package/dist/mcp.js +1447 -167
- package/package.json +5 -3
- package/src/activity.ts +533 -0
- package/src/bidi/driver.ts +231 -8
- package/src/capabilities.ts +1 -1
- package/src/cdp/driver.ts +472 -25
- package/src/driver.ts +161 -3
- package/src/index.ts +26 -4
- package/src/leases-tools.ts +269 -27
- package/src/leases.ts +84 -4
- package/src/manifest.ts +295 -56
- package/src/mcp.ts +10 -2
- package/src/reap.ts +165 -0
- package/src/shared-tools.ts +303 -30
- package/src/tools/browser-session.ts +246 -0
- package/src/tools/dispatch-mouse.ts +108 -0
- package/src/tools/downloads.ts +143 -0
- package/src/tools/permissions.ts +130 -0
package/dist/mcp.js
CHANGED
|
@@ -13,6 +13,16 @@ function leaseTtlMs() {
|
|
|
13
13
|
const raw = Number(process.env.CDP_LEASE_TTL_MS);
|
|
14
14
|
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_LEASE_TTL_MS;
|
|
15
15
|
}
|
|
16
|
+
var longLived = false;
|
|
17
|
+
function markLongLivedProcess(value = true) {
|
|
18
|
+
longLived = value;
|
|
19
|
+
}
|
|
20
|
+
function requireLease() {
|
|
21
|
+
if (!longLived)
|
|
22
|
+
return false;
|
|
23
|
+
const raw = (process.env.CDP_REQUIRE_LEASE ?? "").trim().toLowerCase();
|
|
24
|
+
return raw === "1" || raw === "true" || raw === "yes" || raw === "on";
|
|
25
|
+
}
|
|
16
26
|
function defaultLabel() {
|
|
17
27
|
return `pid-${process.pid}`;
|
|
18
28
|
}
|
|
@@ -99,7 +109,8 @@ async function claimLease(backend, targetId, opts) {
|
|
|
99
109
|
label: opts.label,
|
|
100
110
|
createdAt: now,
|
|
101
111
|
lastUsedAt: now,
|
|
102
|
-
ttlMs: opts.ttlMs ?? leaseTtlMs()
|
|
112
|
+
ttlMs: opts.ttlMs ?? leaseTtlMs(),
|
|
113
|
+
auto: opts.auto === true
|
|
103
114
|
};
|
|
104
115
|
try {
|
|
105
116
|
await writeFile(leaseFile(backend, targetId), JSON.stringify(record), { flag: "wx" });
|
|
@@ -175,8 +186,16 @@ async function assertLeaseOk(backend, targetId, ctx = {}) {
|
|
|
175
186
|
const held = rec && !staleReason(rec, { now, liveIds: ctx.liveIds }) ? rec : undefined;
|
|
176
187
|
const where = describeTarget(targetId, ctx);
|
|
177
188
|
if (token === undefined) {
|
|
178
|
-
if (!held)
|
|
189
|
+
if (!held) {
|
|
190
|
+
if (requireLease()) {
|
|
191
|
+
await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
|
|
192
|
+
}
|
|
179
193
|
return;
|
|
194
|
+
}
|
|
195
|
+
if (held.auto === true && held.pid === process.pid) {
|
|
196
|
+
await touchLease(held, now);
|
|
197
|
+
return;
|
|
198
|
+
}
|
|
180
199
|
throw new LeaseConflictError(`${where} is leased by '${held.label}' (pid ${held.pid}). Pass that lease's token as the 'lease' argument, or call release_page to free it. list_leases shows every active lease.`, targetId, held.label);
|
|
181
200
|
}
|
|
182
201
|
const parts = tokenParts(token);
|
|
@@ -184,8 +203,12 @@ async function assertLeaseOk(backend, targetId, ctx = {}) {
|
|
|
184
203
|
throw new LeaseConflictError(`malformed lease token for ${where}. Pass back the token claim_page returned, never a constructed one.`, targetId);
|
|
185
204
|
}
|
|
186
205
|
if (parts.backend !== backend || parts.targetId !== targetId) {
|
|
187
|
-
if (!held)
|
|
206
|
+
if (!held) {
|
|
207
|
+
if (requireLease()) {
|
|
208
|
+
await claimLease(backend, targetId, { label: defaultLabel(), ttlMs: leaseTtlMs(), auto: true, now });
|
|
209
|
+
}
|
|
188
210
|
return;
|
|
211
|
+
}
|
|
189
212
|
throw new LeaseConflictError(`the lease token you passed is for ${parts.backend} target '${parts.targetId}', not ${where}, which is leased by '${held.label}'. Resolve the tab you actually hold, or claim this one.`, targetId, held.label);
|
|
190
213
|
}
|
|
191
214
|
if (!held) {
|
|
@@ -463,6 +486,141 @@ async function withPage(selector, fn, opts = {}) {
|
|
|
463
486
|
}
|
|
464
487
|
}
|
|
465
488
|
|
|
489
|
+
// src/activity.ts
|
|
490
|
+
var BEACON_DATA_GLOBAL = "__cdpToolkitLastInput";
|
|
491
|
+
var BEACON_INSTALLED_GLOBAL = "__cdpToolkitBeacon";
|
|
492
|
+
var BEACON_EVENTS = ["pointerdown", "mousedown", "keydown", "wheel", "touchstart"];
|
|
493
|
+
var BEACON_BODY = `
|
|
494
|
+
if (!window.${BEACON_INSTALLED_GLOBAL}) {
|
|
495
|
+
window.${BEACON_INSTALLED_GLOBAL} = true;
|
|
496
|
+
var mark = function () { window.${BEACON_DATA_GLOBAL} = Date.now(); };
|
|
497
|
+
var events = ${JSON.stringify([...BEACON_EVENTS])};
|
|
498
|
+
for (var i = 0; i < events.length; i++) {
|
|
499
|
+
window.addEventListener(events[i], mark, { capture: true, passive: true });
|
|
500
|
+
}
|
|
501
|
+
}`;
|
|
502
|
+
var BEACON_SOURCE = `(function () {${BEACON_BODY}
|
|
503
|
+
})();`;
|
|
504
|
+
var BEACON_FUNCTION_DECLARATION = `function () {${BEACON_BODY}
|
|
505
|
+
}`;
|
|
506
|
+
var BEACON_READ_EXPRESSION = `(typeof window.${BEACON_DATA_GLOBAL} === "number" ? window.${BEACON_DATA_GLOBAL} : null)`;
|
|
507
|
+
var DISPATCH_ATTRIBUTION_WINDOW_MS = 1500;
|
|
508
|
+
function dispatchKey(backend, targetId) {
|
|
509
|
+
return `${backend}:${targetId}`;
|
|
510
|
+
}
|
|
511
|
+
var dispatchLog = new Map;
|
|
512
|
+
var DISPATCH_LOG_LIMIT = 512;
|
|
513
|
+
function recordDispatch(backend, targetId, now = Date.now()) {
|
|
514
|
+
const key = dispatchKey(backend, targetId);
|
|
515
|
+
dispatchLog.delete(key);
|
|
516
|
+
dispatchLog.set(key, now);
|
|
517
|
+
while (dispatchLog.size > DISPATCH_LOG_LIMIT) {
|
|
518
|
+
const oldest = dispatchLog.keys().next();
|
|
519
|
+
if (oldest.done)
|
|
520
|
+
break;
|
|
521
|
+
dispatchLog.delete(oldest.value);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
var NON_SYNTHESIZING_INPUT_METHODS = new Set(["Input.setInterceptDrags", "Input.setIgnoreInputEvents"]);
|
|
525
|
+
async function sendInput(conn, backend, targetId, method, params = {}) {
|
|
526
|
+
if (!NON_SYNTHESIZING_INPUT_METHODS.has(method))
|
|
527
|
+
recordDispatch(backend, targetId);
|
|
528
|
+
return conn.send(method, params);
|
|
529
|
+
}
|
|
530
|
+
function lastDispatchAt(backend, targetId) {
|
|
531
|
+
return dispatchLog.get(dispatchKey(backend, targetId));
|
|
532
|
+
}
|
|
533
|
+
function isHumanAttributed(beaconTs, dispatchAt, windowMs = DISPATCH_ATTRIBUTION_WINDOW_MS) {
|
|
534
|
+
if (typeof beaconTs !== "number" || !Number.isFinite(beaconTs))
|
|
535
|
+
return false;
|
|
536
|
+
return beaconTs > (dispatchAt ?? 0) + windowMs;
|
|
537
|
+
}
|
|
538
|
+
function humanActiveMs(beaconTs, dispatchAt, now = Date.now(), windowMs = DISPATCH_ATTRIBUTION_WINDOW_MS) {
|
|
539
|
+
if (!isHumanAttributed(beaconTs, dispatchAt, windowMs))
|
|
540
|
+
return null;
|
|
541
|
+
return Math.max(0, now - beaconTs);
|
|
542
|
+
}
|
|
543
|
+
var CONTENTION_WINDOW_MS = 30000;
|
|
544
|
+
function contentionWarning(ms, opts) {
|
|
545
|
+
if (!opts.takeover)
|
|
546
|
+
return;
|
|
547
|
+
if (ms === null || ms >= CONTENTION_WINDOW_MS)
|
|
548
|
+
return;
|
|
549
|
+
return `a person appears to be using this tab right now: input not dispatched by this server landed ${ms}ms ago. ` + `The claim SUCCEEDED and you hold the lease — this is a warning, not a refusal. ` + `Driving the tab now will fight them for the keyboard and mouse. ` + `Consider claim_page with no target to work in your own tab instead, or ask before continuing.`;
|
|
550
|
+
}
|
|
551
|
+
function beaconSupported(driver) {
|
|
552
|
+
return typeof driver.installActivityBeacon === "function" && typeof driver.readActivityBeacon === "function";
|
|
553
|
+
}
|
|
554
|
+
async function installBeacon(driver, targetId) {
|
|
555
|
+
if (typeof driver.installActivityBeacon !== "function")
|
|
556
|
+
return false;
|
|
557
|
+
return driver.installActivityBeacon(targetId).catch(() => false);
|
|
558
|
+
}
|
|
559
|
+
async function readHumanActiveMs(driver, backend, targetId, now = Date.now()) {
|
|
560
|
+
if (typeof driver.readActivityBeacon !== "function")
|
|
561
|
+
return null;
|
|
562
|
+
const beaconTs = await driver.readActivityBeacon(targetId).catch(() => null);
|
|
563
|
+
return humanActiveMs(beaconTs, lastDispatchAt(backend, targetId), now);
|
|
564
|
+
}
|
|
565
|
+
var RENDERER_PROBE_TIMEOUT_MS = 500;
|
|
566
|
+
var RENDERER_PROBE_EXPRESSION = `[1, ${BEACON_READ_EXPRESSION}]`;
|
|
567
|
+
function rendererProbeSupported(driver) {
|
|
568
|
+
return typeof driver.probeRenderer === "function";
|
|
569
|
+
}
|
|
570
|
+
async function probeRendererActivity(driver, backend, targetId, now = Date.now()) {
|
|
571
|
+
if (typeof driver.probeRenderer !== "function")
|
|
572
|
+
return { responsive: false };
|
|
573
|
+
const { responsive, beaconTs } = await driver.probeRenderer(targetId, RENDERER_PROBE_TIMEOUT_MS).catch(() => ({ responsive: false, beaconTs: null }));
|
|
574
|
+
const ms = humanActiveMs(beaconTs, lastDispatchAt(backend, targetId), now);
|
|
575
|
+
return ms === null ? { responsive } : { responsive, humanActiveMs: ms };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
class BeaconSessions {
|
|
579
|
+
close;
|
|
580
|
+
limit;
|
|
581
|
+
live = new Map;
|
|
582
|
+
constructor(close, limit = 32) {
|
|
583
|
+
this.close = close;
|
|
584
|
+
this.limit = limit;
|
|
585
|
+
}
|
|
586
|
+
get(key) {
|
|
587
|
+
return this.live.get(key);
|
|
588
|
+
}
|
|
589
|
+
set(key, value) {
|
|
590
|
+
const previous = this.live.get(key);
|
|
591
|
+
if (previous !== undefined) {
|
|
592
|
+
this.live.delete(key);
|
|
593
|
+
this.safeClose(previous);
|
|
594
|
+
}
|
|
595
|
+
this.live.set(key, value);
|
|
596
|
+
while (this.live.size > this.limit) {
|
|
597
|
+
const oldest = this.live.keys().next();
|
|
598
|
+
if (oldest.done)
|
|
599
|
+
break;
|
|
600
|
+
this.drop(oldest.value);
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
drop(key) {
|
|
604
|
+
const value = this.live.get(key);
|
|
605
|
+
if (value === undefined)
|
|
606
|
+
return;
|
|
607
|
+
this.live.delete(key);
|
|
608
|
+
this.safeClose(value);
|
|
609
|
+
}
|
|
610
|
+
clear() {
|
|
611
|
+
for (const key of [...this.live.keys()])
|
|
612
|
+
this.drop(key);
|
|
613
|
+
}
|
|
614
|
+
get size() {
|
|
615
|
+
return this.live.size;
|
|
616
|
+
}
|
|
617
|
+
safeClose(value) {
|
|
618
|
+
try {
|
|
619
|
+
this.close(value);
|
|
620
|
+
} catch {}
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
|
|
466
624
|
// src/tools/snapshot.ts
|
|
467
625
|
var INTERACTIVE_ROLES = new Set([
|
|
468
626
|
"button",
|
|
@@ -686,11 +844,22 @@ var REQUIRED_CAPABILITIES = {
|
|
|
686
844
|
stop_screen_recording: ["capture.screencast"],
|
|
687
845
|
mock_request: ["network.intercept"],
|
|
688
846
|
list_mocks: ["network.intercept"],
|
|
689
|
-
clear_mocks: ["network.intercept"]
|
|
847
|
+
clear_mocks: ["network.intercept"],
|
|
848
|
+
dispatch_mouse: ["input.raw"],
|
|
849
|
+
wait_for_download: ["browser.downloads"],
|
|
850
|
+
grant_permissions: ["browser.permissions"]
|
|
690
851
|
};
|
|
691
852
|
function isDriverError(e) {
|
|
692
853
|
return e instanceof Error && typeof e.code === "string";
|
|
693
854
|
}
|
|
855
|
+
function interpolatePoints(from, to, steps) {
|
|
856
|
+
const out = [];
|
|
857
|
+
for (let i = 1;i < steps; i++) {
|
|
858
|
+
out.push({ x: from.x + (to.x - from.x) * i / steps, y: from.y + (to.y - from.y) * i / steps });
|
|
859
|
+
}
|
|
860
|
+
out.push({ x: to.x, y: to.y });
|
|
861
|
+
return out;
|
|
862
|
+
}
|
|
694
863
|
|
|
695
864
|
// src/cdp/driver.ts
|
|
696
865
|
function driverError(code, message, data) {
|
|
@@ -746,12 +915,21 @@ async function centerOf(conn, objectId) {
|
|
|
746
915
|
async function focusElement(conn, objectId) {
|
|
747
916
|
await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: "function(){this.focus&&this.focus();}", returnByValue: true });
|
|
748
917
|
}
|
|
749
|
-
|
|
918
|
+
var ARM_SCROLL_SETTLE_WATCH = "(function(){window.__cdpScrollSettle=new Promise((resolve)=>{let t;const done=()=>{window.removeEventListener('scroll',on,true);resolve(true);};" + "const on=()=>{clearTimeout(t);t=setTimeout(done,60);};window.addEventListener('scroll',on,true);t=setTimeout(done,60);setTimeout(done,500);});})()";
|
|
919
|
+
async function armScrollSettleWatch(conn) {
|
|
920
|
+
await conn.send("Runtime.evaluate", { expression: ARM_SCROLL_SETTLE_WATCH, returnByValue: true });
|
|
921
|
+
}
|
|
922
|
+
async function awaitScrollSettle(conn) {
|
|
923
|
+
await conn.send("Runtime.evaluate", { expression: "window.__cdpScrollSettle", awaitPromise: true, returnByValue: true }).catch(() => {
|
|
924
|
+
return;
|
|
925
|
+
});
|
|
926
|
+
}
|
|
927
|
+
async function setValueOnObject(conn, objectId, value, sendInput2) {
|
|
750
928
|
await focusElement(conn, objectId);
|
|
751
929
|
const fn = "function(){if('value'in this){this.value='';this.dispatchEvent(new Event('input',{bubbles:true}));}else if(this.isContentEditable){this.textContent='';}}";
|
|
752
930
|
await conn.send("Runtime.callFunctionOn", { objectId, functionDeclaration: fn, returnByValue: true });
|
|
753
931
|
if (value.length)
|
|
754
|
-
await
|
|
932
|
+
await sendInput2("Input.insertText", { text: value });
|
|
755
933
|
}
|
|
756
934
|
var NAMED_KEYS = {
|
|
757
935
|
enter: { key: "Enter", code: "Enter", keyCode: 13, text: "\r" },
|
|
@@ -776,6 +954,18 @@ var NAMED_KEYS = {
|
|
|
776
954
|
pagedown: { key: "PageDown", code: "PageDown", keyCode: 34 }
|
|
777
955
|
};
|
|
778
956
|
var MODIFIER_BITS = { alt: 1, control: 2, ctrl: 2, meta: 4, cmd: 4, command: 4, shift: 8 };
|
|
957
|
+
var CLICK_MODIFIER_BITS = { Alt: 1, Control: 2, Meta: 4, Shift: 8 };
|
|
958
|
+
function inputModifierBits(mods) {
|
|
959
|
+
let bits = 0;
|
|
960
|
+
for (const m of mods ?? []) {
|
|
961
|
+
const bit = CLICK_MODIFIER_BITS[m];
|
|
962
|
+
if (bit === undefined)
|
|
963
|
+
throw driverError("page-error", `unknown modifier '${m}' (use Alt, Control, Meta, or Shift)`);
|
|
964
|
+
bits |= bit;
|
|
965
|
+
}
|
|
966
|
+
return bits;
|
|
967
|
+
}
|
|
968
|
+
var HTML5_DRAG_INTERCEPT_TIMEOUT_MS = 5000;
|
|
779
969
|
function resolveKey(key) {
|
|
780
970
|
const named = NAMED_KEYS[key.toLowerCase()];
|
|
781
971
|
if (named)
|
|
@@ -940,7 +1130,11 @@ var CDP_CAPABILITIES = new Set([
|
|
|
940
1130
|
"input.insertTextAtomic",
|
|
941
1131
|
"locate.text",
|
|
942
1132
|
"locate.xpath",
|
|
943
|
-
"capture.screencast"
|
|
1133
|
+
"capture.screencast",
|
|
1134
|
+
"input.raw",
|
|
1135
|
+
"input.html5Drag",
|
|
1136
|
+
"browser.downloads",
|
|
1137
|
+
"browser.permissions"
|
|
944
1138
|
]);
|
|
945
1139
|
|
|
946
1140
|
class CdpPageDriver {
|
|
@@ -960,6 +1154,9 @@ class CdpPageDriver {
|
|
|
960
1154
|
this.enabledDomains.add(name);
|
|
961
1155
|
await this.conn.send(`${name}.enable`);
|
|
962
1156
|
}
|
|
1157
|
+
async dispatchInput(method, params = {}) {
|
|
1158
|
+
return sendInput(this.conn, "chrome", this.info.id, method, params);
|
|
1159
|
+
}
|
|
963
1160
|
async ensureDomainSoft(name) {
|
|
964
1161
|
if (this.enabledDomains.has(name))
|
|
965
1162
|
return;
|
|
@@ -968,11 +1165,22 @@ class CdpPageDriver {
|
|
|
968
1165
|
return;
|
|
969
1166
|
});
|
|
970
1167
|
}
|
|
1168
|
+
async historyEntry(direction) {
|
|
1169
|
+
const h = await this.conn.send("Page.getNavigationHistory");
|
|
1170
|
+
const entries = h.entries ?? [];
|
|
1171
|
+
const entry = entries[h.currentIndex + (direction === "back" ? -1 : 1)];
|
|
1172
|
+
if (!entry) {
|
|
1173
|
+
throw driverError("page-error", `navigate: no history entry to go ${direction} to (at entry ${h.currentIndex + 1} of ${entries.length})`);
|
|
1174
|
+
}
|
|
1175
|
+
return entry;
|
|
1176
|
+
}
|
|
971
1177
|
async navigate(opts) {
|
|
972
1178
|
await this.ensureDomain("Page");
|
|
973
1179
|
const reload = opts.reload === true;
|
|
974
|
-
|
|
1180
|
+
const history = opts.history;
|
|
1181
|
+
if (!reload && !history && !opts.url)
|
|
975
1182
|
throw driverError("page-error", "navigate: 'url' is required unless reload:true");
|
|
1183
|
+
const entry = history ? await this.historyEntry(history) : undefined;
|
|
976
1184
|
const waitUntil = opts.waitUntil ?? "load";
|
|
977
1185
|
const milestoneMethod = waitUntil === "load" ? "Page.loadEventFired" : "Page.domContentEventFired";
|
|
978
1186
|
const milestone = this.conn.waitFor(milestoneMethod, undefined, opts.timeoutMs).then(() => waitUntil).catch(() => {
|
|
@@ -990,6 +1198,10 @@ class CdpPageDriver {
|
|
|
990
1198
|
contextId = tree?.frameTree?.frame?.id ?? "";
|
|
991
1199
|
url = tree?.frameTree?.frame?.url ?? opts.url ?? "";
|
|
992
1200
|
reloaded = true;
|
|
1201
|
+
} else if (entry) {
|
|
1202
|
+
await this.conn.send("Page.navigateToHistoryEntry", { entryId: entry.id }, opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {});
|
|
1203
|
+
contextId = "";
|
|
1204
|
+
url = entry.url;
|
|
993
1205
|
} else {
|
|
994
1206
|
const nav = await this.conn.send("Page.navigate", { url: opts.url }, opts.timeoutMs ? { timeoutMs: opts.timeoutMs } : {});
|
|
995
1207
|
if (nav.errorText)
|
|
@@ -998,7 +1210,20 @@ class CdpPageDriver {
|
|
|
998
1210
|
url = opts.url;
|
|
999
1211
|
}
|
|
1000
1212
|
const settled = await Promise.race([milestone, stopped]);
|
|
1001
|
-
|
|
1213
|
+
if (entry) {
|
|
1214
|
+
const tree = await this.conn.send("Page.getFrameTree").catch(() => {
|
|
1215
|
+
return;
|
|
1216
|
+
});
|
|
1217
|
+
contextId = tree?.frameTree?.frame?.id ?? contextId;
|
|
1218
|
+
url = tree?.frameTree?.frame?.url ?? url;
|
|
1219
|
+
}
|
|
1220
|
+
return {
|
|
1221
|
+
url,
|
|
1222
|
+
contextId,
|
|
1223
|
+
...reloaded ? { reloaded } : {},
|
|
1224
|
+
...history ? { traversed: history } : {},
|
|
1225
|
+
waitedFor: settled ?? "timeout"
|
|
1226
|
+
};
|
|
1002
1227
|
}
|
|
1003
1228
|
async waitForText(text, timeoutMs = 15000, pollMs = 250) {
|
|
1004
1229
|
await this.ensureDomain("Runtime");
|
|
@@ -1078,36 +1303,96 @@ class CdpPageDriver {
|
|
|
1078
1303
|
async hover(loc) {
|
|
1079
1304
|
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1080
1305
|
const { x, y } = await centerOf(this.conn, objectId);
|
|
1081
|
-
await this.
|
|
1306
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseMoved", x, y });
|
|
1082
1307
|
return { x, y };
|
|
1083
1308
|
}
|
|
1084
1309
|
async click(loc, opts) {
|
|
1085
1310
|
const button = opts?.button ?? "left";
|
|
1086
1311
|
const clickCount = opts?.clickCount ?? 1;
|
|
1312
|
+
const modifiers = inputModifierBits(opts?.modifiers);
|
|
1087
1313
|
const { x, y } = await this.hover(loc);
|
|
1088
|
-
await this.
|
|
1089
|
-
await this.
|
|
1314
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mousePressed", x, y, button, clickCount, modifiers });
|
|
1315
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseReleased", x, y, button, clickCount, modifiers });
|
|
1316
|
+
return { x, y };
|
|
1317
|
+
}
|
|
1318
|
+
async scroll(anchor, opts) {
|
|
1319
|
+
await this.ensureDomain("Runtime");
|
|
1320
|
+
const { x, y } = await this.resolveScrollPoint(anchor);
|
|
1321
|
+
await armScrollSettleWatch(this.conn);
|
|
1322
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseWheel", x, y, deltaX: opts.deltaX ?? 0, deltaY: opts.deltaY ?? 0 });
|
|
1323
|
+
await awaitScrollSettle(this.conn);
|
|
1090
1324
|
return { x, y };
|
|
1091
1325
|
}
|
|
1092
|
-
async
|
|
1326
|
+
async resolveScrollPoint(anchor) {
|
|
1327
|
+
if (anchor === undefined) {
|
|
1328
|
+
await this.ensureDomain("Runtime");
|
|
1329
|
+
const { result, exceptionDetails } = await this.conn.send("Runtime.evaluate", { expression: "({x: window.innerWidth / 2, y: window.innerHeight / 2})", returnByValue: true });
|
|
1330
|
+
if (exceptionDetails || !result.value)
|
|
1331
|
+
throw driverError("page-error", `could not measure viewport for scroll center: ${exceptionDetails?.text ?? "no value"}`);
|
|
1332
|
+
return result.value;
|
|
1333
|
+
}
|
|
1334
|
+
if ("x" in anchor)
|
|
1335
|
+
return anchor;
|
|
1336
|
+
const { objectId } = await resolveElementLocator(this.conn, anchor);
|
|
1337
|
+
return centerOf(this.conn, objectId);
|
|
1338
|
+
}
|
|
1339
|
+
async drag(from, to, opts) {
|
|
1093
1340
|
const fromPt = await this.hover(from);
|
|
1094
|
-
const
|
|
1095
|
-
const
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1341
|
+
const toPt = await this.resolveDragDestination(to, fromPt);
|
|
1342
|
+
const steps = Math.max(1, Math.trunc(opts?.steps ?? 2));
|
|
1343
|
+
if ((opts?.mode ?? "mouse") === "html5") {
|
|
1344
|
+
await this.html5Drag(fromPt, toPt, steps);
|
|
1345
|
+
return { from: fromPt, to: toPt };
|
|
1346
|
+
}
|
|
1347
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mousePressed", x: fromPt.x, y: fromPt.y, button: "left", clickCount: 1 });
|
|
1348
|
+
for (const pt of interpolatePoints(fromPt, toPt, steps)) {
|
|
1349
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseMoved", x: pt.x, y: pt.y, button: "left" });
|
|
1350
|
+
}
|
|
1351
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseReleased", x: toPt.x, y: toPt.y, button: "left", clickCount: 1 });
|
|
1100
1352
|
return { from: fromPt, to: toPt };
|
|
1101
1353
|
}
|
|
1354
|
+
async resolveDragDestination(to, fromPt) {
|
|
1355
|
+
if ("dx" in to)
|
|
1356
|
+
return { x: fromPt.x + to.dx, y: fromPt.y + to.dy };
|
|
1357
|
+
if ("x" in to)
|
|
1358
|
+
return to;
|
|
1359
|
+
const { objectId } = await resolveElementLocator(this.conn, to);
|
|
1360
|
+
return centerOf(this.conn, objectId);
|
|
1361
|
+
}
|
|
1362
|
+
async html5Drag(fromPt, toPt, steps) {
|
|
1363
|
+
await this.dispatchInput("Input.setInterceptDrags", { enabled: true });
|
|
1364
|
+
try {
|
|
1365
|
+
const intercepted = this.conn.waitFor("Input.dragIntercepted", undefined, HTML5_DRAG_INTERCEPT_TIMEOUT_MS).catch(() => {
|
|
1366
|
+
return;
|
|
1367
|
+
});
|
|
1368
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mousePressed", x: fromPt.x, y: fromPt.y, button: "left", clickCount: 1 });
|
|
1369
|
+
for (const pt of interpolatePoints(fromPt, toPt, steps)) {
|
|
1370
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseMoved", x: pt.x, y: pt.y, button: "left" });
|
|
1371
|
+
}
|
|
1372
|
+
const event = await intercepted;
|
|
1373
|
+
if (!event?.data) {
|
|
1374
|
+
throw driverError("page-error", `drag mode:"html5": the page never started a drag (no Input.dragIntercepted within ${HTML5_DRAG_INTERCEPT_TIMEOUT_MS}ms). ` + 'The source element is probably not draggable — HTML5 drag-and-drop needs draggable="true" (or a native draggable element) ' + 'plus a dragstart handler. Use the default mode:"mouse" for widgets built on raw pointer events.');
|
|
1375
|
+
}
|
|
1376
|
+
const data = event.data;
|
|
1377
|
+
for (const type of ["dragEnter", "dragOver", "drop"]) {
|
|
1378
|
+
await this.dispatchInput("Input.dispatchDragEvent", { type, x: toPt.x, y: toPt.y, data });
|
|
1379
|
+
}
|
|
1380
|
+
await this.dispatchInput("Input.dispatchMouseEvent", { type: "mouseReleased", x: toPt.x, y: toPt.y, button: "left", clickCount: 1 });
|
|
1381
|
+
} finally {
|
|
1382
|
+
await this.dispatchInput("Input.setInterceptDrags", { enabled: false }).catch(() => {
|
|
1383
|
+
return;
|
|
1384
|
+
});
|
|
1385
|
+
}
|
|
1386
|
+
}
|
|
1102
1387
|
async setValue(loc, value) {
|
|
1103
1388
|
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1104
|
-
await setValueOnObject(this.conn, objectId, value);
|
|
1389
|
+
await setValueOnObject(this.conn, objectId, value, (method, params) => this.dispatchInput(method, params));
|
|
1105
1390
|
}
|
|
1106
1391
|
async typeText(loc, text) {
|
|
1107
1392
|
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
1108
1393
|
await focusElement(this.conn, objectId);
|
|
1109
1394
|
if (text.length)
|
|
1110
|
-
await this.
|
|
1395
|
+
await this.dispatchInput("Input.insertText", { text });
|
|
1111
1396
|
}
|
|
1112
1397
|
async pressKey(press) {
|
|
1113
1398
|
const spec = resolveKey(press.key);
|
|
@@ -1122,8 +1407,8 @@ class CdpPageDriver {
|
|
|
1122
1407
|
const down = { type: "keyDown", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode, modifiers: modBits };
|
|
1123
1408
|
if (spec.text && !suppressText)
|
|
1124
1409
|
down.text = spec.text;
|
|
1125
|
-
await this.
|
|
1126
|
-
await this.
|
|
1410
|
+
await this.dispatchInput("Input.dispatchKeyEvent", down);
|
|
1411
|
+
await this.dispatchInput("Input.dispatchKeyEvent", { type: "keyUp", key: spec.key, code: spec.code, windowsVirtualKeyCode: spec.keyCode, nativeVirtualKeyCode: spec.keyCode, modifiers: modBits });
|
|
1127
1412
|
}
|
|
1128
1413
|
async setFiles(loc, files) {
|
|
1129
1414
|
const { objectId } = await resolveElementLocator(this.conn, loc);
|
|
@@ -1339,6 +1624,36 @@ class CdpPageDriver {
|
|
|
1339
1624
|
this.conn.close();
|
|
1340
1625
|
}
|
|
1341
1626
|
}
|
|
1627
|
+
var beaconSessions = new BeaconSessions((conn) => conn.close());
|
|
1628
|
+
async function targetWsUrl(targetId) {
|
|
1629
|
+
const hit = (await listTargets()).find((t) => t.id === targetId);
|
|
1630
|
+
return hit?.webSocketDebuggerUrl;
|
|
1631
|
+
}
|
|
1632
|
+
async function readBeaconOver(conn) {
|
|
1633
|
+
try {
|
|
1634
|
+
const res = await conn.send("Runtime.evaluate", {
|
|
1635
|
+
expression: BEACON_READ_EXPRESSION,
|
|
1636
|
+
returnByValue: true
|
|
1637
|
+
});
|
|
1638
|
+
const value = res.result?.value;
|
|
1639
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
1640
|
+
} catch {
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async function probeOver(conn, timeoutMs) {
|
|
1645
|
+
try {
|
|
1646
|
+
const res = await conn.send("Runtime.evaluate", { expression: RENDERER_PROBE_EXPRESSION, returnByValue: true }, { timeoutMs });
|
|
1647
|
+
const value = res.result?.value;
|
|
1648
|
+
if (Array.isArray(value) && value[0] === 1) {
|
|
1649
|
+
const ts = value[1];
|
|
1650
|
+
return { responsive: true, beaconTs: typeof ts === "number" && Number.isFinite(ts) ? ts : null };
|
|
1651
|
+
}
|
|
1652
|
+
return { responsive: false, beaconTs: null };
|
|
1653
|
+
} catch {
|
|
1654
|
+
return { responsive: false, beaconTs: null };
|
|
1655
|
+
}
|
|
1656
|
+
}
|
|
1342
1657
|
async function withBrowserConn(fn) {
|
|
1343
1658
|
const conn = await openBrowser();
|
|
1344
1659
|
try {
|
|
@@ -1377,6 +1692,8 @@ class CdpBrowserDriver {
|
|
|
1377
1692
|
async closePage(id) {
|
|
1378
1693
|
return withBrowserConn(async (conn) => {
|
|
1379
1694
|
const res = await conn.send("Target.closeTarget", { targetId: id });
|
|
1695
|
+
if (res.success !== false)
|
|
1696
|
+
beaconSessions.drop(id);
|
|
1380
1697
|
return { success: res.success ?? true };
|
|
1381
1698
|
});
|
|
1382
1699
|
}
|
|
@@ -1389,7 +1706,83 @@ class CdpBrowserDriver {
|
|
|
1389
1706
|
const { conn, target } = await openPage(selector).catch(noSuchTarget);
|
|
1390
1707
|
return new CdpPageDriver(conn, target, this);
|
|
1391
1708
|
}
|
|
1392
|
-
async
|
|
1709
|
+
async installActivityBeacon(targetId) {
|
|
1710
|
+
const held = beaconSessions.get(targetId);
|
|
1711
|
+
if (held) {
|
|
1712
|
+
const rearmed = await held.send("Runtime.evaluate", { expression: BEACON_SOURCE, returnByValue: true }).then(() => true).catch(() => false);
|
|
1713
|
+
if (rearmed)
|
|
1714
|
+
return true;
|
|
1715
|
+
beaconSessions.drop(targetId);
|
|
1716
|
+
}
|
|
1717
|
+
const wsUrl = await targetWsUrl(targetId).catch(() => {
|
|
1718
|
+
return;
|
|
1719
|
+
});
|
|
1720
|
+
if (!wsUrl)
|
|
1721
|
+
return false;
|
|
1722
|
+
const conn = await new CdpConnection(wsUrl).connect().catch(() => {
|
|
1723
|
+
return;
|
|
1724
|
+
});
|
|
1725
|
+
if (!conn)
|
|
1726
|
+
return false;
|
|
1727
|
+
try {
|
|
1728
|
+
await conn.send("Page.enable");
|
|
1729
|
+
await conn.send("Page.addScriptToEvaluateOnNewDocument", { source: BEACON_SOURCE });
|
|
1730
|
+
await conn.send("Runtime.evaluate", { expression: BEACON_SOURCE, returnByValue: true });
|
|
1731
|
+
} catch {
|
|
1732
|
+
conn.close();
|
|
1733
|
+
return false;
|
|
1734
|
+
}
|
|
1735
|
+
beaconSessions.set(targetId, conn);
|
|
1736
|
+
return true;
|
|
1737
|
+
}
|
|
1738
|
+
async readActivityBeacon(targetId) {
|
|
1739
|
+
const held = beaconSessions.get(targetId);
|
|
1740
|
+
if (held) {
|
|
1741
|
+
const value = await readBeaconOver(held);
|
|
1742
|
+
if (value !== undefined)
|
|
1743
|
+
return value;
|
|
1744
|
+
beaconSessions.drop(targetId);
|
|
1745
|
+
}
|
|
1746
|
+
const wsUrl = await targetWsUrl(targetId).catch(() => {
|
|
1747
|
+
return;
|
|
1748
|
+
});
|
|
1749
|
+
if (!wsUrl)
|
|
1750
|
+
return null;
|
|
1751
|
+
const conn = await new CdpConnection(wsUrl).connect().catch(() => {
|
|
1752
|
+
return;
|
|
1753
|
+
});
|
|
1754
|
+
if (!conn)
|
|
1755
|
+
return null;
|
|
1756
|
+
try {
|
|
1757
|
+
return await readBeaconOver(conn) ?? null;
|
|
1758
|
+
} finally {
|
|
1759
|
+
conn.close();
|
|
1760
|
+
}
|
|
1761
|
+
}
|
|
1762
|
+
async probeRenderer(targetId, timeoutMs = RENDERER_PROBE_TIMEOUT_MS) {
|
|
1763
|
+
const held = beaconSessions.get(targetId);
|
|
1764
|
+
if (held)
|
|
1765
|
+
return probeOver(held, timeoutMs);
|
|
1766
|
+
const wsUrl = await targetWsUrl(targetId).catch(() => {
|
|
1767
|
+
return;
|
|
1768
|
+
});
|
|
1769
|
+
if (!wsUrl)
|
|
1770
|
+
return { responsive: false, beaconTs: null };
|
|
1771
|
+
const conn = new CdpConnection(wsUrl, { timeoutMs });
|
|
1772
|
+
try {
|
|
1773
|
+
await conn.connect();
|
|
1774
|
+
} catch {
|
|
1775
|
+
return { responsive: false, beaconTs: null };
|
|
1776
|
+
}
|
|
1777
|
+
try {
|
|
1778
|
+
return await probeOver(conn, timeoutMs);
|
|
1779
|
+
} finally {
|
|
1780
|
+
conn.close();
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
async dispose() {
|
|
1784
|
+
beaconSessions.clear();
|
|
1785
|
+
}
|
|
1393
1786
|
}
|
|
1394
1787
|
function createCdpDriver() {
|
|
1395
1788
|
return new CdpBrowserDriver;
|
|
@@ -1429,6 +1822,20 @@ async function recordOrigin(backend, targetId, opts = {}) {
|
|
|
1429
1822
|
await writeFile2(file, JSON.stringify(record), { flag: "wx" });
|
|
1430
1823
|
} catch {}
|
|
1431
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
|
+
}
|
|
1432
1839
|
async function listOrigins(opts = {}) {
|
|
1433
1840
|
let names = [];
|
|
1434
1841
|
try {
|
|
@@ -1486,59 +1893,51 @@ async function newTrackedPage(driver, backend, opts = {}) {
|
|
|
1486
1893
|
return page;
|
|
1487
1894
|
}
|
|
1488
1895
|
|
|
1489
|
-
// src/
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
throw new LeaseToolError(`claim_page: no page target with id '${targetId}'`);
|
|
1509
|
-
url = existing.url;
|
|
1896
|
+
// src/reap.ts
|
|
1897
|
+
function staleAgentTabs(input) {
|
|
1898
|
+
const live = new Set(input.livePageIds);
|
|
1899
|
+
const out = [];
|
|
1900
|
+
for (const row of input.leases) {
|
|
1901
|
+
if (row.backend !== input.backend)
|
|
1902
|
+
continue;
|
|
1903
|
+
if (row.unreadable !== undefined)
|
|
1904
|
+
continue;
|
|
1905
|
+
if (row.stale !== "dead-pid" && row.stale !== "expired")
|
|
1906
|
+
continue;
|
|
1907
|
+
if (row.stale === "expired" && input.now - row.lastUsedAt <= row.ttlMs + input.reapGraceMs)
|
|
1908
|
+
continue;
|
|
1909
|
+
if (!live.has(row.targetId))
|
|
1910
|
+
continue;
|
|
1911
|
+
const origin = input.origins.get(row.targetId);
|
|
1912
|
+
if (origin === undefined || origin.unreadable !== undefined)
|
|
1913
|
+
continue;
|
|
1914
|
+
out.push({ targetId: row.targetId, label: row.label, reason: row.stale });
|
|
1510
1915
|
}
|
|
1511
|
-
|
|
1512
|
-
const { record, token } = await claimLease(backend, targetId, {
|
|
1513
|
-
label,
|
|
1514
|
-
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
1515
|
-
liveIds
|
|
1516
|
-
});
|
|
1517
|
-
return {
|
|
1518
|
-
lease: token,
|
|
1519
|
-
targetId,
|
|
1520
|
-
url,
|
|
1521
|
-
label: record.label,
|
|
1522
|
-
ttlMs: record.ttlMs,
|
|
1523
|
-
expiresAt: record.lastUsedAt + record.ttlMs
|
|
1524
|
-
};
|
|
1916
|
+
return out;
|
|
1525
1917
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1918
|
+
var DEFAULT_REAP_GRACE_MS = 2700000;
|
|
1919
|
+
function reapGraceMs() {
|
|
1920
|
+
const raw = Number(process.env.CDP_REAP_GRACE_MS);
|
|
1921
|
+
return Number.isFinite(raw) && raw >= 0 ? raw : DEFAULT_REAP_GRACE_MS;
|
|
1922
|
+
}
|
|
1923
|
+
async function reapStaleAgentTabs(driver, backend) {
|
|
1924
|
+
const now = Date.now();
|
|
1925
|
+
const livePageIds = (await driver.listPages()).map((p) => p.id);
|
|
1926
|
+
const [origins, leases] = await Promise.all([
|
|
1927
|
+
originIndex(backend, undefined),
|
|
1928
|
+
listLeases({ now, liveIds: livePageIds, liveBackend: backend }).catch(() => [])
|
|
1929
|
+
]);
|
|
1930
|
+
const candidates = staleAgentTabs({ backend, livePageIds, origins, leases, now, reapGraceMs: reapGraceMs() });
|
|
1931
|
+
const closed = [];
|
|
1932
|
+
for (const c of candidates) {
|
|
1933
|
+
const res = await driver.closePage(c.targetId).catch(() => ({ success: false }));
|
|
1934
|
+
if (res.success !== true)
|
|
1935
|
+
continue;
|
|
1936
|
+
await releaseLeaseFor(backend, c.targetId);
|
|
1937
|
+
closed.push(c);
|
|
1529
1938
|
}
|
|
1530
|
-
return
|
|
1531
|
-
}
|
|
1532
|
-
async function listLeasesTool(driver, _args = {}) {
|
|
1533
|
-
const liveIds = (await driver.listPages()).map((p) => p.id);
|
|
1534
|
-
const leases = await listLeases({ liveIds, liveBackend: backendOf(driver) });
|
|
1535
|
-
return { leases, count: leases.length };
|
|
1939
|
+
return closed;
|
|
1536
1940
|
}
|
|
1537
|
-
var LEASE_TOOLS = {
|
|
1538
|
-
claim_page: claimPage,
|
|
1539
|
-
release_page: releasePage,
|
|
1540
|
-
list_leases: listLeasesTool
|
|
1541
|
-
};
|
|
1542
1941
|
|
|
1543
1942
|
// src/shared-tools.ts
|
|
1544
1943
|
import { mkdir as mkdir3, writeFile as writeFile3 } from "node:fs/promises";
|
|
@@ -1551,7 +1950,7 @@ class SharedToolError extends Error {
|
|
|
1551
1950
|
function target3(p) {
|
|
1552
1951
|
return { id: p.id, url: p.url, title: p.title };
|
|
1553
1952
|
}
|
|
1554
|
-
function
|
|
1953
|
+
function backendOf(driver) {
|
|
1555
1954
|
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
1556
1955
|
}
|
|
1557
1956
|
async function withPage2(driver, target, fn) {
|
|
@@ -1565,7 +1964,7 @@ async function withPage2(driver, target, fn) {
|
|
|
1565
1964
|
async function resolvePage(driver, selector) {
|
|
1566
1965
|
const pages = await driver.listPages();
|
|
1567
1966
|
const hit = await pickPage(driver, pages, selector);
|
|
1568
|
-
await assertLeaseOk(
|
|
1967
|
+
await assertLeaseOk(backendOf(driver), hit.id, { url: hit.url, title: hit.title });
|
|
1569
1968
|
return hit;
|
|
1570
1969
|
}
|
|
1571
1970
|
async function pickPage(driver, pages, selector) {
|
|
@@ -1620,27 +2019,43 @@ async function reapSet(driver, pages, all) {
|
|
|
1620
2019
|
}
|
|
1621
2020
|
}
|
|
1622
2021
|
async function listPages(driver, args = {}) {
|
|
1623
|
-
const
|
|
1624
|
-
const
|
|
1625
|
-
const
|
|
2022
|
+
const backend = backendOf(driver);
|
|
2023
|
+
const reaped = requireLease() ? await reapStaleAgentTabs(driver, backend) : [];
|
|
2024
|
+
const reapedIds = new Set(reaped.map((r) => r.targetId));
|
|
2025
|
+
const pages = (await driver.listPages({ all: args.all })).filter((p) => !reapedIds.has(p.id));
|
|
2026
|
+
const ledger = await originIndex(backend, await reapSet(driver, pages, args.all));
|
|
2027
|
+
const now = Date.now();
|
|
2028
|
+
const leaseByTarget = new Map((await listLeases({ now, liveIds: pages.map((p) => p.id), liveBackend: backend })).filter((l) => l.backend === backend && l.unreadable === undefined).map((l) => [l.targetId, l]));
|
|
2029
|
+
const doProbe = args.probe === true && rendererProbeSupported(driver);
|
|
2030
|
+
const annotated = await Promise.all(pages.map(async (p) => {
|
|
1626
2031
|
const rec = ledger.get(p.id);
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
if (
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
2032
|
+
const base = !rec ? { ...p, origin: "unknown" } : rec.unreadable !== undefined ? { ...p, origin: "unknown", originUnreadable: rec.unreadable } : { ...p, origin: "agent", label: rec.label, createdAt: rec.createdAt };
|
|
2033
|
+
const leaseRow = leaseByTarget.get(p.id);
|
|
2034
|
+
if (leaseRow) {
|
|
2035
|
+
base.lease = { label: leaseRow.label, pid: leaseRow.pid, idleMs: now - leaseRow.lastUsedAt, expiresAt: leaseRow.lastUsedAt + leaseRow.ttlMs, stale: leaseRow.stale };
|
|
2036
|
+
}
|
|
2037
|
+
if (doProbe && (p.type === undefined || p.type === "page")) {
|
|
2038
|
+
const probe = await probeRendererActivity(driver, backend, p.id, now);
|
|
2039
|
+
base.responsive = probe.responsive;
|
|
2040
|
+
if (probe.humanActiveMs !== undefined)
|
|
2041
|
+
base.humanActiveMs = probe.humanActiveMs;
|
|
2042
|
+
}
|
|
2043
|
+
return base;
|
|
2044
|
+
}));
|
|
2045
|
+
return { pages: annotated, count: annotated.length, ...reaped.length ? { reaped } : {} };
|
|
1634
2046
|
}
|
|
1635
2047
|
async function newPage(driver, args = {}) {
|
|
1636
2048
|
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
1637
|
-
const p = await newTrackedPage(driver,
|
|
1638
|
-
|
|
2049
|
+
const p = await newTrackedPage(driver, backendOf(driver), { url: args.url, label });
|
|
2050
|
+
const explicit = args.claim === true;
|
|
2051
|
+
if (!explicit && !requireLease())
|
|
1639
2052
|
return { targetId: p.id, url: p.url };
|
|
1640
|
-
const { record, token } = await claimLease(
|
|
2053
|
+
const { record, token } = await claimLease(backendOf(driver), p.id, {
|
|
1641
2054
|
label,
|
|
1642
|
-
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs()
|
|
2055
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
2056
|
+
auto: !explicit
|
|
1643
2057
|
});
|
|
2058
|
+
await installBeacon(driver, p.id);
|
|
1644
2059
|
return { targetId: p.id, url: p.url, lease: token, label: record.label, expiresAt: record.lastUsedAt + record.ttlMs };
|
|
1645
2060
|
}
|
|
1646
2061
|
async function closePage(driver, args) {
|
|
@@ -1650,7 +2065,7 @@ async function closePage(driver, args) {
|
|
|
1650
2065
|
const res = await driver.closePage(p.id);
|
|
1651
2066
|
if (!res.success)
|
|
1652
2067
|
return { closed: p.id, success: false, leaseReleased: false };
|
|
1653
|
-
const released = await releaseLeaseFor(
|
|
2068
|
+
const released = await releaseLeaseFor(backendOf(driver), p.id);
|
|
1654
2069
|
return { closed: p.id, success: true, ...released.released ? { leaseReleased: true } : {} };
|
|
1655
2070
|
}
|
|
1656
2071
|
async function writeSelectedFile(id) {
|
|
@@ -1672,17 +2087,36 @@ function toLegacyWaitedFor(w) {
|
|
|
1672
2087
|
return "navigate-only";
|
|
1673
2088
|
return w;
|
|
1674
2089
|
}
|
|
1675
|
-
|
|
2090
|
+
function resolveNavigateMode(args) {
|
|
1676
2091
|
const reload = args.reload === true;
|
|
1677
|
-
|
|
1678
|
-
|
|
2092
|
+
const hasUrl = typeof args.url === "string" && args.url.length > 0;
|
|
2093
|
+
const hasHistory = args.history !== undefined && args.history !== null;
|
|
2094
|
+
const given = [hasUrl ? "url" : "", reload ? "reload" : "", hasHistory ? "history" : ""].filter(Boolean);
|
|
2095
|
+
if (given.length > 1) {
|
|
2096
|
+
throw new SharedToolError(`navigate_page: 'url', 'reload' and 'history' are mutually exclusive (got ${given.join(" + ")})`);
|
|
2097
|
+
}
|
|
2098
|
+
if (hasHistory) {
|
|
2099
|
+
if (args.history !== "back" && args.history !== "forward") {
|
|
2100
|
+
throw new SharedToolError(`navigate_page: 'history' must be 'back' or 'forward' (got ${JSON.stringify(args.history)})`);
|
|
2101
|
+
}
|
|
2102
|
+
return { mode: "history", history: args.history };
|
|
2103
|
+
}
|
|
2104
|
+
if (reload)
|
|
2105
|
+
return { mode: "reload" };
|
|
2106
|
+
if (!hasUrl) {
|
|
2107
|
+
throw new SharedToolError("navigate_page: 'url' is required (or pass reload:true to reload the current page, or history:'back'|'forward' to go back/forward in this tab's session history)");
|
|
1679
2108
|
}
|
|
2109
|
+
return { mode: "url", url: args.url };
|
|
2110
|
+
}
|
|
2111
|
+
async function navigatePage(driver, args) {
|
|
2112
|
+
const mode = resolveNavigateMode(args);
|
|
1680
2113
|
return withPage2(driver, args.target, async (page) => {
|
|
1681
|
-
const r = await page.navigate(args);
|
|
2114
|
+
const r = await page.navigate(mode.mode === "history" ? { ...args, history: mode.history } : args);
|
|
1682
2115
|
return {
|
|
1683
2116
|
url: r.url,
|
|
1684
2117
|
frameId: r.contextId,
|
|
1685
2118
|
...r.reloaded ? { reloaded: r.reloaded } : {},
|
|
2119
|
+
...r.traversed ? { traversed: r.traversed } : {},
|
|
1686
2120
|
waitedFor: toLegacyWaitedFor(r.waitedFor)
|
|
1687
2121
|
};
|
|
1688
2122
|
});
|
|
@@ -1828,22 +2262,113 @@ async function takeSnapshot(driver, args = {}) {
|
|
|
1828
2262
|
}
|
|
1829
2263
|
async function click(driver, args) {
|
|
1830
2264
|
return withPage2(driver, args.target, async (page) => {
|
|
1831
|
-
const { x, y } = await page.click(locatorOf(args), { button: args.button ?? "left", clickCount: args.clickCount ?? 1 });
|
|
2265
|
+
const { x, y } = await page.click(locatorOf(args), { button: args.button ?? "left", clickCount: args.clickCount ?? 1, modifiers: args.modifiers });
|
|
1832
2266
|
return { clicked: true, x, y };
|
|
1833
2267
|
});
|
|
1834
2268
|
}
|
|
2269
|
+
function resolveScrollAnchor(args) {
|
|
2270
|
+
const hasUid = args.uid !== undefined && args.uid !== null && args.uid !== "";
|
|
2271
|
+
const hasSelector = typeof args.selector === "string" && args.selector.length > 0;
|
|
2272
|
+
const hasX = args.x !== undefined;
|
|
2273
|
+
const hasY = args.y !== undefined;
|
|
2274
|
+
if (hasX !== hasY)
|
|
2275
|
+
throw new SharedToolError("scroll: 'x' and 'y' must be provided together");
|
|
2276
|
+
const hasXY = hasX && hasY;
|
|
2277
|
+
const anchorCount = [hasUid, hasSelector, hasXY].filter(Boolean).length;
|
|
2278
|
+
if (anchorCount > 1)
|
|
2279
|
+
throw new SharedToolError("scroll: provide at most one of { uid }, { selector }, or { x, y } (omit all three to scroll at the viewport center)");
|
|
2280
|
+
if (hasUid || hasSelector)
|
|
2281
|
+
return locatorOf({ uid: args.uid, selector: args.selector });
|
|
2282
|
+
if (hasXY)
|
|
2283
|
+
return { x: args.x, y: args.y };
|
|
2284
|
+
return;
|
|
2285
|
+
}
|
|
2286
|
+
function resolveScrollDelta(args) {
|
|
2287
|
+
const deltaX = args.deltaX ?? 0;
|
|
2288
|
+
const deltaY = args.deltaY ?? 0;
|
|
2289
|
+
if (deltaX === 0 && deltaY === 0)
|
|
2290
|
+
throw new SharedToolError("scroll requires at least one of { deltaX } or { deltaY }");
|
|
2291
|
+
return { deltaX, deltaY };
|
|
2292
|
+
}
|
|
2293
|
+
async function scroll(driver, args) {
|
|
2294
|
+
const anchor = resolveScrollAnchor(args);
|
|
2295
|
+
const { deltaX, deltaY } = resolveScrollDelta(args);
|
|
2296
|
+
return withPage2(driver, args.target, async (page) => {
|
|
2297
|
+
const { x, y } = await page.scroll(anchor, { deltaX, deltaY });
|
|
2298
|
+
return { x, y, deltaX, deltaY, target: target3(page.info) };
|
|
2299
|
+
});
|
|
2300
|
+
}
|
|
1835
2301
|
async function hover(driver, args) {
|
|
1836
2302
|
return withPage2(driver, args.target, async (page) => {
|
|
1837
2303
|
const { x, y } = await page.hover(locatorOf(args));
|
|
1838
2304
|
return { hovered: true, x, y };
|
|
1839
2305
|
});
|
|
1840
2306
|
}
|
|
2307
|
+
function resolveDragTo(to) {
|
|
2308
|
+
const hasUid = to.uid !== undefined && to.uid !== null && to.uid !== "";
|
|
2309
|
+
const hasSelector = typeof to.selector === "string" && to.selector.length > 0;
|
|
2310
|
+
const hasX = to.x !== undefined;
|
|
2311
|
+
const hasY = to.y !== undefined;
|
|
2312
|
+
if (hasX !== hasY)
|
|
2313
|
+
throw new SharedToolError("drag: 'to.x' and 'to.y' must be provided together");
|
|
2314
|
+
const hasXY = hasX && hasY;
|
|
2315
|
+
const count = [hasUid, hasSelector, hasXY].filter(Boolean).length;
|
|
2316
|
+
if (count !== 1)
|
|
2317
|
+
throw new SharedToolError("drag: 'to' takes exactly one of { uid }, { selector }, or { x, y }");
|
|
2318
|
+
if (hasXY) {
|
|
2319
|
+
if (!Number.isFinite(to.x) || !Number.isFinite(to.y))
|
|
2320
|
+
throw new SharedToolError("drag: 'to.x' and 'to.y' must be finite numbers");
|
|
2321
|
+
return { x: to.x, y: to.y };
|
|
2322
|
+
}
|
|
2323
|
+
return locatorOf({ uid: to.uid, selector: to.selector });
|
|
2324
|
+
}
|
|
2325
|
+
function resolveDragDestination(args) {
|
|
2326
|
+
const hasTo = args.to !== undefined && args.to !== null;
|
|
2327
|
+
const hasBy = args.by !== undefined && args.by !== null;
|
|
2328
|
+
if (hasTo === hasBy)
|
|
2329
|
+
throw new SharedToolError("drag requires exactly one of { to } or { by }");
|
|
2330
|
+
if (hasBy) {
|
|
2331
|
+
const by = args.by;
|
|
2332
|
+
const hasDx = by.dx !== undefined;
|
|
2333
|
+
const hasDy = by.dy !== undefined;
|
|
2334
|
+
if (!hasDx && !hasDy)
|
|
2335
|
+
throw new SharedToolError("drag: 'by' requires at least one of { dx } or { dy }");
|
|
2336
|
+
const dx = by.dx ?? 0;
|
|
2337
|
+
const dy = by.dy ?? 0;
|
|
2338
|
+
if (!Number.isFinite(dx) || !Number.isFinite(dy))
|
|
2339
|
+
throw new SharedToolError("drag: 'by.dx' and 'by.dy' must be finite numbers");
|
|
2340
|
+
return { dx, dy };
|
|
2341
|
+
}
|
|
2342
|
+
return resolveDragTo(args.to);
|
|
2343
|
+
}
|
|
2344
|
+
var MAX_DRAG_STEPS = 500;
|
|
2345
|
+
function resolveDragSteps(steps) {
|
|
2346
|
+
if (steps === undefined)
|
|
2347
|
+
return 2;
|
|
2348
|
+
if (typeof steps !== "number" || !Number.isInteger(steps) || steps < 1 || steps > MAX_DRAG_STEPS) {
|
|
2349
|
+
throw new SharedToolError(`drag: 'steps' must be an integer between 1 and ${MAX_DRAG_STEPS} (default 2)`);
|
|
2350
|
+
}
|
|
2351
|
+
return steps;
|
|
2352
|
+
}
|
|
2353
|
+
function resolveDragMode(mode, capabilities) {
|
|
2354
|
+
if (mode === undefined || mode === "mouse")
|
|
2355
|
+
return "mouse";
|
|
2356
|
+
if (mode !== "html5")
|
|
2357
|
+
throw new SharedToolError(`drag: unknown mode '${mode}' (use 'mouse' or 'html5')`);
|
|
2358
|
+
if (!capabilities.has("input.html5Drag")) {
|
|
2359
|
+
throw new SharedToolError("drag: mode:'html5' is not supported by this backend (WebDriver BiDi has no drag-interception primitive). " + "Omit 'mode' for the synthetic-mouse drag, or use --browser chrome for real HTML5 drag events.");
|
|
2360
|
+
}
|
|
2361
|
+
return "html5";
|
|
2362
|
+
}
|
|
1841
2363
|
async function drag(driver, args) {
|
|
1842
|
-
if (!args.from
|
|
1843
|
-
throw new SharedToolError("drag requires { from }
|
|
2364
|
+
if (!args.from)
|
|
2365
|
+
throw new SharedToolError("drag requires { from }");
|
|
2366
|
+
const destination = resolveDragDestination(args);
|
|
2367
|
+
const steps = resolveDragSteps(args.steps);
|
|
2368
|
+
const mode = resolveDragMode(args.mode, driver.capabilities);
|
|
1844
2369
|
return withPage2(driver, args.target, async (page) => {
|
|
1845
|
-
const { from, to } = await page.drag(locatorOf(args.from),
|
|
1846
|
-
return { dragged: true, from, to };
|
|
2370
|
+
const { from, to } = await page.drag(locatorOf(args.from), destination, { mode, steps });
|
|
2371
|
+
return { dragged: true, mode, steps, from, to };
|
|
1847
2372
|
});
|
|
1848
2373
|
}
|
|
1849
2374
|
async function fill(driver, args) {
|
|
@@ -1964,6 +2489,7 @@ var SHARED_TOOLS = {
|
|
|
1964
2489
|
click,
|
|
1965
2490
|
hover,
|
|
1966
2491
|
drag,
|
|
2492
|
+
scroll,
|
|
1967
2493
|
fill,
|
|
1968
2494
|
fill_form: fillForm,
|
|
1969
2495
|
type_text: typeText,
|
|
@@ -1975,6 +2501,121 @@ var SHARED_TOOLS = {
|
|
|
1975
2501
|
handle_dialog: handleDialog
|
|
1976
2502
|
};
|
|
1977
2503
|
|
|
2504
|
+
// src/leases-tools.ts
|
|
2505
|
+
class LeaseToolError extends Error {
|
|
2506
|
+
}
|
|
2507
|
+
function backendOf2(driver) {
|
|
2508
|
+
return driver.scheme === "bidi" ? "firefox" : "chrome";
|
|
2509
|
+
}
|
|
2510
|
+
async function claimPage(driver, args = {}) {
|
|
2511
|
+
const hasTarget = typeof args.target === "string" && args.target.length > 0;
|
|
2512
|
+
const hasTargetId = typeof args.targetId === "string" && args.targetId.length > 0;
|
|
2513
|
+
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.");
|
|
2515
|
+
}
|
|
2516
|
+
const backend = backendOf2(driver);
|
|
2517
|
+
const label = typeof args.label === "string" && args.label.length ? args.label : defaultLabel();
|
|
2518
|
+
const pages = await driver.listPages();
|
|
2519
|
+
let targetId;
|
|
2520
|
+
let url;
|
|
2521
|
+
let opened;
|
|
2522
|
+
if (hasTarget) {
|
|
2523
|
+
const hit = await pickPage(driver, pages, args.target).catch((err) => {
|
|
2524
|
+
throw new LeaseToolError(`claim_page: no open tab matches target '${args.target}' (${err.message}). No tab was opened: claim_page never creates one when 'target' is given.`);
|
|
2525
|
+
});
|
|
2526
|
+
targetId = hit.id;
|
|
2527
|
+
url = hit.url;
|
|
2528
|
+
opened = false;
|
|
2529
|
+
} else if (hasTargetId) {
|
|
2530
|
+
const existing = pages.find((p) => p.id === args.targetId);
|
|
2531
|
+
if (!existing)
|
|
2532
|
+
throw new LeaseToolError(`claim_page: no page target with id '${args.targetId}'`);
|
|
2533
|
+
targetId = existing.id;
|
|
2534
|
+
url = existing.url;
|
|
2535
|
+
opened = false;
|
|
2536
|
+
} else {
|
|
2537
|
+
const page = await newTrackedPage(driver, backend, { url: args.url, label });
|
|
2538
|
+
targetId = page.id;
|
|
2539
|
+
url = page.url;
|
|
2540
|
+
opened = true;
|
|
2541
|
+
}
|
|
2542
|
+
const humanMs = opened ? null : await readHumanActiveMs(driver, backend, targetId);
|
|
2543
|
+
const liveIds = [...pages.map((p) => p.id), targetId];
|
|
2544
|
+
const { record, token } = await claimLease(backend, targetId, {
|
|
2545
|
+
label,
|
|
2546
|
+
ttlMs: typeof args.ttlMs === "number" && args.ttlMs > 0 ? args.ttlMs : leaseTtlMs(),
|
|
2547
|
+
liveIds
|
|
2548
|
+
});
|
|
2549
|
+
await installBeacon(driver, targetId);
|
|
2550
|
+
const contention = contentionWarning(humanMs, { takeover: !opened });
|
|
2551
|
+
return {
|
|
2552
|
+
lease: token,
|
|
2553
|
+
targetId,
|
|
2554
|
+
url,
|
|
2555
|
+
label: record.label,
|
|
2556
|
+
ttlMs: record.ttlMs,
|
|
2557
|
+
expiresAt: record.lastUsedAt + record.ttlMs,
|
|
2558
|
+
opened,
|
|
2559
|
+
...beaconSupported(driver) ? { humanActiveMs: humanMs } : {},
|
|
2560
|
+
...contention !== undefined ? { contention } : {}
|
|
2561
|
+
};
|
|
2562
|
+
}
|
|
2563
|
+
async function releasePage(driver, args = {}) {
|
|
2564
|
+
const hasLease = typeof args.lease === "string" && args.lease.length > 0;
|
|
2565
|
+
const hasTarget = typeof args.target === "string" && args.target.length > 0;
|
|
2566
|
+
if (hasLease === hasTarget) {
|
|
2567
|
+
throw new LeaseToolError("release_page takes exactly one of 'lease' (the token claim_page returned) or 'target' (a selector for a tab this process holds)");
|
|
2568
|
+
}
|
|
2569
|
+
const backend = backendOf2(driver);
|
|
2570
|
+
let targetId;
|
|
2571
|
+
let released;
|
|
2572
|
+
if (hasLease) {
|
|
2573
|
+
const parts = tokenParts(args.lease);
|
|
2574
|
+
if (!parts)
|
|
2575
|
+
return { released: false, closed: false };
|
|
2576
|
+
targetId = parts.targetId;
|
|
2577
|
+
released = (await releaseLease(args.lease)).released;
|
|
2578
|
+
} else {
|
|
2579
|
+
const page = await resolvePage(driver, args.target);
|
|
2580
|
+
targetId = page.id;
|
|
2581
|
+
released = (await releaseLeaseFor(backend, targetId)).released;
|
|
2582
|
+
}
|
|
2583
|
+
if (!released)
|
|
2584
|
+
return { released: false, closed: false, ...hasTarget ? { targetId } : {} };
|
|
2585
|
+
const shouldClose = args.close === false ? false : args.close === true ? true : await readOrigin(backend, targetId) !== undefined;
|
|
2586
|
+
if (!shouldClose)
|
|
2587
|
+
return { released: true, closed: false, targetId };
|
|
2588
|
+
const res = await driver.closePage(targetId).catch(() => ({ success: false }));
|
|
2589
|
+
return { released: true, closed: res.success === true, targetId };
|
|
2590
|
+
}
|
|
2591
|
+
async function listLeasesTool(driver, _args = {}) {
|
|
2592
|
+
const backend = backendOf2(driver);
|
|
2593
|
+
const reaped = requireLease() ? await reapStaleAgentTabs(driver, backend) : [];
|
|
2594
|
+
const reapedIds = new Set(reaped.map((r) => r.targetId));
|
|
2595
|
+
const livePages = await driver.listPages();
|
|
2596
|
+
const liveIds = livePages.map((p) => p.id);
|
|
2597
|
+
const all = await listLeases({ liveIds, liveBackend: backend });
|
|
2598
|
+
const kept = all.filter((l) => !(l.backend === backend && reapedIds.has(l.targetId)));
|
|
2599
|
+
const liveSet = new Set(liveIds);
|
|
2600
|
+
const now = Date.now();
|
|
2601
|
+
const leases = [];
|
|
2602
|
+
for (const row of kept) {
|
|
2603
|
+
const computed = row.unreadable !== undefined ? row : { ...row, idleMs: now - row.lastUsedAt, expiresAt: row.lastUsedAt + row.ttlMs };
|
|
2604
|
+
if (row.backend !== backend || row.unreadable !== undefined || !liveSet.has(row.targetId)) {
|
|
2605
|
+
leases.push(computed);
|
|
2606
|
+
continue;
|
|
2607
|
+
}
|
|
2608
|
+
const humanMs = await readHumanActiveMs(driver, backend, row.targetId, now).catch(() => null);
|
|
2609
|
+
leases.push(humanMs === null ? computed : { ...computed, humanActiveMs: humanMs });
|
|
2610
|
+
}
|
|
2611
|
+
return { leases, count: leases.length, ...reaped.length ? { reaped } : {} };
|
|
2612
|
+
}
|
|
2613
|
+
var LEASE_TOOLS = {
|
|
2614
|
+
claim_page: claimPage,
|
|
2615
|
+
release_page: releasePage,
|
|
2616
|
+
list_leases: listLeasesTool
|
|
2617
|
+
};
|
|
2618
|
+
|
|
1978
2619
|
// src/tools/console.ts
|
|
1979
2620
|
import { readFile as readFile3 } from "node:fs/promises";
|
|
1980
2621
|
|
|
@@ -3122,6 +3763,273 @@ function truncate(s, max = 600) {
|
|
|
3122
3763
|
return t.length > max ? `${t.slice(0, max)}…` : t;
|
|
3123
3764
|
}
|
|
3124
3765
|
|
|
3766
|
+
// src/tools/dispatch-mouse.ts
|
|
3767
|
+
var CLICK_MODIFIER_BITS2 = { Alt: 1, Control: 2, Meta: 4, Shift: 8 };
|
|
3768
|
+
var BUTTONS = new Set(["left", "right", "middle"]);
|
|
3769
|
+
var CDP_EVENT_TYPE = {
|
|
3770
|
+
move: "mouseMoved",
|
|
3771
|
+
down: "mousePressed",
|
|
3772
|
+
up: "mouseReleased"
|
|
3773
|
+
};
|
|
3774
|
+
function modifierBits(mods) {
|
|
3775
|
+
let bits = 0;
|
|
3776
|
+
for (const m of mods ?? []) {
|
|
3777
|
+
const bit = CLICK_MODIFIER_BITS2[m];
|
|
3778
|
+
if (bit === undefined)
|
|
3779
|
+
throw new Error(`dispatch_mouse: unknown modifier '${m}' (use Alt, Control, Meta, or Shift)`);
|
|
3780
|
+
bits |= bit;
|
|
3781
|
+
}
|
|
3782
|
+
return bits;
|
|
3783
|
+
}
|
|
3784
|
+
function validateDispatchMouseArgs(args) {
|
|
3785
|
+
if (args.action !== "move" && args.action !== "down" && args.action !== "up") {
|
|
3786
|
+
throw new Error("dispatch_mouse requires { action } of 'move', 'down', or 'up'");
|
|
3787
|
+
}
|
|
3788
|
+
if (typeof args.x !== "number" || typeof args.y !== "number" || !Number.isFinite(args.x) || !Number.isFinite(args.y)) {
|
|
3789
|
+
throw new Error("dispatch_mouse requires numeric { x } and { y } on every call");
|
|
3790
|
+
}
|
|
3791
|
+
const button = args.button ?? "left";
|
|
3792
|
+
if (!BUTTONS.has(button))
|
|
3793
|
+
throw new Error(`dispatch_mouse: unknown button '${button}' (use left, right, or middle)`);
|
|
3794
|
+
}
|
|
3795
|
+
async function dispatchMouse(args) {
|
|
3796
|
+
validateDispatchMouseArgs(args);
|
|
3797
|
+
const button = args.button ?? "left";
|
|
3798
|
+
const modifiers = modifierBits(args.modifiers);
|
|
3799
|
+
return withPage(args.target, async (conn, target) => {
|
|
3800
|
+
const params = { type: CDP_EVENT_TYPE[args.action], x: args.x, y: args.y, button, modifiers };
|
|
3801
|
+
if (args.action !== "move")
|
|
3802
|
+
params.clickCount = args.clickCount ?? 1;
|
|
3803
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", params);
|
|
3804
|
+
return { dispatched: args.action, x: args.x, y: args.y };
|
|
3805
|
+
});
|
|
3806
|
+
}
|
|
3807
|
+
|
|
3808
|
+
// src/tools/downloads.ts
|
|
3809
|
+
import { readdir as readdir3, rename, stat as stat4 } from "node:fs/promises";
|
|
3810
|
+
import { basename, extname, join as join8 } from "node:path";
|
|
3811
|
+
|
|
3812
|
+
// src/tools/browser-session.ts
|
|
3813
|
+
import { mkdir as mkdir9 } from "node:fs/promises";
|
|
3814
|
+
import { join as join7 } from "node:path";
|
|
3815
|
+
var ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
3816
|
+
var DOWNLOADS_DIR = join7(ARTIFACT_DIR7, "downloads");
|
|
3817
|
+
var DOWNLOAD_BUFFER_LIMIT = 50;
|
|
3818
|
+
var session;
|
|
3819
|
+
function isDisconnect(e) {
|
|
3820
|
+
const m = e instanceof Error ? e.message : String(e);
|
|
3821
|
+
return /connection (not open|closed)|closed by caller|websocket|ECONNREFUSED|socket/i.test(m);
|
|
3822
|
+
}
|
|
3823
|
+
function record(s, guid) {
|
|
3824
|
+
let r = s.downloads.find((d) => d.guid === guid);
|
|
3825
|
+
if (!r) {
|
|
3826
|
+
r = { guid, state: "inProgress", consumed: false };
|
|
3827
|
+
s.downloads.push(r);
|
|
3828
|
+
while (s.downloads.length > DOWNLOAD_BUFFER_LIMIT) {
|
|
3829
|
+
const i = s.downloads.findIndex((d) => d.consumed);
|
|
3830
|
+
s.downloads.splice(i >= 0 ? i : 0, 1);
|
|
3831
|
+
}
|
|
3832
|
+
}
|
|
3833
|
+
return r;
|
|
3834
|
+
}
|
|
3835
|
+
function subscribe(s) {
|
|
3836
|
+
s.conn.on("Browser.downloadWillBegin", (p) => {
|
|
3837
|
+
const r = record(s, String(p.guid));
|
|
3838
|
+
if (typeof p.url === "string")
|
|
3839
|
+
r.url = p.url;
|
|
3840
|
+
if (typeof p.suggestedFilename === "string")
|
|
3841
|
+
r.suggestedFilename = p.suggestedFilename;
|
|
3842
|
+
});
|
|
3843
|
+
s.conn.on("Browser.downloadProgress", (p) => {
|
|
3844
|
+
const r = record(s, String(p.guid));
|
|
3845
|
+
if (typeof p.receivedBytes === "number")
|
|
3846
|
+
r.bytes = p.receivedBytes;
|
|
3847
|
+
if (typeof p.filePath === "string")
|
|
3848
|
+
r.filePath = p.filePath;
|
|
3849
|
+
const state = String(p.state);
|
|
3850
|
+
if (state === "completed" || state === "canceled") {
|
|
3851
|
+
r.state = state;
|
|
3852
|
+
if (state === "completed") {
|
|
3853
|
+
const waiter = s.waiters.shift();
|
|
3854
|
+
if (waiter) {
|
|
3855
|
+
r.consumed = true;
|
|
3856
|
+
waiter(r);
|
|
3857
|
+
}
|
|
3858
|
+
}
|
|
3859
|
+
}
|
|
3860
|
+
});
|
|
3861
|
+
}
|
|
3862
|
+
async function getBrowserSession() {
|
|
3863
|
+
if (session && !session.dead)
|
|
3864
|
+
return session;
|
|
3865
|
+
const previousPath = session?.armedPath;
|
|
3866
|
+
const conn = await openBrowser();
|
|
3867
|
+
const s = { conn, downloads: [], waiters: [], dead: false };
|
|
3868
|
+
subscribe(s);
|
|
3869
|
+
session = s;
|
|
3870
|
+
if (previousPath)
|
|
3871
|
+
await armDownloadsAt(previousPath);
|
|
3872
|
+
return s;
|
|
3873
|
+
}
|
|
3874
|
+
async function browserSend(method, params = {}) {
|
|
3875
|
+
const s = await getBrowserSession();
|
|
3876
|
+
try {
|
|
3877
|
+
return await s.conn.send(method, params);
|
|
3878
|
+
} catch (e) {
|
|
3879
|
+
if (isDisconnect(e)) {
|
|
3880
|
+
s.dead = true;
|
|
3881
|
+
const fresh = await getBrowserSession();
|
|
3882
|
+
return await fresh.conn.send(method, params);
|
|
3883
|
+
}
|
|
3884
|
+
throw e;
|
|
3885
|
+
}
|
|
3886
|
+
}
|
|
3887
|
+
async function armDownloadsAt(path) {
|
|
3888
|
+
await mkdir9(path, { recursive: true });
|
|
3889
|
+
const s = await getBrowserSession();
|
|
3890
|
+
await s.conn.send("Browser.setDownloadBehavior", { behavior: "allowAndName", downloadPath: path, eventsEnabled: true });
|
|
3891
|
+
s.armedPath = path;
|
|
3892
|
+
}
|
|
3893
|
+
async function armDownloads() {
|
|
3894
|
+
const s = await getBrowserSession();
|
|
3895
|
+
if (s.armedPath !== DOWNLOADS_DIR)
|
|
3896
|
+
await armDownloadsAt(DOWNLOADS_DIR);
|
|
3897
|
+
return { downloadPath: DOWNLOADS_DIR };
|
|
3898
|
+
}
|
|
3899
|
+
function pendingDownloads() {
|
|
3900
|
+
return (session?.downloads ?? []).filter((d) => d.state === "completed" && !d.consumed);
|
|
3901
|
+
}
|
|
3902
|
+
async function nextCompletedDownload(timeoutMs) {
|
|
3903
|
+
const s = await getBrowserSession();
|
|
3904
|
+
const buffered = s.downloads.find((d) => d.state === "completed" && !d.consumed);
|
|
3905
|
+
if (buffered) {
|
|
3906
|
+
buffered.consumed = true;
|
|
3907
|
+
return buffered;
|
|
3908
|
+
}
|
|
3909
|
+
return new Promise((resolve, reject) => {
|
|
3910
|
+
const waiter = (r) => {
|
|
3911
|
+
clearTimeout(timer);
|
|
3912
|
+
resolve(r);
|
|
3913
|
+
};
|
|
3914
|
+
const timer = setTimeout(() => {
|
|
3915
|
+
const i = s.waiters.indexOf(waiter);
|
|
3916
|
+
if (i >= 0)
|
|
3917
|
+
s.waiters.splice(i, 1);
|
|
3918
|
+
reject(new Error(`wait_for_download: no download completed within ${timeoutMs}ms. Downloads are only captured while this server holds the arming connection, so the triggering click must come AFTER a wait_for_download call (use arm:true to arm without waiting).`));
|
|
3919
|
+
}, timeoutMs);
|
|
3920
|
+
s.waiters.push(waiter);
|
|
3921
|
+
});
|
|
3922
|
+
}
|
|
3923
|
+
async function disposeBrowserSession() {
|
|
3924
|
+
const s = session;
|
|
3925
|
+
session = undefined;
|
|
3926
|
+
if (!s)
|
|
3927
|
+
return;
|
|
3928
|
+
s.dead = true;
|
|
3929
|
+
try {
|
|
3930
|
+
s.conn.close();
|
|
3931
|
+
} catch {}
|
|
3932
|
+
}
|
|
3933
|
+
|
|
3934
|
+
// src/tools/downloads.ts
|
|
3935
|
+
function safeFilename(suggested) {
|
|
3936
|
+
const raw = (suggested ?? "").replace(/[\u0000-\u001f]/g, "");
|
|
3937
|
+
const name = basename(raw.split("\\").pop() ?? "").trim();
|
|
3938
|
+
if (!name || name === "." || name === "..")
|
|
3939
|
+
return "download";
|
|
3940
|
+
return name;
|
|
3941
|
+
}
|
|
3942
|
+
function nextFreeName(name, taken) {
|
|
3943
|
+
if (!taken.has(name))
|
|
3944
|
+
return name;
|
|
3945
|
+
const ext = extname(name);
|
|
3946
|
+
const stem = ext ? name.slice(0, -ext.length) : name;
|
|
3947
|
+
for (let i = 1;; i++) {
|
|
3948
|
+
const candidate = `${stem}-${i}${ext}`;
|
|
3949
|
+
if (!taken.has(candidate))
|
|
3950
|
+
return candidate;
|
|
3951
|
+
}
|
|
3952
|
+
}
|
|
3953
|
+
function validateWaitForDownloadArgs(args) {
|
|
3954
|
+
if (args.timeoutMs !== undefined) {
|
|
3955
|
+
if (typeof args.timeoutMs !== "number" || !Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0) {
|
|
3956
|
+
throw new Error(`wait_for_download: 'timeoutMs' must be a positive number (got ${JSON.stringify(args.timeoutMs)})`);
|
|
3957
|
+
}
|
|
3958
|
+
}
|
|
3959
|
+
if (args.arm !== undefined && typeof args.arm !== "boolean") {
|
|
3960
|
+
throw new Error(`wait_for_download: 'arm' must be a boolean (got ${JSON.stringify(args.arm)})`);
|
|
3961
|
+
}
|
|
3962
|
+
}
|
|
3963
|
+
async function waitForDownload(args = {}) {
|
|
3964
|
+
validateWaitForDownloadArgs(args);
|
|
3965
|
+
const t = await resolveTarget(args.target);
|
|
3966
|
+
const target = { id: t.id, url: t.url, title: t.title };
|
|
3967
|
+
const { downloadPath } = await armDownloads();
|
|
3968
|
+
if (args.arm === true)
|
|
3969
|
+
return { armed: true, downloadPath, pending: pendingDownloads().length, target };
|
|
3970
|
+
const rec = await nextCompletedDownload(args.timeoutMs ?? 30000);
|
|
3971
|
+
if (!rec.filePath) {
|
|
3972
|
+
throw new Error(`wait_for_download: download ${rec.guid} completed without a file path (nothing to return)`);
|
|
3973
|
+
}
|
|
3974
|
+
const suggestedFilename = safeFilename(rec.suggestedFilename);
|
|
3975
|
+
const taken = new Set(await readdir3(DOWNLOADS_DIR).catch(() => []));
|
|
3976
|
+
taken.delete(basename(rec.filePath));
|
|
3977
|
+
const finalName = nextFreeName(suggestedFilename, taken);
|
|
3978
|
+
const path = join8(DOWNLOADS_DIR, finalName);
|
|
3979
|
+
await rename(rec.filePath, path);
|
|
3980
|
+
const bytes = (await stat4(path)).size;
|
|
3981
|
+
return { path, suggestedFilename, bytes, ...rec.url ? { url: rec.url } : {}, target };
|
|
3982
|
+
}
|
|
3983
|
+
|
|
3984
|
+
// src/tools/permissions.ts
|
|
3985
|
+
function originOf(url) {
|
|
3986
|
+
try {
|
|
3987
|
+
const o = new URL(url).origin;
|
|
3988
|
+
return o && o !== "null" ? o : undefined;
|
|
3989
|
+
} catch {
|
|
3990
|
+
return;
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
function validateGrantPermissionsArgs(args) {
|
|
3994
|
+
const reset = args.reset === true;
|
|
3995
|
+
if (args.reset !== undefined && typeof args.reset !== "boolean") {
|
|
3996
|
+
throw new Error(`grant_permissions: 'reset' must be a boolean (got ${JSON.stringify(args.reset)})`);
|
|
3997
|
+
}
|
|
3998
|
+
if (args.permissions === undefined) {
|
|
3999
|
+
if (!reset)
|
|
4000
|
+
throw new Error("grant_permissions: 'permissions' is required (or pass reset:true to clear previous grants)");
|
|
4001
|
+
return;
|
|
4002
|
+
}
|
|
4003
|
+
if (!Array.isArray(args.permissions)) {
|
|
4004
|
+
throw new Error(`grant_permissions: 'permissions' must be an array of CDP PermissionType strings (got ${JSON.stringify(args.permissions)})`);
|
|
4005
|
+
}
|
|
4006
|
+
if (args.permissions.length === 0 && !reset) {
|
|
4007
|
+
throw new Error("grant_permissions: 'permissions' must not be empty (pass reset:true if the intent is to clear grants)");
|
|
4008
|
+
}
|
|
4009
|
+
for (const p of args.permissions) {
|
|
4010
|
+
if (typeof p !== "string" || p.length === 0) {
|
|
4011
|
+
throw new Error(`grant_permissions: every entry of 'permissions' must be a non-empty string (got ${JSON.stringify(p)})`);
|
|
4012
|
+
}
|
|
4013
|
+
}
|
|
4014
|
+
}
|
|
4015
|
+
async function grantPermissions(args = {}) {
|
|
4016
|
+
validateGrantPermissionsArgs(args);
|
|
4017
|
+
const t = await resolveTarget(args.target);
|
|
4018
|
+
const target = { id: t.id, url: t.url, title: t.title };
|
|
4019
|
+
const reset = args.reset === true;
|
|
4020
|
+
const permissions = args.permissions ?? [];
|
|
4021
|
+
if (reset)
|
|
4022
|
+
await browserSend("Browser.resetPermissions", {});
|
|
4023
|
+
if (permissions.length === 0)
|
|
4024
|
+
return { reset: true, target };
|
|
4025
|
+
const origin = args.origin ?? originOf(t.url);
|
|
4026
|
+
if (!origin) {
|
|
4027
|
+
throw new Error(`grant_permissions: cannot derive an origin from the target tab's url (${JSON.stringify(t.url)}); pass 'origin' explicitly, e.g. origin:"https://example.com"`);
|
|
4028
|
+
}
|
|
4029
|
+
await browserSend("Browser.grantPermissions", { origin, permissions });
|
|
4030
|
+
return { granted: [...permissions], origin, ...reset ? { reset: true } : {}, target };
|
|
4031
|
+
}
|
|
4032
|
+
|
|
3125
4033
|
// src/index.ts
|
|
3126
4034
|
var cdpDriver = createCdpDriver();
|
|
3127
4035
|
function onCdp(fn) {
|
|
@@ -3142,6 +4050,7 @@ var TOOLS = {
|
|
|
3142
4050
|
click: onCdp(SHARED_TOOLS.click),
|
|
3143
4051
|
hover: onCdp(SHARED_TOOLS.hover),
|
|
3144
4052
|
drag: onCdp(SHARED_TOOLS.drag),
|
|
4053
|
+
scroll: onCdp(SHARED_TOOLS.scroll),
|
|
3145
4054
|
fill: onCdp(SHARED_TOOLS.fill),
|
|
3146
4055
|
fill_form: onCdp(SHARED_TOOLS.fill_form),
|
|
3147
4056
|
type_text: onCdp(SHARED_TOOLS.type_text),
|
|
@@ -3166,6 +4075,9 @@ var TOOLS = {
|
|
|
3166
4075
|
mock_request: mockRequest,
|
|
3167
4076
|
list_mocks: listMocks,
|
|
3168
4077
|
clear_mocks: clearMocks,
|
|
4078
|
+
dispatch_mouse: dispatchMouse,
|
|
4079
|
+
wait_for_download: waitForDownload,
|
|
4080
|
+
grant_permissions: grantPermissions,
|
|
3169
4081
|
claim_page: onCdp(LEASE_TOOLS.claim_page),
|
|
3170
4082
|
release_page: onCdp(LEASE_TOOLS.release_page),
|
|
3171
4083
|
list_leases: onCdp(LEASE_TOOLS.list_leases)
|
|
@@ -3181,17 +4093,21 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
3181
4093
|
var MANIFEST = [
|
|
3182
4094
|
{
|
|
3183
4095
|
name: "list_pages",
|
|
3184
|
-
description: "Enumerate browser page targets via the CDP browser endpoint (GET /json/list). By default returns only page-type tabs; set 'all' to also include workers and background pages. Each entry carries the targetId used as a target selector elsewhere, plus an 'origin' field recording where the tab came from: 'agent' means this toolkit created it (the entry then also carries the creating 'label' and 'createdAt'), and this stays true after the creating agent releases its lease or dies, which is what makes a stray agent tab findable later. 'unknown' means there is no creation record. It never says 'human': the toolkit cannot prove a person opened a tab, so 'unknown' is the honest word for a tab it did not create, one opened before the toolkit ran, or one whose record could not be written. A tab whose record exists but could not be read reports origin 'unknown' plus 'originUnreadable', so a broken record is never mistaken for no record.",
|
|
4096
|
+
description: "Enumerate browser page targets via the CDP browser endpoint (GET /json/list). By default returns only page-type tabs; set 'all' to also include workers and background pages. Each entry carries the targetId used as a target selector elsewhere, plus an 'origin' field recording where the tab came from: 'agent' means this toolkit created it (the entry then also carries the creating 'label' and 'createdAt'), and this stays true after the creating agent releases its lease or dies, which is what makes a stray agent tab findable later. 'unknown' means there is no creation record. It never says 'human': the toolkit cannot prove a person opened a tab, so 'unknown' is the honest word for a tab it did not create, one opened before the toolkit ran, or one whose record could not be written. A tab whose record exists but could not be read reports origin 'unknown' plus 'originUnreadable', so a broken record is never mistaken for no record. Under CDP_REQUIRE_LEASE this call also reaps abandoned agent tabs, but reap-CLOSE and lease-RECLAIMABLE (list_leases' 'stale') now fire at different times: a lease reads reclaimable the moment its TTL elapses, cheap and reversible since another agent may simply take it, while this call only CLOSES the tab, destructively, after an ADDITIONAL grace period past that TTL (CDP_REAP_GRACE_MS, default 2700000ms/45 minutes, so 60 minutes total after last use) — a tab an agent is mid-build between calls on is not destroyed out from under it. A dead-pid lease is reaped immediately regardless of grace: that process is never coming back. Closed tabs are reported in an additive 'reaped' array ({targetId,label,reason}) present only when something was actually closed; a tab with no lease is never reaped. Every entry under an active, readable lease of this backend also carries 'lease': {label, pid, idleMs, expiresAt, stale} — idleMs and expiresAt are computed fresh on every call (now-lastUsedAt and lastUsedAt+ttlMs), never read off disk, and this is unconditional on 'probe'. Set 'probe' to true to also ping each page-type target's renderer: one bounded (500ms) check per target, never more, so a single wedged tab cannot stall the listing beyond its own budget. This adds 'responsive' (true iff the ping answered in time; false on a timeout, a page-side exception, or an unreachable target — never an error for the whole call) and, where that same round trip found human-attributed input, 'humanActiveMs' (see claim_page and list_leases for what that field means and cannot prove). 'responsive' and 'humanActiveMs' are both ABSENT — never false/null — when 'probe' is not set, and 'responsive' is likewise absent on a backend that cannot answer this ping at all.",
|
|
3185
4097
|
inputSchema: {
|
|
3186
4098
|
type: "object",
|
|
3187
4099
|
properties: {
|
|
3188
4100
|
lease: {
|
|
3189
4101
|
type: "string",
|
|
3190
|
-
description: "Opaque lease token from claim_page.
|
|
4102
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3191
4103
|
},
|
|
3192
4104
|
all: {
|
|
3193
4105
|
type: "boolean",
|
|
3194
4106
|
description: "Include non-page targets (service/shared workers, background pages) when true; otherwise only page-type tabs are listed."
|
|
4107
|
+
},
|
|
4108
|
+
probe: {
|
|
4109
|
+
type: "boolean",
|
|
4110
|
+
description: "Ping each page-type target's renderer before returning: one bounded (500ms) Runtime.evaluate per target, never more. Adds 'responsive' to every page-type entry and, where the same round trip finds human-attributed input, 'humanActiveMs'. A wedged or unreachable tab reports responsive:false rather than failing the call. Defaults to false, which is byte-identical to the pre-1.8.0 shape."
|
|
3195
4111
|
}
|
|
3196
4112
|
},
|
|
3197
4113
|
required: [],
|
|
@@ -3206,7 +4122,7 @@ var MANIFEST = [
|
|
|
3206
4122
|
properties: {
|
|
3207
4123
|
lease: {
|
|
3208
4124
|
type: "string",
|
|
3209
|
-
description: "Opaque lease token from claim_page.
|
|
4125
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3210
4126
|
},
|
|
3211
4127
|
url: {
|
|
3212
4128
|
type: "string",
|
|
@@ -3214,7 +4130,7 @@ var MANIFEST = [
|
|
|
3214
4130
|
},
|
|
3215
4131
|
claim: {
|
|
3216
4132
|
type: "boolean",
|
|
3217
|
-
description: "Claim the new tab atomically as part of creating it and return a lease token alongside {targetId,url}. Default false, which is byte-identical to the pre-1.2 behavior."
|
|
4133
|
+
description: "Claim the new tab atomically as part of creating it and return a lease token alongside {targetId,url}. Default false, which is byte-identical to the pre-1.2 behavior. Under CDP_REQUIRE_LEASE the new tab is claimed and a lease returned even without claim:true, because a tab nobody holds is a tab nobody may drive; passing claim:true additionally makes the lease an explicit one, which requires its token on every later call even from this same process."
|
|
3218
4134
|
},
|
|
3219
4135
|
label: {
|
|
3220
4136
|
type: "string",
|
|
@@ -3237,7 +4153,7 @@ var MANIFEST = [
|
|
|
3237
4153
|
properties: {
|
|
3238
4154
|
lease: {
|
|
3239
4155
|
type: "string",
|
|
3240
|
-
description: "Opaque lease token from claim_page.
|
|
4156
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3241
4157
|
},
|
|
3242
4158
|
target: {
|
|
3243
4159
|
type: "string",
|
|
@@ -3258,7 +4174,7 @@ var MANIFEST = [
|
|
|
3258
4174
|
properties: {
|
|
3259
4175
|
lease: {
|
|
3260
4176
|
type: "string",
|
|
3261
|
-
description: "Opaque lease token from claim_page.
|
|
4177
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3262
4178
|
},
|
|
3263
4179
|
target: {
|
|
3264
4180
|
type: "string",
|
|
@@ -3273,13 +4189,13 @@ var MANIFEST = [
|
|
|
3273
4189
|
},
|
|
3274
4190
|
{
|
|
3275
4191
|
name: "navigate_page",
|
|
3276
|
-
description: "Navigate a target page to a URL (Page.navigate)
|
|
4192
|
+
description: "Navigate a target page to a URL (Page.navigate), reload it (reload:true), OR go back/forward in its session history (history:'back'|'forward'), then wait for the load milestone with a bounded timeout so a wedged renderer can't hang. Exactly one of url / reload / history per call. Returns {url,frameId,waitedFor} (plus reloaded:true on a reload, traversed:'back'|'forward' on a history move); waitUntil supports 'load'|'domcontentloaded' only (no 'networkidle'), and there is no auto-snapshot of the new page. Pass reload:true with ignoreCache:true for a hard reload that refetches every subresource (e.g. to pick up a freshly-deployed, non-content-hashed bundle the HTTP cache would serve stale).",
|
|
3277
4193
|
inputSchema: {
|
|
3278
4194
|
type: "object",
|
|
3279
4195
|
properties: {
|
|
3280
4196
|
lease: {
|
|
3281
4197
|
type: "string",
|
|
3282
|
-
description: "Opaque lease token from claim_page.
|
|
4198
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3283
4199
|
},
|
|
3284
4200
|
target: {
|
|
3285
4201
|
type: "string",
|
|
@@ -3293,6 +4209,14 @@ var MANIFEST = [
|
|
|
3293
4209
|
type: "boolean",
|
|
3294
4210
|
description: "Reload the current page (Page.reload) instead of navigating to url. Default false."
|
|
3295
4211
|
},
|
|
4212
|
+
history: {
|
|
4213
|
+
type: "string",
|
|
4214
|
+
enum: [
|
|
4215
|
+
"back",
|
|
4216
|
+
"forward"
|
|
4217
|
+
],
|
|
4218
|
+
description: "Traverse this tab's session history instead of loading a url: 'back' and 'forward' are the browser's Back and Forward buttons. Mutually exclusive with 'url' and 'reload' — passing two of the three is refused by name rather than resolved by precedence. Works on BOTH backends (Chrome: Page.getNavigationHistory + Page.navigateToHistoryEntry; Firefox: browsingContext.traverseHistory) and waits for the same load milestone as an ordinary navigation. Going back from the first entry, or forward from the last, is an ERROR naming the direction — never a silent success that navigated nowhere. The result carries traversed:'back'|'forward' alongside the resulting url."
|
|
4219
|
+
},
|
|
3296
4220
|
ignoreCache: {
|
|
3297
4221
|
type: "boolean",
|
|
3298
4222
|
description: "On reload, bypass the HTTP cache (hard reload) so subresources are refetched. Ignored unless reload:true. Default false."
|
|
@@ -3321,7 +4245,7 @@ var MANIFEST = [
|
|
|
3321
4245
|
properties: {
|
|
3322
4246
|
lease: {
|
|
3323
4247
|
type: "string",
|
|
3324
|
-
description: "Opaque lease token from claim_page.
|
|
4248
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3325
4249
|
},
|
|
3326
4250
|
target: {
|
|
3327
4251
|
type: "string",
|
|
@@ -3354,7 +4278,7 @@ var MANIFEST = [
|
|
|
3354
4278
|
properties: {
|
|
3355
4279
|
lease: {
|
|
3356
4280
|
type: "string",
|
|
3357
|
-
description: "Opaque lease token from claim_page.
|
|
4281
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3358
4282
|
},
|
|
3359
4283
|
target: {
|
|
3360
4284
|
type: "string",
|
|
@@ -3391,7 +4315,7 @@ var MANIFEST = [
|
|
|
3391
4315
|
properties: {
|
|
3392
4316
|
lease: {
|
|
3393
4317
|
type: "string",
|
|
3394
|
-
description: "Opaque lease token from claim_page.
|
|
4318
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3395
4319
|
},
|
|
3396
4320
|
target: {
|
|
3397
4321
|
type: "string",
|
|
@@ -3422,7 +4346,7 @@ var MANIFEST = [
|
|
|
3422
4346
|
properties: {
|
|
3423
4347
|
lease: {
|
|
3424
4348
|
type: "string",
|
|
3425
|
-
description: "Opaque lease token from claim_page.
|
|
4349
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3426
4350
|
},
|
|
3427
4351
|
target: {
|
|
3428
4352
|
type: "string",
|
|
@@ -3478,7 +4402,7 @@ var MANIFEST = [
|
|
|
3478
4402
|
properties: {
|
|
3479
4403
|
lease: {
|
|
3480
4404
|
type: "string",
|
|
3481
|
-
description: "Opaque lease token from claim_page.
|
|
4405
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3482
4406
|
},
|
|
3483
4407
|
target: {
|
|
3484
4408
|
type: "string",
|
|
@@ -3513,7 +4437,7 @@ var MANIFEST = [
|
|
|
3513
4437
|
properties: {
|
|
3514
4438
|
lease: {
|
|
3515
4439
|
type: "string",
|
|
3516
|
-
description: "Opaque lease token from claim_page.
|
|
4440
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3517
4441
|
},
|
|
3518
4442
|
target: {
|
|
3519
4443
|
type: "string",
|
|
@@ -3530,13 +4454,13 @@ var MANIFEST = [
|
|
|
3530
4454
|
},
|
|
3531
4455
|
{
|
|
3532
4456
|
name: "click",
|
|
3533
|
-
description: "Click an element via a synthetic mouse press/release at the element's scrolled-into-view bounding-rect center. Target the element with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector).",
|
|
4457
|
+
description: "Click an element via a synthetic mouse press/release at the element's scrolled-into-view bounding-rect center. Target the element with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector). clickCount:3 triple-clicks (selects a paragraph/line in most editors). 'modifiers' holds Alt/Control/Meta/Shift for the press and release, like a real modifier-click; on the Firefox backend a non-empty 'modifiers' throws (not yet supported over BiDi), so a modifier click there needs --browser chrome.",
|
|
3534
4458
|
inputSchema: {
|
|
3535
4459
|
type: "object",
|
|
3536
4460
|
properties: {
|
|
3537
4461
|
lease: {
|
|
3538
4462
|
type: "string",
|
|
3539
|
-
description: "Opaque lease token from claim_page.
|
|
4463
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3540
4464
|
},
|
|
3541
4465
|
target: {
|
|
3542
4466
|
type: "string",
|
|
@@ -3561,7 +4485,20 @@ var MANIFEST = [
|
|
|
3561
4485
|
},
|
|
3562
4486
|
clickCount: {
|
|
3563
4487
|
type: "number",
|
|
3564
|
-
description: "Number of clicks: 1 = single (default), 2 = double-click."
|
|
4488
|
+
description: "Number of clicks: 1 = single (default), 2 = double-click, 3 = triple-click."
|
|
4489
|
+
},
|
|
4490
|
+
modifiers: {
|
|
4491
|
+
type: "array",
|
|
4492
|
+
items: {
|
|
4493
|
+
type: "string",
|
|
4494
|
+
enum: [
|
|
4495
|
+
"Alt",
|
|
4496
|
+
"Control",
|
|
4497
|
+
"Meta",
|
|
4498
|
+
"Shift"
|
|
4499
|
+
]
|
|
4500
|
+
},
|
|
4501
|
+
description: "Modifier keys held for the click's press and release, e.g. ['Shift'] for a shift-click. Not supported on the Firefox backend: passing a non-empty array there throws."
|
|
3565
4502
|
}
|
|
3566
4503
|
},
|
|
3567
4504
|
required: [],
|
|
@@ -3576,7 +4513,7 @@ var MANIFEST = [
|
|
|
3576
4513
|
properties: {
|
|
3577
4514
|
lease: {
|
|
3578
4515
|
type: "string",
|
|
3579
|
-
description: "Opaque lease token from claim_page.
|
|
4516
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3580
4517
|
},
|
|
3581
4518
|
target: {
|
|
3582
4519
|
type: "string",
|
|
@@ -3597,13 +4534,13 @@ var MANIFEST = [
|
|
|
3597
4534
|
},
|
|
3598
4535
|
{
|
|
3599
4536
|
name: "drag",
|
|
3600
|
-
description:
|
|
4537
|
+
description: `Drag from a source element to a destination. 'from' takes exactly one of uid (a CDP backendDOMNodeId from take_snapshot) or selector. The destination is exactly one of 'to' (an element via uid/selector, or an absolute viewport point via x+y) or 'by' ({dx,dy} offset from the source point — sliders, map panning, resize handles). mode:'mouse' (default) dispatches synthetic mouse press/move/release: right for widgets built on raw pointer events, and Chrome does turn it into a real drag, but WHICH drag events reach the page depends on where the interpolated pointer path happens to land — measured on Chrome 151, the default steps:2 delivers ZERO dragover events, so an HTML5 drop zone written the standard way (preventDefault inside dragover) refuses the drop entirely. mode:'html5' performs the drag deterministically instead: Chrome's drag interception hands back the DragData the page's own dragstart built, and the toolkit replays it as dragEnter/dragOver/drop exactly at the destination, so a draggable="true" / dataTransfer drop zone works regardless of the pointer path. mode:'html5' is CHROME-ONLY: it requires capability 'input.html5Drag' and is rejected with a clear error under the Firefox backend, where the tool itself remains available for mouse-mode drags. 'steps' (default 2) sets how many interpolated mouse-move events are dispatched between source and destination; raise it for DnD libraries with a movement threshold or per-frame sampling.`,
|
|
3601
4538
|
inputSchema: {
|
|
3602
4539
|
type: "object",
|
|
3603
4540
|
properties: {
|
|
3604
4541
|
lease: {
|
|
3605
4542
|
type: "string",
|
|
3606
|
-
description: "Opaque lease token from claim_page.
|
|
4543
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3607
4544
|
},
|
|
3608
4545
|
target: {
|
|
3609
4546
|
type: "string",
|
|
@@ -3626,7 +4563,7 @@ var MANIFEST = [
|
|
|
3626
4563
|
},
|
|
3627
4564
|
to: {
|
|
3628
4565
|
type: "object",
|
|
3629
|
-
description: "
|
|
4566
|
+
description: "Where the drag ends: exactly one of uid, selector, or x+y. Mutually exclusive with 'by'; exactly one of to/by is required.",
|
|
3630
4567
|
properties: {
|
|
3631
4568
|
uid: {
|
|
3632
4569
|
type: "number",
|
|
@@ -3635,18 +4572,222 @@ var MANIFEST = [
|
|
|
3635
4572
|
selector: {
|
|
3636
4573
|
type: "string",
|
|
3637
4574
|
description: "CSS selector for the destination element (resolved via document.querySelector)."
|
|
4575
|
+
},
|
|
4576
|
+
x: {
|
|
4577
|
+
type: "number",
|
|
4578
|
+
description: "Absolute viewport x-coordinate to drop at. Must be given together with 'y'. Use this when there is no droppable element to name (a canvas, a slider track)."
|
|
4579
|
+
},
|
|
4580
|
+
y: {
|
|
4581
|
+
type: "number",
|
|
4582
|
+
description: "Absolute viewport y-coordinate to drop at. Must be given together with 'x'."
|
|
4583
|
+
}
|
|
4584
|
+
},
|
|
4585
|
+
additionalProperties: false
|
|
4586
|
+
},
|
|
4587
|
+
by: {
|
|
4588
|
+
type: "object",
|
|
4589
|
+
description: "Drag by an offset from the source element's center instead of to a destination: at least one of dx/dy, the other defaults to 0. Mutually exclusive with 'to'; exactly one of to/by is required. Use for sliders (by:{dx:40}), map panning, and resize handles.",
|
|
4590
|
+
properties: {
|
|
4591
|
+
dx: {
|
|
4592
|
+
type: "number",
|
|
4593
|
+
description: "Horizontal offset in CSS pixels; positive drags RIGHT."
|
|
4594
|
+
},
|
|
4595
|
+
dy: {
|
|
4596
|
+
type: "number",
|
|
4597
|
+
description: "Vertical offset in CSS pixels; positive drags DOWN."
|
|
3638
4598
|
}
|
|
3639
4599
|
},
|
|
3640
4600
|
additionalProperties: false
|
|
4601
|
+
},
|
|
4602
|
+
mode: {
|
|
4603
|
+
type: "string",
|
|
4604
|
+
enum: [
|
|
4605
|
+
"mouse",
|
|
4606
|
+
"html5"
|
|
4607
|
+
],
|
|
4608
|
+
description: "'mouse' (default): synthetic mouse press/move/release, right for widgets built on raw pointer events. Chrome does turn this into a real HTML5 drag too, but WHICH drag events reach the page depends on the interpolated pointer path: at the default steps:2, an HTML5 drop zone written the standard way (preventDefault inside dragover) sees zero dragover events and refuses the drop. 'html5': real HTML5 drag-and-drop (dragstart/dragEnter/dragOver/drop with the page's own dataTransfer), deterministic regardless of pointer path. 'html5' is CHROME-ONLY and is rejected with a clear error under the Firefox backend."
|
|
4609
|
+
},
|
|
4610
|
+
steps: {
|
|
4611
|
+
type: "number",
|
|
4612
|
+
description: "Number of interpolated mouse-move events dispatched between the source and destination points, evenly spaced, the last landing exactly on the destination. Integer 1-500, default 2 (midpoint then destination). Raise it for DnD libraries with a movement threshold or per-frame sampling."
|
|
4613
|
+
}
|
|
4614
|
+
},
|
|
4615
|
+
required: [
|
|
4616
|
+
"from"
|
|
4617
|
+
],
|
|
4618
|
+
additionalProperties: false
|
|
4619
|
+
}
|
|
4620
|
+
},
|
|
4621
|
+
{
|
|
4622
|
+
name: "scroll",
|
|
4623
|
+
description: "Dispatch a wheel/scroll event at an anchor point: provide at most one of 'uid', 'selector', or 'x'+'y'; omit all three to scroll at the viewport center. An element anchor ('uid' or 'selector') is scrolled into view first, the same as click/hover. At least one of 'deltaX'/'deltaY' is required; positive 'deltaY' scrolls DOWN and positive 'deltaX' scrolls RIGHT (wheel-event convention). Chrome dispatches Input.dispatchMouseEvent{type:'mouseWheel'}; Firefox dispatches WebDriver BiDi's 'wheel' input source. Returns the resolved anchor point ({x,y}) plus the delta actually dispatched.",
|
|
4624
|
+
inputSchema: {
|
|
4625
|
+
type: "object",
|
|
4626
|
+
properties: {
|
|
4627
|
+
lease: {
|
|
4628
|
+
type: "string",
|
|
4629
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4630
|
+
},
|
|
4631
|
+
target: {
|
|
4632
|
+
type: "string",
|
|
4633
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4634
|
+
},
|
|
4635
|
+
uid: {
|
|
4636
|
+
type: "number",
|
|
4637
|
+
description: "CDP backendDOMNodeId of the element to scroll into view and anchor at, obtained from take_snapshot. Provide at most one of uid, selector, or x+y; omit all three to scroll at the viewport center."
|
|
4638
|
+
},
|
|
4639
|
+
selector: {
|
|
4640
|
+
type: "string",
|
|
4641
|
+
description: "CSS selector for the element to scroll into view and anchor at (resolved via document.querySelector). Provide at most one of uid, selector, or x+y; omit all three to scroll at the viewport center."
|
|
4642
|
+
},
|
|
4643
|
+
x: {
|
|
4644
|
+
type: "number",
|
|
4645
|
+
description: "Absolute viewport x-coordinate to anchor the scroll at. Must be given together with 'y'. Provide at most one of uid, selector, or x+y."
|
|
4646
|
+
},
|
|
4647
|
+
y: {
|
|
4648
|
+
type: "number",
|
|
4649
|
+
description: "Absolute viewport y-coordinate to anchor the scroll at. Must be given together with 'x'. Provide at most one of uid, selector, or x+y."
|
|
4650
|
+
},
|
|
4651
|
+
deltaX: {
|
|
4652
|
+
type: "number",
|
|
4653
|
+
description: "Horizontal scroll delta; positive scrolls RIGHT. At least one of deltaX/deltaY is required."
|
|
4654
|
+
},
|
|
4655
|
+
deltaY: {
|
|
4656
|
+
type: "number",
|
|
4657
|
+
description: "Vertical scroll delta; positive scrolls DOWN. At least one of deltaX/deltaY is required."
|
|
4658
|
+
}
|
|
4659
|
+
},
|
|
4660
|
+
required: [],
|
|
4661
|
+
additionalProperties: false
|
|
4662
|
+
}
|
|
4663
|
+
},
|
|
4664
|
+
{
|
|
4665
|
+
name: "dispatch_mouse",
|
|
4666
|
+
description: "Dispatch exactly one raw mouse event ('move', 'down', or 'up') at absolute viewport coordinates: the toolkit's lowest-level input primitive. Compose move/down/move/up calls yourself to reach anything a physical mouse can do that click/drag's fixed sequences cannot — canvas drag-painting, marquee/rubber-band selection, a custom widget with its own hit-testing. Chrome-only (capability 'input.raw'): absent from tools/list under the Firefox backend, never present-and-throwing.",
|
|
4667
|
+
inputSchema: {
|
|
4668
|
+
type: "object",
|
|
4669
|
+
properties: {
|
|
4670
|
+
lease: {
|
|
4671
|
+
type: "string",
|
|
4672
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4673
|
+
},
|
|
4674
|
+
target: {
|
|
4675
|
+
type: "string",
|
|
4676
|
+
description: "Target page selector: 'active' (default) | 'index:N' | 'url:<substr>' | 'title:<substr>' | '<targetId>'."
|
|
4677
|
+
},
|
|
4678
|
+
action: {
|
|
4679
|
+
type: "string",
|
|
4680
|
+
enum: [
|
|
4681
|
+
"move",
|
|
4682
|
+
"down",
|
|
4683
|
+
"up"
|
|
4684
|
+
],
|
|
4685
|
+
description: "Which raw event to dispatch: 'move' (mouseMoved), 'down' (mousePressed), or 'up' (mouseReleased)."
|
|
4686
|
+
},
|
|
4687
|
+
x: {
|
|
4688
|
+
type: "number",
|
|
4689
|
+
description: "Viewport x-coordinate. Required on every call: CDP has no notion of a 'current pointer position' to default from."
|
|
4690
|
+
},
|
|
4691
|
+
y: {
|
|
4692
|
+
type: "number",
|
|
4693
|
+
description: "Viewport y-coordinate. Required on every call."
|
|
4694
|
+
},
|
|
4695
|
+
button: {
|
|
4696
|
+
type: "string",
|
|
4697
|
+
enum: [
|
|
4698
|
+
"left",
|
|
4699
|
+
"right",
|
|
4700
|
+
"middle"
|
|
4701
|
+
],
|
|
4702
|
+
description: "Mouse button: 'left' (default), 'right', or 'middle'."
|
|
4703
|
+
},
|
|
4704
|
+
clickCount: {
|
|
4705
|
+
type: "number",
|
|
4706
|
+
description: "Click-run length for a 'down' or 'up' event (2 = the second half of a double-click, 3 = triple-click). Ignored for 'move'; defaults to 1."
|
|
4707
|
+
},
|
|
4708
|
+
modifiers: {
|
|
4709
|
+
type: "array",
|
|
4710
|
+
items: {
|
|
4711
|
+
type: "string",
|
|
4712
|
+
enum: [
|
|
4713
|
+
"Alt",
|
|
4714
|
+
"Control",
|
|
4715
|
+
"Meta",
|
|
4716
|
+
"Shift"
|
|
4717
|
+
]
|
|
4718
|
+
},
|
|
4719
|
+
description: "Modifier keys held for this one event, e.g. ['Shift']."
|
|
3641
4720
|
}
|
|
3642
4721
|
},
|
|
3643
4722
|
required: [
|
|
3644
|
-
"
|
|
3645
|
-
"
|
|
4723
|
+
"action",
|
|
4724
|
+
"x",
|
|
4725
|
+
"y"
|
|
3646
4726
|
],
|
|
3647
4727
|
additionalProperties: false
|
|
3648
4728
|
}
|
|
3649
4729
|
},
|
|
4730
|
+
{
|
|
4731
|
+
name: "wait_for_download",
|
|
4732
|
+
description: "Wait for a file download to finish and return it as a real file on disk: {path,suggestedFilename,bytes,url,target}. The file is written under the artifact dir's downloads/ folder and renamed from Chrome's internal guid to the page's own filename, collision-suffixed (report.csv, report-1.csv, ...). ORDERING RULE, and it is not optional: download capture must be ARMED BEFORE the click that starts the download — call wait_for_download{arm:true} first (it arms and returns immediately, reporting {armed:true,downloadPath,pending}), then click, then call wait_for_download to collect the finished file. This is Chrome's behavior, not a preference: the download-behavior override is per-connection state that Chrome REVERTS the moment the arming client disconnects, and an unarmed headless Chrome denies the download outright, so a download triggered before anything armed is lost with no file anywhere. SIDE EFFECT, browser-global: arming redirects EVERY download in this browser (all tabs, all origins) into the toolkit's downloads directory for as long as this server runs, and downloads no longer land in the user's normal Downloads folder. Because the arm lives on a connection this server process holds open, this is an MCP-server capability: under the one-shot CLI the connection dies with the process and nothing is captured. Chrome-only (capability 'browser.downloads'): absent from tools/list under the Firefox backend, never present-and-throwing, because WebDriver BiDi has no command to redirect a download to a chosen directory.",
|
|
4733
|
+
inputSchema: {
|
|
4734
|
+
type: "object",
|
|
4735
|
+
properties: {
|
|
4736
|
+
lease: {
|
|
4737
|
+
type: "string",
|
|
4738
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4739
|
+
},
|
|
4740
|
+
target: {
|
|
4741
|
+
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."
|
|
4743
|
+
},
|
|
4744
|
+
arm: {
|
|
4745
|
+
type: "boolean",
|
|
4746
|
+
description: "Arm download capture and return immediately instead of waiting: {armed:true,downloadPath,pending}. Call this BEFORE the click that triggers the download. 'pending' counts downloads that already completed and have not been collected yet, so an arm call doubles as a peek."
|
|
4747
|
+
},
|
|
4748
|
+
timeoutMs: {
|
|
4749
|
+
type: "number",
|
|
4750
|
+
description: "How long to wait for a download to complete, in milliseconds. Default 30000. Ignored with arm:true. A download that already completed and was not yet collected is returned immediately, so this only bounds the wait for a download still in flight."
|
|
4751
|
+
}
|
|
4752
|
+
},
|
|
4753
|
+
required: [],
|
|
4754
|
+
additionalProperties: false
|
|
4755
|
+
}
|
|
4756
|
+
},
|
|
4757
|
+
{
|
|
4758
|
+
name: "grant_permissions",
|
|
4759
|
+
description: "Grant browser permissions for an origin up front, so a page asking for geolocation / notifications / clipboard gets an answer instead of showing a prompt no agent can click. 'permissions' takes CDP PermissionType values ('geolocation', 'notifications', 'clipboardReadWrite', 'camera', 'microphone', 'midi', ...); an unknown name is refused by Chrome with the bad value in the message. Returns {granted,origin,target}, or {reset:true,target} for a reset-only call. Grants are keyed by ORIGIN, not by tab: every tab on that origin is affected, including ones opened later. reset:true clears this server's previous grants first (or instead, when no permissions are given) — note CDP's reset is not origin-scoped, so it clears them for every origin at once. The grant lives on a connection this server process holds open and Chrome DISCARDS IT when that connection closes, so this is an MCP-server capability: under the one-shot CLI the grant dies with the process. Chrome-only (capability 'browser.permissions'): absent from tools/list under the Firefox backend, never present-and-throwing.",
|
|
4760
|
+
inputSchema: {
|
|
4761
|
+
type: "object",
|
|
4762
|
+
properties: {
|
|
4763
|
+
lease: {
|
|
4764
|
+
type: "string",
|
|
4765
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4766
|
+
},
|
|
4767
|
+
target: {
|
|
4768
|
+
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."
|
|
4770
|
+
},
|
|
4771
|
+
permissions: {
|
|
4772
|
+
type: "array",
|
|
4773
|
+
items: {
|
|
4774
|
+
type: "string"
|
|
4775
|
+
},
|
|
4776
|
+
description: "CDP PermissionType values to grant, e.g. ['geolocation'] or ['clipboardReadWrite','clipboardSanitizedWrite']. Required unless reset:true. An empty array is refused rather than treated as a successful no-op."
|
|
4777
|
+
},
|
|
4778
|
+
origin: {
|
|
4779
|
+
type: "string",
|
|
4780
|
+
description: "Origin the grant applies to, e.g. 'https://example.com'. Defaults to the target tab's own origin. Required explicitly when that tab has no grantable origin (data:, blob:, about:blank all serialize to the opaque origin 'null'), which is refused with a message naming the tab's url."
|
|
4781
|
+
},
|
|
4782
|
+
reset: {
|
|
4783
|
+
type: "boolean",
|
|
4784
|
+
description: "Clear this server's previous permission grants (Browser.resetPermissions). With 'permissions' it resets FIRST and then grants, so the result is exactly the listed permissions; on its own it is a reset-only call answering {reset:true}. Not scoped to 'origin': CDP resets every origin at once."
|
|
4785
|
+
}
|
|
4786
|
+
},
|
|
4787
|
+
required: [],
|
|
4788
|
+
additionalProperties: false
|
|
4789
|
+
}
|
|
4790
|
+
},
|
|
3650
4791
|
{
|
|
3651
4792
|
name: "fill",
|
|
3652
4793
|
description: "Fill an element by focusing it, clearing existing content, then inserting 'value' via Input.insertText (atomic paste-like commit, not per-character keystrokes). Target with exactly one of 'uid' (a CDP backendDOMNodeId from take_snapshot) or 'selector' (a CSS selector).",
|
|
@@ -3655,7 +4796,7 @@ var MANIFEST = [
|
|
|
3655
4796
|
properties: {
|
|
3656
4797
|
lease: {
|
|
3657
4798
|
type: "string",
|
|
3658
|
-
description: "Opaque lease token from claim_page.
|
|
4799
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3659
4800
|
},
|
|
3660
4801
|
target: {
|
|
3661
4802
|
type: "string",
|
|
@@ -3688,7 +4829,7 @@ var MANIFEST = [
|
|
|
3688
4829
|
properties: {
|
|
3689
4830
|
lease: {
|
|
3690
4831
|
type: "string",
|
|
3691
|
-
description: "Opaque lease token from claim_page.
|
|
4832
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3692
4833
|
},
|
|
3693
4834
|
target: {
|
|
3694
4835
|
type: "string",
|
|
@@ -3734,7 +4875,7 @@ var MANIFEST = [
|
|
|
3734
4875
|
properties: {
|
|
3735
4876
|
lease: {
|
|
3736
4877
|
type: "string",
|
|
3737
|
-
description: "Opaque lease token from claim_page.
|
|
4878
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3738
4879
|
},
|
|
3739
4880
|
target: {
|
|
3740
4881
|
type: "string",
|
|
@@ -3767,7 +4908,7 @@ var MANIFEST = [
|
|
|
3767
4908
|
properties: {
|
|
3768
4909
|
lease: {
|
|
3769
4910
|
type: "string",
|
|
3770
|
-
description: "Opaque lease token from claim_page.
|
|
4911
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3771
4912
|
},
|
|
3772
4913
|
target: {
|
|
3773
4914
|
type: "string",
|
|
@@ -3799,7 +4940,7 @@ var MANIFEST = [
|
|
|
3799
4940
|
properties: {
|
|
3800
4941
|
lease: {
|
|
3801
4942
|
type: "string",
|
|
3802
|
-
description: "Opaque lease token from claim_page.
|
|
4943
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3803
4944
|
},
|
|
3804
4945
|
target: {
|
|
3805
4946
|
type: "string",
|
|
@@ -3838,7 +4979,7 @@ var MANIFEST = [
|
|
|
3838
4979
|
properties: {
|
|
3839
4980
|
lease: {
|
|
3840
4981
|
type: "string",
|
|
3841
|
-
description: "Opaque lease token from claim_page.
|
|
4982
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3842
4983
|
},
|
|
3843
4984
|
target: {
|
|
3844
4985
|
type: "string",
|
|
@@ -3889,7 +5030,7 @@ var MANIFEST = [
|
|
|
3889
5030
|
properties: {
|
|
3890
5031
|
lease: {
|
|
3891
5032
|
type: "string",
|
|
3892
|
-
description: "Opaque lease token from claim_page.
|
|
5033
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3893
5034
|
},
|
|
3894
5035
|
target: {
|
|
3895
5036
|
type: "string",
|
|
@@ -3989,7 +5130,7 @@ var MANIFEST = [
|
|
|
3989
5130
|
properties: {
|
|
3990
5131
|
lease: {
|
|
3991
5132
|
type: "string",
|
|
3992
|
-
description: "Opaque lease token from claim_page.
|
|
5133
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
3993
5134
|
},
|
|
3994
5135
|
target: {
|
|
3995
5136
|
type: "string",
|
|
@@ -4027,7 +5168,7 @@ var MANIFEST = [
|
|
|
4027
5168
|
properties: {
|
|
4028
5169
|
lease: {
|
|
4029
5170
|
type: "string",
|
|
4030
|
-
description: "Opaque lease token from claim_page.
|
|
5171
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4031
5172
|
},
|
|
4032
5173
|
target: {
|
|
4033
5174
|
type: "string",
|
|
@@ -4064,7 +5205,7 @@ var MANIFEST = [
|
|
|
4064
5205
|
properties: {
|
|
4065
5206
|
lease: {
|
|
4066
5207
|
type: "string",
|
|
4067
|
-
description: "Opaque lease token from claim_page.
|
|
5208
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4068
5209
|
},
|
|
4069
5210
|
target: {
|
|
4070
5211
|
type: "string",
|
|
@@ -4091,7 +5232,7 @@ var MANIFEST = [
|
|
|
4091
5232
|
properties: {
|
|
4092
5233
|
lease: {
|
|
4093
5234
|
type: "string",
|
|
4094
|
-
description: "Opaque lease token from claim_page.
|
|
5235
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4095
5236
|
},
|
|
4096
5237
|
target: {
|
|
4097
5238
|
type: "string",
|
|
@@ -4114,7 +5255,7 @@ var MANIFEST = [
|
|
|
4114
5255
|
properties: {
|
|
4115
5256
|
lease: {
|
|
4116
5257
|
type: "string",
|
|
4117
|
-
description: "Opaque lease token from claim_page.
|
|
5258
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4118
5259
|
},
|
|
4119
5260
|
target: {
|
|
4120
5261
|
type: "string",
|
|
@@ -4145,7 +5286,7 @@ var MANIFEST = [
|
|
|
4145
5286
|
properties: {
|
|
4146
5287
|
lease: {
|
|
4147
5288
|
type: "string",
|
|
4148
|
-
description: "Opaque lease token from claim_page.
|
|
5289
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4149
5290
|
},
|
|
4150
5291
|
target: {
|
|
4151
5292
|
type: "string",
|
|
@@ -4180,7 +5321,7 @@ var MANIFEST = [
|
|
|
4180
5321
|
properties: {
|
|
4181
5322
|
lease: {
|
|
4182
5323
|
type: "string",
|
|
4183
|
-
description: "Opaque lease token from claim_page.
|
|
5324
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4184
5325
|
},
|
|
4185
5326
|
target: {
|
|
4186
5327
|
type: "string",
|
|
@@ -4206,7 +5347,7 @@ var MANIFEST = [
|
|
|
4206
5347
|
properties: {
|
|
4207
5348
|
lease: {
|
|
4208
5349
|
type: "string",
|
|
4209
|
-
description: "Opaque lease token from claim_page.
|
|
5350
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4210
5351
|
},
|
|
4211
5352
|
target: {
|
|
4212
5353
|
type: "string",
|
|
@@ -4225,7 +5366,7 @@ var MANIFEST = [
|
|
|
4225
5366
|
properties: {
|
|
4226
5367
|
lease: {
|
|
4227
5368
|
type: "string",
|
|
4228
|
-
description: "Opaque lease token from claim_page.
|
|
5369
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4229
5370
|
},
|
|
4230
5371
|
tracePath: {
|
|
4231
5372
|
type: "string",
|
|
@@ -4244,7 +5385,7 @@ var MANIFEST = [
|
|
|
4244
5385
|
properties: {
|
|
4245
5386
|
lease: {
|
|
4246
5387
|
type: "string",
|
|
4247
|
-
description: "Opaque lease token from claim_page.
|
|
5388
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4248
5389
|
},
|
|
4249
5390
|
target: {
|
|
4250
5391
|
type: "string",
|
|
@@ -4282,7 +5423,7 @@ var MANIFEST = [
|
|
|
4282
5423
|
properties: {
|
|
4283
5424
|
lease: {
|
|
4284
5425
|
type: "string",
|
|
4285
|
-
description: "Opaque lease token from claim_page.
|
|
5426
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4286
5427
|
},
|
|
4287
5428
|
target: {
|
|
4288
5429
|
type: "string",
|
|
@@ -4329,7 +5470,7 @@ var MANIFEST = [
|
|
|
4329
5470
|
properties: {
|
|
4330
5471
|
lease: {
|
|
4331
5472
|
type: "string",
|
|
4332
|
-
description: "Opaque lease token from claim_page.
|
|
5473
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4333
5474
|
},
|
|
4334
5475
|
target: {
|
|
4335
5476
|
type: "string",
|
|
@@ -4352,7 +5493,7 @@ var MANIFEST = [
|
|
|
4352
5493
|
properties: {
|
|
4353
5494
|
lease: {
|
|
4354
5495
|
type: "string",
|
|
4355
|
-
description: "Opaque lease token from claim_page.
|
|
5496
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4356
5497
|
},
|
|
4357
5498
|
target: {
|
|
4358
5499
|
type: "string",
|
|
@@ -4375,7 +5516,7 @@ var MANIFEST = [
|
|
|
4375
5516
|
properties: {
|
|
4376
5517
|
lease: {
|
|
4377
5518
|
type: "string",
|
|
4378
|
-
description: "Opaque lease token from claim_page.
|
|
5519
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4379
5520
|
},
|
|
4380
5521
|
url: {
|
|
4381
5522
|
type: "string",
|
|
@@ -4419,7 +5560,7 @@ var MANIFEST = [
|
|
|
4419
5560
|
properties: {
|
|
4420
5561
|
lease: {
|
|
4421
5562
|
type: "string",
|
|
4422
|
-
description: "Opaque lease token from claim_page.
|
|
5563
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4423
5564
|
},
|
|
4424
5565
|
target: {
|
|
4425
5566
|
type: "string",
|
|
@@ -4489,7 +5630,7 @@ var MANIFEST = [
|
|
|
4489
5630
|
properties: {
|
|
4490
5631
|
lease: {
|
|
4491
5632
|
type: "string",
|
|
4492
|
-
description: "Opaque lease token from claim_page.
|
|
5633
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4493
5634
|
},
|
|
4494
5635
|
target: {
|
|
4495
5636
|
type: "string",
|
|
@@ -4508,7 +5649,7 @@ var MANIFEST = [
|
|
|
4508
5649
|
properties: {
|
|
4509
5650
|
lease: {
|
|
4510
5651
|
type: "string",
|
|
4511
|
-
description: "Opaque lease token from claim_page.
|
|
5652
|
+
description: "Opaque lease token from claim_page. Omit it for a tab this process already holds: under CDP_REQUIRE_LEASE the gate acquires a lease automatically and one acquired that way passes for any later call from the same process. Required when the tab is held by ANOTHER process, and required for a tab claimed explicitly via claim_page or new_page{claim:true}, which always demands its own token. Without CDP_REQUIRE_LEASE an unleased tab needs no token at all."
|
|
4512
5653
|
},
|
|
4513
5654
|
target: {
|
|
4514
5655
|
type: "string",
|
|
@@ -4525,17 +5666,21 @@ var MANIFEST = [
|
|
|
4525
5666
|
},
|
|
4526
5667
|
{
|
|
4527
5668
|
name: "claim_page",
|
|
4528
|
-
description: "Take exclusive ownership of a browser tab and return an opaque lease token.
|
|
5669
|
+
description: "Take exclusive ownership of a browser tab and return an opaque lease token. Two modes. FRESH TAB: with no target and no targetId, opens a new tab (optionally at url) and claims it, so 'give me my own tab' is one call. TAKEOVER: with target, claims a tab that is already open — this is how you work in a tab the human already has open when they ask you to, instead of opening your own — resolving any target selector against the live page list and never creating anything. The answer's 'opened' flag says which happened (true only when this call created the tab), and that same creation record is what release_page consults, so a tab you took over is left open when you release it. Any later tool call against a leased tab must carry the matching token in its 'lease' argument or it is refused, naming the holder. The lease is reclaimable once its owning process dies, its ttlMs elapses without use, or its tab is closed. Refused from the CLI: a CLI invocation is one process per call, so its lease would be reclaimable immediately. IS SOMEBODY ELSE IN THIS TAB: the answer also carries 'humanActiveMs', milliseconds since input that this server did not dispatch — i.e. since a person last clicked, typed, or scrolled here. null means NO DATA (the tab carries no activity beacon, which is normal the first time anyone claims a person's tab, or every input on it was this server's own); it never means 'no human'. The field is absent entirely on a backend that cannot answer. On a takeover of a tab a human used within the last 30 seconds the answer additionally carries 'contention', a warning string. THE CLAIM IS NEVER REFUSED FOR THIS: taking over a person's tab is what takeover mode is for, so by the time you read the warning you already hold the lease and the tab is yours to drive. It tells you that driving it now means fighting a live person for the keyboard and mouse, so you can open your own tab instead or ask first. Known limits: input inside a cross-origin iframe is invisible to it, and a second MCP server's clicks read as human to this one.",
|
|
4529
5670
|
inputSchema: {
|
|
4530
5671
|
type: "object",
|
|
4531
5672
|
properties: {
|
|
5673
|
+
target: {
|
|
5674
|
+
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."
|
|
5676
|
+
},
|
|
4532
5677
|
targetId: {
|
|
4533
5678
|
type: "string",
|
|
4534
|
-
description: "Claim this already-open page target instead of opening a new tab. Must be an exact target id, not a selector."
|
|
5679
|
+
description: "Claim this already-open page target instead of opening a new tab. Must be an exact target id, not a selector. Kept for back-compat: prefer 'target', which accepts an exact id too plus every other selector form. Mutually exclusive with target."
|
|
4535
5680
|
},
|
|
4536
5681
|
url: {
|
|
4537
5682
|
type: "string",
|
|
4538
|
-
description: "When opening a fresh tab, navigate it here. Ignored when targetId is given."
|
|
5683
|
+
description: "When opening a fresh tab, navigate it here. Ignored when target or targetId is given, since both of those claim a tab that is already open."
|
|
4539
5684
|
},
|
|
4540
5685
|
label: {
|
|
4541
5686
|
type: "string",
|
|
@@ -4552,24 +5697,30 @@ var MANIFEST = [
|
|
|
4552
5697
|
},
|
|
4553
5698
|
{
|
|
4554
5699
|
name: "release_page",
|
|
4555
|
-
description: "
|
|
5700
|
+
description: "Give a lease back, and close the tab if this toolkit opened it. Takes exactly one of 'lease' (the token claim_page returned) or 'target' (a selector for a tab this process holds, which is how you release a lease the gate acquired for you automatically, since that path never hands you a token). A tab with a creation record from this toolkit is closed; a tab that was already open and merely claimed is released and left alone. Override either way with 'close'. Idempotent: an already-released, reclaimed, or expired lease reports released:false, and a release that did not happen never closes anything, because by then the tab may belong to another agent. Answers {released, closed, targetId}.",
|
|
4556
5701
|
inputSchema: {
|
|
4557
5702
|
type: "object",
|
|
4558
5703
|
properties: {
|
|
4559
5704
|
lease: {
|
|
4560
5705
|
type: "string",
|
|
4561
|
-
description: "The opaque token claim_page (or new_page with claim:true) returned."
|
|
5706
|
+
description: "The opaque token claim_page (or new_page with claim:true) returned. Mutually exclusive with 'target'. Under CDP_REQUIRE_LEASE a lease the gate acquired for you automatically never produced a token in the first place, so give 'target' instead."
|
|
5707
|
+
},
|
|
5708
|
+
target: {
|
|
5709
|
+
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."
|
|
5711
|
+
},
|
|
5712
|
+
close: {
|
|
5713
|
+
type: "boolean",
|
|
5714
|
+
description: "Force the close decision instead of letting the creation ledger decide. true closes even a tab this toolkit did not open; false keeps an agent-created tab open. Omit for the default, which closes only tabs this toolkit created."
|
|
4562
5715
|
}
|
|
4563
5716
|
},
|
|
4564
|
-
required: [
|
|
4565
|
-
"lease"
|
|
4566
|
-
],
|
|
5717
|
+
required: [],
|
|
4567
5718
|
additionalProperties: false
|
|
4568
5719
|
}
|
|
4569
5720
|
},
|
|
4570
5721
|
{
|
|
4571
5722
|
name: "list_leases",
|
|
4572
|
-
description:
|
|
5723
|
+
description: "Enumerate every active lease for diagnosis: backend, target id, agent label, pid, createdAt, lastUsedAt, ttlMs, whether the owning process is still alive, and whether the lease is reclaimable and why. Requires no token. Never returns the lease nonce, so this cannot be used to forge a token. A row for a lease file that could not be read or parsed instead carries `unreadable` (the errno, or \"unparseable\"), with label, pid, and the timestamp fields as zero placeholders and stale always false, since stale is what marks a lease free to take and an unreadable one must never read that way. Under CDP_REQUIRE_LEASE this call reaps abandoned agent tabs exactly as list_pages does (see that tool's description for the grace period between a lease reading reclaimable and its tab actually being closed), reporting them in an additive 'reaped' array and omitting their rows from the listing. A row also carries 'humanActiveMs' — milliseconds since input this server did not dispatch, i.e. since a person last used that tab — WHERE THE TAB CAN ANSWER. The field is ABSENT rather than null when it cannot: no activity beacon on that tab, every input on it accounted for by this server, a row belonging to the other backend, a tab no longer open, or a page that would not evaluate. Absence therefore means 'no answer', never 'nobody is there'. Note that lastUsedAt measures TOOLKIT calls only, so a tab a person has been typing in for ten minutes looks perfectly idle by that field and is exactly the case humanActiveMs exists to expose. Two further fields are computed fresh on every call, never read off disk: 'idleMs' (now minus lastUsedAt) and 'expiresAt' (lastUsedAt plus ttlMs — the same value claim_page already returns under that name). Both are omitted on an 'unreadable' row, whose lastUsedAt and ttlMs are the zeroed placeholders described above.",
|
|
4573
5724
|
inputSchema: {
|
|
4574
5725
|
type: "object",
|
|
4575
5726
|
properties: {},
|
|
@@ -5333,6 +6484,8 @@ var KEY_VALUES = {
|
|
|
5333
6484
|
};
|
|
5334
6485
|
var MODIFIER_VALUES = { shift: "", control: "", ctrl: "", alt: "", meta: "", cmd: "", command: "" };
|
|
5335
6486
|
var BUTTON_CODES = { left: 0, middle: 1, right: 2 };
|
|
6487
|
+
var ARM_SCROLL_SETTLE_WATCH_SOURCE = "function(){window.__cdpScrollSettle=new Promise((resolve)=>{let t;const done=()=>{window.removeEventListener('scroll',on,true);resolve(true);};" + "const on=()=>{clearTimeout(t);t=setTimeout(done,60);};window.addEventListener('scroll',on,true);t=setTimeout(done,60);setTimeout(done,500);});}";
|
|
6488
|
+
var AWAIT_SCROLL_SETTLE_SOURCE = "function(){return window.__cdpScrollSettle;}";
|
|
5336
6489
|
var BIDI_CAPABILITIES = new Set([
|
|
5337
6490
|
"emulate.deviceMetrics",
|
|
5338
6491
|
"screenshot.fullPage",
|
|
@@ -5354,6 +6507,7 @@ async function getConnection(port, timeoutMs) {
|
|
|
5354
6507
|
connections.set(port, conn);
|
|
5355
6508
|
return conn;
|
|
5356
6509
|
}
|
|
6510
|
+
var bidiBeaconScripts = new Map;
|
|
5357
6511
|
async function safeTitle(conn, contextId) {
|
|
5358
6512
|
try {
|
|
5359
6513
|
const res = await conn.send("script.evaluate", { expression: "document.title", target: { context: contextId }, awaitPromise: false });
|
|
@@ -5417,9 +6571,31 @@ class BidiPageDriver {
|
|
|
5417
6571
|
this.info = info;
|
|
5418
6572
|
this.browser = browser;
|
|
5419
6573
|
}
|
|
6574
|
+
async urlAndReadyState() {
|
|
6575
|
+
const raw = await this.conn.send("script.evaluate", {
|
|
6576
|
+
expression: "location.href + '\\u0000' + document.readyState",
|
|
6577
|
+
target: { context: this.contextId },
|
|
6578
|
+
awaitPromise: false
|
|
6579
|
+
}).then((r) => r.type === "success" ? String(deserializeRemote(r.result) ?? "") : "").catch(() => "");
|
|
6580
|
+
const [url = "", ready = ""] = raw.split("\x00");
|
|
6581
|
+
return [url, ready];
|
|
6582
|
+
}
|
|
6583
|
+
async settleAfterTraversal(beforeUrl, waitUntil, timeoutMs) {
|
|
6584
|
+
const ready = waitUntil === "domcontentloaded" ? ["interactive", "complete"] : ["complete"];
|
|
6585
|
+
const deadline = Date.now() + timeoutMs;
|
|
6586
|
+
for (;; ) {
|
|
6587
|
+
const [url, state] = await this.urlAndReadyState();
|
|
6588
|
+
if (url && url !== beforeUrl && ready.includes(state))
|
|
6589
|
+
return { waitedFor: waitUntil, url };
|
|
6590
|
+
if (Date.now() >= deadline)
|
|
6591
|
+
return { waitedFor: "timeout", url: url || beforeUrl };
|
|
6592
|
+
await new Promise((r) => setTimeout(r, 50));
|
|
6593
|
+
}
|
|
6594
|
+
}
|
|
5420
6595
|
async navigate(opts) {
|
|
5421
6596
|
const reload = opts.reload === true;
|
|
5422
|
-
|
|
6597
|
+
const history = opts.history;
|
|
6598
|
+
if (!reload && !history && !opts.url)
|
|
5423
6599
|
throw driverError2("page-error", "navigate: 'url' is required unless reload:true");
|
|
5424
6600
|
const waitUntil = opts.waitUntil ?? "load";
|
|
5425
6601
|
const wait = waitUntil === "domcontentloaded" ? "interactive" : "complete";
|
|
@@ -5429,6 +6605,17 @@ class BidiPageDriver {
|
|
|
5429
6605
|
const r2 = await this.conn.send("browsingContext.reload", { context: this.contextId, ignoreCache: opts.ignoreCache === true, wait }, sendOpts);
|
|
5430
6606
|
return { url: r2.url, contextId: this.contextId, reloaded: true, waitedFor: waitUntil };
|
|
5431
6607
|
}
|
|
6608
|
+
if (history) {
|
|
6609
|
+
const [beforeUrl] = await this.urlAndReadyState();
|
|
6610
|
+
await this.conn.send("browsingContext.traverseHistory", { context: this.contextId, delta: history === "back" ? -1 : 1 }, sendOpts).catch((e) => {
|
|
6611
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
6612
|
+
if (/no such history entry|history/i.test(msg))
|
|
6613
|
+
throw driverError2("page-error", `navigate: no history entry to go ${history} to (${msg})`);
|
|
6614
|
+
throw e;
|
|
6615
|
+
});
|
|
6616
|
+
const settled = await this.settleAfterTraversal(beforeUrl, waitUntil, opts.timeoutMs ?? 15000);
|
|
6617
|
+
return { url: settled.url, contextId: this.contextId, traversed: history, waitedFor: settled.waitedFor };
|
|
6618
|
+
}
|
|
5432
6619
|
const r = await this.conn.send("browsingContext.navigate", { context: this.contextId, url: opts.url, wait }, sendOpts);
|
|
5433
6620
|
return { url: r.url, contextId: this.contextId, waitedFor: waitUntil };
|
|
5434
6621
|
} catch (e) {
|
|
@@ -5511,6 +6698,7 @@ class BidiPageDriver {
|
|
|
5511
6698
|
throw driverError2("stale-uid", `uid does not resolve: ${uid}`);
|
|
5512
6699
|
}
|
|
5513
6700
|
async performActions(actions) {
|
|
6701
|
+
recordDispatch("firefox", this.contextId);
|
|
5514
6702
|
try {
|
|
5515
6703
|
await this.conn.send("input.performActions", { context: this.contextId, actions });
|
|
5516
6704
|
} catch (e) {
|
|
@@ -5531,6 +6719,9 @@ class BidiPageDriver {
|
|
|
5531
6719
|
return { x, y };
|
|
5532
6720
|
}
|
|
5533
6721
|
async click(loc, opts) {
|
|
6722
|
+
if (opts?.modifiers && opts.modifiers.length > 0) {
|
|
6723
|
+
throw driverError2("unsupported", "click: 'modifiers' is not supported by the Firefox/BiDi driver; omit 'modifiers' or use --browser chrome for a modifier click.");
|
|
6724
|
+
}
|
|
5534
6725
|
const ref = await resolveElementLocator2(this.conn, this.contextId, loc);
|
|
5535
6726
|
const { x, y } = await this.centerOf(ref);
|
|
5536
6727
|
const button = BUTTON_CODES[opts?.button ?? "left"];
|
|
@@ -5540,19 +6731,53 @@ class BidiPageDriver {
|
|
|
5540
6731
|
await this.performActions([{ type: "pointer", id: "cdp-mouse", actions }]);
|
|
5541
6732
|
return { x, y };
|
|
5542
6733
|
}
|
|
5543
|
-
async
|
|
6734
|
+
async scroll(anchor, opts) {
|
|
6735
|
+
const { x, y } = await this.resolveScrollPoint(anchor);
|
|
6736
|
+
const deltaX = Math.round(opts.deltaX ?? 0);
|
|
6737
|
+
const deltaY = Math.round(opts.deltaY ?? 0);
|
|
6738
|
+
await this.callFunctionValue(ARM_SCROLL_SETTLE_WATCH_SOURCE, []);
|
|
6739
|
+
const actions = [{ type: "scroll", x: Math.round(x), y: Math.round(y), deltaX, deltaY }];
|
|
6740
|
+
await this.performActions([{ type: "wheel", id: "cdp-wheel", actions }]);
|
|
6741
|
+
await this.callFunctionValue(AWAIT_SCROLL_SETTLE_SOURCE, []).catch(() => {
|
|
6742
|
+
return;
|
|
6743
|
+
});
|
|
6744
|
+
return { x, y };
|
|
6745
|
+
}
|
|
6746
|
+
async resolveScrollPoint(anchor) {
|
|
6747
|
+
if (anchor === undefined) {
|
|
6748
|
+
const v = await this.callFunctionValue("function(){return {x: window.innerWidth / 2, y: window.innerHeight / 2};}", []);
|
|
6749
|
+
if (!v || typeof v !== "object")
|
|
6750
|
+
throw driverError2("page-error", "could not measure viewport for scroll center");
|
|
6751
|
+
return v;
|
|
6752
|
+
}
|
|
6753
|
+
if ("x" in anchor)
|
|
6754
|
+
return anchor;
|
|
6755
|
+
return this.centerOf(await resolveElementLocator2(this.conn, this.contextId, anchor));
|
|
6756
|
+
}
|
|
6757
|
+
async drag(from, to, opts) {
|
|
6758
|
+
if (opts?.mode === "html5") {
|
|
6759
|
+
throw driverError2("unsupported", "drag: mode:'html5' is not supported by the Firefox/BiDi driver (WebDriver BiDi has no drag-interception primitive); " + "omit 'mode' for the synthetic-mouse drag, or use --browser chrome for real HTML5 drag events.");
|
|
6760
|
+
}
|
|
5544
6761
|
const fromPt = await this.centerOf(await resolveElementLocator2(this.conn, this.contextId, from));
|
|
5545
|
-
const toPt = await this.
|
|
6762
|
+
const toPt = await this.resolveDragDestination(to, fromPt);
|
|
6763
|
+
const steps = Math.max(1, Math.trunc(opts?.steps ?? 2));
|
|
5546
6764
|
const actions = [
|
|
5547
6765
|
{ type: "pointerMove", x: fromPt.x, y: fromPt.y, duration: 0 },
|
|
5548
|
-
{ type: "pointerDown", button: 0 }
|
|
5549
|
-
{ type: "pointerMove", x: (fromPt.x + toPt.x) / 2, y: (fromPt.y + toPt.y) / 2, duration: 50 },
|
|
5550
|
-
{ type: "pointerMove", x: toPt.x, y: toPt.y, duration: 50 },
|
|
5551
|
-
{ type: "pointerUp", button: 0 }
|
|
6766
|
+
{ type: "pointerDown", button: 0 }
|
|
5552
6767
|
];
|
|
6768
|
+
for (const pt of interpolatePoints(fromPt, toPt, steps))
|
|
6769
|
+
actions.push({ type: "pointerMove", x: pt.x, y: pt.y, duration: 50 });
|
|
6770
|
+
actions.push({ type: "pointerUp", button: 0 });
|
|
5553
6771
|
await this.performActions([{ type: "pointer", id: "cdp-mouse", actions }]);
|
|
5554
6772
|
return { from: fromPt, to: toPt };
|
|
5555
6773
|
}
|
|
6774
|
+
async resolveDragDestination(to, fromPt) {
|
|
6775
|
+
if ("dx" in to)
|
|
6776
|
+
return { x: fromPt.x + to.dx, y: fromPt.y + to.dy };
|
|
6777
|
+
if ("x" in to)
|
|
6778
|
+
return to;
|
|
6779
|
+
return this.centerOf(await resolveElementLocator2(this.conn, this.contextId, to));
|
|
6780
|
+
}
|
|
5556
6781
|
async sendKeys(text) {
|
|
5557
6782
|
if (!text.length)
|
|
5558
6783
|
return;
|
|
@@ -5922,7 +7147,60 @@ class BidiBrowserDriver {
|
|
|
5922
7147
|
const info = await pageInfoOf(conn, ctx);
|
|
5923
7148
|
return new BidiPageDriver(conn, ctx.context, info, this);
|
|
5924
7149
|
}
|
|
7150
|
+
async installActivityBeacon(contextId) {
|
|
7151
|
+
const conn = await getConnection(this.port, this.timeoutMs).catch(() => {
|
|
7152
|
+
return;
|
|
7153
|
+
});
|
|
7154
|
+
if (!conn)
|
|
7155
|
+
return false;
|
|
7156
|
+
try {
|
|
7157
|
+
const key = `${this.port}:${contextId}`;
|
|
7158
|
+
if (!bidiBeaconScripts.has(key)) {
|
|
7159
|
+
const registered = await conn.send("script.addPreloadScript", {
|
|
7160
|
+
functionDeclaration: BEACON_FUNCTION_DECLARATION,
|
|
7161
|
+
contexts: [contextId]
|
|
7162
|
+
});
|
|
7163
|
+
bidiBeaconScripts.set(key, registered.script);
|
|
7164
|
+
}
|
|
7165
|
+
await conn.send("script.evaluate", {
|
|
7166
|
+
expression: BEACON_SOURCE,
|
|
7167
|
+
target: { context: contextId },
|
|
7168
|
+
awaitPromise: false
|
|
7169
|
+
});
|
|
7170
|
+
return true;
|
|
7171
|
+
} catch {
|
|
7172
|
+
return false;
|
|
7173
|
+
} finally {
|
|
7174
|
+
conn.release();
|
|
7175
|
+
}
|
|
7176
|
+
}
|
|
7177
|
+
async readActivityBeacon(contextId) {
|
|
7178
|
+
const conn = await getConnection(this.port, this.timeoutMs).catch(() => {
|
|
7179
|
+
return;
|
|
7180
|
+
});
|
|
7181
|
+
if (!conn)
|
|
7182
|
+
return null;
|
|
7183
|
+
try {
|
|
7184
|
+
const res = await conn.send("script.evaluate", {
|
|
7185
|
+
expression: BEACON_READ_EXPRESSION,
|
|
7186
|
+
target: { context: contextId },
|
|
7187
|
+
awaitPromise: false
|
|
7188
|
+
});
|
|
7189
|
+
if (res.type !== "success")
|
|
7190
|
+
return null;
|
|
7191
|
+
const value = deserializeRemote(res.result);
|
|
7192
|
+
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
7193
|
+
} catch {
|
|
7194
|
+
return null;
|
|
7195
|
+
} finally {
|
|
7196
|
+
conn.release();
|
|
7197
|
+
}
|
|
7198
|
+
}
|
|
5925
7199
|
async dispose() {
|
|
7200
|
+
for (const key of [...bidiBeaconScripts.keys()]) {
|
|
7201
|
+
if (key.startsWith(`${this.port}:`))
|
|
7202
|
+
bidiBeaconScripts.delete(key);
|
|
7203
|
+
}
|
|
5926
7204
|
const conn = connections.get(this.port);
|
|
5927
7205
|
if (!conn)
|
|
5928
7206
|
return;
|
|
@@ -5939,7 +7217,7 @@ import { spawn as spawn3 } from "node:child_process";
|
|
|
5939
7217
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
5940
7218
|
import { createConnection, createServer } from "node:net";
|
|
5941
7219
|
import { tmpdir } from "node:os";
|
|
5942
|
-
import { join as
|
|
7220
|
+
import { join as join9 } from "node:path";
|
|
5943
7221
|
function resolveBinary() {
|
|
5944
7222
|
const candidates = [
|
|
5945
7223
|
process.env.FIREFOX_BIN,
|
|
@@ -6013,7 +7291,7 @@ async function launchFirefox(opts = {}) {
|
|
|
6013
7291
|
const binary = resolveBinary();
|
|
6014
7292
|
const port = await pickPort(opts.port);
|
|
6015
7293
|
const isThrowaway = opts.profilePath === undefined;
|
|
6016
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
7294
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join9(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
6017
7295
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
6018
7296
|
if (opts.headless)
|
|
6019
7297
|
args.push("--headless");
|
|
@@ -6283,7 +7561,7 @@ var FIREFOX_TOOLS = {
|
|
|
6283
7561
|
};
|
|
6284
7562
|
|
|
6285
7563
|
// src/mcp.ts
|
|
6286
|
-
var VERSION = "1.
|
|
7564
|
+
var VERSION = "1.8.0";
|
|
6287
7565
|
var BROWSER = resolveBrowserKind(stripBrowserFlag(process.argv.slice(2)));
|
|
6288
7566
|
var AVAILABILITY = toolAvailability(BROWSER);
|
|
6289
7567
|
var AVAILABLE_NAMES = new Set(AVAILABILITY.available);
|
|
@@ -6330,8 +7608,8 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6330
7608
|
if (!neutralFn)
|
|
6331
7609
|
return { content: [{ type: "text", text: `unknown tool: ${name}` }], isError: true };
|
|
6332
7610
|
try {
|
|
6333
|
-
const
|
|
6334
|
-
const result = await neutralFn(
|
|
7611
|
+
const session2 = await getOrCreateFirefoxSession();
|
|
7612
|
+
const result = await neutralFn(session2.driver, args);
|
|
6335
7613
|
return { content: [{ type: "text", text: JSON.stringify(result, null, 2) }] };
|
|
6336
7614
|
} catch (err) {
|
|
6337
7615
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -6340,6 +7618,7 @@ server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
|
6340
7618
|
});
|
|
6341
7619
|
});
|
|
6342
7620
|
auditCoverage();
|
|
7621
|
+
markLongLivedProcess();
|
|
6343
7622
|
await server.connect(new StdioServerTransport);
|
|
6344
7623
|
console.error(`[cdp-toolkit] MCP server v${VERSION} ready, browser=${BROWSER}, ${AVAILABILITY.available.length} tools, CDP_BASE=${BASE}`);
|
|
6345
7624
|
var shuttingDown = false;
|
|
@@ -6348,6 +7627,7 @@ async function shutdown() {
|
|
|
6348
7627
|
return;
|
|
6349
7628
|
shuttingDown = true;
|
|
6350
7629
|
await disposeFirefoxSession();
|
|
7630
|
+
await disposeBrowserSession();
|
|
6351
7631
|
process.exit(0);
|
|
6352
7632
|
}
|
|
6353
7633
|
process.on("SIGINT", () => void shutdown());
|