cdp-toolkit 2.2.0 → 2.5.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 +33 -0
- package/README.md +43 -17
- package/dist/bidi/mux-daemon.js +2 -0
- package/dist/cli.js +695 -42
- package/dist/index.js +514 -0
- package/dist/mcp.js +724 -30
- package/package.json +5 -3
- package/src/cdp/driver.ts +1 -1
- package/src/driver.ts +3 -0
- package/src/firefox-tools.ts +6 -3
- package/src/index.ts +16 -3
- package/src/manifest.ts +138 -0
- package/src/toolDocs.ts +37 -0
- package/src/toolGroups.ts +3 -2
- package/src/tools/extract.ts +777 -0
- package/src/tools/focus-emulation.ts +185 -0
- package/src/version.ts +1 -1
package/dist/cli.js
CHANGED
|
@@ -1090,6 +1090,8 @@ var init_driver = __esm(() => {
|
|
|
1090
1090
|
list_mocks: ["network.intercept"],
|
|
1091
1091
|
clear_mocks: ["network.intercept"],
|
|
1092
1092
|
dispatch_mouse: ["input.raw"],
|
|
1093
|
+
focus_emulation: ["emulate.focus"],
|
|
1094
|
+
click_focus_gated: ["emulate.focus"],
|
|
1093
1095
|
wait_for_download: ["browser.downloads"],
|
|
1094
1096
|
grant_permissions: ["browser.permissions"]
|
|
1095
1097
|
};
|
|
@@ -1127,7 +1129,7 @@ class BidiConnection {
|
|
|
1127
1129
|
return new Promise((resolve, reject) => {
|
|
1128
1130
|
const ws = new WebSocket(this.wsUrl);
|
|
1129
1131
|
this.ws = ws;
|
|
1130
|
-
const openTimer = setTimeout(() => reject(new BidiError(`connect timeout: ${this.wsUrl}`)), this.opts.timeoutMs ??
|
|
1132
|
+
const openTimer = setTimeout(() => reject(new BidiError(`connect timeout: ${this.wsUrl}`)), this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3);
|
|
1131
1133
|
ws.onopen = () => {
|
|
1132
1134
|
clearTimeout(openTimer);
|
|
1133
1135
|
resolve(this);
|
|
@@ -1187,7 +1189,7 @@ class BidiConnection {
|
|
|
1187
1189
|
if (this.closed || !this.ws)
|
|
1188
1190
|
return Promise.reject(new BidiError("connection not open"));
|
|
1189
1191
|
const id = ++this.nextId;
|
|
1190
|
-
const timeoutMs = opts.timeoutMs ?? this.opts.timeoutMs ??
|
|
1192
|
+
const timeoutMs = opts.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
1191
1193
|
const payload = { id, method, params };
|
|
1192
1194
|
return new Promise((resolve, reject) => {
|
|
1193
1195
|
const start = Date.now();
|
|
@@ -1234,7 +1236,7 @@ class BidiConnection {
|
|
|
1234
1236
|
}
|
|
1235
1237
|
};
|
|
1236
1238
|
}
|
|
1237
|
-
waitFor(event, predicate, timeoutMs = this.opts.timeoutMs ??
|
|
1239
|
+
waitFor(event, predicate, timeoutMs = this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3) {
|
|
1238
1240
|
return new Promise((resolve, reject) => {
|
|
1239
1241
|
const timer = setTimeout(() => {
|
|
1240
1242
|
off();
|
|
@@ -1278,9 +1280,9 @@ async function connectBidiSessionUrl(wsUrl, opts = {}) {
|
|
|
1278
1280
|
conn.retain();
|
|
1279
1281
|
return conn;
|
|
1280
1282
|
}
|
|
1281
|
-
var
|
|
1283
|
+
var DEFAULT_TIMEOUT_MS3, BidiError;
|
|
1282
1284
|
var init_client2 = __esm(() => {
|
|
1283
|
-
|
|
1285
|
+
DEFAULT_TIMEOUT_MS3 = Number(process.env.BIDI_TIMEOUT_MS ?? 15000);
|
|
1284
1286
|
BidiError = class BidiError extends Error {
|
|
1285
1287
|
code;
|
|
1286
1288
|
data;
|
|
@@ -1294,16 +1296,16 @@ var init_client2 = __esm(() => {
|
|
|
1294
1296
|
});
|
|
1295
1297
|
|
|
1296
1298
|
// src/bidi/session-lease.ts
|
|
1297
|
-
import { mkdir as
|
|
1298
|
-
import { join as
|
|
1299
|
+
import { mkdir as mkdir13, readFile as readFile8, rename as rename3, unlink as unlink5, writeFile as writeFile10 } from "node:fs/promises";
|
|
1300
|
+
import { join as join11 } from "node:path";
|
|
1299
1301
|
function safeEndpoint(endpoint) {
|
|
1300
1302
|
return endpoint.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1301
1303
|
}
|
|
1302
1304
|
function sessionSlotFile(endpoint) {
|
|
1303
|
-
return
|
|
1305
|
+
return join11(leaseDir(), `ff-session-${safeEndpoint(endpoint)}.json`);
|
|
1304
1306
|
}
|
|
1305
1307
|
function muxDaemonLogFile(endpoint) {
|
|
1306
|
-
return
|
|
1308
|
+
return join11(leaseDir(), `ff-mux-${safeEndpoint(endpoint)}.log`);
|
|
1307
1309
|
}
|
|
1308
1310
|
function sessionWaitMs() {
|
|
1309
1311
|
const raw = Number(process.env.CDP_FIREFOX_SESSION_WAIT_MS);
|
|
@@ -1341,7 +1343,7 @@ async function acquireSessionSlot(endpoint, opts) {
|
|
|
1341
1343
|
record.marionettePort = opts.marionettePort;
|
|
1342
1344
|
const handle = { endpoint, pid: process.pid, createdAt };
|
|
1343
1345
|
const payload = JSON.stringify(record);
|
|
1344
|
-
await
|
|
1346
|
+
await mkdir13(leaseDir(), { recursive: true });
|
|
1345
1347
|
const deadline = Date.now() + waitMs;
|
|
1346
1348
|
const maxAttempts = Math.max(100, Math.ceil(waitMs / pollMs) + 100);
|
|
1347
1349
|
let attempts = 0;
|
|
@@ -1351,7 +1353,7 @@ async function acquireSessionSlot(endpoint, opts) {
|
|
|
1351
1353
|
throw new SessionSlotBusyError(endpoint, lastLiveHolder);
|
|
1352
1354
|
attempts++;
|
|
1353
1355
|
try {
|
|
1354
|
-
await
|
|
1356
|
+
await writeFile10(file, payload, { flag: "wx" });
|
|
1355
1357
|
return { handle };
|
|
1356
1358
|
} catch (err) {
|
|
1357
1359
|
if (err.code !== "EEXIST")
|
|
@@ -1365,7 +1367,7 @@ async function acquireSessionSlot(endpoint, opts) {
|
|
|
1365
1367
|
return;
|
|
1366
1368
|
});
|
|
1367
1369
|
try {
|
|
1368
|
-
await
|
|
1370
|
+
await writeFile10(file, payload, { flag: "wx" });
|
|
1369
1371
|
return { handle, staleHolder: holder };
|
|
1370
1372
|
} catch (err) {
|
|
1371
1373
|
if (err.code !== "EEXIST")
|
|
@@ -1393,7 +1395,7 @@ async function advertiseMux(handle, muxEndpoint) {
|
|
|
1393
1395
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
1394
1396
|
const next = { ...current, muxEndpoint };
|
|
1395
1397
|
try {
|
|
1396
|
-
await
|
|
1398
|
+
await writeFile10(tmp, JSON.stringify(next), "utf8");
|
|
1397
1399
|
await rename3(tmp, file);
|
|
1398
1400
|
} catch {
|
|
1399
1401
|
await unlink5(tmp).catch(() => {
|
|
@@ -1967,7 +1969,7 @@ function isSlotFreedReply(frame) {
|
|
|
1967
1969
|
return false;
|
|
1968
1970
|
}
|
|
1969
1971
|
function forceClearOrphanSession(marionettePort, opts = {}) {
|
|
1970
|
-
const timeoutMs = opts.timeoutMs !== undefined && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs :
|
|
1972
|
+
const timeoutMs = opts.timeoutMs !== undefined && Number.isFinite(opts.timeoutMs) && opts.timeoutMs > 0 ? opts.timeoutMs : DEFAULT_TIMEOUT_MS4;
|
|
1971
1973
|
return new Promise((resolve) => {
|
|
1972
1974
|
if (!Number.isFinite(marionettePort) || marionettePort <= 0 || marionettePort > 65535) {
|
|
1973
1975
|
resolve(false);
|
|
@@ -2023,7 +2025,7 @@ function forceClearOrphanSession(marionettePort, opts = {}) {
|
|
|
2023
2025
|
}
|
|
2024
2026
|
});
|
|
2025
2027
|
}
|
|
2026
|
-
var DEFAULT_MARIONETTE_PORT = 2828,
|
|
2028
|
+
var DEFAULT_MARIONETTE_PORT = 2828, DEFAULT_TIMEOUT_MS4 = 4000, HOST = "127.0.0.1", COLON = 58, DELETE_SESSION_FRAME, MarionetteProtocolError;
|
|
2027
2029
|
var init_marionette = __esm(() => {
|
|
2028
2030
|
DELETE_SESSION_FRAME = encodeFrame2([0, 1, "WebDriver:DeleteSession", {}]);
|
|
2029
2031
|
MarionetteProtocolError = class MarionetteProtocolError extends Error {
|
|
@@ -4157,6 +4159,7 @@ var CDP_CAPABILITIES = new Set([
|
|
|
4157
4159
|
"capture.screencast",
|
|
4158
4160
|
"input.raw",
|
|
4159
4161
|
"input.html5Drag",
|
|
4162
|
+
"emulate.focus",
|
|
4160
4163
|
"browser.downloads",
|
|
4161
4164
|
"browser.permissions",
|
|
4162
4165
|
"worker.targets"
|
|
@@ -7577,6 +7580,99 @@ async function dispatchMouse(args) {
|
|
|
7577
7580
|
});
|
|
7578
7581
|
}
|
|
7579
7582
|
|
|
7583
|
+
// src/tools/focus-emulation.ts
|
|
7584
|
+
init_activity();
|
|
7585
|
+
init_client();
|
|
7586
|
+
function validateFocusEmulationArgs(args) {
|
|
7587
|
+
if (typeof args.enabled !== "boolean") {
|
|
7588
|
+
throw new Error("focus_emulation requires { enabled } of true or false");
|
|
7589
|
+
}
|
|
7590
|
+
}
|
|
7591
|
+
function validateClickFocusGatedArgs(args) {
|
|
7592
|
+
const hasSel = typeof args.selector === "string" && args.selector.length > 0;
|
|
7593
|
+
const hasText = typeof args.text === "string" && args.text.length > 0;
|
|
7594
|
+
if (hasSel === hasText) {
|
|
7595
|
+
throw new Error("click_focus_gated requires exactly one of { selector } or { text }");
|
|
7596
|
+
}
|
|
7597
|
+
if (args.timeoutMs !== undefined && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
|
|
7598
|
+
throw new Error("click_focus_gated: timeoutMs must be a positive number");
|
|
7599
|
+
}
|
|
7600
|
+
if (args.pollMs !== undefined && (!Number.isFinite(args.pollMs) || args.pollMs <= 0)) {
|
|
7601
|
+
throw new Error("click_focus_gated: pollMs must be a positive number");
|
|
7602
|
+
}
|
|
7603
|
+
}
|
|
7604
|
+
var FIND_BUTTON = `(() => {
|
|
7605
|
+
const SEL = %SEL%;
|
|
7606
|
+
const TEXT = %TEXT%;
|
|
7607
|
+
let btn = null;
|
|
7608
|
+
let matchedBy = null;
|
|
7609
|
+
if (SEL) {
|
|
7610
|
+
btn = document.querySelector(SEL);
|
|
7611
|
+
if (btn) matchedBy = "selector";
|
|
7612
|
+
}
|
|
7613
|
+
if (!btn && TEXT) {
|
|
7614
|
+
btn = Array.from(document.querySelectorAll("button,[role=button],input[type=submit]"))
|
|
7615
|
+
.find((b) => (b.textContent || b.value || "").trim() === TEXT);
|
|
7616
|
+
if (btn) matchedBy = "text";
|
|
7617
|
+
}
|
|
7618
|
+
if (!btn) return null;
|
|
7619
|
+
const r = btn.getBoundingClientRect();
|
|
7620
|
+
if (r.width === 0 || r.height === 0) return null;
|
|
7621
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2,
|
|
7622
|
+
disabled: !!btn.disabled || btn.getAttribute("aria-disabled") === "true",
|
|
7623
|
+
matchedBy, hasFocus: document.hasFocus(), visibility: document.visibilityState };
|
|
7624
|
+
})()`;
|
|
7625
|
+
function findButtonExpression(selector, text) {
|
|
7626
|
+
return FIND_BUTTON.replace("%SEL%", JSON.stringify(selector ?? null)).replace("%TEXT%", JSON.stringify(text ?? null));
|
|
7627
|
+
}
|
|
7628
|
+
async function focusEmulation(args) {
|
|
7629
|
+
validateFocusEmulationArgs(args);
|
|
7630
|
+
return withPage(args.target, async (conn) => {
|
|
7631
|
+
await conn.send("Emulation.setFocusEmulationEnabled", { enabled: args.enabled });
|
|
7632
|
+
return { enabled: args.enabled };
|
|
7633
|
+
}, { lease: args.lease });
|
|
7634
|
+
}
|
|
7635
|
+
async function clickFocusGated(args) {
|
|
7636
|
+
validateClickFocusGatedArgs(args);
|
|
7637
|
+
const timeoutMs = args.timeoutMs ?? 30000;
|
|
7638
|
+
const pollMs = args.pollMs ?? 500;
|
|
7639
|
+
const start = Date.now();
|
|
7640
|
+
return withPage(args.target, async (conn, target) => {
|
|
7641
|
+
await conn.send("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
7642
|
+
try {
|
|
7643
|
+
const expression = findButtonExpression(args.selector, args.text);
|
|
7644
|
+
for (;; ) {
|
|
7645
|
+
const { result } = await conn.send("Runtime.evaluate", {
|
|
7646
|
+
expression,
|
|
7647
|
+
returnByValue: true
|
|
7648
|
+
});
|
|
7649
|
+
const btn = result?.value;
|
|
7650
|
+
if (btn && !btn.disabled) {
|
|
7651
|
+
const base = { x: btn.x, y: btn.y, button: "left", buttons: 1, clickCount: 1 };
|
|
7652
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: btn.x, y: btn.y });
|
|
7653
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", { ...base, type: "mousePressed" });
|
|
7654
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", { ...base, type: "mouseReleased" });
|
|
7655
|
+
return {
|
|
7656
|
+
clicked: true,
|
|
7657
|
+
x: btn.x,
|
|
7658
|
+
y: btn.y,
|
|
7659
|
+
matchedBy: btn.matchedBy,
|
|
7660
|
+
elapsedMs: Date.now() - start
|
|
7661
|
+
};
|
|
7662
|
+
}
|
|
7663
|
+
if (Date.now() - start >= timeoutMs) {
|
|
7664
|
+
throw new Error(`click_focus_gated: button never became clickable within ${timeoutMs}ms` + (btn ? " (found but stayed disabled — focus emulation may not satisfy this gate)" : " (never located)"));
|
|
7665
|
+
}
|
|
7666
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
7667
|
+
}
|
|
7668
|
+
} finally {
|
|
7669
|
+
if (!args.keepFocus) {
|
|
7670
|
+
await conn.send("Emulation.setFocusEmulationEnabled", { enabled: false }).catch(() => {});
|
|
7671
|
+
}
|
|
7672
|
+
}
|
|
7673
|
+
}, { lease: args.lease, timeoutMs: timeoutMs + 5000 });
|
|
7674
|
+
}
|
|
7675
|
+
|
|
7580
7676
|
// src/tools/downloads.ts
|
|
7581
7677
|
init_client();
|
|
7582
7678
|
import { readdir as readdir3, rename as rename2, stat as stat4 } from "node:fs/promises";
|
|
@@ -7805,6 +7901,421 @@ async function grantPermissions(args = {}) {
|
|
|
7805
7901
|
return { granted: [...permissions], origin, ...reset ? { reset: true } : {}, target };
|
|
7806
7902
|
}
|
|
7807
7903
|
|
|
7904
|
+
// src/tools/extract.ts
|
|
7905
|
+
init_client();
|
|
7906
|
+
import { mkdir as mkdir12, writeFile as writeFile9 } from "node:fs/promises";
|
|
7907
|
+
import { dirname as dirname3, join as join10 } from "node:path";
|
|
7908
|
+
var ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
7909
|
+
var DEFAULT_BASE_URL = "http://127.0.0.1:8090/v1";
|
|
7910
|
+
var DEFAULT_MODEL = "schematron";
|
|
7911
|
+
var DEFAULT_TIMEOUT_MS2 = 90000;
|
|
7912
|
+
var DEFAULT_MAX_CHARS = 300000;
|
|
7913
|
+
var MAX_TIMEOUT_MS = 300000;
|
|
7914
|
+
var DEFAULT_PROMPT_MODE = "html";
|
|
7915
|
+
var CHARS_PER_TOKEN = 3.5;
|
|
7916
|
+
var COMPLESION_TOKENS = 8192;
|
|
7917
|
+
var SAFETY_TOKENS = 8000;
|
|
7918
|
+
var CONTEXT_TOKEN_BUDGET = 128000;
|
|
7919
|
+
function extractBaseUrl() {
|
|
7920
|
+
const raw = process.env.CDP_EXTRACT_BASE_URL?.trim();
|
|
7921
|
+
return raw ? raw : DEFAULT_BASE_URL;
|
|
7922
|
+
}
|
|
7923
|
+
function extractModel() {
|
|
7924
|
+
const raw = process.env.CDP_EXTRACT_MODEL?.trim();
|
|
7925
|
+
return raw ? raw : DEFAULT_MODEL;
|
|
7926
|
+
}
|
|
7927
|
+
function extractPromptMode() {
|
|
7928
|
+
const raw = process.env.CDP_EXTRACT_PROMPT?.trim();
|
|
7929
|
+
return raw ? raw : DEFAULT_PROMPT_MODE;
|
|
7930
|
+
}
|
|
7931
|
+
function extractApiKey() {
|
|
7932
|
+
const raw = process.env.CDP_EXTRACT_API_KEY;
|
|
7933
|
+
return raw && raw.length > 0 ? raw : undefined;
|
|
7934
|
+
}
|
|
7935
|
+
function extractTimeoutMs() {
|
|
7936
|
+
const raw = Number(process.env.CDP_EXTRACT_TIMEOUT_MS);
|
|
7937
|
+
return Number.isFinite(raw) && raw > 0 ? Math.min(raw, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS2;
|
|
7938
|
+
}
|
|
7939
|
+
function extractMaxChars() {
|
|
7940
|
+
const raw = Number(process.env.CDP_EXTRACT_MAX_CHARS);
|
|
7941
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_MAX_CHARS;
|
|
7942
|
+
}
|
|
7943
|
+
function hasAnyDescription(node, seen = new Set) {
|
|
7944
|
+
if (typeof node !== "object" || node === null || seen.has(node))
|
|
7945
|
+
return false;
|
|
7946
|
+
seen.add(node);
|
|
7947
|
+
const n = node;
|
|
7948
|
+
if (typeof n.description === "string" && n.description.trim() !== "")
|
|
7949
|
+
return true;
|
|
7950
|
+
const props = n.properties;
|
|
7951
|
+
if (typeof props === "object" && props !== null) {
|
|
7952
|
+
for (const value of Object.values(props))
|
|
7953
|
+
if (hasAnyDescription(value, seen))
|
|
7954
|
+
return true;
|
|
7955
|
+
}
|
|
7956
|
+
if (hasAnyDescription(n.items, seen))
|
|
7957
|
+
return true;
|
|
7958
|
+
for (const key of ["anyOf", "oneOf", "allOf", "prefixItems"]) {
|
|
7959
|
+
const branches = n[key];
|
|
7960
|
+
if (Array.isArray(branches)) {
|
|
7961
|
+
for (const branch of branches)
|
|
7962
|
+
if (hasAnyDescription(branch, seen))
|
|
7963
|
+
return true;
|
|
7964
|
+
}
|
|
7965
|
+
}
|
|
7966
|
+
return false;
|
|
7967
|
+
}
|
|
7968
|
+
function validateExtractArgs(args) {
|
|
7969
|
+
if (args.schema === null || typeof args.schema !== "object" || Array.isArray(args.schema)) {
|
|
7970
|
+
throw new CdpError("extract_page: 'schema' is required and must be a JSON Schema object");
|
|
7971
|
+
}
|
|
7972
|
+
if (args.schema.type !== "object") {
|
|
7973
|
+
throw new CdpError(`extract_page: 'schema.type' must be "object" (got ${JSON.stringify(args.schema.type)})`);
|
|
7974
|
+
}
|
|
7975
|
+
if (args.schema.additionalProperties === true) {
|
|
7976
|
+
throw new CdpError("extract_page: schema must not set additionalProperties:true — strict json_schema mode requires a closed object so the extraction cannot invent fields");
|
|
7977
|
+
}
|
|
7978
|
+
if (!hasAnyDescription(args.schema)) {
|
|
7979
|
+
throw new CdpError("extract_page: no property in 'schema' carries a description. This is prompt-driven extraction: the property descriptions ARE the instructions the extractor follows ('the job title, exactly as printed'). Write them like prompts and retry.");
|
|
7980
|
+
}
|
|
7981
|
+
if (args.selector !== undefined && (typeof args.selector !== "string" || args.selector.length === 0)) {
|
|
7982
|
+
throw new CdpError("extract_page: 'selector' must be a non-empty CSS selector string");
|
|
7983
|
+
}
|
|
7984
|
+
if (args.source !== undefined && args.source !== "dom" && args.source !== "served") {
|
|
7985
|
+
throw new CdpError(`extract_page: 'source' must be "dom" or "served" (got ${JSON.stringify(args.source)})`);
|
|
7986
|
+
}
|
|
7987
|
+
if (args.source === "served") {
|
|
7988
|
+
throw new CdpError("extract_page: source 'served' (the raw server response before rendering) is not implemented; use source 'dom' (the live rendered DOM).");
|
|
7989
|
+
}
|
|
7990
|
+
if (args.clean !== undefined && args.clean !== "standard" && args.clean !== "aggressive" && args.clean !== "none") {
|
|
7991
|
+
throw new CdpError(`extract_page: 'clean' must be "standard", "aggressive" or "none" (got ${JSON.stringify(args.clean)})`);
|
|
7992
|
+
}
|
|
7993
|
+
if (args.prompt !== undefined && args.prompt !== "html" && args.prompt !== "schematron") {
|
|
7994
|
+
throw new CdpError(`extract_page: 'prompt' must be "html" or "schematron" (got ${JSON.stringify(args.prompt)})`);
|
|
7995
|
+
}
|
|
7996
|
+
const envPrompt = process.env.CDP_EXTRACT_PROMPT?.trim();
|
|
7997
|
+
if (args.prompt === undefined && envPrompt && envPrompt !== "html" && envPrompt !== "schematron") {
|
|
7998
|
+
throw new CdpError(`extract_page: CDP_EXTRACT_PROMPT must be "html" or "schematron" (got ${JSON.stringify(envPrompt)})`);
|
|
7999
|
+
}
|
|
8000
|
+
if (args.maxChars !== undefined && (!Number.isFinite(args.maxChars) || args.maxChars <= 0)) {
|
|
8001
|
+
throw new CdpError("extract_page: 'maxChars' must be a positive number");
|
|
8002
|
+
}
|
|
8003
|
+
if (args.timeoutMs !== undefined && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
|
|
8004
|
+
throw new CdpError("extract_page: 'timeoutMs' must be a positive number");
|
|
8005
|
+
}
|
|
8006
|
+
if (args.model !== undefined && (typeof args.model !== "string" || args.model.length === 0)) {
|
|
8007
|
+
throw new CdpError("extract_page: 'model' must be a non-empty string");
|
|
8008
|
+
}
|
|
8009
|
+
if (args.baseUrl !== undefined && (typeof args.baseUrl !== "string" || args.baseUrl.length === 0)) {
|
|
8010
|
+
throw new CdpError("extract_page: 'baseUrl' must be a non-empty string (the API key is env-only: CDP_EXTRACT_API_KEY)");
|
|
8011
|
+
}
|
|
8012
|
+
}
|
|
8013
|
+
function buildCleanFunctionSource(selector, clean) {
|
|
8014
|
+
return `function() {
|
|
8015
|
+
var SEL = ${JSON.stringify(selector ?? null)};
|
|
8016
|
+
var CLEAN = ${JSON.stringify(clean)};
|
|
8017
|
+
var STRIP = ["script","style","noscript","svg","canvas","iframe","video","audio","source","track","template","object","embed","link","meta"];
|
|
8018
|
+
var AGGRO = ["nav","header","footer","aside","form","button"];
|
|
8019
|
+
var WHITELIST = ["href","src","alt","title","value","placeholder","for","name","type","role","aria-label","selected","checked","disabled"];
|
|
8020
|
+
|
|
8021
|
+
function bytes(s) {
|
|
8022
|
+
try { return new TextEncoder().encode(s).length; } catch (e) { return s.length; }
|
|
8023
|
+
}
|
|
8024
|
+
|
|
8025
|
+
function collectShadowScopes(root, out) {
|
|
8026
|
+
var els = root.querySelectorAll("*");
|
|
8027
|
+
for (var i = 0; i < els.length; i++) {
|
|
8028
|
+
var sr = els[i].shadowRoot;
|
|
8029
|
+
if (sr) { out.push(sr); collectShadowScopes(sr, out); }
|
|
8030
|
+
}
|
|
8031
|
+
}
|
|
8032
|
+
|
|
8033
|
+
// Locate SEL piercing open shadow roots (querySelector on document first,
|
|
8034
|
+
// then every open shadow scope). LIMIT: outerHTML serializes the matched
|
|
8035
|
+
// element's light DOM only; a closed shadow root is invisible by design.
|
|
8036
|
+
function deepQuery(sel) {
|
|
8037
|
+
var direct = document.querySelector(sel);
|
|
8038
|
+
if (direct) return direct;
|
|
8039
|
+
var scopes = [];
|
|
8040
|
+
collectShadowScopes(document, scopes);
|
|
8041
|
+
for (var i = 0; i < scopes.length; i++) {
|
|
8042
|
+
var hit = scopes[i].querySelector(sel);
|
|
8043
|
+
if (hit) return hit;
|
|
8044
|
+
}
|
|
8045
|
+
return null;
|
|
8046
|
+
}
|
|
8047
|
+
|
|
8048
|
+
// The visibility heuristic, same shape as bidi/snapshot.ts's isVisible:
|
|
8049
|
+
// hidden attr, aria-hidden, computed display/visibility, then the 0x0
|
|
8050
|
+
// paint check. OPTION/OPTGROUP skip the size check (a closed select's
|
|
8051
|
+
// popup is unpainted though its options are real, extractable content).
|
|
8052
|
+
function isHidden(el) {
|
|
8053
|
+
if (el.hidden) return true;
|
|
8054
|
+
if (el.getAttribute("aria-hidden") === "true") return true;
|
|
8055
|
+
var style = null;
|
|
8056
|
+
try { style = el.ownerDocument.defaultView.getComputedStyle(el); } catch (e) {}
|
|
8057
|
+
if (style && (style.display === "none" || style.visibility === "hidden")) return true;
|
|
8058
|
+
var tag = el.tagName;
|
|
8059
|
+
if (tag !== "OPTION" && tag !== "OPTGROUP") {
|
|
8060
|
+
var rect = el.getBoundingClientRect();
|
|
8061
|
+
if (rect.width === 0 && rect.height === 0) return true;
|
|
8062
|
+
}
|
|
8063
|
+
return false;
|
|
8064
|
+
}
|
|
8065
|
+
|
|
8066
|
+
function countDrop(dropped, tag) {
|
|
8067
|
+
dropped.elements[tag] = (dropped.elements[tag] || 0) + 1;
|
|
8068
|
+
}
|
|
8069
|
+
|
|
8070
|
+
function cleanRoot(root) {
|
|
8071
|
+
var rawHtml = root.outerHTML;
|
|
8072
|
+
var dropped = { comments: 0, hidden: 0, attributes: 0, elements: {} };
|
|
8073
|
+
if (CLEAN === "none") {
|
|
8074
|
+
return { matched: true, html: rawHtml, rawBytes: bytes(rawHtml), cleanBytes: bytes(rawHtml), dropped: dropped };
|
|
8075
|
+
}
|
|
8076
|
+
var live = root.querySelectorAll("*");
|
|
8077
|
+
var clone = root.cloneNode(true);
|
|
8078
|
+
var mirrored = clone.querySelectorAll("*");
|
|
8079
|
+
for (var i = 0; i < mirrored.length; i++) {
|
|
8080
|
+
var cel = mirrored[i];
|
|
8081
|
+
if (!cel.parentNode) continue; // dropped already, with an ancestor
|
|
8082
|
+
var low = cel.tagName.toLowerCase();
|
|
8083
|
+
if (STRIP.indexOf(low) !== -1) { countDrop(dropped, low); cel.parentNode.removeChild(cel); continue; }
|
|
8084
|
+
if (CLEAN === "aggressive" && AGGRO.indexOf(low) !== -1) { countDrop(dropped, low); cel.parentNode.removeChild(cel); continue; }
|
|
8085
|
+
var lel = live[i];
|
|
8086
|
+
if (lel && isHidden(lel)) { dropped.hidden++; cel.parentNode.removeChild(cel); continue; }
|
|
8087
|
+
for (var j = cel.attributes.length - 1; j >= 0; j--) {
|
|
8088
|
+
var attr = cel.attributes[j];
|
|
8089
|
+
if (WHITELIST.indexOf(attr.name.toLowerCase()) === -1) {
|
|
8090
|
+
cel.removeAttribute(attr.name);
|
|
8091
|
+
dropped.attributes++;
|
|
8092
|
+
}
|
|
8093
|
+
}
|
|
8094
|
+
}
|
|
8095
|
+
var walker = clone.ownerDocument.createTreeWalker(clone, NodeFilter.SHOW_COMMENT);
|
|
8096
|
+
var comments = [];
|
|
8097
|
+
while (walker.nextNode()) comments.push(walker.currentNode);
|
|
8098
|
+
for (var k = 0; k < comments.length; k++) {
|
|
8099
|
+
var c = comments[k];
|
|
8100
|
+
if (c.parentNode) c.parentNode.removeChild(c);
|
|
8101
|
+
}
|
|
8102
|
+
dropped.comments = comments.length;
|
|
8103
|
+
// Whitespace collapse at the TEXT-NODE level, never on the serialized
|
|
8104
|
+
// string: a regex over outerHTML would also flatten <pre> content.
|
|
8105
|
+
var tw = clone.ownerDocument.createTreeWalker(clone, NodeFilter.SHOW_TEXT);
|
|
8106
|
+
while (tw.nextNode()) {
|
|
8107
|
+
var node = tw.currentNode;
|
|
8108
|
+
node.nodeValue = node.nodeValue.replace(/\\s+/g, " ");
|
|
8109
|
+
}
|
|
8110
|
+
var html = clone.outerHTML;
|
|
8111
|
+
return { matched: true, html: html, rawBytes: bytes(rawHtml), cleanBytes: bytes(html), dropped: dropped };
|
|
8112
|
+
}
|
|
8113
|
+
|
|
8114
|
+
if (SEL) {
|
|
8115
|
+
var found = deepQuery(SEL);
|
|
8116
|
+
if (!found) return { matched: false };
|
|
8117
|
+
return cleanRoot(found);
|
|
8118
|
+
}
|
|
8119
|
+
return cleanRoot(document.documentElement);
|
|
8120
|
+
}`;
|
|
8121
|
+
}
|
|
8122
|
+
function coerceCleanOutcome(raw, selector) {
|
|
8123
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8124
|
+
throw new CdpError("extract_page: in-page cleaning returned an unexpected shape");
|
|
8125
|
+
}
|
|
8126
|
+
const r = raw;
|
|
8127
|
+
if (r.matched === false) {
|
|
8128
|
+
throw new CdpError(`extract_page: no element matches selector '${selector}' (checked document and every open shadow root)`);
|
|
8129
|
+
}
|
|
8130
|
+
if (typeof r.html !== "string" || typeof r.rawBytes !== "number" || !Number.isFinite(r.rawBytes) || typeof r.cleanBytes !== "number" || !Number.isFinite(r.cleanBytes)) {
|
|
8131
|
+
throw new CdpError("extract_page: in-page cleaning returned an unexpected shape");
|
|
8132
|
+
}
|
|
8133
|
+
return {
|
|
8134
|
+
html: r.html,
|
|
8135
|
+
rawBytes: r.rawBytes,
|
|
8136
|
+
cleanBytes: r.cleanBytes,
|
|
8137
|
+
dropped: typeof r.dropped === "object" && r.dropped !== null ? r.dropped : {}
|
|
8138
|
+
};
|
|
8139
|
+
}
|
|
8140
|
+
function estimateTokens(chars) {
|
|
8141
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
8142
|
+
}
|
|
8143
|
+
function guardSize(outcome, maxChars) {
|
|
8144
|
+
const estTokens = estimateTokens(outcome.html.length);
|
|
8145
|
+
const overChars = outcome.html.length > maxChars;
|
|
8146
|
+
const overContext = estTokens + COMPLESION_TOKENS + SAFETY_TOKENS > CONTEXT_TOKEN_BUDGET;
|
|
8147
|
+
if (overChars || overContext) {
|
|
8148
|
+
const why = overChars ? `cleaned HTML is ${outcome.html.length} chars (${outcome.cleanBytes} bytes), over the ${maxChars}-char cap` : `estimated ${estTokens} prompt tokens + ${COMPLESION_TOKENS} completion + ${SAFETY_TOKENS} safety exceeds the ${CONTEXT_TOKEN_BUDGET}-token context budget`;
|
|
8149
|
+
throw new CdpError(`extract_page: html_too_large — ${why}. Shrink the payload with 'selector' to scope to a subtree, clean:"aggressive" to strip nav/footer/form/button too, or a larger maxChars if you truly need it all. No truncation: a partial page extracted as whole is a wrong answer that looks right.`);
|
|
8150
|
+
}
|
|
8151
|
+
return estTokens;
|
|
8152
|
+
}
|
|
8153
|
+
function redactSecrets(text, apiKey) {
|
|
8154
|
+
let out = text;
|
|
8155
|
+
if (apiKey && apiKey.length > 0) {
|
|
8156
|
+
out = out.split(apiKey).join("[redacted]");
|
|
8157
|
+
}
|
|
8158
|
+
return out.replace(/sk-[A-Za-z0-9_-]{8,}/g, "[redacted]");
|
|
8159
|
+
}
|
|
8160
|
+
function pickCostUsd(usage) {
|
|
8161
|
+
for (const v of [usage.cost_usd, usage.costUsd, usage.cost]) {
|
|
8162
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
8163
|
+
return v;
|
|
8164
|
+
}
|
|
8165
|
+
return;
|
|
8166
|
+
}
|
|
8167
|
+
function excerpt(text, max) {
|
|
8168
|
+
const t = text.trim();
|
|
8169
|
+
return t.length > max ? `${t.slice(0, max)}…` : t;
|
|
8170
|
+
}
|
|
8171
|
+
function buildExtractionMessages(mode, html, schema) {
|
|
8172
|
+
if (mode !== "schematron")
|
|
8173
|
+
return [{ role: "user", content: html }];
|
|
8174
|
+
const user = "You are going to be given a JSON schema following the standardized JSON Schema format. You are going to be given a HTML page and you are going to apply the schema to the HTML page however you see it as applicable and return the results in a JSON object. The schema is as follows:" + `
|
|
8175
|
+
|
|
8176
|
+
` + JSON.stringify(schema) + `
|
|
8177
|
+
|
|
8178
|
+
` + "Here is the HTML page:" + `
|
|
8179
|
+
|
|
8180
|
+
` + html + `
|
|
8181
|
+
|
|
8182
|
+
` + "MAKE SURE ITS VALID JSON.";
|
|
8183
|
+
return [
|
|
8184
|
+
{ role: "system", content: "You are a helpful assistant" },
|
|
8185
|
+
{ role: "user", content: user }
|
|
8186
|
+
];
|
|
8187
|
+
}
|
|
8188
|
+
async function callExtractionEndpoint(opts) {
|
|
8189
|
+
const url = `${opts.baseUrl.replace(/\/+$/, "")}/chat/completions`;
|
|
8190
|
+
const body = JSON.stringify({
|
|
8191
|
+
model: opts.model,
|
|
8192
|
+
messages: buildExtractionMessages(opts.prompt ?? "html", opts.html, opts.schema),
|
|
8193
|
+
temperature: 0,
|
|
8194
|
+
max_tokens: COMPLESION_TOKENS,
|
|
8195
|
+
response_format: { type: "json_schema", json_schema: { name: "extract", strict: true, schema: opts.schema } }
|
|
8196
|
+
});
|
|
8197
|
+
const headers = { "content-type": "application/json" };
|
|
8198
|
+
if (opts.apiKey)
|
|
8199
|
+
headers.authorization = `Bearer ${opts.apiKey}`;
|
|
8200
|
+
let lastStatus = 0;
|
|
8201
|
+
let lastBody = "";
|
|
8202
|
+
for (let attempt = 1;attempt <= 2; attempt++) {
|
|
8203
|
+
let res;
|
|
8204
|
+
try {
|
|
8205
|
+
res = await fetch(url, { method: "POST", headers, body, signal: AbortSignal.timeout(opts.timeoutMs) });
|
|
8206
|
+
} catch (e) {
|
|
8207
|
+
const err = e;
|
|
8208
|
+
if (err.name === "TimeoutError" || err.name === "AbortError") {
|
|
8209
|
+
throw new CdpError(`extract_page: upstream call to ${opts.baseUrl} timed out after ${opts.timeoutMs}ms`);
|
|
8210
|
+
}
|
|
8211
|
+
throw new CdpError(`extract_page: could not reach ${opts.baseUrl} (${redactSecrets(err.message, opts.apiKey)}) — is ferry running?`);
|
|
8212
|
+
}
|
|
8213
|
+
const text = await res.text();
|
|
8214
|
+
if (res.ok) {
|
|
8215
|
+
let parsed;
|
|
8216
|
+
try {
|
|
8217
|
+
parsed = JSON.parse(text);
|
|
8218
|
+
} catch {
|
|
8219
|
+
throw new CdpError(`extract_page: endpoint returned a non-JSON 200 body: ${excerpt(redactSecrets(text, opts.apiKey), 200)}`);
|
|
8220
|
+
}
|
|
8221
|
+
if (parsed.error) {
|
|
8222
|
+
throw new CdpError(`extract_page: upstream_error from ${opts.baseUrl}: ${excerpt(redactSecrets(String(parsed.error.message ?? JSON.stringify(parsed.error)), opts.apiKey), 600)}`);
|
|
8223
|
+
}
|
|
8224
|
+
const content = parsed.choices?.[0]?.message?.content;
|
|
8225
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
8226
|
+
throw new CdpError(`extract_page: endpoint response carried no message content: ${excerpt(redactSecrets(text, opts.apiKey), 200)}`);
|
|
8227
|
+
}
|
|
8228
|
+
const u = parsed.usage ?? {};
|
|
8229
|
+
const usage = {
|
|
8230
|
+
promptTokens: u.prompt_tokens ?? 0,
|
|
8231
|
+
completionTokens: u.completion_tokens ?? 0,
|
|
8232
|
+
totalTokens: u.total_tokens ?? 0,
|
|
8233
|
+
...pickCostUsd(u) !== undefined ? { costUsd: pickCostUsd(u) } : {}
|
|
8234
|
+
};
|
|
8235
|
+
const fr = parsed.choices?.[0]?.finish_reason;
|
|
8236
|
+
const finishReason = typeof fr === "string" ? fr : undefined;
|
|
8237
|
+
return { content, usage, model: parsed.model ?? opts.model, finishReason };
|
|
8238
|
+
}
|
|
8239
|
+
lastStatus = res.status;
|
|
8240
|
+
lastBody = text;
|
|
8241
|
+
if (res.status === 429) {
|
|
8242
|
+
const retryAfter = res.headers.get("retry-after");
|
|
8243
|
+
throw new CdpError(`extract_page: upstream rate-limited (HTTP 429) from ${opts.baseUrl}${retryAfter !== null ? `, retry-after: ${retryAfter}` : " (no retry-after header)"}`);
|
|
8244
|
+
}
|
|
8245
|
+
if (res.status < 500 || res.status >= 600)
|
|
8246
|
+
break;
|
|
8247
|
+
}
|
|
8248
|
+
throw new CdpError(`extract_page: upstream_error HTTP ${lastStatus} from POST ${url}: ${excerpt(redactSecrets(lastBody, opts.apiKey), 600)}`);
|
|
8249
|
+
}
|
|
8250
|
+
async function withPage3(driver, target, fn) {
|
|
8251
|
+
const page = await driver.page(target);
|
|
8252
|
+
try {
|
|
8253
|
+
return await fn(page);
|
|
8254
|
+
} finally {
|
|
8255
|
+
await page.release();
|
|
8256
|
+
}
|
|
8257
|
+
}
|
|
8258
|
+
async function extractPage(driver, args) {
|
|
8259
|
+
validateExtractArgs(args);
|
|
8260
|
+
const clean = args.clean ?? "standard";
|
|
8261
|
+
const maxChars = args.maxChars ?? extractMaxChars();
|
|
8262
|
+
const timeoutMs = Math.min(args.timeoutMs ?? extractTimeoutMs(), MAX_TIMEOUT_MS);
|
|
8263
|
+
const baseUrl = args.baseUrl ?? extractBaseUrl();
|
|
8264
|
+
const model = args.model ?? extractModel();
|
|
8265
|
+
const prompt = args.prompt ?? extractPromptMode();
|
|
8266
|
+
const apiKey = extractApiKey();
|
|
8267
|
+
return withPage3(driver, args.target, async (page) => {
|
|
8268
|
+
const source = `(${buildCleanFunctionSource(args.selector, clean)})()`;
|
|
8269
|
+
const outcome = coerceCleanOutcome(await page.evaluate(source, { awaitPromise: true }), args.selector);
|
|
8270
|
+
const estTokens = guardSize(outcome, maxChars);
|
|
8271
|
+
const { content, usage, model: respondedModel, finishReason } = await callExtractionEndpoint({
|
|
8272
|
+
baseUrl,
|
|
8273
|
+
apiKey,
|
|
8274
|
+
model,
|
|
8275
|
+
html: outcome.html,
|
|
8276
|
+
schema: args.schema,
|
|
8277
|
+
timeoutMs,
|
|
8278
|
+
prompt
|
|
8279
|
+
});
|
|
8280
|
+
if (finishReason === "length") {
|
|
8281
|
+
throw new CdpError(`extract_page: completion truncated at max_tokens=${COMPLESION_TOKENS} (finish_reason=length, ${usage.completionTokens} completion tokens); ` + `narrow the payload with 'selector' or ask the schema for less: ${excerpt(redactSecrets(content, apiKey), 200)}`);
|
|
8282
|
+
}
|
|
8283
|
+
let data;
|
|
8284
|
+
try {
|
|
8285
|
+
data = JSON.parse(content);
|
|
8286
|
+
} catch (e) {
|
|
8287
|
+
throw new CdpError(`extract_page: endpoint returned invalid JSON (${e.message}${finishReason !== undefined ? `, finish_reason=${finishReason}` : ""}): ${excerpt(redactSecrets(content, apiKey), 200)}`);
|
|
8288
|
+
}
|
|
8289
|
+
const result = {
|
|
8290
|
+
data,
|
|
8291
|
+
model: respondedModel,
|
|
8292
|
+
usage,
|
|
8293
|
+
html: {
|
|
8294
|
+
source: "dom",
|
|
8295
|
+
rawBytes: outcome.rawBytes,
|
|
8296
|
+
cleanBytes: outcome.cleanBytes,
|
|
8297
|
+
estTokens,
|
|
8298
|
+
dropped: outcome.dropped
|
|
8299
|
+
},
|
|
8300
|
+
target: { id: page.info.id, url: page.info.url, title: page.info.title }
|
|
8301
|
+
};
|
|
8302
|
+
if (args.savePath === undefined || args.savePath === "")
|
|
8303
|
+
return result;
|
|
8304
|
+
const path = args.savePath.startsWith("/") ? args.savePath : join10(ARTIFACT_DIR7, args.savePath);
|
|
8305
|
+
await mkdir12(dirname3(path), { recursive: true });
|
|
8306
|
+
const json = JSON.stringify(result, null, 2);
|
|
8307
|
+
await writeFile9(path, json, "utf8");
|
|
8308
|
+
const saved = {
|
|
8309
|
+
path,
|
|
8310
|
+
bytes: Buffer.byteLength(json, "utf8"),
|
|
8311
|
+
model: respondedModel,
|
|
8312
|
+
usage,
|
|
8313
|
+
target: result.target
|
|
8314
|
+
};
|
|
8315
|
+
return saved;
|
|
8316
|
+
});
|
|
8317
|
+
}
|
|
8318
|
+
|
|
7808
8319
|
// src/index.ts
|
|
7809
8320
|
init_client();
|
|
7810
8321
|
var cdpDriver = createCdpDriver();
|
|
@@ -7852,8 +8363,11 @@ var TOOLS = {
|
|
|
7852
8363
|
list_mocks: listMocks,
|
|
7853
8364
|
clear_mocks: clearMocks,
|
|
7854
8365
|
dispatch_mouse: dispatchMouse,
|
|
8366
|
+
focus_emulation: focusEmulation,
|
|
8367
|
+
click_focus_gated: clickFocusGated,
|
|
7855
8368
|
wait_for_download: waitForDownload,
|
|
7856
8369
|
grant_permissions: grantPermissions,
|
|
8370
|
+
extract_page: onCdp(extractPage),
|
|
7857
8371
|
claim_page: onCdp(LEASE_TOOLS.claim_page),
|
|
7858
8372
|
release_page: onCdp(LEASE_TOOLS.release_page),
|
|
7859
8373
|
list_leases: onCdp(LEASE_TOOLS.list_leases)
|
|
@@ -7868,7 +8382,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
7868
8382
|
import { existsSync as existsSync2, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7869
8383
|
import { createConnection, createServer as createServer2 } from "node:net";
|
|
7870
8384
|
import { tmpdir } from "node:os";
|
|
7871
|
-
import { join as
|
|
8385
|
+
import { join as join12 } from "node:path";
|
|
7872
8386
|
function resolveBinary() {
|
|
7873
8387
|
const candidates = [
|
|
7874
8388
|
process.env.FIREFOX_BIN,
|
|
@@ -7948,7 +8462,7 @@ var LAUNCH_USER_JS_PREFS = {
|
|
|
7948
8462
|
"dom.file.createInChild": true
|
|
7949
8463
|
};
|
|
7950
8464
|
function writeLaunchUserJs(profilePath) {
|
|
7951
|
-
const target =
|
|
8465
|
+
const target = join12(profilePath, "user.js");
|
|
7952
8466
|
const existing = existsSync2(target) ? readFileSync(target, "utf8") : "";
|
|
7953
8467
|
const present = new Set;
|
|
7954
8468
|
for (const line of existing.split(`
|
|
@@ -7970,7 +8484,7 @@ async function launchFirefox(opts = {}) {
|
|
|
7970
8484
|
const binary = resolveBinary();
|
|
7971
8485
|
const port = await pickPort(opts.port);
|
|
7972
8486
|
const isThrowaway = opts.profilePath === undefined;
|
|
7973
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
8487
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join12(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
7974
8488
|
if (isThrowaway)
|
|
7975
8489
|
writeLaunchUserJs(profilePath);
|
|
7976
8490
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
@@ -8162,7 +8676,7 @@ init_workers();
|
|
|
8162
8676
|
|
|
8163
8677
|
class BidiToolError extends Error {
|
|
8164
8678
|
}
|
|
8165
|
-
async function
|
|
8679
|
+
async function withPage4(driver, target, fn) {
|
|
8166
8680
|
const page = await driver.page(target);
|
|
8167
8681
|
try {
|
|
8168
8682
|
return await fn(page);
|
|
@@ -8206,7 +8720,7 @@ async function captureFresh(page, durationMs) {
|
|
|
8206
8720
|
}
|
|
8207
8721
|
async function listConsoleMessages2(driver, args = {}) {
|
|
8208
8722
|
refuseWorkerSelector(driver, args.target);
|
|
8209
|
-
return
|
|
8723
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8210
8724
|
if (args.reload)
|
|
8211
8725
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
8212
8726
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
@@ -8215,7 +8729,7 @@ async function listConsoleMessages2(driver, args = {}) {
|
|
|
8215
8729
|
}
|
|
8216
8730
|
async function getConsoleMessage2(driver, args = {}) {
|
|
8217
8731
|
refuseWorkerSelector(driver, args.target);
|
|
8218
|
-
return
|
|
8732
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8219
8733
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
8220
8734
|
const idx = args.index ?? 0;
|
|
8221
8735
|
const entry = buf[idx];
|
|
@@ -8226,7 +8740,7 @@ async function getConsoleMessage2(driver, args = {}) {
|
|
|
8226
8740
|
}
|
|
8227
8741
|
async function listNetworkRequests2(driver, args = {}) {
|
|
8228
8742
|
refuseWorkerSelector(driver, args.target);
|
|
8229
|
-
return
|
|
8743
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8230
8744
|
if (args.reload)
|
|
8231
8745
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
8232
8746
|
let buf = bufferFor(networkBuffers, page.info.id);
|
|
@@ -8239,7 +8753,7 @@ async function getNetworkRequest2(driver, args = {}) {
|
|
|
8239
8753
|
refuseWorkerSelector(driver, args.target);
|
|
8240
8754
|
if (!args.requestId && !args.url)
|
|
8241
8755
|
throw new BidiToolError("get_network_request requires requestId or url");
|
|
8242
|
-
return
|
|
8756
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8243
8757
|
const buf = bufferFor(networkBuffers, page.info.id);
|
|
8244
8758
|
const found = args.requestId ? buf.find((r) => r.requestId === args.requestId || r.request === args.requestId) : buf.find((r) => typeof r.url === "string" && r.url.includes(args.url));
|
|
8245
8759
|
if (!found)
|
|
@@ -8251,7 +8765,7 @@ var mockSessions = new Map;
|
|
|
8251
8765
|
async function mockRequest2(driver, args) {
|
|
8252
8766
|
if (!args.urlPattern)
|
|
8253
8767
|
throw new BidiToolError("mock_request requires a { urlPattern }");
|
|
8254
|
-
return
|
|
8768
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8255
8769
|
const existing = mockSessions.get(page.info.id);
|
|
8256
8770
|
const rule = {
|
|
8257
8771
|
urlPattern: args.urlPattern,
|
|
@@ -8299,7 +8813,7 @@ async function clearMocks2(driver, args = {}) {
|
|
|
8299
8813
|
mockSessions.clear();
|
|
8300
8814
|
return { cleared: n };
|
|
8301
8815
|
}
|
|
8302
|
-
return
|
|
8816
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8303
8817
|
const existing = mockSessions.get(page.info.id);
|
|
8304
8818
|
if (!existing)
|
|
8305
8819
|
return { cleared: 0 };
|
|
@@ -8324,7 +8838,8 @@ var BIDI_ONLY_TOOLS = {
|
|
|
8324
8838
|
var FIREFOX_TOOLS = {
|
|
8325
8839
|
...SHARED_TOOLS,
|
|
8326
8840
|
...BIDI_ONLY_TOOLS,
|
|
8327
|
-
...LEASE_TOOLS
|
|
8841
|
+
...LEASE_TOOLS,
|
|
8842
|
+
extract_page: extractPage
|
|
8328
8843
|
};
|
|
8329
8844
|
|
|
8330
8845
|
// src/cli.ts
|
|
@@ -8983,6 +9498,70 @@ var MANIFEST = [
|
|
|
8983
9498
|
additionalProperties: false
|
|
8984
9499
|
}
|
|
8985
9500
|
},
|
|
9501
|
+
{
|
|
9502
|
+
name: "focus_emulation",
|
|
9503
|
+
description: "Toggle page focus emulation: the page reports focused/visible without the OS window moving; Chrome-only.",
|
|
9504
|
+
inputSchema: {
|
|
9505
|
+
type: "object",
|
|
9506
|
+
properties: {
|
|
9507
|
+
lease: {
|
|
9508
|
+
type: "string",
|
|
9509
|
+
description: "Lease token; omit for a tab this process already holds. See server instructions."
|
|
9510
|
+
},
|
|
9511
|
+
target: {
|
|
9512
|
+
type: "string",
|
|
9513
|
+
description: "Page selector (index:/url:/title:/label:/targetId, default active); grammar in server instructions."
|
|
9514
|
+
},
|
|
9515
|
+
enabled: {
|
|
9516
|
+
type: "boolean",
|
|
9517
|
+
description: "true = page reports focused; false = restore real focus state."
|
|
9518
|
+
}
|
|
9519
|
+
},
|
|
9520
|
+
required: [
|
|
9521
|
+
"enabled"
|
|
9522
|
+
],
|
|
9523
|
+
additionalProperties: false
|
|
9524
|
+
}
|
|
9525
|
+
},
|
|
9526
|
+
{
|
|
9527
|
+
name: "click_focus_gated",
|
|
9528
|
+
description: "Emulate page focus, wait for a focus-gated button to enable, trusted-click it, restore; Chrome-only.",
|
|
9529
|
+
inputSchema: {
|
|
9530
|
+
type: "object",
|
|
9531
|
+
properties: {
|
|
9532
|
+
lease: {
|
|
9533
|
+
type: "string",
|
|
9534
|
+
description: "Lease token; omit for a tab this process already holds. See server instructions."
|
|
9535
|
+
},
|
|
9536
|
+
target: {
|
|
9537
|
+
type: "string",
|
|
9538
|
+
description: "Page selector (index:/url:/title:/label:/targetId, default active); grammar in server instructions."
|
|
9539
|
+
},
|
|
9540
|
+
selector: {
|
|
9541
|
+
type: "string",
|
|
9542
|
+
description: "CSS selector of the button; exactly one of selector or text."
|
|
9543
|
+
},
|
|
9544
|
+
text: {
|
|
9545
|
+
type: "string",
|
|
9546
|
+
description: 'Visible-text match for the button (e.g. "Authorize"); exactly one of selector or text.'
|
|
9547
|
+
},
|
|
9548
|
+
timeoutMs: {
|
|
9549
|
+
type: "number",
|
|
9550
|
+
description: "Max ms to poll for the button to become enabled. Default 30000."
|
|
9551
|
+
},
|
|
9552
|
+
pollMs: {
|
|
9553
|
+
type: "number",
|
|
9554
|
+
description: "Poll interval ms. Default 500."
|
|
9555
|
+
},
|
|
9556
|
+
keepFocus: {
|
|
9557
|
+
type: "boolean",
|
|
9558
|
+
description: "Leave focus emulation on after the click (default false = restored)."
|
|
9559
|
+
}
|
|
9560
|
+
},
|
|
9561
|
+
required: [],
|
|
9562
|
+
additionalProperties: false
|
|
9563
|
+
}
|
|
9564
|
+
},
|
|
8986
9565
|
{
|
|
8987
9566
|
name: "wait_for_download",
|
|
8988
9567
|
description: "Arm or collect a browser download; arm capture before triggering the download.",
|
|
@@ -10008,19 +10587,93 @@ var MANIFEST = [
|
|
|
10008
10587
|
required: [],
|
|
10009
10588
|
additionalProperties: false
|
|
10010
10589
|
}
|
|
10590
|
+
},
|
|
10591
|
+
{
|
|
10592
|
+
name: "extract_page",
|
|
10593
|
+
description: "Extract schema-conformant JSON from the target page's cleaned HTML via an OpenAI-compatible endpoint; default endpoint is local loopback.",
|
|
10594
|
+
inputSchema: {
|
|
10595
|
+
type: "object",
|
|
10596
|
+
properties: {
|
|
10597
|
+
lease: {
|
|
10598
|
+
type: "string",
|
|
10599
|
+
description: "Lease token; omit for a tab this process already holds. See server instructions."
|
|
10600
|
+
},
|
|
10601
|
+
target: {
|
|
10602
|
+
type: "string",
|
|
10603
|
+
description: "Page selector (index:/url:/title:/label:/targetId, default active); grammar in server instructions."
|
|
10604
|
+
},
|
|
10605
|
+
schema: {
|
|
10606
|
+
type: "object",
|
|
10607
|
+
description: "JSON Schema (type object) the extracted JSON must conform to; extraction instructions live in its property descriptions."
|
|
10608
|
+
},
|
|
10609
|
+
selector: {
|
|
10610
|
+
type: "string",
|
|
10611
|
+
description: "CSS selector scoping extraction to a subtree."
|
|
10612
|
+
},
|
|
10613
|
+
source: {
|
|
10614
|
+
type: "string",
|
|
10615
|
+
enum: [
|
|
10616
|
+
"dom",
|
|
10617
|
+
"served"
|
|
10618
|
+
],
|
|
10619
|
+
description: "Page source to read; 'served' currently returns not-implemented."
|
|
10620
|
+
},
|
|
10621
|
+
clean: {
|
|
10622
|
+
type: "string",
|
|
10623
|
+
enum: [
|
|
10624
|
+
"standard",
|
|
10625
|
+
"aggressive",
|
|
10626
|
+
"none"
|
|
10627
|
+
],
|
|
10628
|
+
description: "HTML cleaning level."
|
|
10629
|
+
},
|
|
10630
|
+
maxChars: {
|
|
10631
|
+
type: "integer",
|
|
10632
|
+
description: "Character cap on the cleaned HTML; default 300000."
|
|
10633
|
+
},
|
|
10634
|
+
timeoutMs: {
|
|
10635
|
+
type: "integer",
|
|
10636
|
+
description: "Timeout in milliseconds; default 90000, max 300000."
|
|
10637
|
+
},
|
|
10638
|
+
prompt: {
|
|
10639
|
+
type: "string",
|
|
10640
|
+
enum: [
|
|
10641
|
+
"html",
|
|
10642
|
+
"schematron"
|
|
10643
|
+
],
|
|
10644
|
+
description: "Prompt shape: 'html' (default) sends cleaned HTML only; 'schematron' uses the open-weight Schematron model-card prompt with the schema inline (or CDP_EXTRACT_PROMPT)."
|
|
10645
|
+
},
|
|
10646
|
+
model: {
|
|
10647
|
+
type: "string",
|
|
10648
|
+
description: "Extraction model name; default 'schematron' (or CDP_EXTRACT_MODEL)."
|
|
10649
|
+
},
|
|
10650
|
+
baseUrl: {
|
|
10651
|
+
type: "string",
|
|
10652
|
+
description: "OpenAI-compatible endpoint base URL; default local loopback (or CDP_EXTRACT_BASE_URL)."
|
|
10653
|
+
},
|
|
10654
|
+
savePath: {
|
|
10655
|
+
type: "string",
|
|
10656
|
+
description: "Output file path."
|
|
10657
|
+
}
|
|
10658
|
+
},
|
|
10659
|
+
required: [
|
|
10660
|
+
"schema"
|
|
10661
|
+
],
|
|
10662
|
+
additionalProperties: false
|
|
10663
|
+
}
|
|
10011
10664
|
}
|
|
10012
10665
|
];
|
|
10013
10666
|
|
|
10014
10667
|
// src/install/wizard.ts
|
|
10015
10668
|
import { createInterface } from "node:readline";
|
|
10016
10669
|
import { homedir as homedir3 } from "node:os";
|
|
10017
|
-
import { join as
|
|
10670
|
+
import { join as join15 } from "node:path";
|
|
10018
10671
|
|
|
10019
10672
|
// src/install/browser.ts
|
|
10020
10673
|
import { existsSync as existsSync3 } from "node:fs";
|
|
10021
|
-
import { mkdir as
|
|
10674
|
+
import { mkdir as mkdir14, readFile as readFile9, writeFile as writeFile11 } from "node:fs/promises";
|
|
10022
10675
|
import { homedir } from "node:os";
|
|
10023
|
-
import { delimiter, dirname as
|
|
10676
|
+
import { delimiter, dirname as dirname4, join as join13 } from "node:path";
|
|
10024
10677
|
var CANDIDATE_BUILDERS = {
|
|
10025
10678
|
chrome: () => [
|
|
10026
10679
|
process.env.CHROME_BIN,
|
|
@@ -10044,7 +10697,7 @@ function existsOnPath(cmd) {
|
|
|
10044
10697
|
for (const dir of pathEnv.split(delimiter)) {
|
|
10045
10698
|
if (!dir)
|
|
10046
10699
|
continue;
|
|
10047
|
-
if (existsSync3(
|
|
10700
|
+
if (existsSync3(join13(dir, cmd)))
|
|
10048
10701
|
return true;
|
|
10049
10702
|
}
|
|
10050
10703
|
return false;
|
|
@@ -10094,10 +10747,10 @@ function aliasLine(o) {
|
|
|
10094
10747
|
function detectShellRc() {
|
|
10095
10748
|
const shell = process.env.SHELL ?? "";
|
|
10096
10749
|
if (shell.endsWith("zsh"))
|
|
10097
|
-
return
|
|
10750
|
+
return join13(homedir(), ".zshrc");
|
|
10098
10751
|
if (shell.endsWith("bash"))
|
|
10099
|
-
return
|
|
10100
|
-
return
|
|
10752
|
+
return join13(homedir(), ".bashrc");
|
|
10753
|
+
return join13(homedir(), process.platform === "darwin" ? ".zshrc" : ".bashrc");
|
|
10101
10754
|
}
|
|
10102
10755
|
var MARKER_START = "# >>> cdp-toolkit alias >>>";
|
|
10103
10756
|
var MARKER_END = "# <<< cdp-toolkit alias <<<";
|
|
@@ -10130,24 +10783,24 @@ ${MARKER_END}`;
|
|
|
10130
10783
|
}
|
|
10131
10784
|
const next = ensureTrailingNewline(`${before}${block}
|
|
10132
10785
|
${after}`);
|
|
10133
|
-
await
|
|
10786
|
+
await writeFile11(rcPath, next, "utf8");
|
|
10134
10787
|
return { rcPath, action: "replaced-block" };
|
|
10135
10788
|
}
|
|
10136
|
-
await
|
|
10789
|
+
await mkdir14(dirname4(rcPath), { recursive: true });
|
|
10137
10790
|
const sep = existing.length === 0 || existing.endsWith(`
|
|
10138
10791
|
`) ? "" : `
|
|
10139
10792
|
`;
|
|
10140
10793
|
const next = `${existing}${sep}${block}
|
|
10141
10794
|
`;
|
|
10142
|
-
await
|
|
10795
|
+
await writeFile11(rcPath, next, "utf8");
|
|
10143
10796
|
return { rcPath, action: "created-block" };
|
|
10144
10797
|
}
|
|
10145
10798
|
|
|
10146
10799
|
// src/install/harness.ts
|
|
10147
10800
|
import { spawnSync } from "node:child_process";
|
|
10148
|
-
import { mkdir as
|
|
10801
|
+
import { mkdir as mkdir15, readFile as readFile10, writeFile as writeFile12 } from "node:fs/promises";
|
|
10149
10802
|
import { homedir as homedir2 } from "node:os";
|
|
10150
|
-
import { dirname as
|
|
10803
|
+
import { dirname as dirname5, join as join14 } from "node:path";
|
|
10151
10804
|
async function installMcpServer(harness, name, cfg, opts = {}) {
|
|
10152
10805
|
switch (harness) {
|
|
10153
10806
|
case "claude":
|
|
@@ -10163,7 +10816,7 @@ async function installMcpServer(harness, name, cfg, opts = {}) {
|
|
|
10163
10816
|
}
|
|
10164
10817
|
}
|
|
10165
10818
|
async function installClaude(name, cfg, opts) {
|
|
10166
|
-
const path = opts.configPath ??
|
|
10819
|
+
const path = opts.configPath ?? join14(opts.homeDir ?? homedir2(), ".claude.json");
|
|
10167
10820
|
let root;
|
|
10168
10821
|
let fileExisted;
|
|
10169
10822
|
try {
|
|
@@ -10194,8 +10847,8 @@ async function installClaude(name, cfg, opts) {
|
|
|
10194
10847
|
if (action === "unchanged") {
|
|
10195
10848
|
return { harness: "claude", method: "file", path, action, detail: "no change needed" };
|
|
10196
10849
|
}
|
|
10197
|
-
await
|
|
10198
|
-
await
|
|
10850
|
+
await mkdir15(dirname5(path), { recursive: true });
|
|
10851
|
+
await writeFile12(path, JSON.stringify(root, null, 2) + `
|
|
10199
10852
|
`, "utf8");
|
|
10200
10853
|
return {
|
|
10201
10854
|
harness: "claude",
|
|
@@ -10431,7 +11084,7 @@ async function promptPort(def) {
|
|
|
10431
11084
|
}
|
|
10432
11085
|
function harnessTargetDesc(harness, io) {
|
|
10433
11086
|
if (harness === "claude") {
|
|
10434
|
-
return io.configPath ??
|
|
11087
|
+
return io.configPath ?? join15(io.homeDir ?? homedir3(), ".claude.json");
|
|
10435
11088
|
}
|
|
10436
11089
|
return `via \`${harness} mcp add\``;
|
|
10437
11090
|
}
|