mcp-scraper 0.86.4 → 0.87.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -4,6 +4,19 @@ All notable changes to MCP Scraper are documented here. The format is based on [
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.87.0] - 2026-09-02
8
+
9
+ ### Added
10
+
11
+ - Added optional `pages: 2` support to synchronous and durable PAA harvests, capturing a second organic result page before expanding questions on the untouched first page.
12
+ - Added requested/captured page counts and bounded second-page outcomes to live results, durable recovery, the dashboard, and generated client contracts. A failed optional page-two capture preserves first-page evidence.
13
+
14
+ ## [0.86.5] - 2026-09-02
15
+
16
+ ### Fixed
17
+
18
+ - Kept Bright Data telemetry lookup off the Reddit response critical path and reallocated the saved time to 17-second backup attempts, so all four provider attempts can finish before production ends the request.
19
+
7
20
  ## [0.86.4] - 2026-09-02
8
21
 
9
22
  ### Fixed
@@ -1699,7 +1712,8 @@ All notable changes to MCP Scraper are documented here. The format is based on [
1699
1712
  - Write actions remain unavailable until the account owner explicitly enables them.
1700
1713
  - Provider-specific connection data is normalized into one agent-facing contract.
1701
1714
 
1702
- [Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.86.0...HEAD
1715
+ [Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.87.0...HEAD
1716
+ [0.87.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.86.5...v0.87.0
1703
1717
  [0.86.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.85.0...v0.86.0
1704
1718
  [0.84.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.83.5...v0.84.0
1705
1719
  [0.83.5]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.83.4...v0.83.5
package/README.md CHANGED
@@ -175,7 +175,7 @@ Build the branded one-click bundle:
175
175
  npm run build:mcpb
176
176
  ```
177
177
 
178
- The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.86.4`, SHA-256 `b90dc726bfe9dfa7540896de18671423ace99a0a32c86a8c35ca2723bbd6e0ac`). Install it by opening or dragging it into Claude Desktop. Claude displays the `MCP Scraper` install card, icon, API-key configuration field, and manually curated current-release message from the bundle manifest.
178
+ The generated bundle is written to `build/mcpb/mcp-scraper-<version>.mcpb` and copied to `public/downloads/` for the hosted download. The current public bundle is `https://mcpscraper.dev/downloads/mcp-scraper.mcpb` (`0.87.0`, SHA-256 `4805165c18efda420136b9d00862a6d658dd0d4c7bef6d6b77bcb94922ce4d38`). Install it by opening or dragging it into Claude Desktop. Claude displays the `MCP Scraper` install card, icon, API-key configuration field, and manually curated current-release message from the bundle manifest.
179
179
 
180
180
  The MCPB install exposes every tool — web-intelligence plus all `browser_*` tools — through the one `mcp-scraper` server.
181
181
 
@@ -235,9 +235,22 @@ env = { MCP_SCRAPER_API_KEY = "sk_live_your_key" }
235
235
 
236
236
  ## Tools
237
237
 
238
+ ### Two-page PAA harvests
239
+
240
+ Pass this input to `harvest_paa` (or add an `idempotencyKey` for `harvest_paa_start`):
241
+
242
+ ```json
243
+ {"query":"commercial truck insurance","pages":2,"maxQuestions":10}
244
+ ```
245
+
246
+ The harvester attempts page 2 organic results before expanding PAA on the untouched first page. It does not harvest a second PAA graph. `pages` accepts 1 or 2 and defaults to 1; pricing is unchanged. If page 2 is missing, empty, or cannot load, available first-page evidence is retained.
247
+
248
+ Check `pagination.requestedPages`, `pagination.capturedPages`, and `pagination.page2Status` in the MCP result before assuming both pages were captured. Raw REST results place the same fields under `diagnostics.pagination`. Legacy saved results may have no pagination metadata, which MCP formatters expose as `null`.
249
+
238
250
  ### Web-intelligence tools
239
251
 
240
- - `harvest_paa`
252
+ - `harvest_paa` — expand People Also Ask on the original first result page. Optional `pages: 2` captures a second organic result page first; the default is one page.
253
+ - `harvest_paa_start` — start the same harvest as a durable job, with the same optional `pages: 2`.
241
254
  - `search_serp`
242
255
  - `extract_url` — extract normal or Wayback-replayed page copy; Wayback results omit playback chrome and can include a timestamp-matched featured image. Set `preserveMedia:true` to union static and rendered/lazy media, collapse responsive variants, attach up to `maxInlineImages` AI-readable images, and receive an owner-scoped ZIP manifest readable with `archive_read`. Branding output ranks the site logo separately from evidence-bounded proof images such as certifications, awards, memberships, partner/customer marks, and press mentions.
243
256
  - `map_site_urls`
@@ -17,8 +17,8 @@ loadDotEnv();
17
17
  async function main() {
18
18
  const [{ serve }, { app, personalAssistantProductionStartup }, { startWorker }, { migrate }] = await Promise.all([
19
19
  import("@hono/node-server"),
20
- import("../server-4FREXPPR.js"),
21
- import("../worker-OQSFSLNG.js"),
20
+ import("../server-4QWARJET.js"),
21
+ import("../worker-DAVCZ3FP.js"),
22
22
  import("../db-Z34LPZNR.js")
23
23
  ]);
24
24
  const PORT = parseInt(process.env.PORT ?? "3001");
@@ -19,7 +19,7 @@ import {
19
19
  import "../chunk-GGZEC22A.js";
20
20
  import {
21
21
  PACKAGE_VERSION
22
- } from "../chunk-CM62EJ3S.js";
22
+ } from "../chunk-Y3AIFO7C.js";
23
23
 
24
24
  // src/cli/human-cli.ts
25
25
  import { Command } from "commander";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runMcpScraperStdio
4
- } from "../chunk-VFAAYRFS.js";
5
- import "../chunk-FHLNW4L6.js";
4
+ } from "../chunk-6XDNIYGP.js";
5
+ import "../chunk-XX6XUZQM.js";
6
6
  import "../chunk-PGJQDMC2.js";
7
7
  import "../chunk-DNM65UCK.js";
8
8
  import "../chunk-T3MZISOF.js";
@@ -10,10 +10,10 @@ import "../chunk-6HAV7LCE.js";
10
10
  import "../chunk-NKRO2SUO.js";
11
11
  import "../chunk-OM7HVEJ3.js";
12
12
  import "../chunk-OPQIGAFB.js";
13
- import "../chunk-3JRZZVWL.js";
13
+ import "../chunk-UDLPENTY.js";
14
14
  import "../chunk-P7FWOMU7.js";
15
15
  import "../chunk-GGZEC22A.js";
16
- import "../chunk-CM62EJ3S.js";
16
+ import "../chunk-Y3AIFO7C.js";
17
17
  import "../chunk-YXNDOQXN.js";
18
18
 
19
19
  // src/mcp/thorbit-restricted-tool-surface.ts
@@ -1,10 +1,10 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  renderInstallTerminal
4
- } from "../chunk-3JRZZVWL.js";
4
+ } from "../chunk-UDLPENTY.js";
5
5
  import {
6
6
  PACKAGE_VERSION
7
- } from "../chunk-CM62EJ3S.js";
7
+ } from "../chunk-Y3AIFO7C.js";
8
8
 
9
9
  // bin/mcp-scraper-install.ts
10
10
  var noColor = process.argv.includes("--no-color") || process.env.NO_COLOR !== void 0 || process.env.FORCE_COLOR === "0" || !process.stdout.isTTY;
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runMcpScraperStdio
4
- } from "../chunk-VFAAYRFS.js";
5
- import "../chunk-FHLNW4L6.js";
4
+ } from "../chunk-6XDNIYGP.js";
5
+ import "../chunk-XX6XUZQM.js";
6
6
  import "../chunk-PGJQDMC2.js";
7
7
  import "../chunk-DNM65UCK.js";
8
8
  import "../chunk-T3MZISOF.js";
@@ -10,10 +10,10 @@ import "../chunk-6HAV7LCE.js";
10
10
  import "../chunk-NKRO2SUO.js";
11
11
  import "../chunk-OM7HVEJ3.js";
12
12
  import "../chunk-OPQIGAFB.js";
13
- import "../chunk-3JRZZVWL.js";
13
+ import "../chunk-UDLPENTY.js";
14
14
  import "../chunk-P7FWOMU7.js";
15
15
  import "../chunk-GGZEC22A.js";
16
- import "../chunk-CM62EJ3S.js";
16
+ import "../chunk-Y3AIFO7C.js";
17
17
  import "../chunk-YXNDOQXN.js";
18
18
 
19
19
  // bin/mcp-stdio-server.ts
@@ -1,7 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  harvest
4
- } from "../chunk-NH4QAH3X.js";
4
+ } from "../chunk-R2HRN2FW.js";
5
5
  import {
6
6
  browserServiceApiKey
7
7
  } from "../chunk-OM7HVEJ3.js";
@@ -12,13 +12,13 @@ import {
12
12
  registerScheduledResultsMcpTools,
13
13
  registerSerpIntelligenceCaptureTools,
14
14
  resolveDeploymentProfile
15
- } from "./chunk-FHLNW4L6.js";
15
+ } from "./chunk-XX6XUZQM.js";
16
16
  import {
17
17
  renderInstallTerminal
18
- } from "./chunk-3JRZZVWL.js";
18
+ } from "./chunk-UDLPENTY.js";
19
19
  import {
20
20
  PACKAGE_VERSION
21
- } from "./chunk-CM62EJ3S.js";
21
+ } from "./chunk-Y3AIFO7C.js";
22
22
 
23
23
  // src/mcp/stdio-runtime.ts
24
24
  import { readFileSync } from "fs";
@@ -134,6 +134,58 @@ var MapsSelectors = {
134
134
  expandReview: '[data-review-id] button[aria-label*="See more"], [data-review-id] button.w8nwRe'
135
135
  };
136
136
 
137
+ // src/driver/IBrowserDriver.ts
138
+ async function withManagedTemporaryPage(context, setup, operation, signal) {
139
+ let page;
140
+ let ended = false;
141
+ let timer;
142
+ let rejectStop = () => {
143
+ };
144
+ const aborted = () => signal?.reason instanceof DOMException && signal.reason.name === "TimeoutError" ? signal.reason : new RequestAbortedError();
145
+ const onAbort = () => rejectStop(aborted());
146
+ const stop = new Promise((_, reject) => {
147
+ rejectStop = reject;
148
+ });
149
+ const close = async (target) => {
150
+ let closeTimer;
151
+ try {
152
+ await Promise.race([
153
+ target.close().catch(() => {
154
+ }),
155
+ new Promise((resolve) => {
156
+ closeTimer = setTimeout(resolve, 2e3);
157
+ })
158
+ ]);
159
+ } finally {
160
+ if (closeTimer) clearTimeout(closeTimer);
161
+ }
162
+ };
163
+ if (signal?.aborted) throw aborted();
164
+ signal?.addEventListener("abort", onAbort, { once: true });
165
+ timer = setTimeout(() => rejectStop(new DOMException("Optional pagination timed out", "TimeoutError")), 3e4);
166
+ try {
167
+ return await Promise.race([
168
+ (async () => {
169
+ const created = await context.newPage();
170
+ if (ended) {
171
+ await close(created);
172
+ throw new RequestAbortedError();
173
+ }
174
+ page = created;
175
+ await setup(created);
176
+ if (ended) throw new RequestAbortedError();
177
+ return operation(created);
178
+ })(),
179
+ stop
180
+ ]);
181
+ } finally {
182
+ ended = true;
183
+ if (timer) clearTimeout(timer);
184
+ signal?.removeEventListener("abort", onAbort);
185
+ if (page) await close(page);
186
+ }
187
+ }
188
+
137
189
  // src/lib/browser-user-agent.ts
138
190
  var RUNTIME_BROWSER_UA = /\b(?:HeadlessChrome|Chrome|Chromium|Firefox)\/\d+|\bVersion\/\d+.*\bSafari\//;
139
191
  async function detectPageUserAgent(page) {
@@ -523,6 +575,11 @@ var BrowserDriver = class {
523
575
  };
524
576
  });
525
577
  }
578
+ async withTemporaryPage(operation, signal) {
579
+ if (!this.context) throw new Error("Browser context is not available");
580
+ return withManagedTemporaryPage(this.context, async () => {
581
+ }, operation, signal);
582
+ }
526
583
  async navigateToSERP(query, uule, gl, hl, options) {
527
584
  const params = new URLSearchParams({ q: query, gl, hl, pws: "0" });
528
585
  if (options?.tbs) params.set("tbs", options.tbs);
@@ -927,7 +984,10 @@ var BrightDataSerpDriver = class {
927
984
  const customCdp = this.cdpSession;
928
985
  const sessionResponse = await customCdp.send("Browser.getSessionId");
929
986
  this.providerSessionId = validateProviderSessionId(sessionResponse?.sessionId);
930
- await this.page.route("**/*", async (route) => {
987
+ await this.setupPage(this.page);
988
+ }
989
+ async setupPage(page) {
990
+ await page.route("**/*", async (route) => {
931
991
  const request = route.request();
932
992
  const resourceType = request.resourceType();
933
993
  let requestUrl;
@@ -952,6 +1012,10 @@ var BrightDataSerpDriver = class {
952
1012
  await route.continue();
953
1013
  });
954
1014
  }
1015
+ async withTemporaryPage(operation, signal) {
1016
+ if (!this.context || this.closing) throw new Error("Browser context is not available");
1017
+ return withManagedTemporaryPage(this.context, (page) => this.setupPage(page), operation, signal);
1018
+ }
955
1019
  async navigateToSERP(query, uule, gl, hl, options) {
956
1020
  if (!this.page) throw new Error("Browser page is not available");
957
1021
  const params = new URLSearchParams({ q: query, gl, hl, pws: "0" });
@@ -1927,6 +1991,11 @@ function parsedGoogleGoto(value) {
1927
1991
  if (!url || url.protocol !== "https:" || !isGoogleHost(url.hostname) || url.pathname !== "/goto") return null;
1928
1992
  return url.searchParams.get("url") ? url : null;
1929
1993
  }
1994
+ function hasResolvedGotoDestination(value) {
1995
+ if (value.linkType !== "google_goto_redirect" || value.resolutionStatus !== "resolved" || !parsedGoogleGoto(value.rawUrl) || !value.resolvedUrl) return false;
1996
+ const destination = safeHttpUrl(value.resolvedUrl);
1997
+ return Boolean(destination && !isGoogleHost(destination.hostname) && destination.href === value.resolvedUrl && value.url === destination.href);
1998
+ }
1930
1999
  function questionIdFor(question) {
1931
2000
  const normalized = question.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, " ").trim();
1932
2001
  return `paa_${createHash("sha256").update(normalized).digest("hex").slice(0, 16)}`;
@@ -2143,8 +2212,12 @@ async function resolveHarvestResultGoogleGotoLinks(result, options = {}) {
2143
2212
  }
2144
2213
  async function resolveGoogleOutboundLinks(page, values, options = {}) {
2145
2214
  const byRawUrl = /* @__PURE__ */ new Map();
2146
- for (const value of values) byRawUrl.set(value.rawUrl, value);
2147
- const inputs = [...byRawUrl.values()].filter((value) => value.linkType === "google_goto_redirect");
2215
+ for (const value of values) {
2216
+ const existing = byRawUrl.get(value.rawUrl);
2217
+ if (existing && hasResolvedGotoDestination(existing)) continue;
2218
+ byRawUrl.set(value.rawUrl, value.linkType === "google_goto_redirect" && !hasResolvedGotoDestination(value) ? { ...value, url: value.rawUrl, resolvedUrl: null, resolutionStatus: "unresolved" } : value);
2219
+ }
2220
+ const inputs = [...byRawUrl.values()].filter((value) => value.linkType === "google_goto_redirect" && !hasResolvedGotoDestination(value));
2148
2221
  if (inputs.length === 0) return byRawUrl;
2149
2222
  const deadlineMs = Date.now() + (options.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS);
2150
2223
  const direct = await resolveGoogleGotoUrls(inputs.map((value) => value.rawUrl), options);
@@ -2360,7 +2433,7 @@ var PAAExtractor = class {
2360
2433
  ];
2361
2434
  const unique = new Map(candidates.map((link) => [link.rawUrl, link]));
2362
2435
  let resolverNonGoogleRequestsObserved = 0;
2363
- const resolved = await resolveGoogleOutboundLinks(page, unique.values(), {
2436
+ const resolved = await resolveGoogleOutboundLinks(page, candidates, {
2364
2437
  onNetworkAudit: (audit) => {
2365
2438
  resolverNonGoogleRequestsObserved = audit.nonGoogleRequestsObserved;
2366
2439
  }
@@ -2611,6 +2684,7 @@ var PAAExtractor = class {
2611
2684
  diagnostics: {
2612
2685
  completionStatus: "paa_found",
2613
2686
  problem: null,
2687
+ ...material?.pagination ? { pagination: { ...material.pagination } } : {},
2614
2688
  resultQuality: "partial",
2615
2689
  degradedResult: false,
2616
2690
  retryRecommended: true,
@@ -3575,6 +3649,55 @@ var PAAExtractor = class {
3575
3649
  ...locationEvidence ? { locationEvidence } : {}
3576
3650
  };
3577
3651
  }
3652
+ async captureSecondOrganicPage(page, options, signal) {
3653
+ this.throwIfAborted(signal);
3654
+ if (!this.driver.withTemporaryPage) return { organic: [], status: "failed", failureCode: "unsupported_driver" };
3655
+ let guardedFailure;
3656
+ try {
3657
+ const initial = new URL(page.url());
3658
+ const valid = (raw) => {
3659
+ try {
3660
+ const url = new URL(raw, initial);
3661
+ return initial.origin === "https://www.google.com" && url.origin === initial.origin && !url.username && !url.password && url.pathname === "/search" && url.searchParams.getAll("q").length === 1 && url.searchParams.get("q") === options.query && url.searchParams.getAll("start").length === 1 && url.searchParams.get("start") === "10" && (!url.searchParams.has("num") || url.searchParams.get("num") === "10") && ["gl", "hl"].every((key) => !url.searchParams.has(key) || url.searchParams.getAll(key).length === 1 && url.searchParams.get(key) === options[key]) && ["uule", "tbs", "udm", "tbm", "pws"].every((key) => url.searchParams.getAll(key).length <= 1 && url.searchParams.get(key) === initial.searchParams.get(key));
3662
+ } catch {
3663
+ return false;
3664
+ }
3665
+ };
3666
+ const href = await page.evaluate(() => {
3667
+ const next = document.querySelector('a#pnnext, a[rel="next"]');
3668
+ return next?.getAttribute("href") ?? null;
3669
+ });
3670
+ if (!href) return { organic: [], status: "unavailable", failureCode: "missing_next" };
3671
+ if (!valid(href)) return { organic: [], status: "unavailable", failureCode: "invalid_next" };
3672
+ const organic = await this.driver.withTemporaryPage(async (second) => {
3673
+ await second.route("**/*", async (route) => {
3674
+ const request = route.request();
3675
+ if (request.isNavigationRequest() && request.frame() === second.mainFrame() && !valid(request.url())) {
3676
+ guardedFailure = /^https:\/\/www\.google\.com\/sorry(?:\/|\?)/.test(request.url()) ? "captcha" : "invalid_next";
3677
+ await route.abort("blockedbyclient");
3678
+ } else await route.fallback();
3679
+ });
3680
+ await second.goto(new URL(href, initial).href, { waitUntil: "domcontentloaded", timeout: 2e4 });
3681
+ this.throwIfAborted(signal);
3682
+ await this.throwIfCaptcha(second, "Google SERP page 2");
3683
+ if (!valid(second.url())) {
3684
+ guardedFailure = "invalid_next";
3685
+ throw new Error("Invalid pagination landing");
3686
+ }
3687
+ return this.extractOrganicResults(second);
3688
+ }, signal);
3689
+ this.throwIfAborted(signal);
3690
+ return organic.length > 0 ? { organic: organic.map((row) => ({ ...row, position: row.position + 10 })), status: "captured" } : { organic: [], status: "unavailable", failureCode: "empty_page" };
3691
+ } catch (err) {
3692
+ this.throwIfAborted(signal);
3693
+ if (err instanceof RequestAbortedError) throw err;
3694
+ return {
3695
+ organic: [],
3696
+ status: "failed",
3697
+ failureCode: guardedFailure ?? (err instanceof CaptchaError ? "captcha" : err instanceof Error && (err.name === "TimeoutError" || /timed? ?out/i.test(err.message)) ? "timeout" : "navigation_error")
3698
+ };
3699
+ }
3700
+ }
3578
3701
  async extract(options, signal) {
3579
3702
  const startMs = Date.now();
3580
3703
  this.completeness = { paaWithoutAnswer: 0, paaWithoutSource: 0, paaAnswersRecovered: 0, aioShareCaptured: null };
@@ -3778,23 +3901,49 @@ var PAAExtractor = class {
3778
3901
  const initialLocationEvidence = options.debug ? inferSerpLocationEvidence(canonicalLocation, organicResults, localPack) : void 0;
3779
3902
  this.reporter.onVideos(videos);
3780
3903
  this.reporter.onForums(forums);
3904
+ const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
3905
+ let pagination = {
3906
+ requestedPages: (options.pages ?? 1) >= 2 ? 2 : 1,
3907
+ capturedPages: 1,
3908
+ page2Status: (options.pages ?? 1) >= 2 ? "not_attempted" : "not_requested",
3909
+ page1OrganicCount: organicResults.length,
3910
+ page2OrganicCount: 0
3911
+ };
3912
+ let allOrganic = organicResults;
3913
+ this.checkpointMaterial = {
3914
+ surface: aiSurfaces.surface,
3915
+ aiOverview: aiSurfaces.aiOverview,
3916
+ aiMode: aiSurfaces.aiMode,
3917
+ whatPeopleSaying,
3918
+ videos,
3919
+ forums,
3920
+ organicResults: allOrganic,
3921
+ localPack,
3922
+ entityIds,
3923
+ pagination
3924
+ };
3925
+ await this.emitProgress("serp_captured");
3926
+ if (pagination.requestedPages === 2) {
3927
+ const second = await this.captureSecondOrganicPage(page, executionOptions, signal);
3928
+ allOrganic = [...organicResults, ...second.organic];
3929
+ pagination = {
3930
+ ...pagination,
3931
+ capturedPages: second.status === "captured" ? 2 : 1,
3932
+ page2Status: second.status,
3933
+ page2OrganicCount: second.organic.length,
3934
+ ...second.failureCode ? { failureCode: second.failureCode } : {}
3935
+ };
3936
+ this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic, pagination };
3937
+ await this.emitProgress("serp_captured");
3938
+ await this.resolveMaterialLinks(page, [], allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
3939
+ this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic };
3940
+ await this.emitProgress("serp_captured");
3941
+ }
3942
+ const locationEvidence = options.debug ? inferSerpLocationEvidence(canonicalLocation, allOrganic, localPack) : initialLocationEvidence;
3781
3943
  if (!hasPaa) {
3782
- let noPaaOrganic = organicResults;
3783
- let locationEvidence2 = initialLocationEvidence;
3784
- if ((options.pages ?? 1) >= 2) {
3785
- const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
3786
- if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
3787
- if (uule) p2params.set("uule", uule);
3788
- await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
3789
- await this.throwIfCaptcha(page, "Google SERP page 2");
3790
- const p2organic = await this.extractOrganicResults(page);
3791
- noPaaOrganic = [...organicResults, ...p2organic.map((r) => ({ ...r, position: r.position + 10 }))];
3792
- if (options.debug) {
3793
- locationEvidence2 = inferSerpLocationEvidence(canonicalLocation, noPaaOrganic, localPack);
3794
- }
3795
- }
3796
- const aiSurfaces2 = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
3797
- await this.resolveMaterialLinks(page, [], noPaaOrganic, aiSurfaces2.aiOverview, aiSurfaces2.aiMode);
3944
+ await this.resolveMaterialLinks(page, [], allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
3945
+ this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic };
3946
+ await this.emitProgress("serp_captured");
3798
3947
  const stats2 = {
3799
3948
  seed: executionOptions.query,
3800
3949
  totalQuestions: 0,
@@ -3811,39 +3960,27 @@ var PAAExtractor = class {
3811
3960
  completionStatus: "no_paa",
3812
3961
  noPaaObserved: true,
3813
3962
  problem: null,
3963
+ pagination,
3814
3964
  paaLifecycle: { ...this.paaLifecycle },
3815
3965
  completeness: { ...this.completeness },
3816
3966
  links: { ...this.linkDiagnostics },
3817
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2, locationResolution) } : {}
3967
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence, locationResolution) } : {}
3818
3968
  },
3819
3969
  totalQuestions: 0,
3820
- surface: aiSurfaces2.surface,
3821
- aiOverview: aiSurfaces2.aiOverview,
3822
- aiMode: aiSurfaces2.aiMode,
3970
+ surface: aiSurfaces.surface,
3971
+ aiOverview: aiSurfaces.aiOverview,
3972
+ aiMode: aiSurfaces.aiMode,
3823
3973
  whatPeopleSaying,
3824
3974
  tree: [],
3825
3975
  flat: [],
3826
3976
  videos,
3827
3977
  forums,
3828
- organicResults: noPaaOrganic,
3978
+ organicResults: allOrganic,
3829
3979
  localPack,
3830
3980
  entityIds,
3831
3981
  stats: stats2
3832
3982
  };
3833
3983
  }
3834
- const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
3835
- this.checkpointMaterial = {
3836
- surface: aiSurfaces.surface,
3837
- aiOverview: aiSurfaces.aiOverview,
3838
- aiMode: aiSurfaces.aiMode,
3839
- whatPeopleSaying,
3840
- videos,
3841
- forums,
3842
- organicResults,
3843
- localPack,
3844
- entityIds
3845
- };
3846
- await this.emitProgress("serp_captured");
3847
3984
  const flat = await this.runBFS(page, executionOptions, signal);
3848
3985
  this.throwIfAborted(signal);
3849
3986
  const shortVidsParams = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
@@ -3864,20 +4001,6 @@ var PAAExtractor = class {
3864
4001
  }
3865
4002
  }
3866
4003
  this.reporter.onVideos(shortVideos);
3867
- let allOrganic = organicResults;
3868
- let locationEvidence = initialLocationEvidence;
3869
- if ((options.pages ?? 1) >= 2) {
3870
- const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
3871
- if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
3872
- if (uule) p2params.set("uule", uule);
3873
- await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
3874
- await this.throwIfCaptcha(page, "Google SERP page 2");
3875
- const p2organic = await this.extractOrganicResults(page);
3876
- allOrganic = [...organicResults, ...p2organic.map((r) => ({ ...r, position: r.position + 10 }))];
3877
- if (options.debug) {
3878
- locationEvidence = inferSerpLocationEvidence(canonicalLocation, allOrganic, localPack);
3879
- }
3880
- }
3881
4004
  await this.resolveMaterialLinks(page, flat, allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
3882
4005
  this.checkpointMaterial = {
3883
4006
  ...this.checkpointMaterial,
@@ -3887,6 +4010,7 @@ var PAAExtractor = class {
3887
4010
  videos: [...videos, ...shortVideos],
3888
4011
  organicResults: allOrganic
3889
4012
  };
4013
+ await this.emitProgress("expansion_finished");
3890
4014
  const allVideos = [...videos, ...shortVideos];
3891
4015
  const tree = this.buildTree(flat, executionOptions.query);
3892
4016
  const stats = {
@@ -3904,6 +4028,7 @@ var PAAExtractor = class {
3904
4028
  diagnostics: {
3905
4029
  completionStatus: "paa_found",
3906
4030
  problem: null,
4031
+ pagination,
3907
4032
  paaLifecycle: { ...this.paaLifecycle },
3908
4033
  completeness: { ...this.completeness },
3909
4034
  links: { ...this.linkDiagnostics },
@@ -1,6 +1,6 @@
1
1
  // release-message.json
2
2
  var release_message_default = {
3
- message: "Reddit thread hydration now retries two fresh residential sessions and two managed-browser backups, stopping at the first accepted result while preserving attempt and provider-cost telemetry."
3
+ message: "PAA harvests can now request two organic result pages before expanding questions on the original first page. Use pages: 2 in MCP, SDKs, or the dashboard and check the reported capture status; an unavailable second page preserves first-page evidence."
4
4
  };
5
5
 
6
6
  // src/install-terminal.ts