cdp-toolkit 2.3.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/dist/mcp.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 ?? DEFAULT_TIMEOUT_MS2);
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 ?? DEFAULT_TIMEOUT_MS2;
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 ?? DEFAULT_TIMEOUT_MS2) {
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 DEFAULT_TIMEOUT_MS2, BidiError;
1283
+ var DEFAULT_TIMEOUT_MS3, BidiError;
1284
1284
  var init_client2 = __esm(() => {
1285
- DEFAULT_TIMEOUT_MS2 = Number(process.env.BIDI_TIMEOUT_MS ?? 15000);
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 mkdir12, readFile as readFile8, rename as rename3, unlink as unlink5, writeFile as writeFile9 } from "node:fs/promises";
1300
- import { join as join10 } from "node:path";
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 join10(leaseDir(), `ff-session-${safeEndpoint(endpoint)}.json`);
1305
+ return join11(leaseDir(), `ff-session-${safeEndpoint(endpoint)}.json`);
1306
1306
  }
1307
1307
  function muxDaemonLogFile(endpoint) {
1308
- return join10(leaseDir(), `ff-mux-${safeEndpoint(endpoint)}.log`);
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 mkdir12(leaseDir(), { recursive: true });
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 writeFile9(file, payload, { flag: "wx" });
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 writeFile9(file, payload, { flag: "wx" });
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 writeFile9(tmp, JSON.stringify(next), "utf8");
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 : DEFAULT_TIMEOUT_MS3;
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, DEFAULT_TIMEOUT_MS3 = 4000, HOST = "127.0.0.1", COLON = 58, DELETE_SESSION_FRAME, MarionetteProtocolError;
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 {
@@ -7905,6 +7905,421 @@ async function grantPermissions(args = {}) {
7905
7905
  return { granted: [...permissions], origin, ...reset ? { reset: true } : {}, target };
7906
7906
  }
7907
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
+
7908
8323
  // src/index.ts
7909
8324
  init_client();
7910
8325
  var cdpDriver = createCdpDriver();
@@ -7956,6 +8371,7 @@ var TOOLS = {
7956
8371
  click_focus_gated: clickFocusGated,
7957
8372
  wait_for_download: waitForDownload,
7958
8373
  grant_permissions: grantPermissions,
8374
+ extract_page: onCdp(extractPage),
7959
8375
  claim_page: onCdp(LEASE_TOOLS.claim_page),
7960
8376
  release_page: onCdp(LEASE_TOOLS.release_page),
7961
8377
  list_leases: onCdp(LEASE_TOOLS.list_leases)
@@ -9704,6 +10120,80 @@ var MANIFEST = [
9704
10120
  required: [],
9705
10121
  additionalProperties: false
9706
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
+ }
9707
10197
  }
9708
10198
  ];
9709
10199
 
@@ -9715,7 +10205,7 @@ import { spawn as spawn4 } from "node:child_process";
9715
10205
  import { existsSync as existsSync2, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
9716
10206
  import { createConnection, createServer as createServer2 } from "node:net";
9717
10207
  import { tmpdir } from "node:os";
9718
- import { join as join11 } from "node:path";
10208
+ import { join as join12 } from "node:path";
9719
10209
  function resolveBinary() {
9720
10210
  const candidates = [
9721
10211
  process.env.FIREFOX_BIN,
@@ -9795,7 +10285,7 @@ var LAUNCH_USER_JS_PREFS = {
9795
10285
  "dom.file.createInChild": true
9796
10286
  };
9797
10287
  function writeLaunchUserJs(profilePath) {
9798
- const target = join11(profilePath, "user.js");
10288
+ const target = join12(profilePath, "user.js");
9799
10289
  const existing = existsSync2(target) ? readFileSync(target, "utf8") : "";
9800
10290
  const present = new Set;
9801
10291
  for (const line of existing.split(`
@@ -9817,7 +10307,7 @@ async function launchFirefox(opts = {}) {
9817
10307
  const binary = resolveBinary();
9818
10308
  const port = await pickPort(opts.port);
9819
10309
  const isThrowaway = opts.profilePath === undefined;
9820
- const profilePath = opts.profilePath ?? mkdtempSync(join11(tmpdir(), "cdp-toolkit-ff-profile-"));
10310
+ const profilePath = opts.profilePath ?? mkdtempSync(join12(tmpdir(), "cdp-toolkit-ff-profile-"));
9821
10311
  if (isThrowaway)
9822
10312
  writeLaunchUserJs(profilePath);
9823
10313
  const args = ["--profile", profilePath, "--remote-debugging-port", String(port), "--no-remote"];
@@ -10009,7 +10499,7 @@ init_workers();
10009
10499
 
10010
10500
  class BidiToolError extends Error {
10011
10501
  }
10012
- async function withPage3(driver, target, fn) {
10502
+ async function withPage4(driver, target, fn) {
10013
10503
  const page = await driver.page(target);
10014
10504
  try {
10015
10505
  return await fn(page);
@@ -10053,7 +10543,7 @@ async function captureFresh(page, durationMs) {
10053
10543
  }
10054
10544
  async function listConsoleMessages2(driver, args = {}) {
10055
10545
  refuseWorkerSelector(driver, args.target);
10056
- return withPage3(driver, args.target, async (page) => {
10546
+ return withPage4(driver, args.target, async (page) => {
10057
10547
  if (args.reload)
10058
10548
  await captureFresh(page, args.durationMs ?? 2500);
10059
10549
  const buf = bufferFor(consoleBuffers, page.info.id);
@@ -10062,7 +10552,7 @@ async function listConsoleMessages2(driver, args = {}) {
10062
10552
  }
10063
10553
  async function getConsoleMessage2(driver, args = {}) {
10064
10554
  refuseWorkerSelector(driver, args.target);
10065
- return withPage3(driver, args.target, async (page) => {
10555
+ return withPage4(driver, args.target, async (page) => {
10066
10556
  const buf = bufferFor(consoleBuffers, page.info.id);
10067
10557
  const idx = args.index ?? 0;
10068
10558
  const entry = buf[idx];
@@ -10073,7 +10563,7 @@ async function getConsoleMessage2(driver, args = {}) {
10073
10563
  }
10074
10564
  async function listNetworkRequests2(driver, args = {}) {
10075
10565
  refuseWorkerSelector(driver, args.target);
10076
- return withPage3(driver, args.target, async (page) => {
10566
+ return withPage4(driver, args.target, async (page) => {
10077
10567
  if (args.reload)
10078
10568
  await captureFresh(page, args.durationMs ?? 2500);
10079
10569
  let buf = bufferFor(networkBuffers, page.info.id);
@@ -10086,7 +10576,7 @@ async function getNetworkRequest2(driver, args = {}) {
10086
10576
  refuseWorkerSelector(driver, args.target);
10087
10577
  if (!args.requestId && !args.url)
10088
10578
  throw new BidiToolError("get_network_request requires requestId or url");
10089
- return withPage3(driver, args.target, async (page) => {
10579
+ return withPage4(driver, args.target, async (page) => {
10090
10580
  const buf = bufferFor(networkBuffers, page.info.id);
10091
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));
10092
10582
  if (!found)
@@ -10098,7 +10588,7 @@ var mockSessions = new Map;
10098
10588
  async function mockRequest2(driver, args) {
10099
10589
  if (!args.urlPattern)
10100
10590
  throw new BidiToolError("mock_request requires a { urlPattern }");
10101
- return withPage3(driver, args.target, async (page) => {
10591
+ return withPage4(driver, args.target, async (page) => {
10102
10592
  const existing = mockSessions.get(page.info.id);
10103
10593
  const rule = {
10104
10594
  urlPattern: args.urlPattern,
@@ -10146,7 +10636,7 @@ async function clearMocks2(driver, args = {}) {
10146
10636
  mockSessions.clear();
10147
10637
  return { cleared: n };
10148
10638
  }
10149
- return withPage3(driver, args.target, async (page) => {
10639
+ return withPage4(driver, args.target, async (page) => {
10150
10640
  const existing = mockSessions.get(page.info.id);
10151
10641
  if (!existing)
10152
10642
  return { cleared: 0 };
@@ -10171,7 +10661,8 @@ var BIDI_ONLY_TOOLS = {
10171
10661
  var FIREFOX_TOOLS = {
10172
10662
  ...SHARED_TOOLS,
10173
10663
  ...BIDI_ONLY_TOOLS,
10174
- ...LEASE_TOOLS
10664
+ ...LEASE_TOOLS,
10665
+ extract_page: extractPage
10175
10666
  };
10176
10667
 
10177
10668
  // src/mcp.ts
@@ -10191,7 +10682,8 @@ var TOOL_GROUPS = [
10191
10682
  "leases",
10192
10683
  "permissions",
10193
10684
  "dialogs",
10194
- "downloads"
10685
+ "downloads",
10686
+ "extraction"
10195
10687
  ];
10196
10688
  var GROUP_TOOLS = {
10197
10689
  core: [
@@ -10236,7 +10728,8 @@ var GROUP_TOOLS = {
10236
10728
  leases: ["claim_page", "release_page", "list_leases"],
10237
10729
  permissions: ["grant_permissions"],
10238
10730
  dialogs: ["handle_dialog"],
10239
- downloads: ["wait_for_download"]
10731
+ downloads: ["wait_for_download"],
10732
+ extraction: ["extract_page"]
10240
10733
  };
10241
10734
  var TOOL_GROUP = {};
10242
10735
  for (const group of TOOL_GROUPS) {
@@ -10753,11 +11246,28 @@ var TOOL_DOCS = {
10753
11246
  list_leases: {
10754
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.",
10755
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
+ }
10756
11266
  }
10757
11267
  };
10758
11268
 
10759
11269
  // src/version.ts
10760
- var VERSION = "2.3.0";
11270
+ var VERSION = "2.5.0";
10761
11271
 
10762
11272
  // src/mcp.ts
10763
11273
  var { kind: BROWSER, endpoint: FIREFOX_ENDPOINT } = resolveBackend(process.argv.slice(2));