mcp-scraper 0.35.1 → 0.37.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 (90) hide show
  1. package/README.md +9 -9
  2. package/dist/bin/api-server.cjs +25071 -19082
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.cjs +51 -7
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +48 -5
  8. package/dist/bin/mcp-scraper-cli.js.map +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 +995 -222
  13. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  14. package/dist/bin/mcp-stdio-server.js +8 -8
  15. package/dist/bin/paa-harvest.cjs +125 -70
  16. package/dist/bin/paa-harvest.cjs.map +1 -1
  17. package/dist/bin/paa-harvest.js +4 -4
  18. package/dist/chunk-345BQXZH.js +712 -0
  19. package/dist/chunk-345BQXZH.js.map +1 -0
  20. package/dist/chunk-3LWYPAU5.js +7 -0
  21. package/dist/chunk-3LWYPAU5.js.map +1 -0
  22. package/dist/{chunk-M2S27J6Z.js → chunk-44HZLHDV.js} +10 -1
  23. package/dist/chunk-44HZLHDV.js.map +1 -0
  24. package/dist/{chunk-62DQAWPF.js → chunk-CSCD2HNS.js} +498 -43
  25. package/dist/chunk-CSCD2HNS.js.map +1 -0
  26. package/dist/{chunk-ZID3WQID.js → chunk-FQI5PFE7.js} +9 -71
  27. package/dist/chunk-FQI5PFE7.js.map +1 -0
  28. package/dist/{chunk-3HBPKR5G.js → chunk-FSAXLDB3.js} +3 -3
  29. package/dist/chunk-G3P3ZDB4.js +69 -0
  30. package/dist/chunk-G3P3ZDB4.js.map +1 -0
  31. package/dist/{chunk-YRGSEY5L.js → chunk-G7KAVJ3F.js} +2 -2
  32. package/dist/{chunk-YRGSEY5L.js.map → chunk-G7KAVJ3F.js.map} +1 -1
  33. package/dist/{chunk-NPMW5HUS.js → chunk-JK2FRDAP.js} +930 -244
  34. package/dist/chunk-JK2FRDAP.js.map +1 -0
  35. package/dist/{chunk-XVVNKASZ.js → chunk-LOPKN3YL.js} +118 -73
  36. package/dist/chunk-LOPKN3YL.js.map +1 -0
  37. package/dist/{chunk-4ZB3X6BQ.js → chunk-MA5JBAUZ.js} +16 -2
  38. package/dist/{chunk-4ZB3X6BQ.js.map → chunk-MA5JBAUZ.js.map} +1 -1
  39. package/dist/chunk-PUJFYJXB.js +684 -0
  40. package/dist/chunk-PUJFYJXB.js.map +1 -0
  41. package/dist/{chunk-BWXLTWF7.js → chunk-PWPUKR5U.js} +9 -5
  42. package/dist/chunk-PWPUKR5U.js.map +1 -0
  43. package/dist/chunk-Q35WZJJK.js +499 -0
  44. package/dist/chunk-Q35WZJJK.js.map +1 -0
  45. package/dist/chunk-QZXKQB7Y.js +414 -0
  46. package/dist/chunk-QZXKQB7Y.js.map +1 -0
  47. package/dist/{db-YAI5AQOI.js → db-N3YECFWR.js} +12 -2
  48. package/dist/{extract-bundle-ONWZVV55.js → extract-bundle-346R6MXD.js} +284 -98
  49. package/dist/extract-bundle-346R6MXD.js.map +1 -0
  50. package/dist/index.cjs +129 -70
  51. package/dist/index.cjs.map +1 -1
  52. package/dist/index.d.cts +11 -0
  53. package/dist/index.d.ts +11 -0
  54. package/dist/index.js +4 -4
  55. package/dist/location-data-repository-O2VII3ON.js +35 -0
  56. package/dist/{server-GKUTC73B.js → server-QKDDEBVQ.js} +7325 -3762
  57. package/dist/server-QKDDEBVQ.js.map +1 -0
  58. package/dist/site-extract-repository-I3VM6WXN.js +62 -0
  59. package/dist/site-extract-repository-I3VM6WXN.js.map +1 -0
  60. package/dist/{worker-645BZPEK.js → worker-TTFXPPDK.js} +7 -7
  61. package/docs/hosted-location-data.md +108 -0
  62. package/docs/mcp-tool-craft-lint.generated.md +6 -3
  63. package/docs/mcp-tool-manifest.generated.json +1447 -240
  64. package/docs/mcp-tool-quality-spec.md +1 -1
  65. package/docs/specs/connected-services-control-plane-decoupling-spec.md +1044 -0
  66. package/docs/specs/kernel-stealth-captcha-test-matrix.md +278 -0
  67. package/docs/specs/multimodal-image-memory-architecture-spec.md +1022 -0
  68. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +36 -27
  69. package/package.json +7 -6
  70. package/dist/chunk-62DQAWPF.js.map +0 -1
  71. package/dist/chunk-BWXLTWF7.js.map +0 -1
  72. package/dist/chunk-M2S27J6Z.js.map +0 -1
  73. package/dist/chunk-NPMW5HUS.js.map +0 -1
  74. package/dist/chunk-R7EETU7Z.js +0 -419
  75. package/dist/chunk-R7EETU7Z.js.map +0 -1
  76. package/dist/chunk-U44TPRST.js +0 -130
  77. package/dist/chunk-U44TPRST.js.map +0 -1
  78. package/dist/chunk-XVVNKASZ.js.map +0 -1
  79. package/dist/chunk-YR4LJ6AQ.js +0 -7
  80. package/dist/chunk-YR4LJ6AQ.js.map +0 -1
  81. package/dist/chunk-YV2FUEBX.js +0 -851
  82. package/dist/chunk-YV2FUEBX.js.map +0 -1
  83. package/dist/chunk-ZID3WQID.js.map +0 -1
  84. package/dist/extract-bundle-ONWZVV55.js.map +0 -1
  85. package/dist/server-GKUTC73B.js.map +0 -1
  86. package/dist/site-extract-repository-L6BHWVDU.js +0 -30
  87. /package/dist/{chunk-3HBPKR5G.js.map → chunk-FSAXLDB3.js.map} +0 -0
  88. /package/dist/{db-YAI5AQOI.js.map → db-N3YECFWR.js.map} +0 -0
  89. /package/dist/{site-extract-repository-L6BHWVDU.js.map → location-data-repository-O2VII3ON.js.map} +0 -0
  90. /package/dist/{worker-645BZPEK.js.map → worker-TTFXPPDK.js.map} +0 -0
package/dist/index.cjs CHANGED
@@ -341,6 +341,16 @@ var HEADLESS_OPS = /* @__PURE__ */ new Set(["serp", "maps_search", "yt_channel",
341
341
  var FAL_WIZPER_USD_PER_COMPUTE_SEC = 111e-5;
342
342
  var FAL_WIZPER_WALLCLOCK_TO_COMPUTE_SEC_FACTOR = 4;
343
343
  var DEEPINFRA_QWEN_USD_PER_1K_OUT_TOKENS = 4e-4;
344
+ function envRate(name, fallback) {
345
+ const raw = process.env[name]?.trim();
346
+ if (!raw) return fallback;
347
+ const parsed = Number(raw);
348
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
349
+ }
350
+ var NANGO_USD_PER_CONNECTION_MONTH = envRate("NANGO_USD_PER_CONNECTION_MONTH", 1);
351
+ var NANGO_USD_PER_FUNCTION_RUN = envRate("NANGO_USD_PER_FUNCTION_RUN", 1e-4);
352
+ var NANGO_USD_PER_PROXY_REQUEST = envRate("NANGO_USD_PER_PROXY_REQUEST", 1e-4);
353
+ var NANGO_USD_PER_COMPUTE_SEC = envRate("NANGO_USD_PER_COMPUTE_SEC", 2e-4);
344
354
  function kernelCostUsd(ms, headful) {
345
355
  const sec = Math.max(0, ms) / 1e3;
346
356
  return sec * (headful ? KERNEL_HEADFUL_USD_PER_SEC : KERNEL_HEADLESS_USD_PER_SEC);
@@ -354,6 +364,10 @@ function vendorCostUsd(vendor, units) {
354
364
  if (vendor === "openrouter") return Math.max(0, units);
355
365
  if (vendor === "mcp_memory_video") return Math.max(0, units);
356
366
  if (vendor === "mcp_memory_ai") return Math.max(0, units);
367
+ if (vendor === "nango_connection") return Math.max(0, units) * NANGO_USD_PER_CONNECTION_MONTH;
368
+ if (vendor === "nango_function_run") return Math.max(0, units) * NANGO_USD_PER_FUNCTION_RUN;
369
+ if (vendor === "nango_proxy_request") return Math.max(0, units) * NANGO_USD_PER_PROXY_REQUEST;
370
+ if (vendor === "nango_compute") return Math.max(0, units) * NANGO_USD_PER_COMPUTE_SEC;
357
371
  return 0;
358
372
  }
359
373
 
@@ -553,8 +567,6 @@ var MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)
553
567
  var DEFAULT_KERNEL_BROWSER_TIMEOUT_SECONDS = 180;
554
568
  var KERNEL_BROWSER_CLOSE_TIMEOUT_MS = 3e3;
555
569
  var KERNEL_SESSION_DELETE_TIMEOUT_MS = 5e3;
556
- var DEFAULT_SERP_CHALLENGE_SOLVE_WAIT_MS = 2500;
557
- var SERP_CHALLENGE_SOLVE_POLL_MS = 500;
558
570
  function positiveIntFromEnv(name, fallback) {
559
571
  const raw = process.env[name];
560
572
  if (!raw) return fallback;
@@ -926,8 +938,7 @@ var BrowserDriver = class {
926
938
  }
927
939
  if (await this.isSerpChallengeVisible()) {
928
940
  await this.updateSerpNavigationDebug(navDebug, url, { hasPaa: false, captchaDetected: true });
929
- const solved = await this.waitForSerpChallengeToClear(navDebug, url);
930
- if (!solved) throw new CaptchaError(this.captchaMessage());
941
+ throw new CaptchaError(this.captchaMessage());
931
942
  }
932
943
  const fastFound = await this.page.waitForSelector(PAASelectors.item, { timeout: 4e3 }).catch(() => null);
933
944
  if (fastFound) {
@@ -936,8 +947,7 @@ var BrowserDriver = class {
936
947
  }
937
948
  if (await this.isSerpChallengeVisible()) {
938
949
  await this.updateSerpNavigationDebug(navDebug, url, { hasPaa: false, captchaDetected: true });
939
- const solved = await this.waitForSerpChallengeToClear(navDebug, url);
940
- if (!solved) throw new CaptchaError(this.captchaMessage());
950
+ throw new CaptchaError(this.captchaMessage());
941
951
  }
942
952
  for (let i = 1; i <= 6; i++) {
943
953
  await this.page.evaluate((f) => {
@@ -965,23 +975,6 @@ var BrowserDriver = class {
965
975
  }).catch(() => "");
966
976
  return /recaptcha|unusual traffic|are you a robot|about this page|detected unusual traffic/i.test(bodySnippet);
967
977
  }
968
- async waitForSerpChallengeToClear(navDebug, requestedUrl) {
969
- if (!this.kernelClient || !this.page) return false;
970
- const waitMs = positiveIntFromEnv("SERP_CHALLENGE_SOLVE_WAIT_MS", DEFAULT_SERP_CHALLENGE_SOLVE_WAIT_MS);
971
- const deadline = Date.now() + waitMs;
972
- while (Date.now() < deadline) {
973
- await this.page.waitForTimeout(Math.min(SERP_CHALLENGE_SOLVE_POLL_MS, Math.max(250, deadline - Date.now())));
974
- if (!await this.isSerpChallengeVisible()) {
975
- await this.page.waitForTimeout(1e3);
976
- if (!await this.isSerpChallengeVisible()) {
977
- await this.updateSerpNavigationDebug(navDebug, requestedUrl, { hasPaa: null, captchaDetected: false });
978
- return true;
979
- }
980
- }
981
- }
982
- await this.updateSerpNavigationDebug(navDebug, requestedUrl, { hasPaa: false, captchaDetected: true });
983
- return false;
984
- }
985
978
  async updateSerpNavigationDebug(navDebug, requestedUrl, state) {
986
979
  if (!navDebug || !this.page) return;
987
980
  try {
@@ -1030,8 +1023,7 @@ var BrowserDriver = class {
1030
1023
  try {
1031
1024
  await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
1032
1025
  if (/google\.[^/]+\/search\?/i.test(url) && await this.isSerpChallengeVisible()) {
1033
- const solved = await this.waitForSerpChallengeToClear(null, url);
1034
- if (!solved) throw new CaptchaError(this.captchaMessage());
1026
+ throw new CaptchaError(this.captchaMessage());
1035
1027
  }
1036
1028
  } catch (err) {
1037
1029
  if (err instanceof CaptchaError) throw err;
@@ -1261,6 +1253,27 @@ function normalizeLocation(input) {
1261
1253
  return input;
1262
1254
  }
1263
1255
 
1256
+ // src/serp-location-query.ts
1257
+ function normalizedTerms(value) {
1258
+ return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
1259
+ }
1260
+ function includesTermSequence(query, phrase) {
1261
+ const queryTerms = normalizedTerms(query);
1262
+ const phraseTerms = normalizedTerms(phrase);
1263
+ if (phraseTerms.length === 0 || phraseTerms.length > queryTerms.length) return false;
1264
+ return queryTerms.some((_, start) => phraseTerms.every((term, index) => queryTerms[start + index] === term));
1265
+ }
1266
+ function cityFromCanonicalLocation(location, fallback) {
1267
+ return location.split(",", 1)[0]?.trim() || fallback.split(",", 1)[0]?.trim() || fallback.trim();
1268
+ }
1269
+ function buildLocalizedSerpQuery(query, location, canonicalLocation) {
1270
+ const normalizedQuery = query.trim();
1271
+ const requestedLocation = location?.trim();
1272
+ if (!requestedLocation || !canonicalLocation) return normalizedQuery;
1273
+ const city = cityFromCanonicalLocation(canonicalLocation, requestedLocation);
1274
+ return includesTermSequence(normalizedQuery, city) ? normalizedQuery : `${normalizedQuery} ${requestedLocation}`;
1275
+ }
1276
+
1264
1277
  // src/serp-location-debug.ts
1265
1278
  var STATE_TO_CODE = {
1266
1279
  alabama: "AL",
@@ -1514,6 +1527,11 @@ async function extractAISurfacesFromDocument(config) {
1514
1527
  if (!el) return "";
1515
1528
  return (el.innerText ?? el.textContent ?? "").trim();
1516
1529
  }
1530
+ function hasSubstantiveContent(text) {
1531
+ if (!text) return false;
1532
+ const normalized = text.replace(/\s+/g, " ").trim();
1533
+ return normalized.length >= 16 && normalized.split(" ").filter(Boolean).length >= 2;
1534
+ }
1517
1535
  function hasAIOverviewLabel(el) {
1518
1536
  const heading = el.querySelector(selectors.aio.heading);
1519
1537
  if (textOf(heading) === "AI Overview") return true;
@@ -1676,19 +1694,22 @@ async function extractAISurfacesFromDocument(config) {
1676
1694
  aioCitations = extractCitations(aioRoot);
1677
1695
  }
1678
1696
  const aimRoot = document.querySelector(selectors.aim.root);
1679
- const aimDetected = surface === "aim" && !!aimRoot;
1697
+ const aimRootDetected = surface === "aim" && !!aimRoot;
1680
1698
  const aimContainer = aimRoot?.closest(selectors.aim.wrapper) ?? aimRoot;
1681
- const aimText = cleanText(aimContainer);
1699
+ const rawAimText = aimRootDetected ? cleanText(aimContainer) : null;
1700
+ const aimDetected = aimRootDetected && hasSubstantiveContent(rawAimText);
1701
+ const aimText = aimDetected ? rawAimText : null;
1682
1702
  const aimCitations = aimDetected ? extractCitations(aimContainer) : [];
1703
+ const aiOverviewDetected = !!aioRoot && hasSubstantiveContent(aioText);
1683
1704
  return {
1684
1705
  surface,
1685
1706
  aiOverview: {
1686
- detected: !!aioRoot && aioText !== null,
1687
- text: aioText,
1688
- citations: aioCitations,
1689
- expanded: aioExpanded,
1690
- fullyExpanded: aioFullyExpanded,
1691
- sections: aioSections
1707
+ detected: aiOverviewDetected,
1708
+ text: aiOverviewDetected ? aioText : null,
1709
+ citations: aiOverviewDetected ? aioCitations : [],
1710
+ expanded: aiOverviewDetected ? aioExpanded : false,
1711
+ fullyExpanded: aiOverviewDetected ? aioFullyExpanded : false,
1712
+ sections: aiOverviewDetected ? aioSections : []
1692
1713
  },
1693
1714
  aiMode: {
1694
1715
  detected: aimDetected && aimText !== null,
@@ -2462,8 +2483,10 @@ var PAAExtractor = class {
2462
2483
  this.throwIfAborted(signal);
2463
2484
  const canonicalLocation = options.location ? normalizeLocation(options.location) : null;
2464
2485
  const uule = canonicalLocation ? encodeUule(canonicalLocation) : null;
2486
+ const executedQuery = buildLocalizedSerpQuery(options.query, options.location, canonicalLocation);
2487
+ const executionOptions = executedQuery === options.query ? options : { ...options, query: executedQuery };
2465
2488
  const { hasPaa } = await this.driver.navigateToSERP(
2466
- options.query,
2489
+ executionOptions.query,
2467
2490
  uule,
2468
2491
  options.gl,
2469
2492
  options.hl,
@@ -2488,8 +2511,7 @@ var PAAExtractor = class {
2488
2511
  let locationEvidence2 = options.debug ? inferSerpLocationEvidence(canonicalLocation, organicResults2, localPack2) : void 0;
2489
2512
  let allOrganic2 = organicResults2;
2490
2513
  if ((options.pages ?? 1) >= 2) {
2491
- const p2params = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2492
- if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2514
+ const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2493
2515
  if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2494
2516
  if (uule) p2params.set("uule", uule);
2495
2517
  await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
@@ -2501,7 +2523,7 @@ var PAAExtractor = class {
2501
2523
  }
2502
2524
  }
2503
2525
  const stats2 = {
2504
- seed: options.query,
2526
+ seed: executionOptions.query,
2505
2527
  totalQuestions: 0,
2506
2528
  maxDepthReached: 0,
2507
2529
  durationMs: Date.now() - startMs,
@@ -2509,14 +2531,14 @@ var PAAExtractor = class {
2509
2531
  };
2510
2532
  this.reporter.onComplete(stats2);
2511
2533
  return {
2512
- seed: options.query,
2534
+ seed: executionOptions.query,
2513
2535
  location: options.location ?? null,
2514
2536
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2515
2537
  diagnostics: {
2516
2538
  completionStatus: "serp_only",
2517
2539
  problem: null,
2518
2540
  completeness: { ...this.completeness },
2519
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(options, canonicalLocation, uule, locationEvidence2) } : {}
2541
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2) } : {}
2520
2542
  },
2521
2543
  totalQuestions: 0,
2522
2544
  surface: aiSurfaces2.surface,
@@ -2549,8 +2571,7 @@ var PAAExtractor = class {
2549
2571
  let noPaaOrganic = organicResults;
2550
2572
  let locationEvidence2 = initialLocationEvidence;
2551
2573
  if ((options.pages ?? 1) >= 2) {
2552
- const p2params = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2553
- if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2574
+ const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2554
2575
  if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2555
2576
  if (uule) p2params.set("uule", uule);
2556
2577
  await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
@@ -2563,7 +2584,7 @@ var PAAExtractor = class {
2563
2584
  }
2564
2585
  const aiSurfaces2 = await this.extractAISurfaces(page, options);
2565
2586
  const stats2 = {
2566
- seed: options.query,
2587
+ seed: executionOptions.query,
2567
2588
  totalQuestions: 0,
2568
2589
  maxDepthReached: 0,
2569
2590
  durationMs: Date.now() - startMs,
@@ -2571,14 +2592,14 @@ var PAAExtractor = class {
2571
2592
  };
2572
2593
  this.reporter.onComplete(stats2);
2573
2594
  return {
2574
- seed: options.query,
2595
+ seed: executionOptions.query,
2575
2596
  location: options.location ?? null,
2576
2597
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2577
2598
  diagnostics: {
2578
2599
  completionStatus: "no_paa",
2579
2600
  problem: null,
2580
2601
  completeness: { ...this.completeness },
2581
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(options, canonicalLocation, uule, locationEvidence2) } : {}
2602
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2) } : {}
2582
2603
  },
2583
2604
  totalQuestions: 0,
2584
2605
  surface: aiSurfaces2.surface,
@@ -2595,10 +2616,10 @@ var PAAExtractor = class {
2595
2616
  stats: stats2
2596
2617
  };
2597
2618
  }
2598
- const flat = await this.runBFS(page, options, signal);
2619
+ const flat = await this.runBFS(page, executionOptions, signal);
2599
2620
  this.throwIfAborted(signal);
2600
2621
  const aiSurfaces = await this.extractAISurfaces(page, options);
2601
- const shortVidsParams = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
2622
+ const shortVidsParams = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
2602
2623
  if (uule) shortVidsParams.set("uule", uule);
2603
2624
  let shortVideos = [];
2604
2625
  try {
@@ -2617,7 +2638,7 @@ var PAAExtractor = class {
2617
2638
  let allOrganic = organicResults;
2618
2639
  let locationEvidence = initialLocationEvidence;
2619
2640
  if ((options.pages ?? 1) >= 2) {
2620
- const p2params = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2641
+ const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2621
2642
  if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2622
2643
  if (uule) p2params.set("uule", uule);
2623
2644
  await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
@@ -2629,9 +2650,9 @@ var PAAExtractor = class {
2629
2650
  }
2630
2651
  }
2631
2652
  const allVideos = [...videos, ...shortVideos];
2632
- const tree = this.buildTree(flat, options.query);
2653
+ const tree = this.buildTree(flat, executionOptions.query);
2633
2654
  const stats = {
2634
- seed: options.query,
2655
+ seed: executionOptions.query,
2635
2656
  totalQuestions: flat.length,
2636
2657
  maxDepthReached: flat.reduce((m, r) => Math.max(m, r.depth), 0),
2637
2658
  durationMs: Date.now() - startMs,
@@ -2639,7 +2660,7 @@ var PAAExtractor = class {
2639
2660
  };
2640
2661
  this.reporter.onComplete(stats);
2641
2662
  return {
2642
- seed: options.query,
2663
+ seed: executionOptions.query,
2643
2664
  location: options.location ?? null,
2644
2665
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2645
2666
  diagnostics: {
@@ -2647,7 +2668,7 @@ var PAAExtractor = class {
2647
2668
  problem: null,
2648
2669
  completeness: { ...this.completeness },
2649
2670
  ...diagnosticWarnings.length > 0 ? { warnings: diagnosticWarnings } : {},
2650
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(options, canonicalLocation, uule, locationEvidence) } : {}
2671
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence) } : {}
2651
2672
  },
2652
2673
  totalQuestions: flat.length,
2653
2674
  surface: aiSurfaces.surface,
@@ -3374,8 +3395,33 @@ function classifyAttemptError(err) {
3374
3395
  return /timeout|timed out|Timeout \d+ms exceeded|deadline/i.test(message) ? "timeout" : "error";
3375
3396
  }
3376
3397
  function classifyAttemptResult(result) {
3398
+ if (result.diagnostics.degradedResult) return "degraded_result";
3377
3399
  return result.diagnostics?.completionStatus ?? (result.totalQuestions > 0 ? "paa_found" : "no_paa");
3378
3400
  }
3401
+ function resultHasUsableContent(result) {
3402
+ return result.flat.length > 0 || result.organicResults.length > 0 || result.localPack.length > 0 || result.videos.length > 0 || result.forums.length > 0 || result.whatPeopleSaying.length > 0 || result.aiOverview.detected || result.aiMode.detected;
3403
+ }
3404
+ function withResultQuality(result) {
3405
+ const hasUsableContent = resultHasUsableContent(result);
3406
+ const hasWarnings = (result.diagnostics.warnings?.length ?? 0) > 0;
3407
+ const resultQuality = hasUsableContent ? hasWarnings ? "partial" : "complete" : "degraded";
3408
+ const degradedResult = resultQuality === "degraded";
3409
+ result.diagnostics = {
3410
+ ...result.diagnostics,
3411
+ resultQuality,
3412
+ degradedResult,
3413
+ degradationReasons: degradedResult ? ["empty_primary_serp"] : [],
3414
+ retryRecommended: degradedResult
3415
+ };
3416
+ return result;
3417
+ }
3418
+ function degradedResultMessage(result) {
3419
+ const reasons = result.diagnostics.degradationReasons ?? [];
3420
+ if (reasons.includes("empty_primary_serp")) {
3421
+ return "No usable primary SERP surface was captured; the result may be incomplete.";
3422
+ }
3423
+ return "The harvest result was marked degraded.";
3424
+ }
3379
3425
  function errorMessage(err) {
3380
3426
  return err instanceof Error ? err.message : String(err);
3381
3427
  }
@@ -3545,60 +3591,60 @@ async function harvest(rawOptions) {
3545
3591
  const attempt = baseCtx ? await runWithCostContext({ ...baseCtx, forceHeadful: true }, () => extractOnce(attemptOptions, signal)) : await extractOnce(attemptOptions, signal);
3546
3592
  if (attempt.error) {
3547
3593
  const err = attempt.error;
3548
- const outcome = classifyAttemptError(err);
3549
- if ((outcome === "captcha" || outcome === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3594
+ const outcome2 = classifyAttemptError(err);
3595
+ if ((outcome2 === "captcha" || outcome2 === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3550
3596
  proxyFailures++;
3551
3597
  if (proxyFailures >= 2) effectiveProxyMode = "none";
3552
3598
  }
3553
- const willRetry = i < maxAttempts - 1 && (outcome === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome));
3554
- if (outcome === "captcha") {
3599
+ const willRetry2 = i < maxAttempts - 1 && (outcome2 === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome2));
3600
+ if (outcome2 === "captcha") {
3555
3601
  console.warn(JSON.stringify({
3556
3602
  event: "harvest_attempt_captcha",
3557
3603
  attempt_number: attemptNumber,
3558
3604
  max_attempts: maxAttempts,
3559
3605
  message: errorMessage(err),
3560
- will_retry: willRetry
3606
+ will_retry: willRetry2
3561
3607
  }));
3562
- } else if (willRetry) {
3608
+ } else if (willRetry2) {
3563
3609
  console.warn(JSON.stringify({
3564
3610
  event: "harvest_attempt_proxy_retry",
3565
3611
  attempt_number: attemptNumber,
3566
3612
  max_attempts: maxAttempts,
3567
- outcome,
3613
+ outcome: outcome2,
3568
3614
  message: errorMessage(err),
3569
- will_retry: willRetry
3615
+ will_retry: willRetry2
3570
3616
  }));
3571
3617
  }
3572
3618
  await emitAttemptEvent(onAttemptEvent, {
3573
3619
  type: "finished",
3574
3620
  attemptNumber,
3575
3621
  maxAttempts,
3576
- outcome,
3622
+ outcome: outcome2,
3577
3623
  kernelSessionId: attempt.cleanup.kernelSessionId,
3578
3624
  questionCount: 0,
3579
3625
  durationMs: Date.now() - startedAtMs,
3580
3626
  error: errorMessage(err),
3581
- willRetry,
3627
+ willRetry: willRetry2,
3582
3628
  cleanup: attempt.cleanup,
3583
3629
  debug: attempt.debug,
3584
3630
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3585
3631
  });
3586
3632
  await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
3587
3633
  lastError = err;
3588
- if (willRetry) continue;
3634
+ if (willRetry2) continue;
3589
3635
  break;
3590
3636
  }
3591
3637
  const result = attempt.result;
3592
3638
  if (!result) throw new Error("Harvest attempt completed without a result");
3593
3639
  if (shouldRetryLocationMismatch(result, proxyMode)) {
3594
3640
  const err = new LocationMismatchError(locationMismatchMessage(result));
3595
- const willRetry = i < maxAttempts - 1;
3641
+ const willRetry2 = i < maxAttempts - 1;
3596
3642
  console.warn(JSON.stringify({
3597
3643
  event: "harvest_attempt_location_mismatch",
3598
3644
  attempt_number: attemptNumber,
3599
3645
  max_attempts: maxAttempts,
3600
3646
  message: err.message,
3601
- will_retry: willRetry
3647
+ will_retry: willRetry2
3602
3648
  }));
3603
3649
  await emitAttemptEvent(onAttemptEvent, {
3604
3650
  type: "finished",
@@ -3609,32 +3655,45 @@ async function harvest(rawOptions) {
3609
3655
  questionCount: result.totalQuestions,
3610
3656
  durationMs: Date.now() - startedAtMs,
3611
3657
  error: err.message,
3612
- willRetry,
3658
+ willRetry: willRetry2,
3613
3659
  cleanup: attempt.cleanup,
3614
3660
  debug: attempt.debug,
3615
3661
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3616
3662
  });
3617
3663
  await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
3618
3664
  lastError = err;
3619
- if (willRetry) continue;
3665
+ if (willRetry2) continue;
3620
3666
  break;
3621
3667
  }
3622
- const finalResult = stripInternalDebug(result, requestedDebug);
3668
+ const finalResult = withResultQuality(stripInternalDebug(result, requestedDebug));
3669
+ const outcome = classifyAttemptResult(finalResult);
3670
+ const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
3671
+ const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
3672
+ if (willRetry) {
3673
+ console.warn(JSON.stringify({
3674
+ event: "harvest_attempt_degraded_result",
3675
+ attempt_number: attemptNumber,
3676
+ max_attempts: maxAttempts,
3677
+ degradation_reasons: finalResult.diagnostics.degradationReasons,
3678
+ will_retry: true
3679
+ }));
3680
+ }
3623
3681
  await emitAttemptEvent(onAttemptEvent, {
3624
3682
  type: "finished",
3625
3683
  attemptNumber,
3626
3684
  maxAttempts,
3627
- outcome: classifyAttemptResult(finalResult),
3685
+ outcome,
3628
3686
  kernelSessionId: attempt.cleanup.kernelSessionId,
3629
3687
  questionCount: finalResult.totalQuestions,
3630
3688
  durationMs: Date.now() - startedAtMs,
3631
- error: null,
3632
- willRetry: false,
3689
+ error: resultError,
3690
+ willRetry,
3633
3691
  cleanup: attempt.cleanup,
3634
3692
  debug: attempt.debug,
3635
3693
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3636
3694
  });
3637
3695
  await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
3696
+ if (willRetry) continue;
3638
3697
  if (attemptOptions.format === "json" || attemptOptions.format === "both") {
3639
3698
  await serializer.writeJSON(finalResult, attemptOptions.outputDir);
3640
3699
  }