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
@@ -7,19 +7,19 @@ import {
7
7
  registerMemoryMcpTools,
8
8
  registerPaaExtractorMcpTools,
9
9
  registerSerpIntelligenceCaptureTools
10
- } from "../chunk-NPMW5HUS.js";
11
- import "../chunk-R7EETU7Z.js";
12
- import "../chunk-4ZB3X6BQ.js";
10
+ } from "../chunk-JK2FRDAP.js";
11
+ import "../chunk-345BQXZH.js";
12
+ import "../chunk-MA5JBAUZ.js";
13
13
  import {
14
14
  renderInstallTerminal
15
- } from "../chunk-YRGSEY5L.js";
15
+ } from "../chunk-G7KAVJ3F.js";
16
16
  import "../chunk-CB5C3BPB.js";
17
17
  import {
18
18
  PACKAGE_VERSION
19
- } from "../chunk-YR4LJ6AQ.js";
20
- import "../chunk-BWXLTWF7.js";
21
- import "../chunk-M2S27J6Z.js";
22
- import "../chunk-62DQAWPF.js";
19
+ } from "../chunk-3LWYPAU5.js";
20
+ import "../chunk-PWPUKR5U.js";
21
+ import "../chunk-44HZLHDV.js";
22
+ import "../chunk-CSCD2HNS.js";
23
23
 
24
24
  // bin/mcp-stdio-server.ts
25
25
  import { readFileSync } from "fs";
@@ -328,6 +328,16 @@ function currentCostContext() {
328
328
  var KERNEL_HEADLESS_USD_PER_SEC = 166667e-10;
329
329
  var KERNEL_HEADFUL_USD_PER_SEC = 1333336e-10;
330
330
  var HEADLESS_OPS = /* @__PURE__ */ new Set(["serp", "maps_search", "yt_channel", "yt_transcription", "fb_search", "fb_ad", "instagram"]);
331
+ function envRate(name, fallback) {
332
+ const raw = process.env[name]?.trim();
333
+ if (!raw) return fallback;
334
+ const parsed = Number(raw);
335
+ return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback;
336
+ }
337
+ var NANGO_USD_PER_CONNECTION_MONTH = envRate("NANGO_USD_PER_CONNECTION_MONTH", 1);
338
+ var NANGO_USD_PER_FUNCTION_RUN = envRate("NANGO_USD_PER_FUNCTION_RUN", 1e-4);
339
+ var NANGO_USD_PER_PROXY_REQUEST = envRate("NANGO_USD_PER_PROXY_REQUEST", 1e-4);
340
+ var NANGO_USD_PER_COMPUTE_SEC = envRate("NANGO_USD_PER_COMPUTE_SEC", 2e-4);
331
341
  function kernelCostUsd(ms, headful) {
332
342
  const sec = Math.max(0, ms) / 1e3;
333
343
  return sec * (headful ? KERNEL_HEADFUL_USD_PER_SEC : KERNEL_HEADLESS_USD_PER_SEC);
@@ -502,8 +512,6 @@ var MOBILE_USER_AGENT = "Mozilla/5.0 (iPhone; CPU iPhone OS 17_5 like Mac OS X)
502
512
  var DEFAULT_KERNEL_BROWSER_TIMEOUT_SECONDS = 180;
503
513
  var KERNEL_BROWSER_CLOSE_TIMEOUT_MS = 3e3;
504
514
  var KERNEL_SESSION_DELETE_TIMEOUT_MS = 5e3;
505
- var DEFAULT_SERP_CHALLENGE_SOLVE_WAIT_MS = 2500;
506
- var SERP_CHALLENGE_SOLVE_POLL_MS = 500;
507
515
  function positiveIntFromEnv(name, fallback) {
508
516
  const raw = process.env[name];
509
517
  if (!raw) return fallback;
@@ -875,8 +883,7 @@ var BrowserDriver = class {
875
883
  }
876
884
  if (await this.isSerpChallengeVisible()) {
877
885
  await this.updateSerpNavigationDebug(navDebug, url, { hasPaa: false, captchaDetected: true });
878
- const solved = await this.waitForSerpChallengeToClear(navDebug, url);
879
- if (!solved) throw new CaptchaError(this.captchaMessage());
886
+ throw new CaptchaError(this.captchaMessage());
880
887
  }
881
888
  const fastFound = await this.page.waitForSelector(PAASelectors.item, { timeout: 4e3 }).catch(() => null);
882
889
  if (fastFound) {
@@ -885,8 +892,7 @@ var BrowserDriver = class {
885
892
  }
886
893
  if (await this.isSerpChallengeVisible()) {
887
894
  await this.updateSerpNavigationDebug(navDebug, url, { hasPaa: false, captchaDetected: true });
888
- const solved = await this.waitForSerpChallengeToClear(navDebug, url);
889
- if (!solved) throw new CaptchaError(this.captchaMessage());
895
+ throw new CaptchaError(this.captchaMessage());
890
896
  }
891
897
  for (let i = 1; i <= 6; i++) {
892
898
  await this.page.evaluate((f) => {
@@ -914,23 +920,6 @@ var BrowserDriver = class {
914
920
  }).catch(() => "");
915
921
  return /recaptcha|unusual traffic|are you a robot|about this page|detected unusual traffic/i.test(bodySnippet);
916
922
  }
917
- async waitForSerpChallengeToClear(navDebug, requestedUrl) {
918
- if (!this.kernelClient || !this.page) return false;
919
- const waitMs = positiveIntFromEnv("SERP_CHALLENGE_SOLVE_WAIT_MS", DEFAULT_SERP_CHALLENGE_SOLVE_WAIT_MS);
920
- const deadline = Date.now() + waitMs;
921
- while (Date.now() < deadline) {
922
- await this.page.waitForTimeout(Math.min(SERP_CHALLENGE_SOLVE_POLL_MS, Math.max(250, deadline - Date.now())));
923
- if (!await this.isSerpChallengeVisible()) {
924
- await this.page.waitForTimeout(1e3);
925
- if (!await this.isSerpChallengeVisible()) {
926
- await this.updateSerpNavigationDebug(navDebug, requestedUrl, { hasPaa: null, captchaDetected: false });
927
- return true;
928
- }
929
- }
930
- }
931
- await this.updateSerpNavigationDebug(navDebug, requestedUrl, { hasPaa: false, captchaDetected: true });
932
- return false;
933
- }
934
923
  async updateSerpNavigationDebug(navDebug, requestedUrl, state) {
935
924
  if (!navDebug || !this.page) return;
936
925
  try {
@@ -979,8 +968,7 @@ var BrowserDriver = class {
979
968
  try {
980
969
  await this.page.goto(url, { waitUntil: "domcontentloaded", timeout: 45e3 });
981
970
  if (/google\.[^/]+\/search\?/i.test(url) && await this.isSerpChallengeVisible()) {
982
- const solved = await this.waitForSerpChallengeToClear(null, url);
983
- if (!solved) throw new CaptchaError(this.captchaMessage());
971
+ throw new CaptchaError(this.captchaMessage());
984
972
  }
985
973
  } catch (err) {
986
974
  if (err instanceof CaptchaError) throw err;
@@ -1210,6 +1198,27 @@ function normalizeLocation(input) {
1210
1198
  return input;
1211
1199
  }
1212
1200
 
1201
+ // src/serp-location-query.ts
1202
+ function normalizedTerms(value) {
1203
+ return value.normalize("NFKD").replace(/[\u0300-\u036f]/g, "").toLowerCase().split(/[^a-z0-9]+/).filter(Boolean);
1204
+ }
1205
+ function includesTermSequence(query, phrase) {
1206
+ const queryTerms = normalizedTerms(query);
1207
+ const phraseTerms = normalizedTerms(phrase);
1208
+ if (phraseTerms.length === 0 || phraseTerms.length > queryTerms.length) return false;
1209
+ return queryTerms.some((_, start) => phraseTerms.every((term, index) => queryTerms[start + index] === term));
1210
+ }
1211
+ function cityFromCanonicalLocation(location, fallback) {
1212
+ return location.split(",", 1)[0]?.trim() || fallback.split(",", 1)[0]?.trim() || fallback.trim();
1213
+ }
1214
+ function buildLocalizedSerpQuery(query, location, canonicalLocation) {
1215
+ const normalizedQuery = query.trim();
1216
+ const requestedLocation = location?.trim();
1217
+ if (!requestedLocation || !canonicalLocation) return normalizedQuery;
1218
+ const city = cityFromCanonicalLocation(canonicalLocation, requestedLocation);
1219
+ return includesTermSequence(normalizedQuery, city) ? normalizedQuery : `${normalizedQuery} ${requestedLocation}`;
1220
+ }
1221
+
1213
1222
  // src/serp-location-debug.ts
1214
1223
  var STATE_TO_CODE = {
1215
1224
  alabama: "AL",
@@ -1463,6 +1472,11 @@ async function extractAISurfacesFromDocument(config) {
1463
1472
  if (!el) return "";
1464
1473
  return (el.innerText ?? el.textContent ?? "").trim();
1465
1474
  }
1475
+ function hasSubstantiveContent(text) {
1476
+ if (!text) return false;
1477
+ const normalized = text.replace(/\s+/g, " ").trim();
1478
+ return normalized.length >= 16 && normalized.split(" ").filter(Boolean).length >= 2;
1479
+ }
1466
1480
  function hasAIOverviewLabel(el) {
1467
1481
  const heading = el.querySelector(selectors.aio.heading);
1468
1482
  if (textOf(heading) === "AI Overview") return true;
@@ -1625,19 +1639,22 @@ async function extractAISurfacesFromDocument(config) {
1625
1639
  aioCitations = extractCitations(aioRoot);
1626
1640
  }
1627
1641
  const aimRoot = document.querySelector(selectors.aim.root);
1628
- const aimDetected = surface === "aim" && !!aimRoot;
1642
+ const aimRootDetected = surface === "aim" && !!aimRoot;
1629
1643
  const aimContainer = aimRoot?.closest(selectors.aim.wrapper) ?? aimRoot;
1630
- const aimText = cleanText(aimContainer);
1644
+ const rawAimText = aimRootDetected ? cleanText(aimContainer) : null;
1645
+ const aimDetected = aimRootDetected && hasSubstantiveContent(rawAimText);
1646
+ const aimText = aimDetected ? rawAimText : null;
1631
1647
  const aimCitations = aimDetected ? extractCitations(aimContainer) : [];
1648
+ const aiOverviewDetected = !!aioRoot && hasSubstantiveContent(aioText);
1632
1649
  return {
1633
1650
  surface,
1634
1651
  aiOverview: {
1635
- detected: !!aioRoot && aioText !== null,
1636
- text: aioText,
1637
- citations: aioCitations,
1638
- expanded: aioExpanded,
1639
- fullyExpanded: aioFullyExpanded,
1640
- sections: aioSections
1652
+ detected: aiOverviewDetected,
1653
+ text: aiOverviewDetected ? aioText : null,
1654
+ citations: aiOverviewDetected ? aioCitations : [],
1655
+ expanded: aiOverviewDetected ? aioExpanded : false,
1656
+ fullyExpanded: aiOverviewDetected ? aioFullyExpanded : false,
1657
+ sections: aiOverviewDetected ? aioSections : []
1641
1658
  },
1642
1659
  aiMode: {
1643
1660
  detected: aimDetected && aimText !== null,
@@ -2411,8 +2428,10 @@ var PAAExtractor = class {
2411
2428
  this.throwIfAborted(signal);
2412
2429
  const canonicalLocation = options.location ? normalizeLocation(options.location) : null;
2413
2430
  const uule = canonicalLocation ? encodeUule(canonicalLocation) : null;
2431
+ const executedQuery = buildLocalizedSerpQuery(options.query, options.location, canonicalLocation);
2432
+ const executionOptions = executedQuery === options.query ? options : { ...options, query: executedQuery };
2414
2433
  const { hasPaa } = await this.driver.navigateToSERP(
2415
- options.query,
2434
+ executionOptions.query,
2416
2435
  uule,
2417
2436
  options.gl,
2418
2437
  options.hl,
@@ -2437,8 +2456,7 @@ var PAAExtractor = class {
2437
2456
  let locationEvidence2 = options.debug ? inferSerpLocationEvidence(canonicalLocation, organicResults2, localPack2) : void 0;
2438
2457
  let allOrganic2 = organicResults2;
2439
2458
  if ((options.pages ?? 1) >= 2) {
2440
- const p2params = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2441
- if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2459
+ const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2442
2460
  if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2443
2461
  if (uule) p2params.set("uule", uule);
2444
2462
  await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
@@ -2450,7 +2468,7 @@ var PAAExtractor = class {
2450
2468
  }
2451
2469
  }
2452
2470
  const stats2 = {
2453
- seed: options.query,
2471
+ seed: executionOptions.query,
2454
2472
  totalQuestions: 0,
2455
2473
  maxDepthReached: 0,
2456
2474
  durationMs: Date.now() - startMs,
@@ -2458,14 +2476,14 @@ var PAAExtractor = class {
2458
2476
  };
2459
2477
  this.reporter.onComplete(stats2);
2460
2478
  return {
2461
- seed: options.query,
2479
+ seed: executionOptions.query,
2462
2480
  location: options.location ?? null,
2463
2481
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2464
2482
  diagnostics: {
2465
2483
  completionStatus: "serp_only",
2466
2484
  problem: null,
2467
2485
  completeness: { ...this.completeness },
2468
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(options, canonicalLocation, uule, locationEvidence2) } : {}
2486
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2) } : {}
2469
2487
  },
2470
2488
  totalQuestions: 0,
2471
2489
  surface: aiSurfaces2.surface,
@@ -2498,8 +2516,7 @@ var PAAExtractor = class {
2498
2516
  let noPaaOrganic = organicResults;
2499
2517
  let locationEvidence2 = initialLocationEvidence;
2500
2518
  if ((options.pages ?? 1) >= 2) {
2501
- const p2params = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2502
- if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2519
+ const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2503
2520
  if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2504
2521
  if (uule) p2params.set("uule", uule);
2505
2522
  await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
@@ -2512,7 +2529,7 @@ var PAAExtractor = class {
2512
2529
  }
2513
2530
  const aiSurfaces2 = await this.extractAISurfaces(page, options);
2514
2531
  const stats2 = {
2515
- seed: options.query,
2532
+ seed: executionOptions.query,
2516
2533
  totalQuestions: 0,
2517
2534
  maxDepthReached: 0,
2518
2535
  durationMs: Date.now() - startMs,
@@ -2520,14 +2537,14 @@ var PAAExtractor = class {
2520
2537
  };
2521
2538
  this.reporter.onComplete(stats2);
2522
2539
  return {
2523
- seed: options.query,
2540
+ seed: executionOptions.query,
2524
2541
  location: options.location ?? null,
2525
2542
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2526
2543
  diagnostics: {
2527
2544
  completionStatus: "no_paa",
2528
2545
  problem: null,
2529
2546
  completeness: { ...this.completeness },
2530
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(options, canonicalLocation, uule, locationEvidence2) } : {}
2547
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2) } : {}
2531
2548
  },
2532
2549
  totalQuestions: 0,
2533
2550
  surface: aiSurfaces2.surface,
@@ -2544,10 +2561,10 @@ var PAAExtractor = class {
2544
2561
  stats: stats2
2545
2562
  };
2546
2563
  }
2547
- const flat = await this.runBFS(page, options, signal);
2564
+ const flat = await this.runBFS(page, executionOptions, signal);
2548
2565
  this.throwIfAborted(signal);
2549
2566
  const aiSurfaces = await this.extractAISurfaces(page, options);
2550
- const shortVidsParams = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
2567
+ const shortVidsParams = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
2551
2568
  if (uule) shortVidsParams.set("uule", uule);
2552
2569
  let shortVideos = [];
2553
2570
  try {
@@ -2566,7 +2583,7 @@ var PAAExtractor = class {
2566
2583
  let allOrganic = organicResults;
2567
2584
  let locationEvidence = initialLocationEvidence;
2568
2585
  if ((options.pages ?? 1) >= 2) {
2569
- const p2params = new URLSearchParams({ q: options.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2586
+ const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
2570
2587
  if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
2571
2588
  if (uule) p2params.set("uule", uule);
2572
2589
  await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
@@ -2578,9 +2595,9 @@ var PAAExtractor = class {
2578
2595
  }
2579
2596
  }
2580
2597
  const allVideos = [...videos, ...shortVideos];
2581
- const tree = this.buildTree(flat, options.query);
2598
+ const tree = this.buildTree(flat, executionOptions.query);
2582
2599
  const stats = {
2583
- seed: options.query,
2600
+ seed: executionOptions.query,
2584
2601
  totalQuestions: flat.length,
2585
2602
  maxDepthReached: flat.reduce((m, r) => Math.max(m, r.depth), 0),
2586
2603
  durationMs: Date.now() - startMs,
@@ -2588,7 +2605,7 @@ var PAAExtractor = class {
2588
2605
  };
2589
2606
  this.reporter.onComplete(stats);
2590
2607
  return {
2591
- seed: options.query,
2608
+ seed: executionOptions.query,
2592
2609
  location: options.location ?? null,
2593
2610
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2594
2611
  diagnostics: {
@@ -2596,7 +2613,7 @@ var PAAExtractor = class {
2596
2613
  problem: null,
2597
2614
  completeness: { ...this.completeness },
2598
2615
  ...diagnosticWarnings.length > 0 ? { warnings: diagnosticWarnings } : {},
2599
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(options, canonicalLocation, uule, locationEvidence) } : {}
2616
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence) } : {}
2600
2617
  },
2601
2618
  totalQuestions: flat.length,
2602
2619
  surface: aiSurfaces.surface,
@@ -3323,8 +3340,33 @@ function classifyAttemptError(err) {
3323
3340
  return /timeout|timed out|Timeout \d+ms exceeded|deadline/i.test(message) ? "timeout" : "error";
3324
3341
  }
3325
3342
  function classifyAttemptResult(result) {
3343
+ if (result.diagnostics.degradedResult) return "degraded_result";
3326
3344
  return result.diagnostics?.completionStatus ?? (result.totalQuestions > 0 ? "paa_found" : "no_paa");
3327
3345
  }
3346
+ function resultHasUsableContent(result) {
3347
+ 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;
3348
+ }
3349
+ function withResultQuality(result) {
3350
+ const hasUsableContent = resultHasUsableContent(result);
3351
+ const hasWarnings = (result.diagnostics.warnings?.length ?? 0) > 0;
3352
+ const resultQuality = hasUsableContent ? hasWarnings ? "partial" : "complete" : "degraded";
3353
+ const degradedResult = resultQuality === "degraded";
3354
+ result.diagnostics = {
3355
+ ...result.diagnostics,
3356
+ resultQuality,
3357
+ degradedResult,
3358
+ degradationReasons: degradedResult ? ["empty_primary_serp"] : [],
3359
+ retryRecommended: degradedResult
3360
+ };
3361
+ return result;
3362
+ }
3363
+ function degradedResultMessage(result) {
3364
+ const reasons = result.diagnostics.degradationReasons ?? [];
3365
+ if (reasons.includes("empty_primary_serp")) {
3366
+ return "No usable primary SERP surface was captured; the result may be incomplete.";
3367
+ }
3368
+ return "The harvest result was marked degraded.";
3369
+ }
3328
3370
  function errorMessage(err) {
3329
3371
  return err instanceof Error ? err.message : String(err);
3330
3372
  }
@@ -3494,60 +3536,60 @@ async function harvest(rawOptions) {
3494
3536
  const attempt = baseCtx ? await runWithCostContext({ ...baseCtx, forceHeadful: true }, () => extractOnce(attemptOptions, signal)) : await extractOnce(attemptOptions, signal);
3495
3537
  if (attempt.error) {
3496
3538
  const err = attempt.error;
3497
- const outcome = classifyAttemptError(err);
3498
- if ((outcome === "captcha" || outcome === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3539
+ const outcome2 = classifyAttemptError(err);
3540
+ if ((outcome2 === "captcha" || outcome2 === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3499
3541
  proxyFailures++;
3500
3542
  if (proxyFailures >= 2) effectiveProxyMode = "none";
3501
3543
  }
3502
- const willRetry = i < maxAttempts - 1 && (outcome === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome));
3503
- if (outcome === "captcha") {
3544
+ const willRetry2 = i < maxAttempts - 1 && (outcome2 === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome2));
3545
+ if (outcome2 === "captcha") {
3504
3546
  console.warn(JSON.stringify({
3505
3547
  event: "harvest_attempt_captcha",
3506
3548
  attempt_number: attemptNumber,
3507
3549
  max_attempts: maxAttempts,
3508
3550
  message: errorMessage(err),
3509
- will_retry: willRetry
3551
+ will_retry: willRetry2
3510
3552
  }));
3511
- } else if (willRetry) {
3553
+ } else if (willRetry2) {
3512
3554
  console.warn(JSON.stringify({
3513
3555
  event: "harvest_attempt_proxy_retry",
3514
3556
  attempt_number: attemptNumber,
3515
3557
  max_attempts: maxAttempts,
3516
- outcome,
3558
+ outcome: outcome2,
3517
3559
  message: errorMessage(err),
3518
- will_retry: willRetry
3560
+ will_retry: willRetry2
3519
3561
  }));
3520
3562
  }
3521
3563
  await emitAttemptEvent(onAttemptEvent, {
3522
3564
  type: "finished",
3523
3565
  attemptNumber,
3524
3566
  maxAttempts,
3525
- outcome,
3567
+ outcome: outcome2,
3526
3568
  kernelSessionId: attempt.cleanup.kernelSessionId,
3527
3569
  questionCount: 0,
3528
3570
  durationMs: Date.now() - startedAtMs,
3529
3571
  error: errorMessage(err),
3530
- willRetry,
3572
+ willRetry: willRetry2,
3531
3573
  cleanup: attempt.cleanup,
3532
3574
  debug: attempt.debug,
3533
3575
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3534
3576
  });
3535
3577
  await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
3536
3578
  lastError = err;
3537
- if (willRetry) continue;
3579
+ if (willRetry2) continue;
3538
3580
  break;
3539
3581
  }
3540
3582
  const result = attempt.result;
3541
3583
  if (!result) throw new Error("Harvest attempt completed without a result");
3542
3584
  if (shouldRetryLocationMismatch(result, proxyMode)) {
3543
3585
  const err = new LocationMismatchError(locationMismatchMessage(result));
3544
- const willRetry = i < maxAttempts - 1;
3586
+ const willRetry2 = i < maxAttempts - 1;
3545
3587
  console.warn(JSON.stringify({
3546
3588
  event: "harvest_attempt_location_mismatch",
3547
3589
  attempt_number: attemptNumber,
3548
3590
  max_attempts: maxAttempts,
3549
3591
  message: err.message,
3550
- will_retry: willRetry
3592
+ will_retry: willRetry2
3551
3593
  }));
3552
3594
  await emitAttemptEvent(onAttemptEvent, {
3553
3595
  type: "finished",
@@ -3558,32 +3600,45 @@ async function harvest(rawOptions) {
3558
3600
  questionCount: result.totalQuestions,
3559
3601
  durationMs: Date.now() - startedAtMs,
3560
3602
  error: err.message,
3561
- willRetry,
3603
+ willRetry: willRetry2,
3562
3604
  cleanup: attempt.cleanup,
3563
3605
  debug: attempt.debug,
3564
3606
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3565
3607
  });
3566
3608
  await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
3567
3609
  lastError = err;
3568
- if (willRetry) continue;
3610
+ if (willRetry2) continue;
3569
3611
  break;
3570
3612
  }
3571
- const finalResult = stripInternalDebug(result, requestedDebug);
3613
+ const finalResult = withResultQuality(stripInternalDebug(result, requestedDebug));
3614
+ const outcome = classifyAttemptResult(finalResult);
3615
+ const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
3616
+ const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
3617
+ if (willRetry) {
3618
+ console.warn(JSON.stringify({
3619
+ event: "harvest_attempt_degraded_result",
3620
+ attempt_number: attemptNumber,
3621
+ max_attempts: maxAttempts,
3622
+ degradation_reasons: finalResult.diagnostics.degradationReasons,
3623
+ will_retry: true
3624
+ }));
3625
+ }
3572
3626
  await emitAttemptEvent(onAttemptEvent, {
3573
3627
  type: "finished",
3574
3628
  attemptNumber,
3575
3629
  maxAttempts,
3576
- outcome: classifyAttemptResult(finalResult),
3630
+ outcome,
3577
3631
  kernelSessionId: attempt.cleanup.kernelSessionId,
3578
3632
  questionCount: finalResult.totalQuestions,
3579
3633
  durationMs: Date.now() - startedAtMs,
3580
- error: null,
3581
- willRetry: false,
3634
+ error: resultError,
3635
+ willRetry,
3582
3636
  cleanup: attempt.cleanup,
3583
3637
  debug: attempt.debug,
3584
3638
  completedAt: (/* @__PURE__ */ new Date()).toISOString()
3585
3639
  });
3586
3640
  await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
3641
+ if (willRetry) continue;
3587
3642
  if (attemptOptions.format === "json" || attemptOptions.format === "both") {
3588
3643
  await serializer.writeJSON(finalResult, attemptOptions.outputDir);
3589
3644
  }