mcp-scraper 0.69.0 → 0.72.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 (45) hide show
  1. package/CHANGELOG.md +60 -2
  2. package/README.md +6 -2
  3. package/dist/{analytics-repository-62CAZAIP.js → analytics-repository-6YLRSWYO.js} +3 -2
  4. package/dist/bin/api-server.cjs +18592 -12680
  5. package/dist/bin/api-server.js +4 -4
  6. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +3 -3
  9. package/dist/bin/mcp-scraper-install.js +2 -2
  10. package/dist/bin/mcp-stdio-server.cjs +10492 -9129
  11. package/dist/bin/mcp-stdio-server.js +9 -7
  12. package/dist/bin/paa-harvest.cjs +308 -41
  13. package/dist/bin/paa-harvest.js +4 -3
  14. package/dist/{chunk-M7D7WO75.js → chunk-2N4V4LVU.js} +3178 -2019
  15. package/dist/chunk-7RQULQF3.js +263 -0
  16. package/dist/{chunk-UL4ZKAWZ.js → chunk-A66DGFOU.js} +1 -1
  17. package/dist/{chunk-ES25GP6C.js → chunk-CXY5WV45.js} +24 -34
  18. package/dist/{chunk-Y46YNQMM.js → chunk-F5TK4KMT.js} +1 -1
  19. package/dist/{chunk-62LEXS5O.js → chunk-F6MUGMRN.js} +306 -44
  20. package/dist/{chunk-5RFQGIYC.js → chunk-FVL4GUTP.js} +2 -2
  21. package/dist/{chunk-OZIUFN6B.js → chunk-I45JV4EU.js} +2 -2
  22. package/dist/{chunk-2LXTOAKS.js → chunk-KIPIBPUB.js} +9 -1
  23. package/dist/chunk-MB72PA6S.js +2003 -0
  24. package/dist/chunk-OM7HVEJ3.js +26 -0
  25. package/dist/chunk-PJEEKOUM.js +404 -0
  26. package/dist/{chunk-RX2QAHML.js → chunk-RAFQEPJ4.js} +2 -2
  27. package/dist/{chunk-KO5CK5ZT.js → chunk-SR7GSLEA.js} +202 -441
  28. package/dist/chunk-T3MZISOF.js +240 -0
  29. package/dist/{chunk-ULYOCJUJ.js → chunk-TUTQHP4O.js} +1 -1
  30. package/dist/{chunk-65FTMB7G.js → chunk-WEFPBAAG.js} +180 -9
  31. package/dist/{chunk-LN6N3YLK.js → chunk-WFQ2E4WE.js} +1 -1
  32. package/dist/{db-ZGZAKYBW.js → db-566MOHHD.js} +7 -1
  33. package/dist/{extract-bundle-SXSJCPQI.js → extract-bundle-337ZDIIQ.js} +4 -4
  34. package/dist/gmail-service-E276SBJU.js +25 -0
  35. package/dist/index.cjs +322 -49
  36. package/dist/index.d.cts +15 -2
  37. package/dist/index.d.ts +15 -2
  38. package/dist/index.js +4 -3
  39. package/dist/{lead-list-enrichment-repository-CXLVIV3R.js → lead-list-enrichment-repository-WWEMJ7EN.js} +2 -2
  40. package/dist/{location-data-repository-G72M7DTN.js → location-data-repository-H4B2BWGY.js} +2 -2
  41. package/dist/{server-WCNYXILA.js → server-UGWC5YMD.js} +8376 -6616
  42. package/dist/{site-extract-repository-RLB3TFJ3.js → site-extract-repository-ACNAVNDN.js} +3 -3
  43. package/dist/{worker-KHSJG365.js → worker-JQORPCFQ.js} +9 -7
  44. package/package.json +1 -1
  45. package/dist/chunk-2L4C4DAZ.js +0 -103
@@ -1,12 +1,14 @@
1
+ import {
2
+ browserServiceApiKey,
3
+ browserServiceProxyId
4
+ } from "./chunk-OM7HVEJ3.js";
1
5
  import {
2
6
  HEADLESS_HARVEST_TEST_ENV,
3
7
  HEADLESS_OPS,
4
- browserServiceApiKey,
5
- browserServiceProxyId,
6
8
  currentCostContext,
7
9
  recordKernelSession,
8
10
  runWithCostContext
9
- } from "./chunk-ES25GP6C.js";
11
+ } from "./chunk-CXY5WV45.js";
10
12
  import {
11
13
  DEFAULT_PROXY_MODE,
12
14
  HarvestOptionsSchema,
@@ -21,7 +23,7 @@ import {
21
23
  RECAPTCHA_INSTRUCTIONS,
22
24
  RequestAbortedError,
23
25
  sanitizeVendorName
24
- } from "./chunk-65FTMB7G.js";
26
+ } from "./chunk-WEFPBAAG.js";
25
27
 
26
28
  // src/driver/BrowserDriver.ts
27
29
  import { chromium } from "playwright-extra";
@@ -659,6 +661,13 @@ var BrowserDriver = class {
659
661
  getKernelSessionId() {
660
662
  return this.kernelSessionId;
661
663
  }
664
+ getProviderSessionMetadata() {
665
+ return {
666
+ provider: this.kernelSessionId ? "kernel" : "local",
667
+ providerSessionId: this.kernelSessionId,
668
+ disconnectObservation: null
669
+ };
670
+ }
662
671
  getDebugSnapshot() {
663
672
  return this.debugSnapshot;
664
673
  }
@@ -673,6 +682,7 @@ var BrowserDriver = class {
673
682
  const proxySource = this.kernelProxySource;
674
683
  const proxyType = this.kernelProxyType;
675
684
  const headlessSent = this.kernelHeadlessSent;
685
+ const provider = this.getProviderSessionMetadata();
676
686
  this.browser = null;
677
687
  this.context = null;
678
688
  this.page = null;
@@ -707,7 +717,11 @@ var BrowserDriver = class {
707
717
  kernelDeleteSucceeded: deleteResult.status === "fulfilled",
708
718
  kernelDeleteError: deleteResult.status === "rejected" ? deleteResult.reason instanceof Error ? deleteResult.reason.message : String(deleteResult.reason) : null,
709
719
  browserCloseSucceeded: closeResult.status === "fulfilled",
710
- browserCloseError: closeResult.status === "rejected" ? closeResult.reason instanceof Error ? closeResult.reason.message : String(closeResult.reason) : null
720
+ browserCloseError: closeResult.status === "rejected" ? closeResult.reason instanceof Error ? closeResult.reason.message : String(closeResult.reason) : null,
721
+ provider: provider.provider,
722
+ providerSessionId: provider.providerSessionId,
723
+ providerDisconnectObserved: false,
724
+ providerDisconnectMessage: null
711
725
  };
712
726
  if (deleteResult.status === "rejected") {
713
727
  console.warn(JSON.stringify({
@@ -739,7 +753,11 @@ var BrowserDriver = class {
739
753
  kernelDeleteSucceeded: null,
740
754
  kernelDeleteError: null,
741
755
  browserCloseSucceeded: true,
742
- browserCloseError: null
756
+ browserCloseError: null,
757
+ provider: provider.provider,
758
+ providerSessionId: provider.providerSessionId,
759
+ providerDisconnectObserved: false,
760
+ providerDisconnectMessage: null
743
761
  };
744
762
  } else if (this.context) {
745
763
  const ctx = this.context;
@@ -752,7 +770,11 @@ var BrowserDriver = class {
752
770
  kernelDeleteSucceeded: null,
753
771
  kernelDeleteError: null,
754
772
  browserCloseSucceeded: true,
755
- browserCloseError: null
773
+ browserCloseError: null,
774
+ provider: "local",
775
+ providerSessionId: null,
776
+ providerDisconnectObserved: false,
777
+ providerDisconnectMessage: null
756
778
  };
757
779
  }
758
780
  return {
@@ -761,13 +783,64 @@ var BrowserDriver = class {
761
783
  kernelDeleteSucceeded: null,
762
784
  kernelDeleteError: null,
763
785
  browserCloseSucceeded: null,
764
- browserCloseError: null
786
+ browserCloseError: null,
787
+ provider: "local",
788
+ providerSessionId: null,
789
+ providerDisconnectObserved: false,
790
+ providerDisconnectMessage: null
765
791
  };
766
792
  }
767
793
  };
768
794
 
769
795
  // src/driver/BrightDataSerpDriver.ts
770
796
  import { chromium as chromium2 } from "playwright";
797
+
798
+ // src/driver/browser-provider-telemetry.ts
799
+ var MAX_PROVIDER_SESSION_ID_LENGTH = 512;
800
+ var MAX_PROVIDER_MESSAGE_LENGTH = 320;
801
+ function boundedText(value, maxLength) {
802
+ if (typeof value !== "string") return null;
803
+ const normalized = value.replace(/[\u0000-\u001f\u007f]+/g, " ").replace(/\s+/g, " ").trim();
804
+ if (!normalized) return null;
805
+ return normalized.slice(0, maxLength);
806
+ }
807
+ function boundedProviderMessage(value) {
808
+ const message = boundedText(value, MAX_PROVIDER_MESSAGE_LENGTH);
809
+ if (!message) return null;
810
+ return message.replace(/\b(?:wss?|https?):\/\/\S+/gi, "[redacted endpoint]").replace(/\b(?:authorization|proxy-authorization):\s*\S+/gi, "$1: [redacted]").replace(/\bBearer\s+\S+/gi, "Bearer [redacted]").slice(0, MAX_PROVIDER_MESSAGE_LENGTH);
811
+ }
812
+ function sanitizeProviderLocalMessage(value) {
813
+ return boundedProviderMessage(value);
814
+ }
815
+ function validateProviderSessionId(value) {
816
+ if (typeof value !== "string" || value.length === 0 || value.length > MAX_PROVIDER_SESSION_ID_LENGTH || value.trim() !== value || /[\s\u0000-\u001f\u007f]/.test(value) || /:\/\//.test(value)) {
817
+ throw new Error("Bright Data did not return a valid browser session ID");
818
+ }
819
+ return value;
820
+ }
821
+ function makeProviderInterruptionObservation(source, message, observedAt = (/* @__PURE__ */ new Date()).toISOString()) {
822
+ const safeMessage = boundedProviderMessage(message);
823
+ return {
824
+ source,
825
+ message: safeMessage ?? source.replaceAll("_", " "),
826
+ observedAt
827
+ };
828
+ }
829
+ function projectProviderSessionPublic(metadata) {
830
+ return {
831
+ provider: metadata.provider,
832
+ providerDisconnectObserved: metadata.disconnectObservation !== null,
833
+ providerDisconnectMessage: metadata.disconnectObservation?.message ?? null
834
+ };
835
+ }
836
+ function projectProviderSessionPrivate(metadata) {
837
+ return {
838
+ ...projectProviderSessionPublic(metadata),
839
+ providerSessionId: metadata.providerSessionId
840
+ };
841
+ }
842
+
843
+ // src/driver/BrightDataSerpDriver.ts
771
844
  var BLOCKED_TYPES = /* @__PURE__ */ new Set(["image", "media", "font", "stylesheet", "ping"]);
772
845
  var BLOCKED_HOST_PARTS = [
773
846
  "doubleclick.net",
@@ -817,8 +890,19 @@ var BrightDataSerpDriver = class {
817
890
  browser = null;
818
891
  context = null;
819
892
  page = null;
893
+ cdpSession = null;
894
+ providerSessionId = null;
895
+ disconnectObservation = null;
896
+ closing = false;
897
+ observeDisconnect(source, message) {
898
+ if (this.closing || this.disconnectObservation) return;
899
+ this.disconnectObservation = makeProviderInterruptionObservation(source, message);
900
+ }
820
901
  async launch(_config) {
821
902
  if (this.page) return;
903
+ this.closing = false;
904
+ this.providerSessionId = null;
905
+ this.disconnectObservation = null;
822
906
  const endpoint = brightDataBrowserEndpoint();
823
907
  if (!endpoint) throw new Error("SERP browser service is not configured");
824
908
  this.browser = await chromium2.connectOverCDP(endpoint);
@@ -834,6 +918,14 @@ var BrightDataSerpDriver = class {
834
918
  }
835
919
  });
836
920
  this.page = this.context.pages()[0] ?? await this.context.newPage();
921
+ this.browser.on("disconnected", () => this.observeDisconnect("browser_disconnected"));
922
+ this.context.on("close", () => this.observeDisconnect("context_closed"));
923
+ this.page.on("close", () => this.observeDisconnect("page_closed"));
924
+ this.page.on("crash", () => this.observeDisconnect("page_crashed"));
925
+ this.cdpSession = await this.context.newCDPSession(this.page);
926
+ const customCdp = this.cdpSession;
927
+ const sessionResponse = await customCdp.send("Browser.getSessionId");
928
+ this.providerSessionId = validateProviderSessionId(sessionResponse?.sessionId);
837
929
  await this.page.route("**/*", async (route) => {
838
930
  const request = route.request();
839
931
  const resourceType = request.resourceType();
@@ -889,23 +981,38 @@ var BrightDataSerpDriver = class {
889
981
  getKernelSessionId() {
890
982
  return null;
891
983
  }
984
+ getProviderSessionMetadata() {
985
+ return {
986
+ provider: "bright_data_browser_api",
987
+ providerSessionId: this.providerSessionId,
988
+ disconnectObservation: this.disconnectObservation
989
+ };
990
+ }
892
991
  getDebugSnapshot() {
893
992
  return { kernel: null, context: null, networkLocation: null, serpNavigation: null };
894
993
  }
895
994
  async close() {
995
+ this.closing = true;
896
996
  let error = null;
897
997
  try {
898
998
  await this.browser?.close();
899
999
  } catch (caught) {
900
- error = caught instanceof Error ? caught.message : String(caught);
1000
+ error = sanitizeProviderLocalMessage(caught instanceof Error ? caught.message : String(caught)) ?? "Bright Data browser close failed";
1001
+ this.disconnectObservation ??= makeProviderInterruptionObservation("browser_close_error", error);
901
1002
  }
1003
+ const provider = projectProviderSessionPrivate(this.getProviderSessionMetadata());
1004
+ this.browser = null;
1005
+ this.context = null;
1006
+ this.page = null;
1007
+ this.cdpSession = null;
902
1008
  return {
903
1009
  kernelSessionId: null,
904
1010
  kernelDeleteStarted: false,
905
1011
  kernelDeleteSucceeded: null,
906
1012
  kernelDeleteError: null,
907
1013
  browserCloseSucceeded: error === null,
908
- browserCloseError: error
1014
+ browserCloseError: error,
1015
+ ...provider
909
1016
  };
910
1017
  }
911
1018
  };
@@ -1801,12 +1908,14 @@ async function extractAISurfacesFromDocument(config) {
1801
1908
 
1802
1909
  // src/extractor/PAAExtractor.ts
1803
1910
  var PAAExtractor = class {
1804
- constructor(driver, reporter) {
1911
+ constructor(driver, reporter, onPaaProgress) {
1805
1912
  this.driver = driver;
1806
1913
  this.reporter = reporter;
1914
+ this.onPaaProgress = onPaaProgress;
1807
1915
  }
1808
1916
  driver;
1809
1917
  reporter;
1918
+ onPaaProgress;
1810
1919
  completeness = {
1811
1920
  paaWithoutAnswer: 0,
1812
1921
  paaWithoutSource: 0,
@@ -1816,6 +1925,8 @@ var PAAExtractor = class {
1816
1925
  aioShareUrlEarly = null;
1817
1926
  collectedRows = [];
1818
1927
  partialContext = null;
1928
+ progressSequence = 0;
1929
+ clickedQuestions = /* @__PURE__ */ new Set();
1819
1930
  normalizeQuestion(q) {
1820
1931
  return q.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim();
1821
1932
  }
@@ -1852,6 +1963,34 @@ var PAAExtractor = class {
1852
1963
  function cleanSourceLabel(value) {
1853
1964
  return (value ?? "").replace(/\s*\(\+\d+\)\s*-\s*View related links.*$/i, "").replace(/\s*-\s*Opens in new tab.*$/i, "").trim();
1854
1965
  }
1966
+ function isGoogleHost(host) {
1967
+ const normalized = host.toLowerCase().replace(/\.$/, "");
1968
+ return normalized === "google.com" || normalized.endsWith(".google.com") || /^google\.[a-z.]+$/.test(normalized) || /\.google\.[a-z.]+$/.test(normalized) || normalized === "googleusercontent.com" || normalized.endsWith(".googleusercontent.com");
1969
+ }
1970
+ function safeCitationTarget(rawHref) {
1971
+ let parsed;
1972
+ try {
1973
+ parsed = new URL(rawHref);
1974
+ } catch {
1975
+ return null;
1976
+ }
1977
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
1978
+ if (parsed.username || parsed.password) return null;
1979
+ if (isGoogleHost(parsed.hostname)) {
1980
+ if (parsed.pathname !== "/url") return null;
1981
+ const redirected = parsed.searchParams.get("q") ?? parsed.searchParams.get("url");
1982
+ if (!redirected) return null;
1983
+ try {
1984
+ parsed = new URL(redirected);
1985
+ } catch {
1986
+ return null;
1987
+ }
1988
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
1989
+ if (parsed.username || parsed.password) return null;
1990
+ }
1991
+ if (isGoogleHost(parsed.hostname)) return null;
1992
+ return { href: parsed.href, host: parsed.hostname };
1993
+ }
1855
1994
  return Array.from(document.querySelectorAll(selectors.item)).filter((pair) => !onlyQuestion2 || (pair.getAttribute(selectors.itemDataQ) || pair.getAttribute(selectors.itemDataInitQ) || pair.querySelector(selectors.itemQuestionEl)?.innerText?.trim() || "") === onlyQuestion2).map((pair) => {
1856
1995
  const clickTarget = pair.querySelector(selectors.clickTarget);
1857
1996
  const expanded = pair.classList.contains(selectors.expandedClass) || clickTarget?.getAttribute("aria-expanded") === "true";
@@ -1873,21 +2012,14 @@ var PAAExtractor = class {
1873
2012
  ...Array.from(anchorRoot.querySelectorAll("a[href]"))
1874
2013
  ];
1875
2014
  for (const a of prioritized) {
1876
- const href = a.href;
1877
- if (!/^https?:\/\//.test(href) || seenHrefs.has(href)) continue;
1878
- let host = "";
1879
- try {
1880
- host = new URL(href).hostname;
1881
- } catch {
1882
- continue;
1883
- }
1884
- if (/(^|\.)google(usercontent)?\.[a-z.]+$/.test(host)) continue;
1885
- seenHrefs.add(href);
2015
+ const target = safeCitationTarget(a.href);
2016
+ if (!target || seenHrefs.has(target.href)) continue;
2017
+ seenHrefs.add(target.href);
1886
2018
  anchors.push({
1887
2019
  text: (a.textContent || "").trim(),
1888
2020
  label: cleanSourceLabel(a.getAttribute("aria-label")),
1889
- href,
1890
- host
2021
+ href: target.href,
2022
+ host: target.host
1891
2023
  });
1892
2024
  }
1893
2025
  const primary = anchors.find((x) => x.label.length > 2 || x.text.length > 2) ?? anchors[0];
@@ -1944,10 +2076,53 @@ var PAAExtractor = class {
1944
2076
  extracted_at: (/* @__PURE__ */ new Date()).toISOString()
1945
2077
  };
1946
2078
  }
2079
+ upsertCollectedRow(row) {
2080
+ const key = this.normalizeQuestion(row.question);
2081
+ const index = this.collectedRows.findIndex((existing2) => this.normalizeQuestion(existing2.question) === key);
2082
+ if (index < 0) {
2083
+ this.collectedRows.push({ ...row });
2084
+ return true;
2085
+ }
2086
+ const existing = this.collectedRows[index];
2087
+ const merged = {
2088
+ ...existing,
2089
+ ...row,
2090
+ answer: row.answer || existing.answer,
2091
+ source_title: row.source_title || existing.source_title,
2092
+ source_site: row.source_site || existing.source_site,
2093
+ source_cite: row.source_cite || existing.source_cite,
2094
+ extracted_at: row.extracted_at || existing.extracted_at
2095
+ };
2096
+ const changed = merged.answer !== existing.answer || merged.source_title !== existing.source_title || merged.source_site !== existing.source_site || merged.source_cite !== existing.source_cite;
2097
+ if (changed) this.collectedRows[index] = merged;
2098
+ return changed;
2099
+ }
2100
+ async emitProgress(phase) {
2101
+ if (!this.onPaaProgress || this.collectedRows.length === 0) return;
2102
+ const snapshot = {
2103
+ sequence: ++this.progressSequence,
2104
+ observedAt: (/* @__PURE__ */ new Date()).toISOString(),
2105
+ clickedQuestions: [...this.clickedQuestions],
2106
+ phase,
2107
+ records: this.collectedRows.map((row) => ({ ...row }))
2108
+ };
2109
+ try {
2110
+ await this.onPaaProgress(snapshot);
2111
+ } catch (err) {
2112
+ console.warn(JSON.stringify({
2113
+ event: "paa_progress_sink_failed",
2114
+ sequence: snapshot.sequence,
2115
+ phase: snapshot.phase,
2116
+ message: err instanceof Error ? err.message : String(err)
2117
+ }));
2118
+ }
2119
+ }
1947
2120
  getPartialResult() {
1948
2121
  const ctx = this.partialContext;
1949
2122
  if (!ctx || this.collectedRows.length === 0) return null;
1950
- const flat = [...this.collectedRows];
2123
+ const flat = this.collectedRows.map((row) => ({ ...row }));
2124
+ const paaWithoutAnswer = ctx.questionsOnly ? 0 : flat.filter((row) => !row.answer?.trim()).length;
2125
+ const paaWithoutSource = ctx.questionsOnly ? 0 : flat.filter((row) => !row.source_cite?.trim()).length;
1951
2126
  const stats = {
1952
2127
  seed: ctx.seed,
1953
2128
  totalQuestions: flat.length,
@@ -1962,7 +2137,17 @@ var PAAExtractor = class {
1962
2137
  diagnostics: {
1963
2138
  completionStatus: "paa_found",
1964
2139
  problem: null,
1965
- completeness: { ...this.completeness },
2140
+ resultQuality: "partial",
2141
+ degradedResult: false,
2142
+ retryRecommended: true,
2143
+ completeness: {
2144
+ ...this.completeness,
2145
+ paaWithoutAnswer,
2146
+ paaWithoutSource,
2147
+ paaRequested: ctx.maxQuestions,
2148
+ paaReturned: flat.length,
2149
+ paaUnique: flat.length
2150
+ },
1966
2151
  warnings: [{
1967
2152
  code: "paa_partial",
1968
2153
  surface: "paa",
@@ -1971,6 +2156,9 @@ var PAAExtractor = class {
1971
2156
  }]
1972
2157
  },
1973
2158
  totalQuestions: flat.length,
2159
+ surface: "web",
2160
+ aiOverview: { detected: false, text: null, citations: [] },
2161
+ aiMode: { detected: false, text: null, citations: [] },
1974
2162
  whatPeopleSaying: [],
1975
2163
  tree: this.buildTree(flat, ctx.seed),
1976
2164
  flat,
@@ -1978,7 +2166,7 @@ var PAAExtractor = class {
1978
2166
  forums: [],
1979
2167
  organicResults: [],
1980
2168
  localPack: [],
1981
- entityIds: {},
2169
+ entityIds: { entities: [], kgIds: [], cids: [], gcids: [] },
1982
2170
  stats
1983
2171
  };
1984
2172
  }
@@ -1988,7 +2176,8 @@ var PAAExtractor = class {
1988
2176
  const seenQs = /* @__PURE__ */ new Set();
1989
2177
  const orderedQs = [];
1990
2178
  this.collectedRows = [];
1991
- const results = this.collectedRows;
2179
+ this.progressSequence = 0;
2180
+ this.clickedQuestions = /* @__PURE__ */ new Set();
1992
2181
  const abandonedNeverReclick = /* @__PURE__ */ new Set();
1993
2182
  const clickedOnceEver = /* @__PURE__ */ new Set();
1994
2183
  const capturedItems = /* @__PURE__ */ new Map();
@@ -2036,12 +2225,17 @@ var PAAExtractor = class {
2036
2225
  if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
2037
2226
  this.throwIfAborted(signal);
2038
2227
  const states = await readItemStates();
2228
+ let discovered = false;
2039
2229
  for (const s of states) {
2040
2230
  if (!seenQs.has(s.q)) {
2041
2231
  seenQs.add(s.q);
2042
2232
  orderedQs.push(s.q);
2233
+ if (orderedQs.length <= options.maxQuestions) {
2234
+ discovered = this.upsertCollectedRow(this.toFlatRow({ question: s.q }, 1, null, options.query)) || discovered;
2235
+ }
2043
2236
  }
2044
2237
  }
2238
+ if (discovered) await this.emitProgress("discovered");
2045
2239
  if (options.questionsOnly && seenQs.size >= options.maxQuestions) break;
2046
2240
  const kept = new Set(orderedQs.slice(0, options.maxQuestions));
2047
2241
  const clickable = states.filter((s) => kept.has(s.q) && !clickedOnceEver.has(s.q) && !abandonedNeverReclick.has(s.q));
@@ -2061,6 +2255,7 @@ var PAAExtractor = class {
2061
2255
  this.reporter.onDepth(++round);
2062
2256
  await this.throwIfCaptcha(page, "Google PAA expansion");
2063
2257
  clickedOnceEver.add(target.q);
2258
+ this.clickedQuestions.add(target.q);
2064
2259
  const expansionStatus = await expandOneItemSerially(target.q);
2065
2260
  if (!options.questionsOnly) {
2066
2261
  let clickedItem = (await this.extractVisibleItems(page, target.q))[0];
@@ -2068,7 +2263,12 @@ var PAAExtractor = class {
2068
2263
  await page.waitForTimeout(1300);
2069
2264
  clickedItem = (await this.extractVisibleItems(page, target.q))[0];
2070
2265
  }
2071
- if (clickedItem?.answer) capturedItems.set(target.q, clickedItem);
2266
+ if (clickedItem) {
2267
+ capturedItems.set(target.q, clickedItem);
2268
+ if (this.upsertCollectedRow(this.toFlatRow(clickedItem, 1, null, options.query))) {
2269
+ await this.emitProgress("answer_captured");
2270
+ }
2271
+ }
2072
2272
  }
2073
2273
  if (expansionStatus === "failed" && !capturedItems.has(target.q)) {
2074
2274
  abandonedNeverReclick.add(target.q);
@@ -2084,14 +2284,19 @@ var PAAExtractor = class {
2084
2284
  if (!seenQs.has(state.q)) {
2085
2285
  seenQs.add(state.q);
2086
2286
  orderedQs.push(state.q);
2287
+ if (orderedQs.length <= options.maxQuestions) {
2288
+ this.upsertCollectedRow(this.toFlatRow({ question: state.q }, 1, null, options.query));
2289
+ }
2087
2290
  }
2088
2291
  }
2292
+ await this.emitProgress("discovered");
2089
2293
  }
2090
2294
  }
2091
2295
  const itemMap = options.questionsOnly ? /* @__PURE__ */ new Map() : new Map(capturedItems);
2092
2296
  if (!options.questionsOnly) {
2093
2297
  await this.fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick);
2094
2298
  }
2299
+ const results = [];
2095
2300
  for (const q of orderedQs) {
2096
2301
  if (results.length >= options.maxQuestions) break;
2097
2302
  const key = this.normalizeQuestion(q);
@@ -2099,14 +2304,22 @@ var PAAExtractor = class {
2099
2304
  seenKeys.add(key);
2100
2305
  const item = itemMap.get(q);
2101
2306
  if (item) {
2102
- results.push(this.toFlatRow(item, 1, null, options.query));
2307
+ const row = this.toFlatRow(item, 1, null, options.query);
2308
+ this.upsertCollectedRow(row);
2309
+ results.push(row);
2103
2310
  this.reporter.onQuestion({ question: item.question, answer: item.answer ?? null, sourceTitle: item.sourceTitle ?? null, sourceSite: item.sourceSite ?? null, sourceCite: item.sourceCite ?? null, depth: 1, parentQuestion: null, children: [] });
2104
2311
  } else {
2105
- results.push(this.toFlatRow({ question: q, answer: void 0, sourceTitle: void 0, sourceSite: void 0, sourceCite: void 0 }, 1, null, options.query));
2312
+ const row = this.toFlatRow({ question: q, answer: void 0, sourceTitle: void 0, sourceSite: void 0, sourceCite: void 0 }, 1, null, options.query);
2313
+ this.upsertCollectedRow(row);
2314
+ results.push(row);
2106
2315
  }
2107
2316
  }
2108
2317
  this.completeness.paaWithoutAnswer = options.questionsOnly ? 0 : results.filter((r) => !r.answer).length;
2109
- this.completeness.paaWithoutSource = options.questionsOnly ? 0 : results.filter((r) => !r.source_title && !r.source_site && !r.source_cite).length;
2318
+ this.completeness.paaWithoutSource = options.questionsOnly ? 0 : results.filter((r) => !r.source_cite).length;
2319
+ this.completeness.paaRequested = options.maxQuestions;
2320
+ this.completeness.paaReturned = results.length;
2321
+ this.completeness.paaUnique = seenKeys.size;
2322
+ await this.emitProgress("fill_incomplete");
2110
2323
  return results;
2111
2324
  }
2112
2325
  async fillIncompleteItems(page, orderedQs, itemMap, options, abandonedNeverReclick) {
@@ -2131,6 +2344,10 @@ var PAAExtractor = class {
2131
2344
  sourceSite: item.sourceSite ?? existing?.sourceSite,
2132
2345
  sourceCite: item.sourceCite ?? existing?.sourceCite
2133
2346
  });
2347
+ const upgraded = itemMap.get(q);
2348
+ if (upgraded && this.upsertCollectedRow(this.toFlatRow(upgraded, 1, null, options.query))) {
2349
+ await this.emitProgress("fill_incomplete");
2350
+ }
2134
2351
  }
2135
2352
  this.completeness.paaAnswersRecovered = [...missingAnswersBefore].filter((q) => itemMap.get(q)?.answer).length;
2136
2353
  }
@@ -2595,7 +2812,15 @@ var PAAExtractor = class {
2595
2812
  this.completeness = { paaWithoutAnswer: 0, paaWithoutSource: 0, paaAnswersRecovered: 0, aioShareCaptured: null };
2596
2813
  this.aioShareUrlEarly = null;
2597
2814
  this.collectedRows = [];
2598
- this.partialContext = { seed: options.query, location: options.location ?? null, startMs };
2815
+ this.progressSequence = 0;
2816
+ this.clickedQuestions = /* @__PURE__ */ new Set();
2817
+ this.partialContext = {
2818
+ seed: options.query,
2819
+ location: options.location ?? null,
2820
+ startMs,
2821
+ maxQuestions: options.maxQuestions,
2822
+ questionsOnly: options.questionsOnly
2823
+ };
2599
2824
  const isMobile = options.device === "mobile";
2600
2825
  const config = {
2601
2826
  headless: options.headless,
@@ -2667,6 +2892,7 @@ var PAAExtractor = class {
2667
2892
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2668
2893
  diagnostics: {
2669
2894
  completionStatus: hasPaa ? "paa_found" : "no_paa",
2895
+ ...!hasPaa ? { noPaaObserved: true } : {},
2670
2896
  problem: null,
2671
2897
  completeness: { ...this.completeness },
2672
2898
  ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, void 0, locationResolution) } : {}
@@ -2786,6 +3012,7 @@ var PAAExtractor = class {
2786
3012
  extractedAt: (/* @__PURE__ */ new Date()).toISOString(),
2787
3013
  diagnostics: {
2788
3014
  completionStatus: "no_paa",
3015
+ noPaaObserved: true,
2789
3016
  problem: null,
2790
3017
  completeness: { ...this.completeness },
2791
3018
  ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2, locationResolution) } : {}
@@ -2877,6 +3104,8 @@ var PAAExtractor = class {
2877
3104
  };
2878
3105
  } catch (err) {
2879
3106
  errorCount++;
3107
+ await this.emitProgress("attempt_ending").catch(() => {
3108
+ });
2880
3109
  this.reporter.onError(err instanceof Error ? err : new Error(String(err)));
2881
3110
  throw err;
2882
3111
  }
@@ -3560,6 +3789,11 @@ function getAttemptLogSink(rawOptions) {
3560
3789
  const sink = rawOptions.onAttemptEvent;
3561
3790
  return typeof sink === "function" ? sink : void 0;
3562
3791
  }
3792
+ function getPaaProgressSink(rawOptions) {
3793
+ if (!rawOptions || typeof rawOptions !== "object") return void 0;
3794
+ const sink = rawOptions.onPaaProgress;
3795
+ return typeof sink === "function" ? sink : void 0;
3796
+ }
3563
3797
  async function emitAttemptEvent(sink, event) {
3564
3798
  if (!sink) return;
3565
3799
  try {
@@ -3592,17 +3826,32 @@ function classifyAttemptResult(result) {
3592
3826
  function resultHasUsableContent(result) {
3593
3827
  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;
3594
3828
  }
3595
- function withResultQuality(result) {
3829
+ function withResultQuality(result, requestedPaaCount, questionsOnly) {
3596
3830
  const hasUsableContent = resultHasUsableContent(result);
3597
3831
  const hasWarnings = (result.diagnostics.warnings?.length ?? 0) > 0;
3598
- const resultQuality = hasUsableContent ? hasWarnings ? "partial" : "complete" : "degraded";
3832
+ const uniquePaaCount = new Set(result.flat.map((row) => row.question.toLowerCase().replace(/[^\w\s]/g, "").replace(/\s+/g, " ").trim())).size;
3833
+ const paaWithoutAnswer = questionsOnly ? 0 : result.flat.filter((row) => !row.answer?.trim()).length;
3834
+ const paaWithoutSource = questionsOnly ? 0 : result.flat.filter((row) => !row.source_cite?.trim()).length;
3835
+ const noPaa = result.diagnostics.completionStatus === "no_paa" && (result.diagnostics.noPaaObserved === true || hasUsableContent);
3836
+ const hasPaa = result.diagnostics.completionStatus === "paa_found";
3837
+ const paaIncomplete = hasPaa && (uniquePaaCount < requestedPaaCount || paaWithoutAnswer > 0 || paaWithoutSource > 0 || Boolean(result.diagnostics.interruption));
3838
+ const resultQuality = noPaa ? "complete" : hasPaa && uniquePaaCount > 0 ? paaIncomplete || hasWarnings ? "partial" : "complete" : hasUsableContent ? hasWarnings ? "partial" : "complete" : "degraded";
3599
3839
  const degradedResult = resultQuality === "degraded";
3600
3840
  result.diagnostics = {
3601
3841
  ...result.diagnostics,
3602
3842
  resultQuality,
3603
3843
  degradedResult,
3604
3844
  degradationReasons: degradedResult ? ["empty_primary_serp"] : [],
3605
- retryRecommended: degradedResult
3845
+ retryRecommended: degradedResult || resultQuality === "partial" && (paaIncomplete || result.diagnostics.warnings?.some((warning) => warning.retryable) === true),
3846
+ completeness: {
3847
+ paaWithoutAnswer,
3848
+ paaWithoutSource,
3849
+ paaAnswersRecovered: result.diagnostics.completeness?.paaAnswersRecovered ?? 0,
3850
+ aioShareCaptured: result.diagnostics.completeness?.aioShareCaptured ?? null,
3851
+ paaRequested: requestedPaaCount,
3852
+ paaReturned: result.flat.length,
3853
+ paaUnique: uniquePaaCount
3854
+ }
3606
3855
  };
3607
3856
  return result;
3608
3857
  }
@@ -3662,10 +3911,10 @@ async function cleanupDisposableProxy(kernelApiKey, proxyId) {
3662
3911
  }));
3663
3912
  }
3664
3913
  }
3665
- async function extractOnce(options, signal, forceManagedSerp = false) {
3914
+ async function extractOnce(options, signal, forceManagedSerp = false, onPaaProgress) {
3666
3915
  const driver = createSerpDriver(options, { forceManaged: forceManagedSerp });
3667
3916
  const reporter = new ProgressReporter();
3668
- const extractor = new PAAExtractor(driver, reporter);
3917
+ const extractor = new PAAExtractor(driver, reporter, onPaaProgress);
3669
3918
  if (signal?.aborted) {
3670
3919
  return {
3671
3920
  result: null,
@@ -3717,17 +3966,25 @@ async function extractOnce(options, signal, forceManagedSerp = false) {
3717
3966
  }
3718
3967
  if (error) {
3719
3968
  const outcome = classifyAttemptError(error);
3720
- const budgetExhausted = outcome === "timeout" || outcome === "request_aborted";
3969
+ const interruptionCode = outcome === "timeout" || outcome === "request_aborted" || outcome === "browser_session_interrupted" ? outcome : null;
3721
3970
  let salvaged = null;
3722
- if (budgetExhausted) {
3971
+ if (interruptionCode) {
3723
3972
  try {
3724
3973
  salvaged = typeof extractor.getPartialResult === "function" ? extractor.getPartialResult() : null;
3725
3974
  } catch {
3726
3975
  salvaged = null;
3727
3976
  }
3728
3977
  }
3729
- if (salvaged) {
3730
- return { result: salvaged, error: null, cleanup, debug, salvagedFrom: error };
3978
+ if (salvaged && interruptionCode) {
3979
+ salvaged.diagnostics = {
3980
+ ...salvaged.diagnostics,
3981
+ interruption: {
3982
+ code: interruptionCode,
3983
+ reasonSource: "client_runtime",
3984
+ message: errorMessage(error)
3985
+ }
3986
+ };
3987
+ return { result: salvaged, error, cleanup, debug, salvagedFrom: error };
3731
3988
  }
3732
3989
  return { result: null, error, cleanup, debug };
3733
3990
  }
@@ -3737,6 +3994,7 @@ async function harvest(rawOptions) {
3737
3994
  const raw = typeof rawOptions === "object" && rawOptions !== null ? rawOptions : {};
3738
3995
  const signal = getAbortSignal(rawOptions);
3739
3996
  const onAttemptEvent = getAttemptLogSink(rawOptions);
3997
+ const onPaaProgress = getPaaProgressSink(rawOptions);
3740
3998
  const forceManagedSerp = raw.forceManagedSerp === true;
3741
3999
  const requestedProxyMode = raw.proxyMode;
3742
4000
  const proxyMode = requestedProxyMode === "location" || requestedProxyMode === "none" || requestedProxyMode === "configured" ? requestedProxyMode : DEFAULT_PROXY_MODE;
@@ -3806,7 +4064,7 @@ async function harvest(rawOptions) {
3806
4064
  ...baseCtx,
3807
4065
  forceHeadless: allowHeadlessTest,
3808
4066
  forceHeadful: !allowHeadlessTest
3809
- }, () => extractOnce(attemptOptions, signal, forceManagedSerp)) : await extractOnce(attemptOptions, signal, forceManagedSerp);
4067
+ }, () => extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress)) : await extractOnce(attemptOptions, signal, forceManagedSerp, onPaaProgress);
3810
4068
  if (attempt.error) {
3811
4069
  const err = attempt.error;
3812
4070
  const outcome2 = classifyAttemptError(err);
@@ -3839,7 +4097,7 @@ async function harvest(rawOptions) {
3839
4097
  maxAttempts,
3840
4098
  outcome: outcome2,
3841
4099
  kernelSessionId: attempt.cleanup.kernelSessionId,
3842
- questionCount: 0,
4100
+ questionCount: attempt.result?.totalQuestions ?? 0,
3843
4101
  durationMs: Date.now() - startedAtMs,
3844
4102
  error: errorMessage(err),
3845
4103
  willRetry: willRetry2,
@@ -3850,6 +4108,9 @@ async function harvest(rawOptions) {
3850
4108
  await cleanupDisposableProxy(kernelApiKey, resolution2.disposableProxyId);
3851
4109
  lastError = err;
3852
4110
  if (willRetry2) continue;
4111
+ if (attempt.result) {
4112
+ return withResultQuality(stripInternalDebug(attempt.result, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
4113
+ }
3853
4114
  break;
3854
4115
  }
3855
4116
  const result = attempt.result;
@@ -3883,7 +4144,7 @@ async function harvest(rawOptions) {
3883
4144
  if (willRetry2) continue;
3884
4145
  break;
3885
4146
  }
3886
- const finalResult = withResultQuality(stripInternalDebug(result, requestedDebug));
4147
+ const finalResult = withResultQuality(stripInternalDebug(result, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
3887
4148
  const outcome = classifyAttemptResult(finalResult);
3888
4149
  const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
3889
4150
  const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
@@ -3995,6 +4256,7 @@ export {
3995
4256
  serpArrivalUrl,
3996
4257
  buildYouTubeChannelVideosUrl,
3997
4258
  BrowserDriver,
4259
+ validateProviderSessionId,
3998
4260
  brightDataSerpEnabled,
3999
4261
  BACKUP_LAST_RESORT_ATTEMPTS,
4000
4262
  backupProxyAvailable,