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/mcp.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 {
|
|
@@ -4161,6 +4163,7 @@ var CDP_CAPABILITIES = new Set([
|
|
|
4161
4163
|
"capture.screencast",
|
|
4162
4164
|
"input.raw",
|
|
4163
4165
|
"input.html5Drag",
|
|
4166
|
+
"emulate.focus",
|
|
4164
4167
|
"browser.downloads",
|
|
4165
4168
|
"browser.permissions",
|
|
4166
4169
|
"worker.targets"
|
|
@@ -7581,6 +7584,99 @@ async function dispatchMouse(args) {
|
|
|
7581
7584
|
});
|
|
7582
7585
|
}
|
|
7583
7586
|
|
|
7587
|
+
// src/tools/focus-emulation.ts
|
|
7588
|
+
init_activity();
|
|
7589
|
+
init_client();
|
|
7590
|
+
function validateFocusEmulationArgs(args) {
|
|
7591
|
+
if (typeof args.enabled !== "boolean") {
|
|
7592
|
+
throw new Error("focus_emulation requires { enabled } of true or false");
|
|
7593
|
+
}
|
|
7594
|
+
}
|
|
7595
|
+
function validateClickFocusGatedArgs(args) {
|
|
7596
|
+
const hasSel = typeof args.selector === "string" && args.selector.length > 0;
|
|
7597
|
+
const hasText = typeof args.text === "string" && args.text.length > 0;
|
|
7598
|
+
if (hasSel === hasText) {
|
|
7599
|
+
throw new Error("click_focus_gated requires exactly one of { selector } or { text }");
|
|
7600
|
+
}
|
|
7601
|
+
if (args.timeoutMs !== undefined && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
|
|
7602
|
+
throw new Error("click_focus_gated: timeoutMs must be a positive number");
|
|
7603
|
+
}
|
|
7604
|
+
if (args.pollMs !== undefined && (!Number.isFinite(args.pollMs) || args.pollMs <= 0)) {
|
|
7605
|
+
throw new Error("click_focus_gated: pollMs must be a positive number");
|
|
7606
|
+
}
|
|
7607
|
+
}
|
|
7608
|
+
var FIND_BUTTON = `(() => {
|
|
7609
|
+
const SEL = %SEL%;
|
|
7610
|
+
const TEXT = %TEXT%;
|
|
7611
|
+
let btn = null;
|
|
7612
|
+
let matchedBy = null;
|
|
7613
|
+
if (SEL) {
|
|
7614
|
+
btn = document.querySelector(SEL);
|
|
7615
|
+
if (btn) matchedBy = "selector";
|
|
7616
|
+
}
|
|
7617
|
+
if (!btn && TEXT) {
|
|
7618
|
+
btn = Array.from(document.querySelectorAll("button,[role=button],input[type=submit]"))
|
|
7619
|
+
.find((b) => (b.textContent || b.value || "").trim() === TEXT);
|
|
7620
|
+
if (btn) matchedBy = "text";
|
|
7621
|
+
}
|
|
7622
|
+
if (!btn) return null;
|
|
7623
|
+
const r = btn.getBoundingClientRect();
|
|
7624
|
+
if (r.width === 0 || r.height === 0) return null;
|
|
7625
|
+
return { x: r.left + r.width / 2, y: r.top + r.height / 2,
|
|
7626
|
+
disabled: !!btn.disabled || btn.getAttribute("aria-disabled") === "true",
|
|
7627
|
+
matchedBy, hasFocus: document.hasFocus(), visibility: document.visibilityState };
|
|
7628
|
+
})()`;
|
|
7629
|
+
function findButtonExpression(selector, text) {
|
|
7630
|
+
return FIND_BUTTON.replace("%SEL%", JSON.stringify(selector ?? null)).replace("%TEXT%", JSON.stringify(text ?? null));
|
|
7631
|
+
}
|
|
7632
|
+
async function focusEmulation(args) {
|
|
7633
|
+
validateFocusEmulationArgs(args);
|
|
7634
|
+
return withPage(args.target, async (conn) => {
|
|
7635
|
+
await conn.send("Emulation.setFocusEmulationEnabled", { enabled: args.enabled });
|
|
7636
|
+
return { enabled: args.enabled };
|
|
7637
|
+
}, { lease: args.lease });
|
|
7638
|
+
}
|
|
7639
|
+
async function clickFocusGated(args) {
|
|
7640
|
+
validateClickFocusGatedArgs(args);
|
|
7641
|
+
const timeoutMs = args.timeoutMs ?? 30000;
|
|
7642
|
+
const pollMs = args.pollMs ?? 500;
|
|
7643
|
+
const start = Date.now();
|
|
7644
|
+
return withPage(args.target, async (conn, target) => {
|
|
7645
|
+
await conn.send("Emulation.setFocusEmulationEnabled", { enabled: true });
|
|
7646
|
+
try {
|
|
7647
|
+
const expression = findButtonExpression(args.selector, args.text);
|
|
7648
|
+
for (;; ) {
|
|
7649
|
+
const { result } = await conn.send("Runtime.evaluate", {
|
|
7650
|
+
expression,
|
|
7651
|
+
returnByValue: true
|
|
7652
|
+
});
|
|
7653
|
+
const btn = result?.value;
|
|
7654
|
+
if (btn && !btn.disabled) {
|
|
7655
|
+
const base = { x: btn.x, y: btn.y, button: "left", buttons: 1, clickCount: 1 };
|
|
7656
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", { type: "mouseMoved", x: btn.x, y: btn.y });
|
|
7657
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", { ...base, type: "mousePressed" });
|
|
7658
|
+
await sendInput(conn, "chrome", target.id, "Input.dispatchMouseEvent", { ...base, type: "mouseReleased" });
|
|
7659
|
+
return {
|
|
7660
|
+
clicked: true,
|
|
7661
|
+
x: btn.x,
|
|
7662
|
+
y: btn.y,
|
|
7663
|
+
matchedBy: btn.matchedBy,
|
|
7664
|
+
elapsedMs: Date.now() - start
|
|
7665
|
+
};
|
|
7666
|
+
}
|
|
7667
|
+
if (Date.now() - start >= timeoutMs) {
|
|
7668
|
+
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)"));
|
|
7669
|
+
}
|
|
7670
|
+
await new Promise((r) => setTimeout(r, pollMs));
|
|
7671
|
+
}
|
|
7672
|
+
} finally {
|
|
7673
|
+
if (!args.keepFocus) {
|
|
7674
|
+
await conn.send("Emulation.setFocusEmulationEnabled", { enabled: false }).catch(() => {});
|
|
7675
|
+
}
|
|
7676
|
+
}
|
|
7677
|
+
}, { lease: args.lease, timeoutMs: timeoutMs + 5000 });
|
|
7678
|
+
}
|
|
7679
|
+
|
|
7584
7680
|
// src/tools/downloads.ts
|
|
7585
7681
|
init_client();
|
|
7586
7682
|
import { readdir as readdir3, rename as rename2, stat as stat4 } from "node:fs/promises";
|
|
@@ -7809,6 +7905,421 @@ async function grantPermissions(args = {}) {
|
|
|
7809
7905
|
return { granted: [...permissions], origin, ...reset ? { reset: true } : {}, target };
|
|
7810
7906
|
}
|
|
7811
7907
|
|
|
7908
|
+
// src/tools/extract.ts
|
|
7909
|
+
init_client();
|
|
7910
|
+
import { mkdir as mkdir12, writeFile as writeFile9 } from "node:fs/promises";
|
|
7911
|
+
import { dirname as dirname3, join as join10 } from "node:path";
|
|
7912
|
+
var ARTIFACT_DIR7 = process.env.CDP_ARTIFACT_DIR ?? "/tmp/cdp-toolkit";
|
|
7913
|
+
var DEFAULT_BASE_URL = "http://127.0.0.1:8090/v1";
|
|
7914
|
+
var DEFAULT_MODEL = "schematron";
|
|
7915
|
+
var DEFAULT_TIMEOUT_MS2 = 90000;
|
|
7916
|
+
var DEFAULT_MAX_CHARS = 300000;
|
|
7917
|
+
var MAX_TIMEOUT_MS = 300000;
|
|
7918
|
+
var DEFAULT_PROMPT_MODE = "html";
|
|
7919
|
+
var CHARS_PER_TOKEN = 3.5;
|
|
7920
|
+
var COMPLESION_TOKENS = 8192;
|
|
7921
|
+
var SAFETY_TOKENS = 8000;
|
|
7922
|
+
var CONTEXT_TOKEN_BUDGET = 128000;
|
|
7923
|
+
function extractBaseUrl() {
|
|
7924
|
+
const raw = process.env.CDP_EXTRACT_BASE_URL?.trim();
|
|
7925
|
+
return raw ? raw : DEFAULT_BASE_URL;
|
|
7926
|
+
}
|
|
7927
|
+
function extractModel() {
|
|
7928
|
+
const raw = process.env.CDP_EXTRACT_MODEL?.trim();
|
|
7929
|
+
return raw ? raw : DEFAULT_MODEL;
|
|
7930
|
+
}
|
|
7931
|
+
function extractPromptMode() {
|
|
7932
|
+
const raw = process.env.CDP_EXTRACT_PROMPT?.trim();
|
|
7933
|
+
return raw ? raw : DEFAULT_PROMPT_MODE;
|
|
7934
|
+
}
|
|
7935
|
+
function extractApiKey() {
|
|
7936
|
+
const raw = process.env.CDP_EXTRACT_API_KEY;
|
|
7937
|
+
return raw && raw.length > 0 ? raw : undefined;
|
|
7938
|
+
}
|
|
7939
|
+
function extractTimeoutMs() {
|
|
7940
|
+
const raw = Number(process.env.CDP_EXTRACT_TIMEOUT_MS);
|
|
7941
|
+
return Number.isFinite(raw) && raw > 0 ? Math.min(raw, MAX_TIMEOUT_MS) : DEFAULT_TIMEOUT_MS2;
|
|
7942
|
+
}
|
|
7943
|
+
function extractMaxChars() {
|
|
7944
|
+
const raw = Number(process.env.CDP_EXTRACT_MAX_CHARS);
|
|
7945
|
+
return Number.isFinite(raw) && raw > 0 ? raw : DEFAULT_MAX_CHARS;
|
|
7946
|
+
}
|
|
7947
|
+
function hasAnyDescription(node, seen = new Set) {
|
|
7948
|
+
if (typeof node !== "object" || node === null || seen.has(node))
|
|
7949
|
+
return false;
|
|
7950
|
+
seen.add(node);
|
|
7951
|
+
const n = node;
|
|
7952
|
+
if (typeof n.description === "string" && n.description.trim() !== "")
|
|
7953
|
+
return true;
|
|
7954
|
+
const props = n.properties;
|
|
7955
|
+
if (typeof props === "object" && props !== null) {
|
|
7956
|
+
for (const value of Object.values(props))
|
|
7957
|
+
if (hasAnyDescription(value, seen))
|
|
7958
|
+
return true;
|
|
7959
|
+
}
|
|
7960
|
+
if (hasAnyDescription(n.items, seen))
|
|
7961
|
+
return true;
|
|
7962
|
+
for (const key of ["anyOf", "oneOf", "allOf", "prefixItems"]) {
|
|
7963
|
+
const branches = n[key];
|
|
7964
|
+
if (Array.isArray(branches)) {
|
|
7965
|
+
for (const branch of branches)
|
|
7966
|
+
if (hasAnyDescription(branch, seen))
|
|
7967
|
+
return true;
|
|
7968
|
+
}
|
|
7969
|
+
}
|
|
7970
|
+
return false;
|
|
7971
|
+
}
|
|
7972
|
+
function validateExtractArgs(args) {
|
|
7973
|
+
if (args.schema === null || typeof args.schema !== "object" || Array.isArray(args.schema)) {
|
|
7974
|
+
throw new CdpError("extract_page: 'schema' is required and must be a JSON Schema object");
|
|
7975
|
+
}
|
|
7976
|
+
if (args.schema.type !== "object") {
|
|
7977
|
+
throw new CdpError(`extract_page: 'schema.type' must be "object" (got ${JSON.stringify(args.schema.type)})`);
|
|
7978
|
+
}
|
|
7979
|
+
if (args.schema.additionalProperties === true) {
|
|
7980
|
+
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");
|
|
7981
|
+
}
|
|
7982
|
+
if (!hasAnyDescription(args.schema)) {
|
|
7983
|
+
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.");
|
|
7984
|
+
}
|
|
7985
|
+
if (args.selector !== undefined && (typeof args.selector !== "string" || args.selector.length === 0)) {
|
|
7986
|
+
throw new CdpError("extract_page: 'selector' must be a non-empty CSS selector string");
|
|
7987
|
+
}
|
|
7988
|
+
if (args.source !== undefined && args.source !== "dom" && args.source !== "served") {
|
|
7989
|
+
throw new CdpError(`extract_page: 'source' must be "dom" or "served" (got ${JSON.stringify(args.source)})`);
|
|
7990
|
+
}
|
|
7991
|
+
if (args.source === "served") {
|
|
7992
|
+
throw new CdpError("extract_page: source 'served' (the raw server response before rendering) is not implemented; use source 'dom' (the live rendered DOM).");
|
|
7993
|
+
}
|
|
7994
|
+
if (args.clean !== undefined && args.clean !== "standard" && args.clean !== "aggressive" && args.clean !== "none") {
|
|
7995
|
+
throw new CdpError(`extract_page: 'clean' must be "standard", "aggressive" or "none" (got ${JSON.stringify(args.clean)})`);
|
|
7996
|
+
}
|
|
7997
|
+
if (args.prompt !== undefined && args.prompt !== "html" && args.prompt !== "schematron") {
|
|
7998
|
+
throw new CdpError(`extract_page: 'prompt' must be "html" or "schematron" (got ${JSON.stringify(args.prompt)})`);
|
|
7999
|
+
}
|
|
8000
|
+
const envPrompt = process.env.CDP_EXTRACT_PROMPT?.trim();
|
|
8001
|
+
if (args.prompt === undefined && envPrompt && envPrompt !== "html" && envPrompt !== "schematron") {
|
|
8002
|
+
throw new CdpError(`extract_page: CDP_EXTRACT_PROMPT must be "html" or "schematron" (got ${JSON.stringify(envPrompt)})`);
|
|
8003
|
+
}
|
|
8004
|
+
if (args.maxChars !== undefined && (!Number.isFinite(args.maxChars) || args.maxChars <= 0)) {
|
|
8005
|
+
throw new CdpError("extract_page: 'maxChars' must be a positive number");
|
|
8006
|
+
}
|
|
8007
|
+
if (args.timeoutMs !== undefined && (!Number.isFinite(args.timeoutMs) || args.timeoutMs <= 0)) {
|
|
8008
|
+
throw new CdpError("extract_page: 'timeoutMs' must be a positive number");
|
|
8009
|
+
}
|
|
8010
|
+
if (args.model !== undefined && (typeof args.model !== "string" || args.model.length === 0)) {
|
|
8011
|
+
throw new CdpError("extract_page: 'model' must be a non-empty string");
|
|
8012
|
+
}
|
|
8013
|
+
if (args.baseUrl !== undefined && (typeof args.baseUrl !== "string" || args.baseUrl.length === 0)) {
|
|
8014
|
+
throw new CdpError("extract_page: 'baseUrl' must be a non-empty string (the API key is env-only: CDP_EXTRACT_API_KEY)");
|
|
8015
|
+
}
|
|
8016
|
+
}
|
|
8017
|
+
function buildCleanFunctionSource(selector, clean) {
|
|
8018
|
+
return `function() {
|
|
8019
|
+
var SEL = ${JSON.stringify(selector ?? null)};
|
|
8020
|
+
var CLEAN = ${JSON.stringify(clean)};
|
|
8021
|
+
var STRIP = ["script","style","noscript","svg","canvas","iframe","video","audio","source","track","template","object","embed","link","meta"];
|
|
8022
|
+
var AGGRO = ["nav","header","footer","aside","form","button"];
|
|
8023
|
+
var WHITELIST = ["href","src","alt","title","value","placeholder","for","name","type","role","aria-label","selected","checked","disabled"];
|
|
8024
|
+
|
|
8025
|
+
function bytes(s) {
|
|
8026
|
+
try { return new TextEncoder().encode(s).length; } catch (e) { return s.length; }
|
|
8027
|
+
}
|
|
8028
|
+
|
|
8029
|
+
function collectShadowScopes(root, out) {
|
|
8030
|
+
var els = root.querySelectorAll("*");
|
|
8031
|
+
for (var i = 0; i < els.length; i++) {
|
|
8032
|
+
var sr = els[i].shadowRoot;
|
|
8033
|
+
if (sr) { out.push(sr); collectShadowScopes(sr, out); }
|
|
8034
|
+
}
|
|
8035
|
+
}
|
|
8036
|
+
|
|
8037
|
+
// Locate SEL piercing open shadow roots (querySelector on document first,
|
|
8038
|
+
// then every open shadow scope). LIMIT: outerHTML serializes the matched
|
|
8039
|
+
// element's light DOM only; a closed shadow root is invisible by design.
|
|
8040
|
+
function deepQuery(sel) {
|
|
8041
|
+
var direct = document.querySelector(sel);
|
|
8042
|
+
if (direct) return direct;
|
|
8043
|
+
var scopes = [];
|
|
8044
|
+
collectShadowScopes(document, scopes);
|
|
8045
|
+
for (var i = 0; i < scopes.length; i++) {
|
|
8046
|
+
var hit = scopes[i].querySelector(sel);
|
|
8047
|
+
if (hit) return hit;
|
|
8048
|
+
}
|
|
8049
|
+
return null;
|
|
8050
|
+
}
|
|
8051
|
+
|
|
8052
|
+
// The visibility heuristic, same shape as bidi/snapshot.ts's isVisible:
|
|
8053
|
+
// hidden attr, aria-hidden, computed display/visibility, then the 0x0
|
|
8054
|
+
// paint check. OPTION/OPTGROUP skip the size check (a closed select's
|
|
8055
|
+
// popup is unpainted though its options are real, extractable content).
|
|
8056
|
+
function isHidden(el) {
|
|
8057
|
+
if (el.hidden) return true;
|
|
8058
|
+
if (el.getAttribute("aria-hidden") === "true") return true;
|
|
8059
|
+
var style = null;
|
|
8060
|
+
try { style = el.ownerDocument.defaultView.getComputedStyle(el); } catch (e) {}
|
|
8061
|
+
if (style && (style.display === "none" || style.visibility === "hidden")) return true;
|
|
8062
|
+
var tag = el.tagName;
|
|
8063
|
+
if (tag !== "OPTION" && tag !== "OPTGROUP") {
|
|
8064
|
+
var rect = el.getBoundingClientRect();
|
|
8065
|
+
if (rect.width === 0 && rect.height === 0) return true;
|
|
8066
|
+
}
|
|
8067
|
+
return false;
|
|
8068
|
+
}
|
|
8069
|
+
|
|
8070
|
+
function countDrop(dropped, tag) {
|
|
8071
|
+
dropped.elements[tag] = (dropped.elements[tag] || 0) + 1;
|
|
8072
|
+
}
|
|
8073
|
+
|
|
8074
|
+
function cleanRoot(root) {
|
|
8075
|
+
var rawHtml = root.outerHTML;
|
|
8076
|
+
var dropped = { comments: 0, hidden: 0, attributes: 0, elements: {} };
|
|
8077
|
+
if (CLEAN === "none") {
|
|
8078
|
+
return { matched: true, html: rawHtml, rawBytes: bytes(rawHtml), cleanBytes: bytes(rawHtml), dropped: dropped };
|
|
8079
|
+
}
|
|
8080
|
+
var live = root.querySelectorAll("*");
|
|
8081
|
+
var clone = root.cloneNode(true);
|
|
8082
|
+
var mirrored = clone.querySelectorAll("*");
|
|
8083
|
+
for (var i = 0; i < mirrored.length; i++) {
|
|
8084
|
+
var cel = mirrored[i];
|
|
8085
|
+
if (!cel.parentNode) continue; // dropped already, with an ancestor
|
|
8086
|
+
var low = cel.tagName.toLowerCase();
|
|
8087
|
+
if (STRIP.indexOf(low) !== -1) { countDrop(dropped, low); cel.parentNode.removeChild(cel); continue; }
|
|
8088
|
+
if (CLEAN === "aggressive" && AGGRO.indexOf(low) !== -1) { countDrop(dropped, low); cel.parentNode.removeChild(cel); continue; }
|
|
8089
|
+
var lel = live[i];
|
|
8090
|
+
if (lel && isHidden(lel)) { dropped.hidden++; cel.parentNode.removeChild(cel); continue; }
|
|
8091
|
+
for (var j = cel.attributes.length - 1; j >= 0; j--) {
|
|
8092
|
+
var attr = cel.attributes[j];
|
|
8093
|
+
if (WHITELIST.indexOf(attr.name.toLowerCase()) === -1) {
|
|
8094
|
+
cel.removeAttribute(attr.name);
|
|
8095
|
+
dropped.attributes++;
|
|
8096
|
+
}
|
|
8097
|
+
}
|
|
8098
|
+
}
|
|
8099
|
+
var walker = clone.ownerDocument.createTreeWalker(clone, NodeFilter.SHOW_COMMENT);
|
|
8100
|
+
var comments = [];
|
|
8101
|
+
while (walker.nextNode()) comments.push(walker.currentNode);
|
|
8102
|
+
for (var k = 0; k < comments.length; k++) {
|
|
8103
|
+
var c = comments[k];
|
|
8104
|
+
if (c.parentNode) c.parentNode.removeChild(c);
|
|
8105
|
+
}
|
|
8106
|
+
dropped.comments = comments.length;
|
|
8107
|
+
// Whitespace collapse at the TEXT-NODE level, never on the serialized
|
|
8108
|
+
// string: a regex over outerHTML would also flatten <pre> content.
|
|
8109
|
+
var tw = clone.ownerDocument.createTreeWalker(clone, NodeFilter.SHOW_TEXT);
|
|
8110
|
+
while (tw.nextNode()) {
|
|
8111
|
+
var node = tw.currentNode;
|
|
8112
|
+
node.nodeValue = node.nodeValue.replace(/\\s+/g, " ");
|
|
8113
|
+
}
|
|
8114
|
+
var html = clone.outerHTML;
|
|
8115
|
+
return { matched: true, html: html, rawBytes: bytes(rawHtml), cleanBytes: bytes(html), dropped: dropped };
|
|
8116
|
+
}
|
|
8117
|
+
|
|
8118
|
+
if (SEL) {
|
|
8119
|
+
var found = deepQuery(SEL);
|
|
8120
|
+
if (!found) return { matched: false };
|
|
8121
|
+
return cleanRoot(found);
|
|
8122
|
+
}
|
|
8123
|
+
return cleanRoot(document.documentElement);
|
|
8124
|
+
}`;
|
|
8125
|
+
}
|
|
8126
|
+
function coerceCleanOutcome(raw, selector) {
|
|
8127
|
+
if (typeof raw !== "object" || raw === null) {
|
|
8128
|
+
throw new CdpError("extract_page: in-page cleaning returned an unexpected shape");
|
|
8129
|
+
}
|
|
8130
|
+
const r = raw;
|
|
8131
|
+
if (r.matched === false) {
|
|
8132
|
+
throw new CdpError(`extract_page: no element matches selector '${selector}' (checked document and every open shadow root)`);
|
|
8133
|
+
}
|
|
8134
|
+
if (typeof r.html !== "string" || typeof r.rawBytes !== "number" || !Number.isFinite(r.rawBytes) || typeof r.cleanBytes !== "number" || !Number.isFinite(r.cleanBytes)) {
|
|
8135
|
+
throw new CdpError("extract_page: in-page cleaning returned an unexpected shape");
|
|
8136
|
+
}
|
|
8137
|
+
return {
|
|
8138
|
+
html: r.html,
|
|
8139
|
+
rawBytes: r.rawBytes,
|
|
8140
|
+
cleanBytes: r.cleanBytes,
|
|
8141
|
+
dropped: typeof r.dropped === "object" && r.dropped !== null ? r.dropped : {}
|
|
8142
|
+
};
|
|
8143
|
+
}
|
|
8144
|
+
function estimateTokens(chars) {
|
|
8145
|
+
return Math.ceil(chars / CHARS_PER_TOKEN);
|
|
8146
|
+
}
|
|
8147
|
+
function guardSize(outcome, maxChars) {
|
|
8148
|
+
const estTokens = estimateTokens(outcome.html.length);
|
|
8149
|
+
const overChars = outcome.html.length > maxChars;
|
|
8150
|
+
const overContext = estTokens + COMPLESION_TOKENS + SAFETY_TOKENS > CONTEXT_TOKEN_BUDGET;
|
|
8151
|
+
if (overChars || overContext) {
|
|
8152
|
+
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`;
|
|
8153
|
+
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.`);
|
|
8154
|
+
}
|
|
8155
|
+
return estTokens;
|
|
8156
|
+
}
|
|
8157
|
+
function redactSecrets(text, apiKey) {
|
|
8158
|
+
let out = text;
|
|
8159
|
+
if (apiKey && apiKey.length > 0) {
|
|
8160
|
+
out = out.split(apiKey).join("[redacted]");
|
|
8161
|
+
}
|
|
8162
|
+
return out.replace(/sk-[A-Za-z0-9_-]{8,}/g, "[redacted]");
|
|
8163
|
+
}
|
|
8164
|
+
function pickCostUsd(usage) {
|
|
8165
|
+
for (const v of [usage.cost_usd, usage.costUsd, usage.cost]) {
|
|
8166
|
+
if (typeof v === "number" && Number.isFinite(v))
|
|
8167
|
+
return v;
|
|
8168
|
+
}
|
|
8169
|
+
return;
|
|
8170
|
+
}
|
|
8171
|
+
function excerpt(text, max) {
|
|
8172
|
+
const t = text.trim();
|
|
8173
|
+
return t.length > max ? `${t.slice(0, max)}…` : t;
|
|
8174
|
+
}
|
|
8175
|
+
function buildExtractionMessages(mode, html, schema) {
|
|
8176
|
+
if (mode !== "schematron")
|
|
8177
|
+
return [{ role: "user", content: html }];
|
|
8178
|
+
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:" + `
|
|
8179
|
+
|
|
8180
|
+
` + JSON.stringify(schema) + `
|
|
8181
|
+
|
|
8182
|
+
` + "Here is the HTML page:" + `
|
|
8183
|
+
|
|
8184
|
+
` + html + `
|
|
8185
|
+
|
|
8186
|
+
` + "MAKE SURE ITS VALID JSON.";
|
|
8187
|
+
return [
|
|
8188
|
+
{ role: "system", content: "You are a helpful assistant" },
|
|
8189
|
+
{ role: "user", content: user }
|
|
8190
|
+
];
|
|
8191
|
+
}
|
|
8192
|
+
async function callExtractionEndpoint(opts) {
|
|
8193
|
+
const url = `${opts.baseUrl.replace(/\/+$/, "")}/chat/completions`;
|
|
8194
|
+
const body = JSON.stringify({
|
|
8195
|
+
model: opts.model,
|
|
8196
|
+
messages: buildExtractionMessages(opts.prompt ?? "html", opts.html, opts.schema),
|
|
8197
|
+
temperature: 0,
|
|
8198
|
+
max_tokens: COMPLESION_TOKENS,
|
|
8199
|
+
response_format: { type: "json_schema", json_schema: { name: "extract", strict: true, schema: opts.schema } }
|
|
8200
|
+
});
|
|
8201
|
+
const headers = { "content-type": "application/json" };
|
|
8202
|
+
if (opts.apiKey)
|
|
8203
|
+
headers.authorization = `Bearer ${opts.apiKey}`;
|
|
8204
|
+
let lastStatus = 0;
|
|
8205
|
+
let lastBody = "";
|
|
8206
|
+
for (let attempt = 1;attempt <= 2; attempt++) {
|
|
8207
|
+
let res;
|
|
8208
|
+
try {
|
|
8209
|
+
res = await fetch(url, { method: "POST", headers, body, signal: AbortSignal.timeout(opts.timeoutMs) });
|
|
8210
|
+
} catch (e) {
|
|
8211
|
+
const err = e;
|
|
8212
|
+
if (err.name === "TimeoutError" || err.name === "AbortError") {
|
|
8213
|
+
throw new CdpError(`extract_page: upstream call to ${opts.baseUrl} timed out after ${opts.timeoutMs}ms`);
|
|
8214
|
+
}
|
|
8215
|
+
throw new CdpError(`extract_page: could not reach ${opts.baseUrl} (${redactSecrets(err.message, opts.apiKey)}) — is ferry running?`);
|
|
8216
|
+
}
|
|
8217
|
+
const text = await res.text();
|
|
8218
|
+
if (res.ok) {
|
|
8219
|
+
let parsed;
|
|
8220
|
+
try {
|
|
8221
|
+
parsed = JSON.parse(text);
|
|
8222
|
+
} catch {
|
|
8223
|
+
throw new CdpError(`extract_page: endpoint returned a non-JSON 200 body: ${excerpt(redactSecrets(text, opts.apiKey), 200)}`);
|
|
8224
|
+
}
|
|
8225
|
+
if (parsed.error) {
|
|
8226
|
+
throw new CdpError(`extract_page: upstream_error from ${opts.baseUrl}: ${excerpt(redactSecrets(String(parsed.error.message ?? JSON.stringify(parsed.error)), opts.apiKey), 600)}`);
|
|
8227
|
+
}
|
|
8228
|
+
const content = parsed.choices?.[0]?.message?.content;
|
|
8229
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
8230
|
+
throw new CdpError(`extract_page: endpoint response carried no message content: ${excerpt(redactSecrets(text, opts.apiKey), 200)}`);
|
|
8231
|
+
}
|
|
8232
|
+
const u = parsed.usage ?? {};
|
|
8233
|
+
const usage = {
|
|
8234
|
+
promptTokens: u.prompt_tokens ?? 0,
|
|
8235
|
+
completionTokens: u.completion_tokens ?? 0,
|
|
8236
|
+
totalTokens: u.total_tokens ?? 0,
|
|
8237
|
+
...pickCostUsd(u) !== undefined ? { costUsd: pickCostUsd(u) } : {}
|
|
8238
|
+
};
|
|
8239
|
+
const fr = parsed.choices?.[0]?.finish_reason;
|
|
8240
|
+
const finishReason = typeof fr === "string" ? fr : undefined;
|
|
8241
|
+
return { content, usage, model: parsed.model ?? opts.model, finishReason };
|
|
8242
|
+
}
|
|
8243
|
+
lastStatus = res.status;
|
|
8244
|
+
lastBody = text;
|
|
8245
|
+
if (res.status === 429) {
|
|
8246
|
+
const retryAfter = res.headers.get("retry-after");
|
|
8247
|
+
throw new CdpError(`extract_page: upstream rate-limited (HTTP 429) from ${opts.baseUrl}${retryAfter !== null ? `, retry-after: ${retryAfter}` : " (no retry-after header)"}`);
|
|
8248
|
+
}
|
|
8249
|
+
if (res.status < 500 || res.status >= 600)
|
|
8250
|
+
break;
|
|
8251
|
+
}
|
|
8252
|
+
throw new CdpError(`extract_page: upstream_error HTTP ${lastStatus} from POST ${url}: ${excerpt(redactSecrets(lastBody, opts.apiKey), 600)}`);
|
|
8253
|
+
}
|
|
8254
|
+
async function withPage3(driver, target, fn) {
|
|
8255
|
+
const page = await driver.page(target);
|
|
8256
|
+
try {
|
|
8257
|
+
return await fn(page);
|
|
8258
|
+
} finally {
|
|
8259
|
+
await page.release();
|
|
8260
|
+
}
|
|
8261
|
+
}
|
|
8262
|
+
async function extractPage(driver, args) {
|
|
8263
|
+
validateExtractArgs(args);
|
|
8264
|
+
const clean = args.clean ?? "standard";
|
|
8265
|
+
const maxChars = args.maxChars ?? extractMaxChars();
|
|
8266
|
+
const timeoutMs = Math.min(args.timeoutMs ?? extractTimeoutMs(), MAX_TIMEOUT_MS);
|
|
8267
|
+
const baseUrl = args.baseUrl ?? extractBaseUrl();
|
|
8268
|
+
const model = args.model ?? extractModel();
|
|
8269
|
+
const prompt = args.prompt ?? extractPromptMode();
|
|
8270
|
+
const apiKey = extractApiKey();
|
|
8271
|
+
return withPage3(driver, args.target, async (page) => {
|
|
8272
|
+
const source = `(${buildCleanFunctionSource(args.selector, clean)})()`;
|
|
8273
|
+
const outcome = coerceCleanOutcome(await page.evaluate(source, { awaitPromise: true }), args.selector);
|
|
8274
|
+
const estTokens = guardSize(outcome, maxChars);
|
|
8275
|
+
const { content, usage, model: respondedModel, finishReason } = await callExtractionEndpoint({
|
|
8276
|
+
baseUrl,
|
|
8277
|
+
apiKey,
|
|
8278
|
+
model,
|
|
8279
|
+
html: outcome.html,
|
|
8280
|
+
schema: args.schema,
|
|
8281
|
+
timeoutMs,
|
|
8282
|
+
prompt
|
|
8283
|
+
});
|
|
8284
|
+
if (finishReason === "length") {
|
|
8285
|
+
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)}`);
|
|
8286
|
+
}
|
|
8287
|
+
let data;
|
|
8288
|
+
try {
|
|
8289
|
+
data = JSON.parse(content);
|
|
8290
|
+
} catch (e) {
|
|
8291
|
+
throw new CdpError(`extract_page: endpoint returned invalid JSON (${e.message}${finishReason !== undefined ? `, finish_reason=${finishReason}` : ""}): ${excerpt(redactSecrets(content, apiKey), 200)}`);
|
|
8292
|
+
}
|
|
8293
|
+
const result = {
|
|
8294
|
+
data,
|
|
8295
|
+
model: respondedModel,
|
|
8296
|
+
usage,
|
|
8297
|
+
html: {
|
|
8298
|
+
source: "dom",
|
|
8299
|
+
rawBytes: outcome.rawBytes,
|
|
8300
|
+
cleanBytes: outcome.cleanBytes,
|
|
8301
|
+
estTokens,
|
|
8302
|
+
dropped: outcome.dropped
|
|
8303
|
+
},
|
|
8304
|
+
target: { id: page.info.id, url: page.info.url, title: page.info.title }
|
|
8305
|
+
};
|
|
8306
|
+
if (args.savePath === undefined || args.savePath === "")
|
|
8307
|
+
return result;
|
|
8308
|
+
const path = args.savePath.startsWith("/") ? args.savePath : join10(ARTIFACT_DIR7, args.savePath);
|
|
8309
|
+
await mkdir12(dirname3(path), { recursive: true });
|
|
8310
|
+
const json = JSON.stringify(result, null, 2);
|
|
8311
|
+
await writeFile9(path, json, "utf8");
|
|
8312
|
+
const saved = {
|
|
8313
|
+
path,
|
|
8314
|
+
bytes: Buffer.byteLength(json, "utf8"),
|
|
8315
|
+
model: respondedModel,
|
|
8316
|
+
usage,
|
|
8317
|
+
target: result.target
|
|
8318
|
+
};
|
|
8319
|
+
return saved;
|
|
8320
|
+
});
|
|
8321
|
+
}
|
|
8322
|
+
|
|
7812
8323
|
// src/index.ts
|
|
7813
8324
|
init_client();
|
|
7814
8325
|
var cdpDriver = createCdpDriver();
|
|
@@ -7856,8 +8367,11 @@ var TOOLS = {
|
|
|
7856
8367
|
list_mocks: listMocks,
|
|
7857
8368
|
clear_mocks: clearMocks,
|
|
7858
8369
|
dispatch_mouse: dispatchMouse,
|
|
8370
|
+
focus_emulation: focusEmulation,
|
|
8371
|
+
click_focus_gated: clickFocusGated,
|
|
7859
8372
|
wait_for_download: waitForDownload,
|
|
7860
8373
|
grant_permissions: grantPermissions,
|
|
8374
|
+
extract_page: onCdp(extractPage),
|
|
7861
8375
|
claim_page: onCdp(LEASE_TOOLS.claim_page),
|
|
7862
8376
|
release_page: onCdp(LEASE_TOOLS.release_page),
|
|
7863
8377
|
list_leases: onCdp(LEASE_TOOLS.list_leases)
|
|
@@ -8517,6 +9031,70 @@ var MANIFEST = [
|
|
|
8517
9031
|
additionalProperties: false
|
|
8518
9032
|
}
|
|
8519
9033
|
},
|
|
9034
|
+
{
|
|
9035
|
+
name: "focus_emulation",
|
|
9036
|
+
description: "Toggle page focus emulation: the page reports focused/visible without the OS window moving; Chrome-only.",
|
|
9037
|
+
inputSchema: {
|
|
9038
|
+
type: "object",
|
|
9039
|
+
properties: {
|
|
9040
|
+
lease: {
|
|
9041
|
+
type: "string",
|
|
9042
|
+
description: "Lease token; omit for a tab this process already holds. See server instructions."
|
|
9043
|
+
},
|
|
9044
|
+
target: {
|
|
9045
|
+
type: "string",
|
|
9046
|
+
description: "Page selector (index:/url:/title:/label:/targetId, default active); grammar in server instructions."
|
|
9047
|
+
},
|
|
9048
|
+
enabled: {
|
|
9049
|
+
type: "boolean",
|
|
9050
|
+
description: "true = page reports focused; false = restore real focus state."
|
|
9051
|
+
}
|
|
9052
|
+
},
|
|
9053
|
+
required: [
|
|
9054
|
+
"enabled"
|
|
9055
|
+
],
|
|
9056
|
+
additionalProperties: false
|
|
9057
|
+
}
|
|
9058
|
+
},
|
|
9059
|
+
{
|
|
9060
|
+
name: "click_focus_gated",
|
|
9061
|
+
description: "Emulate page focus, wait for a focus-gated button to enable, trusted-click it, restore; Chrome-only.",
|
|
9062
|
+
inputSchema: {
|
|
9063
|
+
type: "object",
|
|
9064
|
+
properties: {
|
|
9065
|
+
lease: {
|
|
9066
|
+
type: "string",
|
|
9067
|
+
description: "Lease token; omit for a tab this process already holds. See server instructions."
|
|
9068
|
+
},
|
|
9069
|
+
target: {
|
|
9070
|
+
type: "string",
|
|
9071
|
+
description: "Page selector (index:/url:/title:/label:/targetId, default active); grammar in server instructions."
|
|
9072
|
+
},
|
|
9073
|
+
selector: {
|
|
9074
|
+
type: "string",
|
|
9075
|
+
description: "CSS selector of the button; exactly one of selector or text."
|
|
9076
|
+
},
|
|
9077
|
+
text: {
|
|
9078
|
+
type: "string",
|
|
9079
|
+
description: 'Visible-text match for the button (e.g. "Authorize"); exactly one of selector or text.'
|
|
9080
|
+
},
|
|
9081
|
+
timeoutMs: {
|
|
9082
|
+
type: "number",
|
|
9083
|
+
description: "Max ms to poll for the button to become enabled. Default 30000."
|
|
9084
|
+
},
|
|
9085
|
+
pollMs: {
|
|
9086
|
+
type: "number",
|
|
9087
|
+
description: "Poll interval ms. Default 500."
|
|
9088
|
+
},
|
|
9089
|
+
keepFocus: {
|
|
9090
|
+
type: "boolean",
|
|
9091
|
+
description: "Leave focus emulation on after the click (default false = restored)."
|
|
9092
|
+
}
|
|
9093
|
+
},
|
|
9094
|
+
required: [],
|
|
9095
|
+
additionalProperties: false
|
|
9096
|
+
}
|
|
9097
|
+
},
|
|
8520
9098
|
{
|
|
8521
9099
|
name: "wait_for_download",
|
|
8522
9100
|
description: "Arm or collect a browser download; arm capture before triggering the download.",
|
|
@@ -9542,6 +10120,80 @@ var MANIFEST = [
|
|
|
9542
10120
|
required: [],
|
|
9543
10121
|
additionalProperties: false
|
|
9544
10122
|
}
|
|
10123
|
+
},
|
|
10124
|
+
{
|
|
10125
|
+
name: "extract_page",
|
|
10126
|
+
description: "Extract schema-conformant JSON from the target page's cleaned HTML via an OpenAI-compatible endpoint; default endpoint is local loopback.",
|
|
10127
|
+
inputSchema: {
|
|
10128
|
+
type: "object",
|
|
10129
|
+
properties: {
|
|
10130
|
+
lease: {
|
|
10131
|
+
type: "string",
|
|
10132
|
+
description: "Lease token; omit for a tab this process already holds. See server instructions."
|
|
10133
|
+
},
|
|
10134
|
+
target: {
|
|
10135
|
+
type: "string",
|
|
10136
|
+
description: "Page selector (index:/url:/title:/label:/targetId, default active); grammar in server instructions."
|
|
10137
|
+
},
|
|
10138
|
+
schema: {
|
|
10139
|
+
type: "object",
|
|
10140
|
+
description: "JSON Schema (type object) the extracted JSON must conform to; extraction instructions live in its property descriptions."
|
|
10141
|
+
},
|
|
10142
|
+
selector: {
|
|
10143
|
+
type: "string",
|
|
10144
|
+
description: "CSS selector scoping extraction to a subtree."
|
|
10145
|
+
},
|
|
10146
|
+
source: {
|
|
10147
|
+
type: "string",
|
|
10148
|
+
enum: [
|
|
10149
|
+
"dom",
|
|
10150
|
+
"served"
|
|
10151
|
+
],
|
|
10152
|
+
description: "Page source to read; 'served' currently returns not-implemented."
|
|
10153
|
+
},
|
|
10154
|
+
clean: {
|
|
10155
|
+
type: "string",
|
|
10156
|
+
enum: [
|
|
10157
|
+
"standard",
|
|
10158
|
+
"aggressive",
|
|
10159
|
+
"none"
|
|
10160
|
+
],
|
|
10161
|
+
description: "HTML cleaning level."
|
|
10162
|
+
},
|
|
10163
|
+
maxChars: {
|
|
10164
|
+
type: "integer",
|
|
10165
|
+
description: "Character cap on the cleaned HTML; default 300000."
|
|
10166
|
+
},
|
|
10167
|
+
timeoutMs: {
|
|
10168
|
+
type: "integer",
|
|
10169
|
+
description: "Timeout in milliseconds; default 90000, max 300000."
|
|
10170
|
+
},
|
|
10171
|
+
prompt: {
|
|
10172
|
+
type: "string",
|
|
10173
|
+
enum: [
|
|
10174
|
+
"html",
|
|
10175
|
+
"schematron"
|
|
10176
|
+
],
|
|
10177
|
+
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)."
|
|
10178
|
+
},
|
|
10179
|
+
model: {
|
|
10180
|
+
type: "string",
|
|
10181
|
+
description: "Extraction model name; default 'schematron' (or CDP_EXTRACT_MODEL)."
|
|
10182
|
+
},
|
|
10183
|
+
baseUrl: {
|
|
10184
|
+
type: "string",
|
|
10185
|
+
description: "OpenAI-compatible endpoint base URL; default local loopback (or CDP_EXTRACT_BASE_URL)."
|
|
10186
|
+
},
|
|
10187
|
+
savePath: {
|
|
10188
|
+
type: "string",
|
|
10189
|
+
description: "Output file path."
|
|
10190
|
+
}
|
|
10191
|
+
},
|
|
10192
|
+
required: [
|
|
10193
|
+
"schema"
|
|
10194
|
+
],
|
|
10195
|
+
additionalProperties: false
|
|
10196
|
+
}
|
|
9545
10197
|
}
|
|
9546
10198
|
];
|
|
9547
10199
|
|
|
@@ -9553,7 +10205,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
9553
10205
|
import { existsSync as existsSync2, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9554
10206
|
import { createConnection, createServer as createServer2 } from "node:net";
|
|
9555
10207
|
import { tmpdir } from "node:os";
|
|
9556
|
-
import { join as
|
|
10208
|
+
import { join as join12 } from "node:path";
|
|
9557
10209
|
function resolveBinary() {
|
|
9558
10210
|
const candidates = [
|
|
9559
10211
|
process.env.FIREFOX_BIN,
|
|
@@ -9633,7 +10285,7 @@ var LAUNCH_USER_JS_PREFS = {
|
|
|
9633
10285
|
"dom.file.createInChild": true
|
|
9634
10286
|
};
|
|
9635
10287
|
function writeLaunchUserJs(profilePath) {
|
|
9636
|
-
const target =
|
|
10288
|
+
const target = join12(profilePath, "user.js");
|
|
9637
10289
|
const existing = existsSync2(target) ? readFileSync(target, "utf8") : "";
|
|
9638
10290
|
const present = new Set;
|
|
9639
10291
|
for (const line of existing.split(`
|
|
@@ -9655,7 +10307,7 @@ async function launchFirefox(opts = {}) {
|
|
|
9655
10307
|
const binary = resolveBinary();
|
|
9656
10308
|
const port = await pickPort(opts.port);
|
|
9657
10309
|
const isThrowaway = opts.profilePath === undefined;
|
|
9658
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
10310
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join12(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
9659
10311
|
if (isThrowaway)
|
|
9660
10312
|
writeLaunchUserJs(profilePath);
|
|
9661
10313
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
@@ -9847,7 +10499,7 @@ init_workers();
|
|
|
9847
10499
|
|
|
9848
10500
|
class BidiToolError extends Error {
|
|
9849
10501
|
}
|
|
9850
|
-
async function
|
|
10502
|
+
async function withPage4(driver, target, fn) {
|
|
9851
10503
|
const page = await driver.page(target);
|
|
9852
10504
|
try {
|
|
9853
10505
|
return await fn(page);
|
|
@@ -9891,7 +10543,7 @@ async function captureFresh(page, durationMs) {
|
|
|
9891
10543
|
}
|
|
9892
10544
|
async function listConsoleMessages2(driver, args = {}) {
|
|
9893
10545
|
refuseWorkerSelector(driver, args.target);
|
|
9894
|
-
return
|
|
10546
|
+
return withPage4(driver, args.target, async (page) => {
|
|
9895
10547
|
if (args.reload)
|
|
9896
10548
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
9897
10549
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
@@ -9900,7 +10552,7 @@ async function listConsoleMessages2(driver, args = {}) {
|
|
|
9900
10552
|
}
|
|
9901
10553
|
async function getConsoleMessage2(driver, args = {}) {
|
|
9902
10554
|
refuseWorkerSelector(driver, args.target);
|
|
9903
|
-
return
|
|
10555
|
+
return withPage4(driver, args.target, async (page) => {
|
|
9904
10556
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
9905
10557
|
const idx = args.index ?? 0;
|
|
9906
10558
|
const entry = buf[idx];
|
|
@@ -9911,7 +10563,7 @@ async function getConsoleMessage2(driver, args = {}) {
|
|
|
9911
10563
|
}
|
|
9912
10564
|
async function listNetworkRequests2(driver, args = {}) {
|
|
9913
10565
|
refuseWorkerSelector(driver, args.target);
|
|
9914
|
-
return
|
|
10566
|
+
return withPage4(driver, args.target, async (page) => {
|
|
9915
10567
|
if (args.reload)
|
|
9916
10568
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
9917
10569
|
let buf = bufferFor(networkBuffers, page.info.id);
|
|
@@ -9924,7 +10576,7 @@ async function getNetworkRequest2(driver, args = {}) {
|
|
|
9924
10576
|
refuseWorkerSelector(driver, args.target);
|
|
9925
10577
|
if (!args.requestId && !args.url)
|
|
9926
10578
|
throw new BidiToolError("get_network_request requires requestId or url");
|
|
9927
|
-
return
|
|
10579
|
+
return withPage4(driver, args.target, async (page) => {
|
|
9928
10580
|
const buf = bufferFor(networkBuffers, page.info.id);
|
|
9929
10581
|
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));
|
|
9930
10582
|
if (!found)
|
|
@@ -9936,7 +10588,7 @@ var mockSessions = new Map;
|
|
|
9936
10588
|
async function mockRequest2(driver, args) {
|
|
9937
10589
|
if (!args.urlPattern)
|
|
9938
10590
|
throw new BidiToolError("mock_request requires a { urlPattern }");
|
|
9939
|
-
return
|
|
10591
|
+
return withPage4(driver, args.target, async (page) => {
|
|
9940
10592
|
const existing = mockSessions.get(page.info.id);
|
|
9941
10593
|
const rule = {
|
|
9942
10594
|
urlPattern: args.urlPattern,
|
|
@@ -9984,7 +10636,7 @@ async function clearMocks2(driver, args = {}) {
|
|
|
9984
10636
|
mockSessions.clear();
|
|
9985
10637
|
return { cleared: n };
|
|
9986
10638
|
}
|
|
9987
|
-
return
|
|
10639
|
+
return withPage4(driver, args.target, async (page) => {
|
|
9988
10640
|
const existing = mockSessions.get(page.info.id);
|
|
9989
10641
|
if (!existing)
|
|
9990
10642
|
return { cleared: 0 };
|
|
@@ -10009,7 +10661,8 @@ var BIDI_ONLY_TOOLS = {
|
|
|
10009
10661
|
var FIREFOX_TOOLS = {
|
|
10010
10662
|
...SHARED_TOOLS,
|
|
10011
10663
|
...BIDI_ONLY_TOOLS,
|
|
10012
|
-
...LEASE_TOOLS
|
|
10664
|
+
...LEASE_TOOLS,
|
|
10665
|
+
extract_page: extractPage
|
|
10013
10666
|
};
|
|
10014
10667
|
|
|
10015
10668
|
// src/mcp.ts
|
|
@@ -10029,7 +10682,8 @@ var TOOL_GROUPS = [
|
|
|
10029
10682
|
"leases",
|
|
10030
10683
|
"permissions",
|
|
10031
10684
|
"dialogs",
|
|
10032
|
-
"downloads"
|
|
10685
|
+
"downloads",
|
|
10686
|
+
"extraction"
|
|
10033
10687
|
];
|
|
10034
10688
|
var GROUP_TOOLS = {
|
|
10035
10689
|
core: [
|
|
@@ -10053,7 +10707,9 @@ var GROUP_TOOLS = {
|
|
|
10053
10707
|
"dispatch_mouse",
|
|
10054
10708
|
"press_key",
|
|
10055
10709
|
"fill_form",
|
|
10056
|
-
"upload_file"
|
|
10710
|
+
"upload_file",
|
|
10711
|
+
"focus_emulation",
|
|
10712
|
+
"click_focus_gated"
|
|
10057
10713
|
],
|
|
10058
10714
|
cookies: ["list_cookies", "set_cookie", "delete_cookies"],
|
|
10059
10715
|
network: ["list_network_requests", "get_network_request"],
|
|
@@ -10072,7 +10728,8 @@ var GROUP_TOOLS = {
|
|
|
10072
10728
|
leases: ["claim_page", "release_page", "list_leases"],
|
|
10073
10729
|
permissions: ["grant_permissions"],
|
|
10074
10730
|
dialogs: ["handle_dialog"],
|
|
10075
|
-
downloads: ["wait_for_download"]
|
|
10731
|
+
downloads: ["wait_for_download"],
|
|
10732
|
+
extraction: ["extract_page"]
|
|
10076
10733
|
};
|
|
10077
10734
|
var TOOL_GROUP = {};
|
|
10078
10735
|
for (const group of TOOL_GROUPS) {
|
|
@@ -10278,6 +10935,26 @@ var TOOL_DOCS = {
|
|
|
10278
10935
|
modifiers: "Modifier keys held for this one event, e.g. ['Shift']."
|
|
10279
10936
|
}
|
|
10280
10937
|
},
|
|
10938
|
+
focus_emulation: {
|
|
10939
|
+
description: "Toggles CDP Emulation.setFocusEmulationEnabled on a tab: the page then BELIEVES it is focused (document.hasFocus()→true, visibilitychange stays 'visible', focus events fire) while the real OS window and active tab stay put — no focus theft. This is the primitive that unlocks focus-GATED UI (a button the site disables unless its tab is focused, e.g. Anthropic's Claude Code OAuth Authorize button on claude.ai/oauth/*) on a background tab. Chrome-only: WebDriver BiDi's emulation module has no focus primitive, so this tool is absent from tools/list under --browser firefox. Ports the core mechanism of the danbuhler/claude-code-auto-authorize extension. Remember to call with enabled:false to restore, or use click_focus_gated which restores automatically.",
|
|
10940
|
+
params: {
|
|
10941
|
+
lease: "Lease token from claim_page. Omit for a tab this process holds; required for a tab held by another process. Auto-acquired under CDP_REQUIRE_LEASE (then pass target, not lease). See server instructions.",
|
|
10942
|
+
target: "Page selector: 'active' (default), 'index:N', 'url:<substr>', 'title:<substr>', 'label:<name>', or a 32-hex '<targetId>'.",
|
|
10943
|
+
enabled: "true = page reports focused/visible; false = restore the real focus state."
|
|
10944
|
+
}
|
|
10945
|
+
},
|
|
10946
|
+
click_focus_gated: {
|
|
10947
|
+
description: "One-call compose of focus emulation + trusted click for a focus-gated button: enables Emulation.setFocusEmulationEnabled, polls until the button (located by CSS selector OR exact visible-text match, e.g. 'Authorize') exists AND is enabled, dispatches a trusted move/press/release click via Input.dispatchMouseEvent (isTrusted:true — a content script's synthesized events can never satisfy a focus gate), then restores focus emulation off unless keepFocus. Built for the Claude Code OAuth Authorize button but works on any focus-gated control on a background tab. Throws with a diagnostic ('never located' vs 'found but stayed disabled') on timeout. Chrome-only; absent under --browser firefox.",
|
|
10948
|
+
params: {
|
|
10949
|
+
lease: "Lease token from claim_page. Omit for a tab this process holds. Auto-acquired under CDP_REQUIRE_LEASE (then pass target, not lease).",
|
|
10950
|
+
target: "Page selector: 'active' (default), 'index:N', 'url:<substr>', 'title:<substr>', 'label:<name>', or a 32-hex '<targetId>'.",
|
|
10951
|
+
selector: "CSS selector of the button. Give exactly one of selector or text.",
|
|
10952
|
+
text: "Exact visible-text match of the button (trims whitespace, matches button/[role=button]/submit inputs), e.g. 'Authorize'. Give exactly one of selector or text.",
|
|
10953
|
+
timeoutMs: "Max ms to poll for the button to appear and become enabled. Default 30000.",
|
|
10954
|
+
pollMs: "Poll interval in ms. Default 500.",
|
|
10955
|
+
keepFocus: "Leave focus emulation ON after the click (default false — restored even on throw, so a failed click never strands the page fake-focused)."
|
|
10956
|
+
}
|
|
10957
|
+
},
|
|
10281
10958
|
wait_for_download: {
|
|
10282
10959
|
description: "Wait for a file download to finish and return it as a real file on disk: {path,suggestedFilename,bytes,url,target}, written under the artifact dir's downloads/. ORDERING RULE, not optional: capture must be ARMED BEFORE the click that starts the download — call wait_for_download{arm:true} first (returns {armed:true,downloadPath,pending}), then click, then call again to collect the file. The download-behavior override is per-connection state Chrome REVERTS the moment the arming client disconnects, and an unarmed headless Chrome denies the download outright, so one triggered before arming is lost. SIDE EFFECT, browser-global: arming redirects EVERY download in this browser (all tabs, all origins) into the toolkit's downloads dir until this server exits — they no longer land in the user's normal Downloads folder. The arm lives on a connection this server holds open (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 Firefox.",
|
|
10283
10960
|
params: {
|
|
@@ -10569,11 +11246,28 @@ var TOOL_DOCS = {
|
|
|
10569
11246
|
list_leases: {
|
|
10570
11247
|
description: "Enumerate every active lease for diagnosis - backend, target id, label, pid, createdAt, lastUsedAt, ttlMs, whether the owning process is alive, and whether the lease is reclaimable and why. No token required; it never returns the lease nonce, so it cannot be used to forge one. An unreadable or unparseable lease row instead carries unreadable, with label, pid and timestamps zeroed and stale forced false (an unreadable lease must never read as free to take). Under CDP_REQUIRE_LEASE it reaps abandoned agent tabs exactly as list_pages does, into an additive reaped array, omitting their rows. Where the tab can answer, a row also carries humanActiveMs, ms since input this server did not dispatch - note lastUsedAt tracks TOOLKIT calls only, so a tab a person has typed in for ten minutes looks idle, which is what this exposes; the field is absent, never null, when the tab cannot answer, and that absence means 'no answer', never 'nobody is there'. idleMs (now - lastUsedAt) and expiresAt (lastUsedAt + ttlMs) are computed fresh each call and omitted on an unreadable row.",
|
|
10571
11248
|
params: {}
|
|
11249
|
+
},
|
|
11250
|
+
extract_page: {
|
|
11251
|
+
description: "Structured extraction: take the target page's HTML, clean it, and send it to an OpenAI-compatible /chat/completions endpoint along with your JSON Schema, getting back schema-conformant JSON instead of prose. THE DEFAULT ENDPOINT IS LOOPBACK - llm-ferry at http://127.0.0.1:8090/v1 with model 'schematron' - so page content stays ON THIS MACHINE unless the operator overrides baseUrl (or sets CDP_EXTRACT_BASE_URL). Configuration: CDP_EXTRACT_BASE_URL (endpoint override), CDP_EXTRACT_MODEL (model override), CDP_EXTRACT_API_KEY (bearer token; never logged and REDACTED from every error), CDP_EXTRACT_TIMEOUT_MS, CDP_EXTRACT_MAX_CHARS, CDP_EXTRACT_PROMPT. PROMPT SHAPE: by default only the cleaned HTML is sent and the schema rides in response_format, which is what the HOSTED Schematron API expects (it injects the schema server-side); set prompt:'schematron' when baseUrl points at a LOCALLY SERVED open-weight Schematron (e.g. an MLX schematron8B), because that model was fine-tuned on a prompt with the schema INSIDE the user message and an HTML-only prompt returns garbage from it even under constrained decoding. THE SCHEMA-DESCRIPTION RULE: this is prompt-driven extraction, and the extraction INSTRUCTIONS live in the schema's property descriptions - write them like prompts ('the job title, exactly as printed', 'ISO date, not 'yesterday'). A schema whose properties carry no descriptions is REJECTED up front with an error saying so, because it would produce garbage extraction with no hint why. Source: 'dom' (live DOM, the rendered, JS-executed page) is the default; 'served' (the raw server response before rendering) currently returns a not-implemented error rather than guessing. Size guard: cleaned HTML over maxChars (default 300000) fails with html_too_large naming the actual size - shrink the payload with 'selector' to scope to a subtree, clean:'aggressive' to strip harder (standard already drops scripts/styles/hidden), or a larger maxChars if you truly need it all. Endpoint failures surface as upstream_error (with status and body, API key redacted) or timeout; the response never contains the key. A completion the endpoint cut off at its 8192-token output cap fails naming finish_reason=length and the fix (narrow with 'selector', or ask the schema for less) - it is NOT reported as invalid JSON, because a truncated tail is a size problem, not a model one. COST IS ALWAYS VISIBLE: every successful call reports the endpoint's usage token counts inline, so a slow/bloated extraction is visible in the answer itself, not discovered on a bill later.",
|
|
11252
|
+
params: {
|
|
11253
|
+
lease: "Lease token from claim_page. Omit for a tab this process holds; required for a tab held by another process or one claimed explicitly. Auto-acquired under CDP_REQUIRE_LEASE (then pass target, not lease). See server instructions.",
|
|
11254
|
+
target: "Page selector: 'active' (default), 'index:N', 'url:<substr>', 'title:<substr>', 'label:<name>', or a 32-hex '<targetId>'. Grammar: see server instructions.",
|
|
11255
|
+
schema: "REQUIRED JSON Schema object (type object) describing the JSON you want back. This is both the contract AND the prompt: each property's 'description' tells the extractor what to put there, so make them specific instructions. A schema with no property descriptions at all is rejected before any page work happens - empty descriptions extract garbage and the error says exactly what to fix. Nested objects and arrays of objects work; their inner descriptions count too.",
|
|
11256
|
+
selector: "CSS selector scoping extraction to that subtree's HTML only (pierces open shadow roots). The cheapest fix for html_too_large: extract the article or table, not the whole page. Omit to send the full cleaned document.",
|
|
11257
|
+
source: "'dom' (default) reads the live, rendered DOM - what the page shows after JS ran. 'served' would read the raw HTTP response before rendering; it currently returns a not-implemented error naming the arm, so callers can detect and fall back rather than guessing.",
|
|
11258
|
+
clean: "HTML cleaning level before the payload is measured and sent. 'standard' (default) strips scripts, styles, hidden elements and other non-content noise. 'aggressive' additionally collapses whitespace, drops nav/footer/boilerplate - use when html_too_large bites or the extractor chases junk. 'none' sends the raw HTML; almost never what you want.",
|
|
11259
|
+
maxChars: "Character cap on the cleaned HTML payload. Default 300000 (also settable via CDP_EXTRACT_MAX_CHARS). Exceeding it fails with html_too_large reporting actual vs cap - it does NOT silently truncate, because a half page extracted as though it were whole is a wrong answer that looks right.",
|
|
11260
|
+
timeoutMs: "Total budget in ms for the endpoint call. Default 90000, hard max 300000 (CDP_EXTRACT_TIMEOUT_MS overrides the default). Expiry fails with a timeout error, never a hang.",
|
|
11261
|
+
prompt: "Prompt shape sent upstream. 'html' (default, also CDP_EXTRACT_PROMPT=html) sends the cleaned HTML as the only user message and lets response_format carry the schema - correct for the hosted Schematron API, which injects the schema server-side. 'schematron' sends the open-weight Schematron model card's own messages: a 'You are a helpful assistant' system message plus a user message with the compact JSON-stringified schema inline, then the HTML, then 'MAKE SURE ITS VALID JSON.' - use it when baseUrl points at a locally served open-weight Schematron. It adds roughly the stringified schema's length to the prompt (maxChars caps the HTML only); response_format is sent in both modes. The arg wins over CDP_EXTRACT_PROMPT, and any other value is rejected up front.",
|
|
11262
|
+
model: "Model name for the endpoint call. Default 'schematron' on the loopback llm-ferry; override per-call or via CDP_EXTRACT_MODEL.",
|
|
11263
|
+
baseUrl: "OpenAI-compatible endpoint base URL (the server appends /chat/completions). DEFAULT IS http://127.0.0.1:8090/v1 - loopback llm-ferry, page content never leaves the machine. Setting this (or CDP_EXTRACT_BASE_URL) to a remote host is an operator decision about where page content may travel; the response never echoes the URL with credentials.",
|
|
11264
|
+
savePath: "Write the extracted JSON here instead of returning it inline: the response then carries the file path and usage counts only, keeping large payloads out of the transcript. An absolute path (starting with /) is used as-is; a relative path resolves under the artifact dir (/tmp/cdp-toolkit); missing parents are created."
|
|
11265
|
+
}
|
|
10572
11266
|
}
|
|
10573
11267
|
};
|
|
10574
11268
|
|
|
10575
11269
|
// src/version.ts
|
|
10576
|
-
var VERSION = "2.
|
|
11270
|
+
var VERSION = "2.5.0";
|
|
10577
11271
|
|
|
10578
11272
|
// src/mcp.ts
|
|
10579
11273
|
var { kind: BROWSER, endpoint: FIREFOX_ENDPOINT } = resolveBackend(process.argv.slice(2));
|