mcp-scraper 0.64.0 → 0.65.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.
Files changed (41) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +2 -2
  3. package/dist/bin/api-server.cjs +1028 -497
  4. package/dist/bin/api-server.cjs.map +1 -1
  5. package/dist/bin/api-server.js +2 -2
  6. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  7. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  8. package/dist/bin/mcp-scraper-cli.js +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs +2 -2
  10. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  11. package/dist/bin/mcp-scraper-install.js +2 -2
  12. package/dist/bin/mcp-stdio-server.cjs +49 -9
  13. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-stdio-server.js +4 -4
  15. package/dist/{chunk-TRSZEK2D.js → chunk-2XUXVKT4.js} +3 -3
  16. package/dist/{chunk-TRSZEK2D.js.map → chunk-2XUXVKT4.js.map} +1 -1
  17. package/dist/chunk-5BODYBIP.js +7 -0
  18. package/dist/chunk-5BODYBIP.js.map +1 -0
  19. package/dist/{chunk-BRKCPONM.js → chunk-5PIO7QBG.js} +2 -2
  20. package/dist/{chunk-5QYSY5KI.js → chunk-AMLFKPLL.js} +139 -3
  21. package/dist/chunk-AMLFKPLL.js.map +1 -0
  22. package/dist/{chunk-3NXLGTC6.js → chunk-DCWXVAQT.js} +2 -2
  23. package/dist/{chunk-3NXLGTC6.js.map → chunk-DCWXVAQT.js.map} +1 -1
  24. package/dist/{chunk-EEW3DMI5.js → chunk-PSKRQDGN.js} +50 -10
  25. package/dist/chunk-PSKRQDGN.js.map +1 -0
  26. package/dist/{extract-bundle-FW23CEMG.js → extract-bundle-UWKJT4MU.js} +371 -14
  27. package/dist/extract-bundle-UWKJT4MU.js.map +1 -0
  28. package/dist/{server-TMAWQYZE.js → server-AFEA235I.js} +170 -163
  29. package/dist/server-AFEA235I.js.map +1 -0
  30. package/dist/{site-extract-repository-UXSFD2QM.js → site-extract-repository-J3RH3MOS.js} +3 -3
  31. package/dist/{worker-DD243CON.js → worker-YKRIK5LS.js} +2 -2
  32. package/package.json +3 -1
  33. package/dist/chunk-5QYSY5KI.js.map +0 -1
  34. package/dist/chunk-DYSXI6QU.js +0 -7
  35. package/dist/chunk-DYSXI6QU.js.map +0 -1
  36. package/dist/chunk-EEW3DMI5.js.map +0 -1
  37. package/dist/extract-bundle-FW23CEMG.js.map +0 -1
  38. package/dist/server-TMAWQYZE.js.map +0 -1
  39. /package/dist/{chunk-BRKCPONM.js.map → chunk-5PIO7QBG.js.map} +0 -0
  40. /package/dist/{site-extract-repository-UXSFD2QM.js.map → site-extract-repository-J3RH3MOS.js.map} +0 -0
  41. /package/dist/{worker-DD243CON.js.map → worker-YKRIK5LS.js.map} +0 -0
@@ -1,12 +1,16 @@
1
1
  import {
2
2
  classifyExtractionProblem,
3
+ commonsSemanticSearchConfigured,
3
4
  discoverStaticMedia,
5
+ embedQueuedCommonsDocuments,
4
6
  extractionProblemResponse,
5
7
  extractionWireStatus,
6
8
  harvestPageMedia,
7
9
  publicizeExtractionFailure,
10
+ queuedCommonsDocumentCount,
11
+ semanticCommonsEntityScores,
8
12
  typeFromMime
9
- } from "./chunk-5QYSY5KI.js";
13
+ } from "./chunk-AMLFKPLL.js";
10
14
  import {
11
15
  REQUIRED_LOCATION_STATE_CODES,
12
16
  importHostedCensusPlacesCsv,
@@ -199,7 +203,7 @@ import {
199
203
  resolveDeploymentProfile,
200
204
  resolveLocalSourcebookSchemaType,
201
205
  transcribeMediaUrl
202
- } from "./chunk-EEW3DMI5.js";
206
+ } from "./chunk-PSKRQDGN.js";
203
207
  import {
204
208
  auditImageUrls,
205
209
  auditImages,
@@ -261,7 +265,7 @@ import {
261
265
  } from "./chunk-UVFB24RZ.js";
262
266
  import {
263
267
  PACKAGE_VERSION
264
- } from "./chunk-DYSXI6QU.js";
268
+ } from "./chunk-5BODYBIP.js";
265
269
  import {
266
270
  SITE_EXTRACT_ARTIFACT_PREFIX,
267
271
  abandonExtractSettlement,
@@ -292,7 +296,7 @@ import {
292
296
  setExtractJobTotal,
293
297
  settleExtractJob,
294
298
  terminalExtractJobStatus
295
- } from "./chunk-BRKCPONM.js";
299
+ } from "./chunk-5PIO7QBG.js";
296
300
  import {
297
301
  createPrivateArtifact,
298
302
  privateArtifactOwnerId,
@@ -352,7 +356,7 @@ import {
352
356
  tierChangeLeavesPrivateOffer,
353
357
  tierCreditsPerInterval,
354
358
  vendorCostUsdToMc
355
- } from "./chunk-TRSZEK2D.js";
359
+ } from "./chunk-2XUXVKT4.js";
356
360
  import {
357
361
  CaptchaError,
358
362
  MapsSearchIncompletePageError,
@@ -5291,8 +5295,9 @@ async function fetchText(url, auditedHost, timeoutMs = 1e4) {
5291
5295
  }
5292
5296
  function extractSitemapLocs(xml) {
5293
5297
  const locs = [];
5294
- for (const m of xml.matchAll(/<loc>\s*(https?:\/\/[^<\s]+)\s*<\/loc>/gi)) {
5295
- const candidate = m[1].trim();
5298
+ for (const m of xml.matchAll(/<loc\b[^>]*>\s*(?:<!\[CDATA\[([\s\S]*?)\]\]>|([^<]+))\s*<\/loc>/gi)) {
5299
+ const candidate = (m[1] ?? m[2] ?? "").replace(/&amp;/gi, "&").replace(/&lt;/gi, "<").replace(/&gt;/gi, ">").replace(/&quot;/gi, '"').replace(/&#39;|&apos;/gi, "'").trim();
5300
+ if (!/^https?:\/\//i.test(candidate)) continue;
5296
5301
  if (boundedDiscoveredUrl(candidate)) locs.push(candidate);
5297
5302
  }
5298
5303
  return locs;
@@ -5519,6 +5524,7 @@ import { chromium as chromium3 } from "playwright";
5519
5524
  var DEFAULT_CONCURRENCY = 1;
5520
5525
  var DEFAULT_URLS_PER_BROWSER = 10;
5521
5526
  var DEFAULT_HEADLESS = true;
5527
+ var MAX_RENDERED_DOM_BYTES = 256 * 1024;
5522
5528
  async function discoverSitemapUrls2(startUrl, maxUrls) {
5523
5529
  const domain = new URL(startUrl).hostname.replace(/^www\./, "");
5524
5530
  return discoverSitemapUrls(startUrl, domain, maxUrls);
@@ -5538,7 +5544,52 @@ async function createBrowserWithRetry(client2, headless) {
5538
5544
  throw new Error("unreachable");
5539
5545
  }
5540
5546
  var PAGE_HARD_TIMEOUT_MS = 35e3;
5541
- async function crawlPage(context, url, headless, telemetry) {
5547
+ async function settleRenderedPage(page) {
5548
+ await page.waitForLoadState("load", { timeout: 8e3 }).catch(() => void 0);
5549
+ await page.waitForLoadState("networkidle", { timeout: 5e3 }).catch(() => void 0);
5550
+ await page.evaluate(async () => {
5551
+ const wait = (ms) => new Promise((resolve2) => setTimeout(resolve2, ms));
5552
+ const height = Math.max(document.documentElement?.scrollHeight ?? 0, document.body?.scrollHeight ?? 0);
5553
+ const viewport = Math.max(1, window.innerHeight);
5554
+ const steps = Math.min(12, Math.max(1, Math.ceil(height / viewport)));
5555
+ for (let index = 1; index <= steps; index++) {
5556
+ window.scrollTo(0, Math.min(height, index * viewport));
5557
+ await wait(120);
5558
+ }
5559
+ window.scrollTo(0, 0);
5560
+ await wait(250);
5561
+ }).catch(() => void 0);
5562
+ }
5563
+ async function captureInertRenderedDom(page) {
5564
+ return page.evaluate((maxBytes) => {
5565
+ const root = document.documentElement?.cloneNode(true);
5566
+ if (!root) return { renderedDom: "", renderedDomTruncated: false, renderedDomSanitized: true };
5567
+ root.querySelectorAll('script,noscript,template,iframe,object,embed,meta[http-equiv="refresh"]').forEach((node) => node.remove());
5568
+ root.querySelectorAll("*").forEach((node) => {
5569
+ for (const attribute of [...node.attributes]) {
5570
+ const name = attribute.name.toLowerCase();
5571
+ const value = attribute.value.trim().toLowerCase();
5572
+ if (name.startsWith("on") || (name === "href" || name === "src" || name === "action") && value.startsWith("javascript:")) {
5573
+ node.removeAttribute(attribute.name);
5574
+ }
5575
+ }
5576
+ });
5577
+ const html = "<!doctype html>\n" + root.outerHTML;
5578
+ const encoder = new TextEncoder();
5579
+ if (encoder.encode(html).byteLength <= maxBytes) {
5580
+ return { renderedDom: html, renderedDomTruncated: false, renderedDomSanitized: true };
5581
+ }
5582
+ let low = 0;
5583
+ let high = html.length;
5584
+ while (low < high) {
5585
+ const middle = Math.ceil((low + high) / 2);
5586
+ if (encoder.encode(html.slice(0, middle)).byteLength <= maxBytes) low = middle;
5587
+ else high = middle - 1;
5588
+ }
5589
+ return { renderedDom: html.slice(0, low), renderedDomTruncated: true, renderedDomSanitized: true };
5590
+ }, MAX_RENDERED_DOM_BYTES);
5591
+ }
5592
+ async function crawlPage(context, url, headless, telemetry, options) {
5542
5593
  const startedAt = Date.now();
5543
5594
  let page = null;
5544
5595
  let guardState = null;
@@ -5547,6 +5598,7 @@ async function crawlPage(context, url, headless, telemetry) {
5547
5598
  guardState = await installBrowserRequestGuard(page, url);
5548
5599
  const work = (async (p) => {
5549
5600
  const resp = await p.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
5601
+ if (options.renderJavaScript || options.captureRenderedDom) await settleRenderedPage(p);
5550
5602
  let html = await p.evaluate((maxBytes) => {
5551
5603
  const value = document.documentElement?.outerHTML ?? "";
5552
5604
  if (new TextEncoder().encode(value).byteLength > maxBytes) throw new Error(`Browser HTML exceeds ${maxBytes} byte limit`);
@@ -5582,6 +5634,7 @@ async function crawlPage(context, url, headless, telemetry) {
5582
5634
  };
5583
5635
  }
5584
5636
  const isHttpError = resp != null && (resp.status() < 200 || resp.status() >= 300);
5637
+ const rendered = options.captureRenderedDom ? await captureInertRenderedDom(p) : null;
5585
5638
  return {
5586
5639
  url,
5587
5640
  html,
@@ -5593,10 +5646,12 @@ async function crawlPage(context, url, headless, telemetry) {
5593
5646
  ...isHttpError ? { errorCode: `browser_http_${resp.status()}`, error: `Browser fetch returned HTTP ${resp.status()}` } : {},
5594
5647
  browserMode: headless ? "stealth_headless" : "stealth_headful",
5595
5648
  challengeDetected: false,
5596
- stopReason: isHttpError ? "http_error" : "usable_page"
5649
+ stopReason: isHttpError ? "http_error" : "usable_page",
5650
+ ...rendered ?? {}
5597
5651
  };
5598
5652
  })(page);
5599
- const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("page-hard-timeout")), PAGE_HARD_TIMEOUT_MS));
5653
+ const hardTimeoutMs = options.renderJavaScript || options.captureRenderedDom ? 55e3 : PAGE_HARD_TIMEOUT_MS;
5654
+ const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("page-hard-timeout")), hardTimeoutMs));
5600
5655
  return await Promise.race([work, timeout]);
5601
5656
  } catch (error) {
5602
5657
  const message = guardState?.blockedReason ?? (error instanceof Error ? error.message : String(error));
@@ -5668,7 +5723,7 @@ async function crawlWithRotation(urls, opts) {
5668
5723
  }
5669
5724
  const context = await browser.newContext(withRuntimeUserAgent({ serviceWorkers: "block" }, runtimeUserAgent));
5670
5725
  try {
5671
- r = await crawlPage(context, url, passHeadless, { client: client2, sessionId: kb.session_id });
5726
+ r = await crawlPage(context, url, passHeadless, { client: client2, sessionId: kb.session_id }, opts);
5672
5727
  } finally {
5673
5728
  await context.close().catch(() => {
5674
5729
  });
@@ -6826,17 +6881,29 @@ async function mapWithConcurrency(items, concurrency, fn) {
6826
6881
  }
6827
6882
  async function extractPagesRotating(urls, opts) {
6828
6883
  const uniqueUrls = [...new Set(urls)];
6829
- const plainPages = await mapWithConcurrency(uniqueUrls, opts.concurrency ?? EXTRACT_CONCURRENCY, async (url) => {
6884
+ const initialPages = await mapWithConcurrency(uniqueUrls, opts.concurrency ?? EXTRACT_CONCURRENCY, async (url) => {
6830
6885
  const checked = await validatePublicHttpUrl(url, { field: "page URL" });
6831
- if (checked.error || !checked.parsed) return fetchPagePlain(url);
6832
- if (normalizeUrl(checked.parsed.href, checked.parsed.href)) return fetchPagePlain(url);
6833
- const attempt = failedAttempt("fetch", null, 0, "non_page_url", "The URL points to an asset, feed, API, account route, or other non-page path excluded from text-page extraction.");
6834
- const page = emptyPageData(url, null, "fetch", { code: attempt.errorCode, message: attempt.error, attempts: [attempt] });
6886
+ if (checked.error || !checked.parsed) {
6887
+ if (!opts.forceBrowserRender) return fetchPagePlain(url);
6888
+ const attempt2 = failedAttempt("browser", null, 0, "unsafe_url", checked.error ?? "Page URL was rejected.");
6889
+ return emptyPageData(url, null, "browser", { code: attempt2.errorCode, message: attempt2.error, attempts: [attempt2] });
6890
+ }
6891
+ if (normalizeUrl(checked.parsed.href, checked.parsed.href)) {
6892
+ if (!opts.forceBrowserRender) return fetchPagePlain(url);
6893
+ return emptyPageData(url, null, "browser", {
6894
+ code: "browser_pending",
6895
+ message: "Awaiting forced JavaScript rendering.",
6896
+ attempts: []
6897
+ });
6898
+ }
6899
+ const method = opts.forceBrowserRender ? "browser" : "fetch";
6900
+ const attempt = failedAttempt(method, null, 0, "non_page_url", "The URL points to an asset, feed, API, account route, or other non-page path excluded from text-page extraction.");
6901
+ const page = emptyPageData(url, null, method, { code: attempt.errorCode, message: attempt.error, attempts: [attempt] });
6835
6902
  page.contentKind = "excluded_url";
6836
6903
  return page;
6837
6904
  });
6838
- const byUrl = new Map(plainPages.map((page) => [page.url, page]));
6839
- const browserTargets = plainPages.filter((page) => !isPageExtractionSuccessful(page) && ![
6905
+ const byUrl = new Map(initialPages.map((page) => [page.url, page]));
6906
+ const browserTargets = opts.forceBrowserRender ? initialPages.filter((page) => page.failureCode === "browser_pending").map((page) => page.url) : initialPages.filter((page) => !isPageExtractionSuccessful(page) && ![
6840
6907
  "unsafe_url",
6841
6908
  "unsafe_redirect",
6842
6909
  "cross_site_redirect",
@@ -6848,7 +6915,9 @@ async function extractPagesRotating(urls, opts) {
6848
6915
  const fetched = await crawlWithRotation(browserTargets, {
6849
6916
  apiKey: opts.kernelApiKey,
6850
6917
  concurrency: opts.concurrency,
6851
- urlsPerBrowser: opts.urlsPerBrowser
6918
+ urlsPerBrowser: opts.urlsPerBrowser,
6919
+ renderJavaScript: opts.forceBrowserRender,
6920
+ captureRenderedDom: opts.captureRenderedDom
6852
6921
  });
6853
6922
  for (const result of fetched) {
6854
6923
  const plainPage = byUrl.get(result.url);
@@ -6865,12 +6934,17 @@ async function extractPagesRotating(urls, opts) {
6865
6934
  const browserOk = !redirectRejected && Boolean(result.html) && result.status >= 200 && result.status < 300;
6866
6935
  if (browserOk) {
6867
6936
  const replay = parseWaybackReplayUrl(result.url);
6868
- byUrl.set(result.url, parsePageData(result.url, result.html, result.status, "browser", {
6937
+ const page = parsePageData(result.url, result.renderedDom ?? result.html, result.status, "browser", {
6869
6938
  headers: result.headers,
6870
6939
  responseTimeMs,
6871
6940
  redirectUrl: result.redirectUrl,
6872
6941
  documentUrl: replay?.originalUrl ?? result.redirectUrl ?? result.url
6873
- }, [...priorAttempts, ...browserAttemptEntries(result, true)]));
6942
+ }, [...priorAttempts, ...browserAttemptEntries(result, true)]);
6943
+ if (result.renderedDom != null) {
6944
+ page.renderedDomTruncated = result.renderedDomTruncated === true;
6945
+ page.renderedDomSanitized = result.renderedDomSanitized === true;
6946
+ }
6947
+ byUrl.set(result.url, page);
6874
6948
  continue;
6875
6949
  }
6876
6950
  const code = redirectRejected ? "unsafe_browser_redirect" : result.errorCode ?? (result.status > 0 ? `browser_http_${result.status}` : result.html ? "browser_invalid_status" : "browser_empty_response");
@@ -6884,7 +6958,16 @@ async function extractPagesRotating(urls, opts) {
6884
6958
  }));
6885
6959
  }
6886
6960
  }
6887
- return urls.map((url) => byUrl.get(url) ?? emptyPageData(url, null, "fetch", {
6961
+ if (opts.forceBrowserRender && !opts.kernelApiKey) {
6962
+ for (const url of browserTargets) {
6963
+ byUrl.set(url, emptyPageData(url, null, "browser", {
6964
+ code: "browser_service_unconfigured",
6965
+ message: "Forced JavaScript rendering requires the hosted browser service.",
6966
+ attempts: [failedAttempt("browser", null, 0, "browser_service_unconfigured", "Forced JavaScript rendering requires the hosted browser service.")]
6967
+ }));
6968
+ }
6969
+ }
6970
+ return urls.map((url) => byUrl.get(url) ?? emptyPageData(url, null, opts.forceBrowserRender ? "browser" : "fetch", {
6888
6971
  code: "result_missing",
6889
6972
  message: "No extraction result was recorded."
6890
6973
  }));
@@ -9326,6 +9409,8 @@ var siteExtractFn = inngest.createFunction(
9326
9409
  const maxPages = Number(job.options.maxPages ?? 1e4);
9327
9410
  const concurrency = Number(job.options.concurrency ?? 1);
9328
9411
  const urlsPerBrowser = Number(job.options.urlsPerBrowser ?? job.options.rotateProxyEvery ?? 10);
9412
+ const captureRenderedDom = job.options.captureRenderedDom === true;
9413
+ const forceBrowserRender = job.options.renderJavaScript === true || job.options.semanticSimilarity === true || captureRenderedDom;
9329
9414
  const batchSize = siteExtractBatchSize(concurrency, urlsPerBrowser);
9330
9415
  const waybackReplay = parseWaybackReplayUrl(job.startUrl);
9331
9416
  const waybackTimeline = job.options.waybackTimeline;
@@ -9408,7 +9493,13 @@ var siteExtractFn = inngest.createFunction(
9408
9493
  if (!batch.length) break;
9409
9494
  const frontierCapacity = Math.max(0, maxPages - seen.size);
9410
9495
  const newLinks = await step.run(`crawl-batch-${i}`, async () => {
9411
- const pages = await runWithCostContext(costContext, () => extractPagesRotating(batch, { kernelApiKey: key, concurrency, urlsPerBrowser }));
9496
+ const pages = await runWithCostContext(costContext, () => extractPagesRotating(batch, {
9497
+ kernelApiKey: key,
9498
+ concurrency,
9499
+ urlsPerBrowser,
9500
+ forceBrowserRender,
9501
+ captureRenderedDom
9502
+ }));
9412
9503
  const storedPages = pages.map((p) => withWaybackProvenance({
9413
9504
  ...p,
9414
9505
  // parsePageData deliberately keeps large bodies non-enumerable so
@@ -9462,7 +9553,7 @@ var siteExtractFn = inngest.createFunction(
9462
9553
  });
9463
9554
  }) : null;
9464
9555
  const finalized = await step.run("finalize", async () => {
9465
- const { assembleExtractArtifacts } = await import("./extract-bundle-FW23CEMG.js");
9556
+ const { assembleExtractArtifacts } = await import("./extract-bundle-UWKJT4MU.js");
9466
9557
  const current = await getExtractJob(jobId2);
9467
9558
  if (!current) throw new Error("extract job disappeared before finalization");
9468
9559
  const stored = await assembleExtractArtifacts(current, { branding, imageAudit });
@@ -16187,6 +16278,11 @@ async function prepareSiteExtractStart(input) {
16187
16278
  urlsPerBrowser: input.urlsPerBrowser,
16188
16279
  formats: input.formats,
16189
16280
  downloadImages: input.downloadImages,
16281
+ renderJavaScript: input.renderJavaScript === true,
16282
+ captureRenderedDom: input.captureRenderedDom === true,
16283
+ semanticSimilarity: input.semanticSimilarity === true,
16284
+ similarityThreshold: input.similarityThreshold,
16285
+ similarityMaxPairs: input.similarityMaxPairs,
16190
16286
  ...input.waybackReplay ? {
16191
16287
  waybackReplay: input.waybackReplay,
16192
16288
  disableLinkDiscovery: true
@@ -19383,7 +19479,27 @@ var ExtractSiteBodySchema = z17.object({
19383
19479
  downloadImages: z17.boolean().optional(),
19384
19480
  preserveMedia: z17.boolean().optional(),
19385
19481
  delivery: z17.enum(["auto", "artifact"]).optional(),
19386
- formats: z17.array(z17.enum(["markdown", "html", "links", "json", "images", "branding", "issues"])).optional()
19482
+ formats: z17.array(z17.enum(["markdown", "html", "links", "json", "images", "branding", "issues"])).optional(),
19483
+ renderJavaScript: z17.boolean().optional(),
19484
+ captureRenderedDom: z17.boolean().optional(),
19485
+ semanticSimilarity: z17.boolean().optional(),
19486
+ similarityThreshold: z17.number().min(0).max(1).optional(),
19487
+ similarityMaxPairs: z17.number().int().min(1).max(5e4).optional()
19488
+ }).superRefine((value, ctx) => {
19489
+ if (value.semanticSimilarity && (value.maxPages ?? 100) > 500) {
19490
+ ctx.addIssue({
19491
+ code: z17.ZodIssueCode.custom,
19492
+ path: ["maxPages"],
19493
+ message: "semanticSimilarity supports at most 500 pages per analysis."
19494
+ });
19495
+ }
19496
+ if (value.semanticSimilarity && value.wayback) {
19497
+ ctx.addIssue({
19498
+ code: z17.ZodIssueCode.custom,
19499
+ path: ["wayback"],
19500
+ message: "semanticSimilarity analyzes one live rendered corpus and cannot be combined with a Wayback timeline."
19501
+ });
19502
+ }
19387
19503
  });
19388
19504
  var SiteExportReadBodySchema = z17.object({
19389
19505
  jobId: z17.string().trim().min(1),
@@ -36327,134 +36443,6 @@ async function hostEntityImages(input) {
36327
36443
  return rewrite;
36328
36444
  }
36329
36445
 
36330
- // src/api/commons-embeddings.ts
36331
- import { neon } from "@neondatabase/serverless";
36332
- var COMMONS_EMBED_PROVIDER = "jina";
36333
- var _vectorSql = null;
36334
- var vectorSchemaReady = false;
36335
- function commonsEmbedModel() {
36336
- return (process.env.JINA_EMBED_MODEL ?? "jina-embeddings-v5-omni-small").trim();
36337
- }
36338
- function commonsEmbedDim() {
36339
- return Number((process.env.JINA_EMBED_DIM ?? "1024").trim());
36340
- }
36341
- function commonsSemanticSearchConfigured() {
36342
- return Boolean(process.env.JINA_API_KEY?.trim() && process.env.MEMORY_DATABASE_URL?.trim());
36343
- }
36344
- function vectorSql() {
36345
- if (_vectorSql) return _vectorSql;
36346
- const url = process.env.MEMORY_DATABASE_URL?.trim();
36347
- if (!url) throw new Error("MEMORY_DATABASE_URL is not set; Commons semantic search needs the shared Postgres.");
36348
- _vectorSql = neon(url);
36349
- return _vectorSql;
36350
- }
36351
- async function ensureCommonsVectorSchema() {
36352
- if (vectorSchemaReady) return;
36353
- const dimension = commonsEmbedDim();
36354
- await vectorSql().query("CREATE EXTENSION IF NOT EXISTS vector");
36355
- await vectorSql().query(`
36356
- CREATE TABLE IF NOT EXISTS commons_index_vectors (
36357
- document_id TEXT PRIMARY KEY,
36358
- entity_id TEXT NOT NULL,
36359
- document_type TEXT NOT NULL,
36360
- title TEXT NOT NULL,
36361
- embedding vector(${dimension}) NOT NULL,
36362
- model TEXT NOT NULL,
36363
- updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
36364
- )
36365
- `);
36366
- await vectorSql().query("CREATE INDEX IF NOT EXISTS commons_index_vectors_entity ON commons_index_vectors(entity_id)");
36367
- vectorSchemaReady = true;
36368
- }
36369
- async function embedCommonsTexts(texts) {
36370
- const apiKey = process.env.JINA_API_KEY?.trim();
36371
- if (!apiKey) throw new Error("JINA_API_KEY is not set; Commons semantic search cannot embed.");
36372
- if (!texts.length) return [];
36373
- const response = await fetch("https://api.jina.ai/v1/embeddings", {
36374
- method: "POST",
36375
- headers: { authorization: `Bearer ${apiKey}`, "content-type": "application/json" },
36376
- body: JSON.stringify({
36377
- model: commonsEmbedModel(),
36378
- dimensions: commonsEmbedDim(),
36379
- input: texts.map((text2) => ({ text: text2.slice(0, 8e3) }))
36380
- }),
36381
- signal: AbortSignal.timeout(6e4)
36382
- });
36383
- if (!response.ok) {
36384
- throw new Error(`Jina embedding request failed with HTTP ${response.status}: ${(await response.text()).slice(0, 200)}`);
36385
- }
36386
- const payload = await response.json();
36387
- const vectors = (payload.data ?? []).map((item) => item.embedding);
36388
- if (vectors.length !== texts.length) {
36389
- throw new Error(`Jina returned ${vectors.length} embeddings for ${texts.length} inputs.`);
36390
- }
36391
- return vectors;
36392
- }
36393
- async function embedQueuedCommonsDocuments(limit = 50) {
36394
- if (!commonsSemanticSearchConfigured()) return { claimed: 0, embedded: 0, failed: 0, remaining: 0 };
36395
- await ensureCommonsVectorSchema();
36396
- const bounded2 = Math.max(1, Math.min(200, Math.floor(limit)));
36397
- const queued = await getDb().execute({
36398
- sql: `SELECT id, entity_id, document_type, title, text FROM commons_index_documents
36399
- WHERE embedding_status IN ('queued', 'failed') ORDER BY updated_at ASC LIMIT ?`,
36400
- args: [bounded2]
36401
- });
36402
- const rows = queued.rows;
36403
- if (!rows.length) return { claimed: 0, embedded: 0, failed: 0, remaining: await queuedCommonsDocumentCount() };
36404
- let embedded = 0;
36405
- let failed = 0;
36406
- const model = commonsEmbedModel();
36407
- try {
36408
- const vectors = await embedCommonsTexts(rows.map((row) => `${row.title}
36409
-
36410
- ${row.text}`));
36411
- for (const [index, row] of rows.entries()) {
36412
- const literal = `[${vectors[index].join(",")}]`;
36413
- await vectorSql().query(
36414
- `INSERT INTO commons_index_vectors (document_id, entity_id, document_type, title, embedding, model, updated_at)
36415
- VALUES ($1, $2, $3, $4, $5::vector, $6, now())
36416
- ON CONFLICT (document_id) DO UPDATE SET entity_id = EXCLUDED.entity_id, document_type = EXCLUDED.document_type,
36417
- title = EXCLUDED.title, embedding = EXCLUDED.embedding, model = EXCLUDED.model, updated_at = now()`,
36418
- [row.id, row.entity_id, row.document_type, row.title, literal, model]
36419
- );
36420
- await getDb().execute({
36421
- sql: `UPDATE commons_index_documents SET embedding_status = 'indexed', embedding_provider = ?, embedding_model = ?,
36422
- vector_ref = ?, indexed_at = ?, error = NULL WHERE id = ?`,
36423
- args: [COMMONS_EMBED_PROVIDER, model, row.id, (/* @__PURE__ */ new Date()).toISOString(), row.id]
36424
- });
36425
- embedded += 1;
36426
- }
36427
- } catch (error) {
36428
- failed = rows.length - embedded;
36429
- const message = (error instanceof Error ? error.message : String(error)).slice(0, 500);
36430
- for (const row of rows.slice(embedded)) {
36431
- await getDb().execute({
36432
- sql: `UPDATE commons_index_documents SET embedding_status = 'failed', error = ? WHERE id = ?`,
36433
- args: [message, row.id]
36434
- }).catch(() => void 0);
36435
- }
36436
- }
36437
- return { claimed: rows.length, embedded, failed, remaining: await queuedCommonsDocumentCount() };
36438
- }
36439
- async function queuedCommonsDocumentCount() {
36440
- const result = await getDb().execute(`SELECT COUNT(*) AS n FROM commons_index_documents WHERE embedding_status IN ('queued', 'failed')`);
36441
- return Number(result.rows[0]?.n ?? 0);
36442
- }
36443
- async function semanticCommonsEntityScores(query, limit = 40) {
36444
- const scores = /* @__PURE__ */ new Map();
36445
- if (!commonsSemanticSearchConfigured() || !query.trim()) return scores;
36446
- await ensureCommonsVectorSchema();
36447
- const [vector] = await embedCommonsTexts([query]);
36448
- if (!vector) return scores;
36449
- const rows = await vectorSql().query(
36450
- `SELECT entity_id, MAX(1 - (embedding <=> $1::vector)) AS score
36451
- FROM commons_index_vectors GROUP BY entity_id ORDER BY score DESC LIMIT $2`,
36452
- [`[${vector.join(",")}]`, Math.max(1, Math.min(100, limit))]
36453
- );
36454
- for (const row of rows) scores.set(String(row.entity_id), Number(row.score));
36455
- return scores;
36456
- }
36457
-
36458
36446
  // src/api/schema-presence.ts
36459
36447
  var cache = null;
36460
36448
  async function loadSchemaObjects() {
@@ -43194,7 +43182,7 @@ function requestedSiteMaxPages(maxPages) {
43194
43182
  return Math.min(ABSOLUTE_SITE_MAX_PAGES, Math.max(1, maxPages ?? DEFAULT_SITE_MAX_PAGES));
43195
43183
  }
43196
43184
  function shouldRunSiteExtractInBackground(input) {
43197
- return input.background === true || input.downloadImages === true || input.preserveMedia === true || requestedSiteMaxPages(input.maxPages) > MAX_SYNCHRONOUS_SITE_PAGES;
43185
+ return input.background === true || input.downloadImages === true || input.preserveMedia === true || input.renderJavaScript === true || input.captureRenderedDom === true || input.semanticSimilarity === true || requestedSiteMaxPages(input.maxPages) > MAX_SYNCHRONOUS_SITE_PAGES;
43198
43186
  }
43199
43187
 
43200
43188
  // src/api/page-media-artifacts.ts
@@ -45013,7 +45001,7 @@ async function getFreeCreditBreakdown(userId) {
45013
45001
  }
45014
45002
 
45015
45003
  // src/api/memory-db.ts
45016
- import { neon as neon2 } from "@neondatabase/serverless";
45004
+ import { neon } from "@neondatabase/serverless";
45017
45005
 
45018
45006
  // src/api/memory-universe.ts
45019
45007
  var SEP = "\n";
@@ -45110,7 +45098,7 @@ function sql() {
45110
45098
  if (_sql) return _sql;
45111
45099
  const url = process.env.MEMORY_DATABASE_URL?.trim();
45112
45100
  if (!url) throw new Error("MEMORY_DATABASE_URL is not set (paa-crawler needs mcp-memory Neon URL for reads)");
45113
- _sql = neon2(url);
45101
+ _sql = neon(url);
45114
45102
  return _sql;
45115
45103
  }
45116
45104
  function physicalVault(owner, vault) {
@@ -50323,6 +50311,13 @@ app.post("/extract-site", auth4, async (c) => {
50323
50311
  const bodyResult = ExtractSiteBodySchema.safeParse(raw);
50324
50312
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
50325
50313
  const body = bodyResult.data;
50314
+ if (body.semanticSimilarity && !process.env.JINA_API_KEY?.trim()) {
50315
+ return c.json({
50316
+ error: "Semantic site similarity is not configured on this deployment.",
50317
+ errorCode: "semantic_similarity_unconfigured",
50318
+ retryable: false
50319
+ }, 503);
50320
+ }
50326
50321
  if (body.preserveMedia !== void 0 && body.downloadImages !== void 0 && body.preserveMedia !== body.downloadImages) {
50327
50322
  return c.json({ error: "preserveMedia conflicts with deprecated downloadImages." }, 400);
50328
50323
  }
@@ -50376,7 +50371,12 @@ app.post("/extract-site", auth4, async (c) => {
50376
50371
  maxPages: requestedMaxPages,
50377
50372
  rotateProxyEvery: body.rotateProxyEvery ?? 10,
50378
50373
  formats: [...body.formats ?? []].sort(),
50379
- downloadImages
50374
+ downloadImages,
50375
+ renderJavaScript: body.renderJavaScript === true,
50376
+ captureRenderedDom: body.captureRenderedDom === true,
50377
+ semanticSimilarity: body.semanticSimilarity === true,
50378
+ similarityThreshold: body.similarityThreshold ?? null,
50379
+ similarityMaxPairs: body.similarityMaxPairs ?? null
50380
50380
  }));
50381
50381
  const prepared = await prepareSiteExtractStart({
50382
50382
  jobId: jobId2,
@@ -50390,6 +50390,11 @@ app.post("/extract-site", auth4, async (c) => {
50390
50390
  urlsPerBrowser: body.rotateProxyEvery ?? 10,
50391
50391
  formats: body.formats,
50392
50392
  downloadImages,
50393
+ renderJavaScript: body.renderJavaScript === true,
50394
+ captureRenderedDom: body.captureRenderedDom === true,
50395
+ semanticSimilarity: body.semanticSimilarity === true,
50396
+ similarityThreshold: body.similarityThreshold,
50397
+ similarityMaxPairs: body.similarityMaxPairs,
50393
50398
  debitKey: `site-extract:${user.id}:${jobId2}:hold`,
50394
50399
  waybackReplay: waybackReplay && !body.wayback ? {
50395
50400
  timestamp: waybackReplay.timestamp,
@@ -50483,8 +50488,10 @@ app.post("/extract-site", auth4, async (c) => {
50483
50488
  startUrl: crawlStartUrl,
50484
50489
  maxPages: siteMaxPages,
50485
50490
  seedUrls: waybackCaptures?.map((capture) => capture.rawReplayUrl),
50486
- kernelApiKey: rotateProxies || wantsBranding || body.browserFallback || body.kernelFallback ? browserServiceApiKey() : void 0,
50491
+ kernelApiKey: rotateProxies || wantsBranding || body.browserFallback || body.kernelFallback || body.renderJavaScript || body.captureRenderedDom || body.semanticSimilarity ? browserServiceApiKey() : void 0,
50487
50492
  formats: body.formats,
50493
+ forceBrowserRender: body.renderJavaScript || body.captureRenderedDom || body.semanticSimilarity,
50494
+ captureRenderedDom: body.captureRenderedDom,
50488
50495
  ...rotateProxies ? {
50489
50496
  rotateProxyEvery: body.rotateProxyEvery ?? 30,
50490
50497
  parallelism: concurrencyLimitForUser(user)
@@ -51006,7 +51013,7 @@ app.get("/cron/tick", async (c) => {
51006
51013
  return c.json({ error: "Unauthorized" }, 401);
51007
51014
  }
51008
51015
  const startedAt = Date.now();
51009
- const { drainQueue } = await import("./worker-DD243CON.js");
51016
+ const { drainQueue } = await import("./worker-YKRIK5LS.js");
51010
51017
  const budget = { maxJobs: 10, deadlineMs: startedAt + CRON_TICK_DRAIN_BUDGET_MS };
51011
51018
  const origin = `${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`;
51012
51019
  const unfinished = [];
@@ -51086,8 +51093,8 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
51086
51093
  finishExtractJob: finishExtractJob2,
51087
51094
  terminalExtractJobStatus: terminalExtractJobStatus2,
51088
51095
  extractJobLimitInfo: extractJobLimitInfo2
51089
- } = await import("./site-extract-repository-UXSFD2QM.js");
51090
- const { assembleExtractArtifacts } = await import("./extract-bundle-FW23CEMG.js");
51096
+ } = await import("./site-extract-repository-J3RH3MOS.js");
51097
+ const { assembleExtractArtifacts } = await import("./extract-bundle-UWKJT4MU.js");
51091
51098
  const existing = await getExtractJob2(jobId2);
51092
51099
  if (!existing) return c.json({ error: "job not found" }, 404);
51093
51100
  if (existing.status !== "failed") {
@@ -51114,7 +51121,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
51114
51121
  if (!updated) return c.json({ error: "job state changed during re-finalization" }, 409);
51115
51122
  let settlement = "already_settled_or_refunded";
51116
51123
  if (job.billedMc == null && job.userId != null) {
51117
- const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-UXSFD2QM.js");
51124
+ const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-J3RH3MOS.js");
51118
51125
  const heldMc = Number(job.options.heldMc ?? 0);
51119
51126
  const successful = await countSuccessfulPages2(jobId2);
51120
51127
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -51301,4 +51308,4 @@ app.get("/blog/:slug/", (c) => {
51301
51308
  export {
51302
51309
  app
51303
51310
  };
51304
- //# sourceMappingURL=server-TMAWQYZE.js.map
51311
+ //# sourceMappingURL=server-AFEA235I.js.map