cdp-toolkit 2.3.0 → 2.5.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +42 -16
- package/dist/cli.js +537 -42
- package/dist/index.js +420 -0
- package/dist/mcp.js +543 -29
- package/package.json +4 -3
- package/src/firefox-tools.ts +6 -3
- package/src/index.ts +10 -3
- package/src/manifest.ts +74 -0
- package/src/toolDocs.ts +17 -0
- package/src/toolGroups.ts +2 -1
- package/src/tools/extract.ts +803 -0
- package/src/version.ts +1 -1
package/dist/cli.js
CHANGED
|
@@ -1129,7 +1129,7 @@ class BidiConnection {
|
|
|
1129
1129
|
return new Promise((resolve, reject) => {
|
|
1130
1130
|
const ws = new WebSocket(this.wsUrl);
|
|
1131
1131
|
this.ws = ws;
|
|
1132
|
-
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);
|
|
1133
1133
|
ws.onopen = () => {
|
|
1134
1134
|
clearTimeout(openTimer);
|
|
1135
1135
|
resolve(this);
|
|
@@ -1189,7 +1189,7 @@ class BidiConnection {
|
|
|
1189
1189
|
if (this.closed || !this.ws)
|
|
1190
1190
|
return Promise.reject(new BidiError("connection not open"));
|
|
1191
1191
|
const id = ++this.nextId;
|
|
1192
|
-
const timeoutMs = opts.timeoutMs ?? this.opts.timeoutMs ??
|
|
1192
|
+
const timeoutMs = opts.timeoutMs ?? this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3;
|
|
1193
1193
|
const payload = { id, method, params };
|
|
1194
1194
|
return new Promise((resolve, reject) => {
|
|
1195
1195
|
const start = Date.now();
|
|
@@ -1236,7 +1236,7 @@ class BidiConnection {
|
|
|
1236
1236
|
}
|
|
1237
1237
|
};
|
|
1238
1238
|
}
|
|
1239
|
-
waitFor(event, predicate, timeoutMs = this.opts.timeoutMs ??
|
|
1239
|
+
waitFor(event, predicate, timeoutMs = this.opts.timeoutMs ?? DEFAULT_TIMEOUT_MS3) {
|
|
1240
1240
|
return new Promise((resolve, reject) => {
|
|
1241
1241
|
const timer = setTimeout(() => {
|
|
1242
1242
|
off();
|
|
@@ -1280,9 +1280,9 @@ async function connectBidiSessionUrl(wsUrl, opts = {}) {
|
|
|
1280
1280
|
conn.retain();
|
|
1281
1281
|
return conn;
|
|
1282
1282
|
}
|
|
1283
|
-
var
|
|
1283
|
+
var DEFAULT_TIMEOUT_MS3, BidiError;
|
|
1284
1284
|
var init_client2 = __esm(() => {
|
|
1285
|
-
|
|
1285
|
+
DEFAULT_TIMEOUT_MS3 = Number(process.env.BIDI_TIMEOUT_MS ?? 15000);
|
|
1286
1286
|
BidiError = class BidiError extends Error {
|
|
1287
1287
|
code;
|
|
1288
1288
|
data;
|
|
@@ -1296,16 +1296,16 @@ var init_client2 = __esm(() => {
|
|
|
1296
1296
|
});
|
|
1297
1297
|
|
|
1298
1298
|
// src/bidi/session-lease.ts
|
|
1299
|
-
import { mkdir as
|
|
1300
|
-
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";
|
|
1301
1301
|
function safeEndpoint(endpoint) {
|
|
1302
1302
|
return endpoint.replace(/[^A-Za-z0-9._-]/g, "_");
|
|
1303
1303
|
}
|
|
1304
1304
|
function sessionSlotFile(endpoint) {
|
|
1305
|
-
return
|
|
1305
|
+
return join11(leaseDir(), `ff-session-${safeEndpoint(endpoint)}.json`);
|
|
1306
1306
|
}
|
|
1307
1307
|
function muxDaemonLogFile(endpoint) {
|
|
1308
|
-
return
|
|
1308
|
+
return join11(leaseDir(), `ff-mux-${safeEndpoint(endpoint)}.log`);
|
|
1309
1309
|
}
|
|
1310
1310
|
function sessionWaitMs() {
|
|
1311
1311
|
const raw = Number(process.env.CDP_FIREFOX_SESSION_WAIT_MS);
|
|
@@ -1343,7 +1343,7 @@ async function acquireSessionSlot(endpoint, opts) {
|
|
|
1343
1343
|
record.marionettePort = opts.marionettePort;
|
|
1344
1344
|
const handle = { endpoint, pid: process.pid, createdAt };
|
|
1345
1345
|
const payload = JSON.stringify(record);
|
|
1346
|
-
await
|
|
1346
|
+
await mkdir13(leaseDir(), { recursive: true });
|
|
1347
1347
|
const deadline = Date.now() + waitMs;
|
|
1348
1348
|
const maxAttempts = Math.max(100, Math.ceil(waitMs / pollMs) + 100);
|
|
1349
1349
|
let attempts = 0;
|
|
@@ -1353,7 +1353,7 @@ async function acquireSessionSlot(endpoint, opts) {
|
|
|
1353
1353
|
throw new SessionSlotBusyError(endpoint, lastLiveHolder);
|
|
1354
1354
|
attempts++;
|
|
1355
1355
|
try {
|
|
1356
|
-
await
|
|
1356
|
+
await writeFile10(file, payload, { flag: "wx" });
|
|
1357
1357
|
return { handle };
|
|
1358
1358
|
} catch (err) {
|
|
1359
1359
|
if (err.code !== "EEXIST")
|
|
@@ -1367,7 +1367,7 @@ async function acquireSessionSlot(endpoint, opts) {
|
|
|
1367
1367
|
return;
|
|
1368
1368
|
});
|
|
1369
1369
|
try {
|
|
1370
|
-
await
|
|
1370
|
+
await writeFile10(file, payload, { flag: "wx" });
|
|
1371
1371
|
return { handle, staleHolder: holder };
|
|
1372
1372
|
} catch (err) {
|
|
1373
1373
|
if (err.code !== "EEXIST")
|
|
@@ -1395,7 +1395,7 @@ async function advertiseMux(handle, muxEndpoint) {
|
|
|
1395
1395
|
const tmp = `${file}.tmp-${process.pid}`;
|
|
1396
1396
|
const next = { ...current, muxEndpoint };
|
|
1397
1397
|
try {
|
|
1398
|
-
await
|
|
1398
|
+
await writeFile10(tmp, JSON.stringify(next), "utf8");
|
|
1399
1399
|
await rename3(tmp, file);
|
|
1400
1400
|
} catch {
|
|
1401
1401
|
await unlink5(tmp).catch(() => {
|
|
@@ -1969,7 +1969,7 @@ function isSlotFreedReply(frame) {
|
|
|
1969
1969
|
return false;
|
|
1970
1970
|
}
|
|
1971
1971
|
function forceClearOrphanSession(marionettePort, opts = {}) {
|
|
1972
|
-
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;
|
|
1973
1973
|
return new Promise((resolve) => {
|
|
1974
1974
|
if (!Number.isFinite(marionettePort) || marionettePort <= 0 || marionettePort > 65535) {
|
|
1975
1975
|
resolve(false);
|
|
@@ -2025,7 +2025,7 @@ function forceClearOrphanSession(marionettePort, opts = {}) {
|
|
|
2025
2025
|
}
|
|
2026
2026
|
});
|
|
2027
2027
|
}
|
|
2028
|
-
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;
|
|
2029
2029
|
var init_marionette = __esm(() => {
|
|
2030
2030
|
DELETE_SESSION_FRAME = encodeFrame2([0, 1, "WebDriver:DeleteSession", {}]);
|
|
2031
2031
|
MarionetteProtocolError = class MarionetteProtocolError extends Error {
|
|
@@ -7901,6 +7901,425 @@ async function grantPermissions(args = {}) {
|
|
|
7901
7901
|
return { granted: [...permissions], origin, ...reset ? { reset: true } : {}, target };
|
|
7902
7902
|
}
|
|
7903
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 promptMode = opts.prompt ?? "html";
|
|
8191
|
+
const body = JSON.stringify({
|
|
8192
|
+
model: opts.model,
|
|
8193
|
+
messages: buildExtractionMessages(promptMode, opts.html, opts.schema),
|
|
8194
|
+
temperature: 0,
|
|
8195
|
+
max_tokens: COMPLESION_TOKENS,
|
|
8196
|
+
...promptMode === "schematron" ? {} : { response_format: { type: "json_schema", json_schema: { name: "extract", strict: true, schema: opts.schema } } }
|
|
8197
|
+
});
|
|
8198
|
+
const headers = { "content-type": "application/json" };
|
|
8199
|
+
if (opts.apiKey)
|
|
8200
|
+
headers.authorization = `Bearer ${opts.apiKey}`;
|
|
8201
|
+
let lastStatus = 0;
|
|
8202
|
+
let lastBody = "";
|
|
8203
|
+
for (let attempt = 1;attempt <= 2; attempt++) {
|
|
8204
|
+
let res;
|
|
8205
|
+
try {
|
|
8206
|
+
res = await fetch(url, { method: "POST", headers, body, signal: AbortSignal.timeout(opts.timeoutMs) });
|
|
8207
|
+
} catch (e) {
|
|
8208
|
+
const err = e;
|
|
8209
|
+
if (err.name === "TimeoutError" || err.name === "AbortError") {
|
|
8210
|
+
throw new CdpError(`extract_page: upstream call to ${opts.baseUrl} timed out after ${opts.timeoutMs}ms`);
|
|
8211
|
+
}
|
|
8212
|
+
throw new CdpError(`extract_page: could not reach ${opts.baseUrl} (${redactSecrets(err.message, opts.apiKey)}) — is ferry running?`);
|
|
8213
|
+
}
|
|
8214
|
+
const text = await res.text();
|
|
8215
|
+
if (res.ok) {
|
|
8216
|
+
let parsed;
|
|
8217
|
+
try {
|
|
8218
|
+
parsed = JSON.parse(text);
|
|
8219
|
+
} catch {
|
|
8220
|
+
throw new CdpError(`extract_page: endpoint returned a non-JSON 200 body: ${excerpt(redactSecrets(text, opts.apiKey), 200)}`);
|
|
8221
|
+
}
|
|
8222
|
+
if (parsed.error) {
|
|
8223
|
+
throw new CdpError(`extract_page: upstream_error from ${opts.baseUrl}: ${excerpt(redactSecrets(String(parsed.error.message ?? JSON.stringify(parsed.error)), opts.apiKey), 600)}`);
|
|
8224
|
+
}
|
|
8225
|
+
const content = parsed.choices?.[0]?.message?.content;
|
|
8226
|
+
if (typeof content !== "string" || content.length === 0) {
|
|
8227
|
+
throw new CdpError(`extract_page: endpoint response carried no message content: ${excerpt(redactSecrets(text, opts.apiKey), 200)}`);
|
|
8228
|
+
}
|
|
8229
|
+
const u = parsed.usage ?? {};
|
|
8230
|
+
const usage = {
|
|
8231
|
+
promptTokens: u.prompt_tokens ?? 0,
|
|
8232
|
+
completionTokens: u.completion_tokens ?? 0,
|
|
8233
|
+
totalTokens: u.total_tokens ?? 0,
|
|
8234
|
+
...pickCostUsd(u) !== undefined ? { costUsd: pickCostUsd(u) } : {}
|
|
8235
|
+
};
|
|
8236
|
+
const fr = parsed.choices?.[0]?.finish_reason;
|
|
8237
|
+
const finishReason = typeof fr === "string" ? fr : undefined;
|
|
8238
|
+
return { content, usage, model: parsed.model ?? opts.model, finishReason };
|
|
8239
|
+
}
|
|
8240
|
+
lastStatus = res.status;
|
|
8241
|
+
lastBody = text;
|
|
8242
|
+
if (res.status === 429) {
|
|
8243
|
+
const retryAfter = res.headers.get("retry-after");
|
|
8244
|
+
throw new CdpError(`extract_page: upstream rate-limited (HTTP 429) from ${opts.baseUrl}${retryAfter !== null ? `, retry-after: ${retryAfter}` : " (no retry-after header)"}`);
|
|
8245
|
+
}
|
|
8246
|
+
if (res.status < 500 || res.status >= 600)
|
|
8247
|
+
break;
|
|
8248
|
+
}
|
|
8249
|
+
throw new CdpError(`extract_page: upstream_error HTTP ${lastStatus} from POST ${url}: ${excerpt(redactSecrets(lastBody, opts.apiKey), 600)}`);
|
|
8250
|
+
}
|
|
8251
|
+
async function withPage3(driver, target, fn) {
|
|
8252
|
+
const page = await driver.page(target);
|
|
8253
|
+
try {
|
|
8254
|
+
return await fn(page);
|
|
8255
|
+
} finally {
|
|
8256
|
+
await page.release();
|
|
8257
|
+
}
|
|
8258
|
+
}
|
|
8259
|
+
async function extractPage(driver, args) {
|
|
8260
|
+
validateExtractArgs(args);
|
|
8261
|
+
const clean = args.clean ?? "standard";
|
|
8262
|
+
const maxChars = args.maxChars ?? extractMaxChars();
|
|
8263
|
+
const timeoutMs = Math.min(args.timeoutMs ?? extractTimeoutMs(), MAX_TIMEOUT_MS);
|
|
8264
|
+
const baseUrl = args.baseUrl ?? extractBaseUrl();
|
|
8265
|
+
const model = args.model ?? extractModel();
|
|
8266
|
+
const prompt = args.prompt ?? extractPromptMode();
|
|
8267
|
+
const apiKey = extractApiKey();
|
|
8268
|
+
return withPage3(driver, args.target, async (page) => {
|
|
8269
|
+
const source = `(${buildCleanFunctionSource(args.selector, clean)})()`;
|
|
8270
|
+
const outcome = coerceCleanOutcome(await page.evaluate(source, { awaitPromise: true }), args.selector);
|
|
8271
|
+
const estTokens = guardSize(outcome, maxChars);
|
|
8272
|
+
const { content, usage, model: respondedModel, finishReason } = await callExtractionEndpoint({
|
|
8273
|
+
baseUrl,
|
|
8274
|
+
apiKey,
|
|
8275
|
+
model,
|
|
8276
|
+
html: outcome.html,
|
|
8277
|
+
schema: args.schema,
|
|
8278
|
+
timeoutMs,
|
|
8279
|
+
prompt
|
|
8280
|
+
});
|
|
8281
|
+
if (finishReason === "length") {
|
|
8282
|
+
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)}`);
|
|
8283
|
+
}
|
|
8284
|
+
let data;
|
|
8285
|
+
try {
|
|
8286
|
+
data = JSON.parse(content);
|
|
8287
|
+
} catch (e) {
|
|
8288
|
+
throw new CdpError(`extract_page: endpoint returned invalid JSON (${e.message}${finishReason !== undefined ? `, finish_reason=${finishReason}` : ""}): ${excerpt(redactSecrets(content, apiKey), 200)}`);
|
|
8289
|
+
}
|
|
8290
|
+
if (data === null || typeof data !== "object" || Array.isArray(data)) {
|
|
8291
|
+
throw new CdpError(`extract_page: endpoint returned invalid JSON (expected a JSON object${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
|
+
|
|
7904
8323
|
// src/index.ts
|
|
7905
8324
|
init_client();
|
|
7906
8325
|
var cdpDriver = createCdpDriver();
|
|
@@ -7952,6 +8371,7 @@ var TOOLS = {
|
|
|
7952
8371
|
click_focus_gated: clickFocusGated,
|
|
7953
8372
|
wait_for_download: waitForDownload,
|
|
7954
8373
|
grant_permissions: grantPermissions,
|
|
8374
|
+
extract_page: onCdp(extractPage),
|
|
7955
8375
|
claim_page: onCdp(LEASE_TOOLS.claim_page),
|
|
7956
8376
|
release_page: onCdp(LEASE_TOOLS.release_page),
|
|
7957
8377
|
list_leases: onCdp(LEASE_TOOLS.list_leases)
|
|
@@ -7966,7 +8386,7 @@ import { spawn as spawn4 } from "node:child_process";
|
|
|
7966
8386
|
import { existsSync as existsSync2, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
7967
8387
|
import { createConnection, createServer as createServer2 } from "node:net";
|
|
7968
8388
|
import { tmpdir } from "node:os";
|
|
7969
|
-
import { join as
|
|
8389
|
+
import { join as join12 } from "node:path";
|
|
7970
8390
|
function resolveBinary() {
|
|
7971
8391
|
const candidates = [
|
|
7972
8392
|
process.env.FIREFOX_BIN,
|
|
@@ -8046,7 +8466,7 @@ var LAUNCH_USER_JS_PREFS = {
|
|
|
8046
8466
|
"dom.file.createInChild": true
|
|
8047
8467
|
};
|
|
8048
8468
|
function writeLaunchUserJs(profilePath) {
|
|
8049
|
-
const target =
|
|
8469
|
+
const target = join12(profilePath, "user.js");
|
|
8050
8470
|
const existing = existsSync2(target) ? readFileSync(target, "utf8") : "";
|
|
8051
8471
|
const present = new Set;
|
|
8052
8472
|
for (const line of existing.split(`
|
|
@@ -8068,7 +8488,7 @@ async function launchFirefox(opts = {}) {
|
|
|
8068
8488
|
const binary = resolveBinary();
|
|
8069
8489
|
const port = await pickPort(opts.port);
|
|
8070
8490
|
const isThrowaway = opts.profilePath === undefined;
|
|
8071
|
-
const profilePath = opts.profilePath ?? mkdtempSync(
|
|
8491
|
+
const profilePath = opts.profilePath ?? mkdtempSync(join12(tmpdir(), "cdp-toolkit-ff-profile-"));
|
|
8072
8492
|
if (isThrowaway)
|
|
8073
8493
|
writeLaunchUserJs(profilePath);
|
|
8074
8494
|
const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
|
|
@@ -8260,7 +8680,7 @@ init_workers();
|
|
|
8260
8680
|
|
|
8261
8681
|
class BidiToolError extends Error {
|
|
8262
8682
|
}
|
|
8263
|
-
async function
|
|
8683
|
+
async function withPage4(driver, target, fn) {
|
|
8264
8684
|
const page = await driver.page(target);
|
|
8265
8685
|
try {
|
|
8266
8686
|
return await fn(page);
|
|
@@ -8304,7 +8724,7 @@ async function captureFresh(page, durationMs) {
|
|
|
8304
8724
|
}
|
|
8305
8725
|
async function listConsoleMessages2(driver, args = {}) {
|
|
8306
8726
|
refuseWorkerSelector(driver, args.target);
|
|
8307
|
-
return
|
|
8727
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8308
8728
|
if (args.reload)
|
|
8309
8729
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
8310
8730
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
@@ -8313,7 +8733,7 @@ async function listConsoleMessages2(driver, args = {}) {
|
|
|
8313
8733
|
}
|
|
8314
8734
|
async function getConsoleMessage2(driver, args = {}) {
|
|
8315
8735
|
refuseWorkerSelector(driver, args.target);
|
|
8316
|
-
return
|
|
8736
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8317
8737
|
const buf = bufferFor(consoleBuffers, page.info.id);
|
|
8318
8738
|
const idx = args.index ?? 0;
|
|
8319
8739
|
const entry = buf[idx];
|
|
@@ -8324,7 +8744,7 @@ async function getConsoleMessage2(driver, args = {}) {
|
|
|
8324
8744
|
}
|
|
8325
8745
|
async function listNetworkRequests2(driver, args = {}) {
|
|
8326
8746
|
refuseWorkerSelector(driver, args.target);
|
|
8327
|
-
return
|
|
8747
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8328
8748
|
if (args.reload)
|
|
8329
8749
|
await captureFresh(page, args.durationMs ?? 2500);
|
|
8330
8750
|
let buf = bufferFor(networkBuffers, page.info.id);
|
|
@@ -8337,7 +8757,7 @@ async function getNetworkRequest2(driver, args = {}) {
|
|
|
8337
8757
|
refuseWorkerSelector(driver, args.target);
|
|
8338
8758
|
if (!args.requestId && !args.url)
|
|
8339
8759
|
throw new BidiToolError("get_network_request requires requestId or url");
|
|
8340
|
-
return
|
|
8760
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8341
8761
|
const buf = bufferFor(networkBuffers, page.info.id);
|
|
8342
8762
|
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));
|
|
8343
8763
|
if (!found)
|
|
@@ -8349,7 +8769,7 @@ var mockSessions = new Map;
|
|
|
8349
8769
|
async function mockRequest2(driver, args) {
|
|
8350
8770
|
if (!args.urlPattern)
|
|
8351
8771
|
throw new BidiToolError("mock_request requires a { urlPattern }");
|
|
8352
|
-
return
|
|
8772
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8353
8773
|
const existing = mockSessions.get(page.info.id);
|
|
8354
8774
|
const rule = {
|
|
8355
8775
|
urlPattern: args.urlPattern,
|
|
@@ -8397,7 +8817,7 @@ async function clearMocks2(driver, args = {}) {
|
|
|
8397
8817
|
mockSessions.clear();
|
|
8398
8818
|
return { cleared: n };
|
|
8399
8819
|
}
|
|
8400
|
-
return
|
|
8820
|
+
return withPage4(driver, args.target, async (page) => {
|
|
8401
8821
|
const existing = mockSessions.get(page.info.id);
|
|
8402
8822
|
if (!existing)
|
|
8403
8823
|
return { cleared: 0 };
|
|
@@ -8422,7 +8842,8 @@ var BIDI_ONLY_TOOLS = {
|
|
|
8422
8842
|
var FIREFOX_TOOLS = {
|
|
8423
8843
|
...SHARED_TOOLS,
|
|
8424
8844
|
...BIDI_ONLY_TOOLS,
|
|
8425
|
-
...LEASE_TOOLS
|
|
8845
|
+
...LEASE_TOOLS,
|
|
8846
|
+
extract_page: extractPage
|
|
8426
8847
|
};
|
|
8427
8848
|
|
|
8428
8849
|
// src/cli.ts
|
|
@@ -10170,19 +10591,93 @@ var MANIFEST = [
|
|
|
10170
10591
|
required: [],
|
|
10171
10592
|
additionalProperties: false
|
|
10172
10593
|
}
|
|
10594
|
+
},
|
|
10595
|
+
{
|
|
10596
|
+
name: "extract_page",
|
|
10597
|
+
description: "Extract schema-conformant JSON from the target page's cleaned HTML via an OpenAI-compatible endpoint; default endpoint is local loopback.",
|
|
10598
|
+
inputSchema: {
|
|
10599
|
+
type: "object",
|
|
10600
|
+
properties: {
|
|
10601
|
+
lease: {
|
|
10602
|
+
type: "string",
|
|
10603
|
+
description: "Lease token; omit for a tab this process already holds. See server instructions."
|
|
10604
|
+
},
|
|
10605
|
+
target: {
|
|
10606
|
+
type: "string",
|
|
10607
|
+
description: "Page selector (index:/url:/title:/label:/targetId, default active); grammar in server instructions."
|
|
10608
|
+
},
|
|
10609
|
+
schema: {
|
|
10610
|
+
type: "object",
|
|
10611
|
+
description: "JSON Schema (type object) the extracted JSON must conform to; extraction instructions live in its property descriptions."
|
|
10612
|
+
},
|
|
10613
|
+
selector: {
|
|
10614
|
+
type: "string",
|
|
10615
|
+
description: "CSS selector scoping extraction to a subtree."
|
|
10616
|
+
},
|
|
10617
|
+
source: {
|
|
10618
|
+
type: "string",
|
|
10619
|
+
enum: [
|
|
10620
|
+
"dom",
|
|
10621
|
+
"served"
|
|
10622
|
+
],
|
|
10623
|
+
description: "Page source to read; 'served' currently returns not-implemented."
|
|
10624
|
+
},
|
|
10625
|
+
clean: {
|
|
10626
|
+
type: "string",
|
|
10627
|
+
enum: [
|
|
10628
|
+
"standard",
|
|
10629
|
+
"aggressive",
|
|
10630
|
+
"none"
|
|
10631
|
+
],
|
|
10632
|
+
description: "HTML cleaning level."
|
|
10633
|
+
},
|
|
10634
|
+
maxChars: {
|
|
10635
|
+
type: "integer",
|
|
10636
|
+
description: "Character cap on the cleaned HTML; default 300000."
|
|
10637
|
+
},
|
|
10638
|
+
timeoutMs: {
|
|
10639
|
+
type: "integer",
|
|
10640
|
+
description: "Timeout in milliseconds; default 90000, max 300000."
|
|
10641
|
+
},
|
|
10642
|
+
prompt: {
|
|
10643
|
+
type: "string",
|
|
10644
|
+
enum: [
|
|
10645
|
+
"html",
|
|
10646
|
+
"schematron"
|
|
10647
|
+
],
|
|
10648
|
+
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)."
|
|
10649
|
+
},
|
|
10650
|
+
model: {
|
|
10651
|
+
type: "string",
|
|
10652
|
+
description: "Extraction model name; default 'schematron' (or CDP_EXTRACT_MODEL)."
|
|
10653
|
+
},
|
|
10654
|
+
baseUrl: {
|
|
10655
|
+
type: "string",
|
|
10656
|
+
description: "OpenAI-compatible endpoint base URL; default local loopback (or CDP_EXTRACT_BASE_URL)."
|
|
10657
|
+
},
|
|
10658
|
+
savePath: {
|
|
10659
|
+
type: "string",
|
|
10660
|
+
description: "Output file path."
|
|
10661
|
+
}
|
|
10662
|
+
},
|
|
10663
|
+
required: [
|
|
10664
|
+
"schema"
|
|
10665
|
+
],
|
|
10666
|
+
additionalProperties: false
|
|
10667
|
+
}
|
|
10173
10668
|
}
|
|
10174
10669
|
];
|
|
10175
10670
|
|
|
10176
10671
|
// src/install/wizard.ts
|
|
10177
10672
|
import { createInterface } from "node:readline";
|
|
10178
10673
|
import { homedir as homedir3 } from "node:os";
|
|
10179
|
-
import { join as
|
|
10674
|
+
import { join as join15 } from "node:path";
|
|
10180
10675
|
|
|
10181
10676
|
// src/install/browser.ts
|
|
10182
10677
|
import { existsSync as existsSync3 } from "node:fs";
|
|
10183
|
-
import { mkdir as
|
|
10678
|
+
import { mkdir as mkdir14, readFile as readFile9, writeFile as writeFile11 } from "node:fs/promises";
|
|
10184
10679
|
import { homedir } from "node:os";
|
|
10185
|
-
import { delimiter, dirname as
|
|
10680
|
+
import { delimiter, dirname as dirname4, join as join13 } from "node:path";
|
|
10186
10681
|
var CANDIDATE_BUILDERS = {
|
|
10187
10682
|
chrome: () => [
|
|
10188
10683
|
process.env.CHROME_BIN,
|
|
@@ -10206,7 +10701,7 @@ function existsOnPath(cmd) {
|
|
|
10206
10701
|
for (const dir of pathEnv.split(delimiter)) {
|
|
10207
10702
|
if (!dir)
|
|
10208
10703
|
continue;
|
|
10209
|
-
if (existsSync3(
|
|
10704
|
+
if (existsSync3(join13(dir, cmd)))
|
|
10210
10705
|
return true;
|
|
10211
10706
|
}
|
|
10212
10707
|
return false;
|
|
@@ -10256,10 +10751,10 @@ function aliasLine(o) {
|
|
|
10256
10751
|
function detectShellRc() {
|
|
10257
10752
|
const shell = process.env.SHELL ?? "";
|
|
10258
10753
|
if (shell.endsWith("zsh"))
|
|
10259
|
-
return
|
|
10754
|
+
return join13(homedir(), ".zshrc");
|
|
10260
10755
|
if (shell.endsWith("bash"))
|
|
10261
|
-
return
|
|
10262
|
-
return
|
|
10756
|
+
return join13(homedir(), ".bashrc");
|
|
10757
|
+
return join13(homedir(), process.platform === "darwin" ? ".zshrc" : ".bashrc");
|
|
10263
10758
|
}
|
|
10264
10759
|
var MARKER_START = "# >>> cdp-toolkit alias >>>";
|
|
10265
10760
|
var MARKER_END = "# <<< cdp-toolkit alias <<<";
|
|
@@ -10292,24 +10787,24 @@ ${MARKER_END}`;
|
|
|
10292
10787
|
}
|
|
10293
10788
|
const next = ensureTrailingNewline(`${before}${block}
|
|
10294
10789
|
${after}`);
|
|
10295
|
-
await
|
|
10790
|
+
await writeFile11(rcPath, next, "utf8");
|
|
10296
10791
|
return { rcPath, action: "replaced-block" };
|
|
10297
10792
|
}
|
|
10298
|
-
await
|
|
10793
|
+
await mkdir14(dirname4(rcPath), { recursive: true });
|
|
10299
10794
|
const sep = existing.length === 0 || existing.endsWith(`
|
|
10300
10795
|
`) ? "" : `
|
|
10301
10796
|
`;
|
|
10302
10797
|
const next = `${existing}${sep}${block}
|
|
10303
10798
|
`;
|
|
10304
|
-
await
|
|
10799
|
+
await writeFile11(rcPath, next, "utf8");
|
|
10305
10800
|
return { rcPath, action: "created-block" };
|
|
10306
10801
|
}
|
|
10307
10802
|
|
|
10308
10803
|
// src/install/harness.ts
|
|
10309
10804
|
import { spawnSync } from "node:child_process";
|
|
10310
|
-
import { mkdir as
|
|
10805
|
+
import { mkdir as mkdir15, readFile as readFile10, writeFile as writeFile12 } from "node:fs/promises";
|
|
10311
10806
|
import { homedir as homedir2 } from "node:os";
|
|
10312
|
-
import { dirname as
|
|
10807
|
+
import { dirname as dirname5, join as join14 } from "node:path";
|
|
10313
10808
|
async function installMcpServer(harness, name, cfg, opts = {}) {
|
|
10314
10809
|
switch (harness) {
|
|
10315
10810
|
case "claude":
|
|
@@ -10325,7 +10820,7 @@ async function installMcpServer(harness, name, cfg, opts = {}) {
|
|
|
10325
10820
|
}
|
|
10326
10821
|
}
|
|
10327
10822
|
async function installClaude(name, cfg, opts) {
|
|
10328
|
-
const path = opts.configPath ??
|
|
10823
|
+
const path = opts.configPath ?? join14(opts.homeDir ?? homedir2(), ".claude.json");
|
|
10329
10824
|
let root;
|
|
10330
10825
|
let fileExisted;
|
|
10331
10826
|
try {
|
|
@@ -10356,8 +10851,8 @@ async function installClaude(name, cfg, opts) {
|
|
|
10356
10851
|
if (action === "unchanged") {
|
|
10357
10852
|
return { harness: "claude", method: "file", path, action, detail: "no change needed" };
|
|
10358
10853
|
}
|
|
10359
|
-
await
|
|
10360
|
-
await
|
|
10854
|
+
await mkdir15(dirname5(path), { recursive: true });
|
|
10855
|
+
await writeFile12(path, JSON.stringify(root, null, 2) + `
|
|
10361
10856
|
`, "utf8");
|
|
10362
10857
|
return {
|
|
10363
10858
|
harness: "claude",
|
|
@@ -10593,7 +11088,7 @@ async function promptPort(def) {
|
|
|
10593
11088
|
}
|
|
10594
11089
|
function harnessTargetDesc(harness, io) {
|
|
10595
11090
|
if (harness === "claude") {
|
|
10596
|
-
return io.configPath ??
|
|
11091
|
+
return io.configPath ?? join15(io.homeDir ?? homedir3(), ".claude.json");
|
|
10597
11092
|
}
|
|
10598
11093
|
return `via \`${harness} mcp add\``;
|
|
10599
11094
|
}
|