mcp-scraper 0.79.0 → 0.79.2

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,23 @@ All notable changes to MCP Scraper are documented here. The format is based on [
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ### Fixed
8
+
9
+ - Routed canonical `/assistant` page loads to the web app and the redacted private Assistant readiness endpoint to the main API function, preventing production 404s after the 0.79.1 launch.
10
+
11
+ ## [0.79.2] - 2026-08-28
12
+
13
+ ### Fixed
14
+
15
+ - Rejected Google loading and error placeholders as PAA answers, retried interrupted controls without misreporting frontier exhaustion, preserved complete answer/source material from retained DOM, and resolved surviving Google `/goto` evidence after a late browser disconnect without visiting publisher pages.
16
+ - Advanced requested question counts through bounded newest-frontier click batches with 0.7–1.4 second observation windows, reducing a verified 35-question complete harvest from 347 seconds to 83 seconds while retaining actual multi-question click growth.
17
+
18
+ ## [0.79.1] - 2026-08-28
19
+
20
+ ### Fixed
21
+
22
+ - Contained unresolved Personal Assistant provider-authority gates to the assistant surface, which remains fail-closed with 503 readiness, instead of crashing unrelated MCP, billing, analytics, migration, and cron routes in the shared API process.
23
+
7
24
  ## [0.79.0] - 2026-08-28
8
25
 
9
26
  ### Added
@@ -1472,7 +1489,16 @@ All notable changes to MCP Scraper are documented here. The format is based on [
1472
1489
  - Write actions remain unavailable until the account owner explicitly enables them.
1473
1490
  - Provider-specific connection data is normalized into one agent-facing contract.
1474
1491
 
1475
- [Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.73.5...HEAD
1492
+ [Unreleased]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.79.2...HEAD
1493
+ [0.79.2]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.79.1...v0.79.2
1494
+ [0.79.1]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.79.0...v0.79.1
1495
+ [0.79.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.78.0...v0.79.0
1496
+ [0.78.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.77.0...v0.78.0
1497
+ [0.77.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.76.0...v0.77.0
1498
+ [0.76.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.75.1...v0.76.0
1499
+ [0.75.1]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.75.0...v0.75.1
1500
+ [0.75.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.74.0...v0.75.0
1501
+ [0.74.0]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.73.5...v0.74.0
1476
1502
  [0.73.5]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.73.4...v0.73.5
1477
1503
  [0.73.4]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.73.3...v0.73.4
1478
1504
  [0.73.3]: https://github.com/VilovietaSEO/mcp-scraper/compare/v0.73.2...v0.73.3
package/README.md CHANGED
@@ -173,7 +173,7 @@ Build the branded one-click bundle:
173
173
  npm run build:mcpb
174
174
  ```
175
175
 
176
- 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.79.0`, SHA-256 `c4728bfebff747f752f5f8b0e4188d45c46f794e2756238b71f951661ed048b2`). 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.
176
+ 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.79.2`, SHA-256 `5d9ac8818c9dc6e8667b5734bd4253d3012df82a6976e45eeee22f37bb2026a1`). 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.
177
177
 
178
178
  The MCPB install exposes every tool — web-intelligence plus all `browser_*` tools — through the one `mcp-scraper` server.
179
179
 
@@ -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-QJUBOH6L.js"),
21
- import("../worker-VU6JCCLG.js"),
20
+ import("../server-5OSHEEVT.js"),
21
+ import("../worker-2TYCYFPM.js"),
22
22
  import("../db-TA567JLA.js")
23
23
  ]);
24
24
  const PORT = parseInt(process.env.PORT ?? "3001");
@@ -13,7 +13,7 @@ import "../chunk-VXLU74YZ.js";
13
13
  import "../chunk-GGZEC22A.js";
14
14
  import {
15
15
  PACKAGE_VERSION
16
- } from "../chunk-ATIVTEML.js";
16
+ } from "../chunk-2RTELL6M.js";
17
17
 
18
18
  // src/cli/human-cli.ts
19
19
  import { Command } from "commander";
@@ -1,8 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  runMcpScraperStdio
4
- } from "../chunk-Q54QVYJ2.js";
5
- import "../chunk-2JF4ECSJ.js";
4
+ } from "../chunk-XUM2LEGU.js";
5
+ import "../chunk-JECPQBLZ.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-B6L73AO4.js";
10
10
  import "../chunk-VFCUGTCF.js";
11
11
  import "../chunk-OM7HVEJ3.js";
12
12
  import "../chunk-NGLZ6UJR.js";
13
- import "../chunk-R4YRH5PG.js";
13
+ import "../chunk-6FL7ZZCF.js";
14
14
  import "../chunk-VXLU74YZ.js";
15
15
  import "../chunk-GGZEC22A.js";
16
- import "../chunk-ATIVTEML.js";
16
+ import "../chunk-2RTELL6M.js";
17
17
  import "../chunk-OU4FV2RP.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-R4YRH5PG.js";
4
+ } from "../chunk-6FL7ZZCF.js";
5
5
  import {
6
6
  PACKAGE_VERSION
7
- } from "../chunk-ATIVTEML.js";
7
+ } from "../chunk-2RTELL6M.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-Q54QVYJ2.js";
5
- import "../chunk-2JF4ECSJ.js";
4
+ } from "../chunk-XUM2LEGU.js";
5
+ import "../chunk-JECPQBLZ.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-B6L73AO4.js";
10
10
  import "../chunk-VFCUGTCF.js";
11
11
  import "../chunk-OM7HVEJ3.js";
12
12
  import "../chunk-NGLZ6UJR.js";
13
- import "../chunk-R4YRH5PG.js";
13
+ import "../chunk-6FL7ZZCF.js";
14
14
  import "../chunk-VXLU74YZ.js";
15
15
  import "../chunk-GGZEC22A.js";
16
- import "../chunk-ATIVTEML.js";
16
+ import "../chunk-2RTELL6M.js";
17
17
  import "../chunk-OU4FV2RP.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-UFJJGXVO.js";
4
+ } from "../chunk-WEEAXPWK.js";
5
5
  import {
6
6
  browserServiceApiKey
7
7
  } from "../chunk-OM7HVEJ3.js";
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var PACKAGE_VERSION = "0.79.0";
2
+ var PACKAGE_VERSION = "0.79.2";
3
3
 
4
4
  export {
5
5
  PACKAGE_VERSION
@@ -1,6 +1,6 @@
1
1
  // release-message.json
2
2
  var release_message_default = {
3
- message: "PAA harvests now distinguish a proven end of the question frontier from an interrupted browser, automatically resume the same durable job, and preserve captured questions, answers, AI Overview citations, and ranked SERP links."
3
+ message: "PAA harvests now reject Google error placeholders, retry interrupted controls without claiming the frontier ended, advance the newest question frontier in bounded batches, preserve complete answers and sources, and resolve retained Google redirect evidence even after the browser session closes."
4
4
  };
5
5
 
6
6
  // src/install-terminal.ts
@@ -45,7 +45,7 @@ import {
45
45
  } from "./chunk-VXLU74YZ.js";
46
46
  import {
47
47
  PACKAGE_VERSION
48
- } from "./chunk-ATIVTEML.js";
48
+ } from "./chunk-2RTELL6M.js";
49
49
  import {
50
50
  PUBLIC_ERROR_CODES,
51
51
  buildPublicErrorEnvelope,
@@ -2054,6 +2054,93 @@ async function resolveGoogleGotoUrls(rawUrls, options = {}) {
2054
2054
  await Promise.all(Array.from({ length: Math.min(concurrency, inputs.length) }, () => worker()));
2055
2055
  return { resolutions: [...records.values()], nonGoogleRequestsObserved: 0 };
2056
2056
  }
2057
+ function resolvedSite(link, fallback) {
2058
+ try {
2059
+ return new URL(link.resolvedUrl ?? link.url).hostname.replace(/^www\./, "");
2060
+ } catch {
2061
+ return fallback;
2062
+ }
2063
+ }
2064
+ async function resolveHarvestResultGoogleGotoLinks(result, options = {}) {
2065
+ const treeNodes = [];
2066
+ const visit = (nodes) => {
2067
+ for (const node of nodes) {
2068
+ treeNodes.push(node);
2069
+ visit(node.children ?? []);
2070
+ }
2071
+ };
2072
+ visit(result.tree ?? []);
2073
+ const candidates = [
2074
+ ...result.flat.flatMap((row) => row.sources ?? []),
2075
+ ...treeNodes.flatMap((node) => node.sources),
2076
+ ...result.organicResults ?? [],
2077
+ ...(result.aiOverview?.citations ?? []).map(({ text: _text, href, ...link }) => ({ ...link, url: href })),
2078
+ ...(result.aiMode?.citations ?? []).map(({ text: _text, href, ...link }) => ({ ...link, url: href }))
2079
+ ];
2080
+ const unique = new Map(candidates.filter((link) => Boolean(link?.rawUrl)).map((link) => [link.rawUrl, link]));
2081
+ const gotoUrls = [...unique.values()].filter((link) => link.linkType === "google_goto_redirect" && link.resolutionStatus !== "resolved").map((link) => link.rawUrl);
2082
+ if (gotoUrls.length > 0) {
2083
+ const batch = await resolveGoogleGotoUrls(gotoUrls, options);
2084
+ for (const resolution2 of batch.resolutions) {
2085
+ const link = unique.get(resolution2.rawUrl);
2086
+ if (!link) continue;
2087
+ unique.set(resolution2.rawUrl, resolution2.resolutionStatus === "resolved" && resolution2.resolvedUrl ? { ...link, url: resolution2.resolvedUrl, resolvedUrl: resolution2.resolvedUrl, resolutionStatus: "resolved" } : { ...link, resolutionStatus: resolution2.resolutionStatus });
2088
+ }
2089
+ }
2090
+ const replace = (link) => ({
2091
+ ...link,
2092
+ ...unique.get(link.rawUrl) ?? link
2093
+ });
2094
+ const replaceSources = (sources) => sources.map((source) => {
2095
+ const link = replace(source);
2096
+ return { ...link, site: resolvedSite(link, source.site) };
2097
+ });
2098
+ for (const row of result.flat) {
2099
+ row.sources = replaceSources(row.sources ?? []);
2100
+ const primary = row.sources[0];
2101
+ row.source_title = primary?.title ?? row.source_title;
2102
+ row.source_site = primary?.site ?? row.source_site;
2103
+ row.source_cite = primary?.url ?? row.source_cite;
2104
+ }
2105
+ for (const node of treeNodes) {
2106
+ node.sources = replaceSources(node.sources ?? []);
2107
+ const primary = node.sources[0];
2108
+ node.sourceTitle = primary?.title ?? node.sourceTitle;
2109
+ node.sourceSite = primary?.site ?? node.sourceSite;
2110
+ node.sourceCite = primary?.url ?? node.sourceCite;
2111
+ }
2112
+ for (let index = 0; index < (result.organicResults?.length ?? 0); index++) {
2113
+ const link = replace(result.organicResults[index]);
2114
+ result.organicResults[index] = { ...link, domain: resolvedSite(link, result.organicResults[index].domain) ?? "" };
2115
+ }
2116
+ const replaceCitation = (citation) => {
2117
+ const link = replace({
2118
+ url: citation.href,
2119
+ rawUrl: citation.rawUrl,
2120
+ resolvedUrl: citation.resolvedUrl,
2121
+ linkType: citation.linkType,
2122
+ resolutionStatus: citation.resolutionStatus
2123
+ });
2124
+ const { url, ...metadata } = link;
2125
+ return { ...citation, ...metadata, href: url };
2126
+ };
2127
+ if (result.aiOverview) result.aiOverview.citations = (result.aiOverview.citations ?? []).map(replaceCitation);
2128
+ if (result.aiMode) result.aiMode.citations = (result.aiMode.citations ?? []).map(replaceCitation);
2129
+ const values = [...unique.values()];
2130
+ result.diagnostics.links = {
2131
+ linkCandidatesObserved: unique.size,
2132
+ plainLinksObserved: values.filter((link) => link.linkType === "plain").length,
2133
+ googleUrlRedirectsObserved: values.filter((link) => link.linkType === "google_url_redirect").length,
2134
+ googleGotoRedirectsObserved: values.filter((link) => link.linkType === "google_goto_redirect").length,
2135
+ redirectsResolved: values.filter((link) => link.linkType !== "plain" && link.resolutionStatus === "resolved").length,
2136
+ redirectsUnresolved: values.filter((link) => link.resolutionStatus === "unresolved").length,
2137
+ redirectsRejected: values.filter((link) => link.resolutionStatus === "rejected").length,
2138
+ resolverNonGoogleRequestsObserved: 0,
2139
+ paaSourcesPreserved: result.flat.reduce((total, row) => total + row.sources.length, 0),
2140
+ aiOverviewLabelValidated: result.aiOverview?.detected ?? false
2141
+ };
2142
+ return result;
2143
+ }
2057
2144
  async function resolveGoogleOutboundLinks(page, values, options = {}) {
2058
2145
  const byRawUrl = /* @__PURE__ */ new Map();
2059
2146
  for (const value of values) byRawUrl.set(value.rawUrl, value);
@@ -2174,6 +2261,10 @@ async function resolveGoogleOutboundLinks(page, values, options = {}) {
2174
2261
  }
2175
2262
 
2176
2263
  // src/extractor/PAAExtractor.ts
2264
+ var PAA_ANSWER_PLACEHOLDER_PATTERN = String.raw`^(?:an error has occurred(?:[.!…]\s*)?(?:please try again later[.!…]?)?|loading|please wait|try again|something went wrong)[.!…\s]*$`;
2265
+ var PAA_CLICK_EARLIEST_CONFIRMATION_MS = 700;
2266
+ var PAA_CLICK_ATTEMPT_BUDGET_MS = 1400;
2267
+ var PAA_FRONTIER_BATCH_SIZE = 3;
2177
2268
  var PAAExtractor = class {
2178
2269
  constructor(driver, reporter, onPaaProgress) {
2179
2270
  this.driver = driver;
@@ -2318,7 +2409,7 @@ var PAAExtractor = class {
2318
2409
  }
2319
2410
  async extractVisibleItems(page, onlyQuestion) {
2320
2411
  const sels = PAASelectors;
2321
- const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2 }) => {
2412
+ const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2, answerPlaceholderPattern }) => {
2322
2413
  function cleanText(el) {
2323
2414
  if (!el) return "";
2324
2415
  const parts = [];
@@ -2345,12 +2436,13 @@ var PAAExtractor = class {
2345
2436
  const aiAnswerBody = answerContainer?.querySelector(selectors.aiAnswerBody);
2346
2437
  const answerElement = aiAnswerBody ?? answerContainer;
2347
2438
  const answerText = cleanText(answerElement);
2439
+ const answerIsPlaceholder = new RegExp(answerPlaceholderPattern, "i").test(answerText);
2348
2440
  const citationCandidates = [
2349
2441
  ...Array.from((answerElement ?? pair).querySelectorAll("a[href]")),
2350
2442
  ...Array.from(pair.querySelectorAll(selectors.aiCitation)),
2351
2443
  ...Array.from(pair.querySelectorAll(selectors.aiSourceCard))
2352
2444
  ];
2353
- const hasLoadedAnswer = expanded || answerText.length > 30 && !/an error has occurred/i.test(answerText.slice(0, 80)) && citationCandidates.some((anchor) => /^https?:\/\//.test(anchor.href));
2445
+ const hasLoadedAnswer = !answerIsPlaceholder && (expanded && answerText.length >= 10 || answerText.length > 30 && citationCandidates.some((anchor) => /^https?:\/\//.test(anchor.href)));
2354
2446
  let sourceTitle = hasLoadedAnswer ? pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0 : void 0;
2355
2447
  let sourceSite = hasLoadedAnswer ? pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0 : void 0;
2356
2448
  const sources = [];
@@ -2374,7 +2466,7 @@ var PAAExtractor = class {
2374
2466
  sources
2375
2467
  };
2376
2468
  });
2377
- }, { selectors: sels, onlyQuestion });
2469
+ }, { selectors: sels, onlyQuestion, answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN });
2378
2470
  const baseUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
2379
2471
  return raw.flatMap((item) => {
2380
2472
  const sources = [];
@@ -2565,44 +2657,52 @@ var PAAExtractor = class {
2565
2657
  questionEl: PAASelectors.itemQuestionEl,
2566
2658
  answerSel: PAASelectors.answerContainer,
2567
2659
  expandedClass: PAASelectors.expandedClass,
2568
- clickTarget: PAASelectors.clickTarget
2660
+ clickTarget: PAASelectors.clickTarget,
2661
+ answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN,
2662
+ questionsOnly: options.questionsOnly
2569
2663
  };
2570
2664
  const readItemStates = () => page.evaluate(
2571
- ({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel }) => {
2572
- return Array.from(document.querySelectorAll(sel)).map((el) => ({
2573
- q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
2574
- done: el.classList.contains(expandedClass) || el.querySelector(clickTarget)?.getAttribute("aria-expanded") === "true" || Boolean((el.querySelector(answerSel)?.textContent ?? "").trim())
2575
- })).filter((s) => s.q);
2665
+ ({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel, answerPlaceholderPattern, questionsOnly }) => {
2666
+ return Array.from(document.querySelectorAll(sel)).map((el, index) => {
2667
+ const answerText = (el.querySelector(answerSel)?.textContent ?? "").replace(/\s+/g, " ").trim();
2668
+ const expanded = el.classList.contains(expandedClass) || el.querySelector(clickTarget)?.getAttribute("aria-expanded") === "true";
2669
+ const answerReady = answerText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(answerText);
2670
+ return {
2671
+ q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
2672
+ done: expanded && (questionsOnly || answerReady),
2673
+ expanded,
2674
+ index
2675
+ };
2676
+ }).filter((s) => s.q);
2576
2677
  },
2577
2678
  itemArgs
2578
2679
  );
2579
- const expandOneItemSerially = async (q, beforeCount) => {
2680
+ const expandOneItemSerially = async (q, beforeCount, itemIndex, wasExpanded) => {
2580
2681
  const clickStartedMs = Date.now();
2581
- const items = page.locator(itemArgs.sel);
2582
- const itemIndex = await items.evaluateAll(
2583
- (elements, { dataQ, dataInitQ, target }) => elements.findIndex((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ)) === target),
2584
- { dataQ: itemArgs.dataQ, dataInitQ: itemArgs.dataInitQ, target: q }
2585
- ).catch(() => -1);
2586
- const clickTarget = itemIndex >= 0 ? items.nth(itemIndex).locator(itemArgs.clickTarget) : null;
2682
+ const clickTarget = itemIndex >= 0 ? page.locator(itemArgs.sel).nth(itemIndex).locator(itemArgs.clickTarget) : null;
2587
2683
  const clicked = clickTarget ? await (async () => {
2588
- await clickTarget.scrollIntoViewIfNeeded({ timeout: 5e3 });
2589
- const box = await clickTarget.boundingBox({ timeout: 5e3 });
2590
- if (!box) return false;
2591
- await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
2684
+ if (wasExpanded) {
2685
+ await clickTarget.click({ timeout: 2e3, force: true });
2686
+ await page.waitForTimeout(250);
2687
+ }
2688
+ await clickTarget.click({ timeout: 2e3, force: true });
2592
2689
  return true;
2593
2690
  })().catch(() => false) : false;
2594
2691
  if (!clicked) return { dispatched: false, confirmed: false };
2595
- const remainingHumanClickDelayMs = 700 - (Date.now() - clickStartedMs);
2596
- if (remainingHumanClickDelayMs > 0) await page.waitForTimeout(remainingHumanClickDelayMs);
2692
+ const remainingConfirmationMs = Math.max(100, PAA_CLICK_ATTEMPT_BUDGET_MS - (Date.now() - clickStartedMs));
2597
2693
  const confirmed = await page.waitForFunction(
2598
- ({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, target, min }) => {
2599
- const items2 = Array.from(document.querySelectorAll(sel));
2600
- const item = items2.find((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ) || element.querySelector(questionEl)?.innerText?.trim()) === target);
2601
- if (!item) return items2.length > min;
2602
- return item.classList.contains(expandedClass) || item.querySelector(clickTarget2)?.getAttribute("aria-expanded") === "true" || Boolean((item.querySelector(answerSel)?.textContent ?? "").trim()) || items2.length > min;
2694
+ ({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, answerPlaceholderPattern, questionsOnly, target, min, earliestConfirmationAt }) => {
2695
+ if (Date.now() < earliestConfirmationAt) return false;
2696
+ const items = Array.from(document.querySelectorAll(sel));
2697
+ const item = items.find((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ) || element.querySelector(questionEl)?.innerText?.trim()) === target);
2698
+ if (!item) return false;
2699
+ const expanded = item.classList.contains(expandedClass) || item.querySelector(clickTarget2)?.getAttribute("aria-expanded") === "true";
2700
+ if (questionsOnly) return expanded || items.length > min;
2701
+ const answerText = (item.querySelector(answerSel)?.textContent ?? "").replace(/\s+/g, " ").trim();
2702
+ return answerText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(answerText);
2603
2703
  },
2604
- { ...itemArgs, target: q, min: beforeCount },
2605
- { timeout: 2500, polling: 100 }
2704
+ { ...itemArgs, target: q, min: beforeCount, earliestConfirmationAt: clickStartedMs + PAA_CLICK_EARLIEST_CONFIRMATION_MS },
2705
+ { timeout: remainingConfirmationMs, polling: 100 }
2606
2706
  ).catch(() => null);
2607
2707
  return { dispatched: true, confirmed: Boolean(confirmed) };
2608
2708
  };
@@ -2647,10 +2747,43 @@ var PAAExtractor = class {
2647
2747
  };
2648
2748
  break;
2649
2749
  }
2750
+ if (!options.questionsOnly && seenQs.size >= options.maxQuestions) {
2751
+ let materialChanged = false;
2752
+ const keptQuestions = new Set(orderedQs.slice(0, options.maxQuestions));
2753
+ for (const item of await this.extractVisibleItems(page)) {
2754
+ if (!keptQuestions.has(item.question)) continue;
2755
+ const existing = capturedItems.get(item.question);
2756
+ const merged = {
2757
+ ...item,
2758
+ answer: item.answer ?? existing?.answer,
2759
+ sourceTitle: item.sourceTitle ?? existing?.sourceTitle,
2760
+ sourceSite: item.sourceSite ?? existing?.sourceSite,
2761
+ sourceCite: item.sourceCite ?? existing?.sourceCite,
2762
+ sources: this.mergePaaSources(existing?.sources, item.sources)
2763
+ };
2764
+ capturedItems.set(item.question, merged);
2765
+ materialChanged = this.upsertCollectedRow(this.toFlatRow(merged, 1, null, options.query)) || materialChanged;
2766
+ }
2767
+ if (materialChanged) await this.emitProgress("answer_captured");
2768
+ if ([...keptQuestions].every((question) => {
2769
+ const item = capturedItems.get(question);
2770
+ return Boolean(item?.answer?.trim() && item.sources.length > 0);
2771
+ })) {
2772
+ failedInteractions.clear();
2773
+ this.paaLifecycle = {
2774
+ ...this.paaLifecycle,
2775
+ discoveryStatus: "target_reached",
2776
+ targetStatus: "met",
2777
+ failedInteractions: 0,
2778
+ completionReason: "The requested unique-question count and its answer/source material were observed."
2779
+ };
2780
+ break;
2781
+ }
2782
+ }
2650
2783
  const kept = new Set(orderedQs.slice(0, options.maxQuestions));
2651
2784
  const clickable = states.filter((state) => kept.has(state.q) && !state.done && (clickAttempts.get(state.q) ?? 0) < MAX_CLICK_ATTEMPTS && !abandonedNeverReclick.has(state.q));
2652
- const target = clickable[0];
2653
- if (!target) {
2785
+ const targets = clickable.slice(-PAA_FRONTIER_BATCH_SIZE);
2786
+ if (targets.length === 0) {
2654
2787
  if (seenQs.size >= options.maxQuestions) {
2655
2788
  this.paaLifecycle = {
2656
2789
  ...this.paaLifecycle,
@@ -2698,31 +2831,43 @@ var PAAExtractor = class {
2698
2831
  };
2699
2832
  break;
2700
2833
  }
2701
- this.reporter.onDepth(++round);
2702
2834
  await this.throwIfCaptcha(page, "Google PAA expansion");
2703
- clickAttempts.set(target.q, (clickAttempts.get(target.q) ?? 0) + 1);
2704
- const expansionResult = await expandOneItemSerially(target.q, states.length);
2705
- if (expansionResult.dispatched) this.clickedQuestions.add(target.q);
2706
- if (!options.questionsOnly) {
2707
- let clickedItem = (await this.extractVisibleItems(page, target.q))[0];
2708
- if (!clickedItem?.answer) {
2709
- await page.waitForTimeout(1300);
2710
- clickedItem = (await this.extractVisibleItems(page, target.q))[0];
2835
+ for (const target of targets) {
2836
+ this.reporter.onDepth(++round);
2837
+ if ((clickAttempts.get(target.q) ?? 0) > 0) {
2838
+ this.paaLifecycle = {
2839
+ ...this.paaLifecycle,
2840
+ automaticRetries: this.paaLifecycle.automaticRetries + 1
2841
+ };
2711
2842
  }
2712
- if (clickedItem) {
2713
- capturedItems.set(target.q, clickedItem);
2843
+ clickAttempts.set(target.q, (clickAttempts.get(target.q) ?? 0) + 1);
2844
+ }
2845
+ const expansionResults = await Promise.all(targets.map(async (target) => ({
2846
+ target,
2847
+ result: await expandOneItemSerially(target.q, states.length, target.index, target.expanded)
2848
+ })));
2849
+ for (const { target, result } of expansionResults) {
2850
+ if (result.dispatched) this.clickedQuestions.add(target.q);
2851
+ }
2852
+ if (!options.questionsOnly) {
2853
+ const targetQuestions = new Set(targets.map((target) => target.q));
2854
+ for (const clickedItem of await this.extractVisibleItems(page)) {
2855
+ if (!targetQuestions.has(clickedItem.question)) continue;
2856
+ capturedItems.set(clickedItem.question, clickedItem);
2714
2857
  if (this.upsertCollectedRow(this.toFlatRow(clickedItem, 1, null, options.query))) {
2715
2858
  await this.emitProgress("answer_captured");
2716
2859
  }
2717
2860
  }
2718
2861
  }
2719
- const captured = capturedItems.get(target.q);
2720
- if (expansionResult.confirmed || Boolean(captured?.answer?.trim())) {
2721
- processedQuestions.add(target.q);
2722
- failedInteractions.delete(target.q);
2723
- } else if ((clickAttempts.get(target.q) ?? 0) >= MAX_CLICK_ATTEMPTS) {
2724
- failedInteractions.add(target.q);
2725
- abandonedNeverReclick.add(target.q);
2862
+ for (const { target, result } of expansionResults) {
2863
+ const captured = capturedItems.get(target.q);
2864
+ if (result.confirmed || Boolean(captured?.answer?.trim())) {
2865
+ processedQuestions.add(target.q);
2866
+ failedInteractions.delete(target.q);
2867
+ } else if ((clickAttempts.get(target.q) ?? 0) >= MAX_CLICK_ATTEMPTS) {
2868
+ failedInteractions.add(target.q);
2869
+ abandonedNeverReclick.add(target.q);
2870
+ }
2726
2871
  }
2727
2872
  this.paaLifecycle = {
2728
2873
  ...this.paaLifecycle,
@@ -4430,6 +4575,17 @@ function stripInternalDebug(result, keepDebug) {
4430
4575
  delete diagnostics.debug;
4431
4576
  return { ...result, diagnostics };
4432
4577
  }
4578
+ async function resolveSurvivingGotoLinks(result) {
4579
+ try {
4580
+ return await resolveHarvestResultGoogleGotoLinks(result, { totalBudgetMs: 1e4 });
4581
+ } catch (error) {
4582
+ console.warn(JSON.stringify({
4583
+ event: "harvest_goto_resolution_failed",
4584
+ message: errorMessage(error)
4585
+ }));
4586
+ return result;
4587
+ }
4588
+ }
4433
4589
  async function cleanupDisposableProxy(kernelApiKey, proxyId) {
4434
4590
  if (!kernelApiKey || !proxyId) return;
4435
4591
  try {
@@ -4644,7 +4800,8 @@ async function harvest(rawOptions) {
4644
4800
  lastError = err;
4645
4801
  if (willRetry2) continue;
4646
4802
  if (attempt.result) {
4647
- return withResultQuality(stripInternalDebug(attempt.result, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
4803
+ const resolvedResult2 = await resolveSurvivingGotoLinks(attempt.result);
4804
+ return withResultQuality(stripInternalDebug(resolvedResult2, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
4648
4805
  }
4649
4806
  break;
4650
4807
  }
@@ -4679,7 +4836,8 @@ async function harvest(rawOptions) {
4679
4836
  if (willRetry2) continue;
4680
4837
  break;
4681
4838
  }
4682
- const finalResult = withResultQuality(stripInternalDebug(result, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
4839
+ const resolvedResult = await resolveSurvivingGotoLinks(result);
4840
+ const finalResult = withResultQuality(stripInternalDebug(resolvedResult, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
4683
4841
  const outcome = classifyAttemptResult(finalResult);
4684
4842
  const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
4685
4843
  const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
@@ -12,13 +12,13 @@ import {
12
12
  registerScheduledResultsMcpTools,
13
13
  registerSerpIntelligenceCaptureTools,
14
14
  resolveDeploymentProfile
15
- } from "./chunk-2JF4ECSJ.js";
15
+ } from "./chunk-JECPQBLZ.js";
16
16
  import {
17
17
  renderInstallTerminal
18
- } from "./chunk-R4YRH5PG.js";
18
+ } from "./chunk-6FL7ZZCF.js";
19
19
  import {
20
20
  PACKAGE_VERSION
21
- } from "./chunk-ATIVTEML.js";
21
+ } from "./chunk-2RTELL6M.js";
22
22
 
23
23
  // src/mcp/stdio-runtime.ts
24
24
  import { readFileSync } from "fs";
package/dist/index.cjs CHANGED
@@ -2598,6 +2598,93 @@ async function resolveGoogleGotoUrls(rawUrls, options = {}) {
2598
2598
  await Promise.all(Array.from({ length: Math.min(concurrency, inputs.length) }, () => worker()));
2599
2599
  return { resolutions: [...records.values()], nonGoogleRequestsObserved: 0 };
2600
2600
  }
2601
+ function resolvedSite(link, fallback) {
2602
+ try {
2603
+ return new URL(link.resolvedUrl ?? link.url).hostname.replace(/^www\./, "");
2604
+ } catch {
2605
+ return fallback;
2606
+ }
2607
+ }
2608
+ async function resolveHarvestResultGoogleGotoLinks(result, options = {}) {
2609
+ const treeNodes = [];
2610
+ const visit = (nodes) => {
2611
+ for (const node of nodes) {
2612
+ treeNodes.push(node);
2613
+ visit(node.children ?? []);
2614
+ }
2615
+ };
2616
+ visit(result.tree ?? []);
2617
+ const candidates = [
2618
+ ...result.flat.flatMap((row) => row.sources ?? []),
2619
+ ...treeNodes.flatMap((node) => node.sources),
2620
+ ...result.organicResults ?? [],
2621
+ ...(result.aiOverview?.citations ?? []).map(({ text: _text, href, ...link }) => ({ ...link, url: href })),
2622
+ ...(result.aiMode?.citations ?? []).map(({ text: _text, href, ...link }) => ({ ...link, url: href }))
2623
+ ];
2624
+ const unique = new Map(candidates.filter((link) => Boolean(link?.rawUrl)).map((link) => [link.rawUrl, link]));
2625
+ const gotoUrls = [...unique.values()].filter((link) => link.linkType === "google_goto_redirect" && link.resolutionStatus !== "resolved").map((link) => link.rawUrl);
2626
+ if (gotoUrls.length > 0) {
2627
+ const batch = await resolveGoogleGotoUrls(gotoUrls, options);
2628
+ for (const resolution2 of batch.resolutions) {
2629
+ const link = unique.get(resolution2.rawUrl);
2630
+ if (!link) continue;
2631
+ unique.set(resolution2.rawUrl, resolution2.resolutionStatus === "resolved" && resolution2.resolvedUrl ? { ...link, url: resolution2.resolvedUrl, resolvedUrl: resolution2.resolvedUrl, resolutionStatus: "resolved" } : { ...link, resolutionStatus: resolution2.resolutionStatus });
2632
+ }
2633
+ }
2634
+ const replace = (link) => ({
2635
+ ...link,
2636
+ ...unique.get(link.rawUrl) ?? link
2637
+ });
2638
+ const replaceSources = (sources) => sources.map((source) => {
2639
+ const link = replace(source);
2640
+ return { ...link, site: resolvedSite(link, source.site) };
2641
+ });
2642
+ for (const row of result.flat) {
2643
+ row.sources = replaceSources(row.sources ?? []);
2644
+ const primary = row.sources[0];
2645
+ row.source_title = primary?.title ?? row.source_title;
2646
+ row.source_site = primary?.site ?? row.source_site;
2647
+ row.source_cite = primary?.url ?? row.source_cite;
2648
+ }
2649
+ for (const node of treeNodes) {
2650
+ node.sources = replaceSources(node.sources ?? []);
2651
+ const primary = node.sources[0];
2652
+ node.sourceTitle = primary?.title ?? node.sourceTitle;
2653
+ node.sourceSite = primary?.site ?? node.sourceSite;
2654
+ node.sourceCite = primary?.url ?? node.sourceCite;
2655
+ }
2656
+ for (let index = 0; index < (result.organicResults?.length ?? 0); index++) {
2657
+ const link = replace(result.organicResults[index]);
2658
+ result.organicResults[index] = { ...link, domain: resolvedSite(link, result.organicResults[index].domain) ?? "" };
2659
+ }
2660
+ const replaceCitation = (citation) => {
2661
+ const link = replace({
2662
+ url: citation.href,
2663
+ rawUrl: citation.rawUrl,
2664
+ resolvedUrl: citation.resolvedUrl,
2665
+ linkType: citation.linkType,
2666
+ resolutionStatus: citation.resolutionStatus
2667
+ });
2668
+ const { url, ...metadata } = link;
2669
+ return { ...citation, ...metadata, href: url };
2670
+ };
2671
+ if (result.aiOverview) result.aiOverview.citations = (result.aiOverview.citations ?? []).map(replaceCitation);
2672
+ if (result.aiMode) result.aiMode.citations = (result.aiMode.citations ?? []).map(replaceCitation);
2673
+ const values = [...unique.values()];
2674
+ result.diagnostics.links = {
2675
+ linkCandidatesObserved: unique.size,
2676
+ plainLinksObserved: values.filter((link) => link.linkType === "plain").length,
2677
+ googleUrlRedirectsObserved: values.filter((link) => link.linkType === "google_url_redirect").length,
2678
+ googleGotoRedirectsObserved: values.filter((link) => link.linkType === "google_goto_redirect").length,
2679
+ redirectsResolved: values.filter((link) => link.linkType !== "plain" && link.resolutionStatus === "resolved").length,
2680
+ redirectsUnresolved: values.filter((link) => link.resolutionStatus === "unresolved").length,
2681
+ redirectsRejected: values.filter((link) => link.resolutionStatus === "rejected").length,
2682
+ resolverNonGoogleRequestsObserved: 0,
2683
+ paaSourcesPreserved: result.flat.reduce((total, row) => total + row.sources.length, 0),
2684
+ aiOverviewLabelValidated: result.aiOverview?.detected ?? false
2685
+ };
2686
+ return result;
2687
+ }
2601
2688
  async function resolveGoogleOutboundLinks(page, values, options = {}) {
2602
2689
  const byRawUrl = /* @__PURE__ */ new Map();
2603
2690
  for (const value of values) byRawUrl.set(value.rawUrl, value);
@@ -2718,6 +2805,10 @@ async function resolveGoogleOutboundLinks(page, values, options = {}) {
2718
2805
  }
2719
2806
 
2720
2807
  // src/extractor/PAAExtractor.ts
2808
+ var PAA_ANSWER_PLACEHOLDER_PATTERN = String.raw`^(?:an error has occurred(?:[.!…]\s*)?(?:please try again later[.!…]?)?|loading|please wait|try again|something went wrong)[.!…\s]*$`;
2809
+ var PAA_CLICK_EARLIEST_CONFIRMATION_MS = 700;
2810
+ var PAA_CLICK_ATTEMPT_BUDGET_MS = 1400;
2811
+ var PAA_FRONTIER_BATCH_SIZE = 3;
2721
2812
  var PAAExtractor = class {
2722
2813
  constructor(driver, reporter, onPaaProgress) {
2723
2814
  this.driver = driver;
@@ -2862,7 +2953,7 @@ var PAAExtractor = class {
2862
2953
  }
2863
2954
  async extractVisibleItems(page, onlyQuestion) {
2864
2955
  const sels = PAASelectors;
2865
- const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2 }) => {
2956
+ const raw = await page.evaluate(({ selectors, onlyQuestion: onlyQuestion2, answerPlaceholderPattern }) => {
2866
2957
  function cleanText(el) {
2867
2958
  if (!el) return "";
2868
2959
  const parts = [];
@@ -2889,12 +2980,13 @@ var PAAExtractor = class {
2889
2980
  const aiAnswerBody = answerContainer?.querySelector(selectors.aiAnswerBody);
2890
2981
  const answerElement = aiAnswerBody ?? answerContainer;
2891
2982
  const answerText = cleanText(answerElement);
2983
+ const answerIsPlaceholder = new RegExp(answerPlaceholderPattern, "i").test(answerText);
2892
2984
  const citationCandidates = [
2893
2985
  ...Array.from((answerElement ?? pair).querySelectorAll("a[href]")),
2894
2986
  ...Array.from(pair.querySelectorAll(selectors.aiCitation)),
2895
2987
  ...Array.from(pair.querySelectorAll(selectors.aiSourceCard))
2896
2988
  ];
2897
- const hasLoadedAnswer = expanded || answerText.length > 30 && !/an error has occurred/i.test(answerText.slice(0, 80)) && citationCandidates.some((anchor) => /^https?:\/\//.test(anchor.href));
2989
+ const hasLoadedAnswer = !answerIsPlaceholder && (expanded && answerText.length >= 10 || answerText.length > 30 && citationCandidates.some((anchor) => /^https?:\/\//.test(anchor.href)));
2898
2990
  let sourceTitle = hasLoadedAnswer ? pair.querySelector(selectors.sourceTitle)?.innerText?.trim() || void 0 : void 0;
2899
2991
  let sourceSite = hasLoadedAnswer ? pair.querySelector(selectors.sourceSite)?.innerText?.trim() || void 0 : void 0;
2900
2992
  const sources = [];
@@ -2918,7 +3010,7 @@ var PAAExtractor = class {
2918
3010
  sources
2919
3011
  };
2920
3012
  });
2921
- }, { selectors: sels, onlyQuestion });
3013
+ }, { selectors: sels, onlyQuestion, answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN });
2922
3014
  const baseUrl = typeof page.url === "function" ? page.url() : "https://www.google.com/";
2923
3015
  return raw.flatMap((item) => {
2924
3016
  const sources = [];
@@ -3109,44 +3201,52 @@ var PAAExtractor = class {
3109
3201
  questionEl: PAASelectors.itemQuestionEl,
3110
3202
  answerSel: PAASelectors.answerContainer,
3111
3203
  expandedClass: PAASelectors.expandedClass,
3112
- clickTarget: PAASelectors.clickTarget
3204
+ clickTarget: PAASelectors.clickTarget,
3205
+ answerPlaceholderPattern: PAA_ANSWER_PLACEHOLDER_PATTERN,
3206
+ questionsOnly: options.questionsOnly
3113
3207
  };
3114
3208
  const readItemStates = () => page.evaluate(
3115
- ({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel }) => {
3116
- return Array.from(document.querySelectorAll(sel)).map((el) => ({
3117
- q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
3118
- done: el.classList.contains(expandedClass) || el.querySelector(clickTarget)?.getAttribute("aria-expanded") === "true" || Boolean((el.querySelector(answerSel)?.textContent ?? "").trim())
3119
- })).filter((s) => s.q);
3209
+ ({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget, answerSel, answerPlaceholderPattern, questionsOnly }) => {
3210
+ return Array.from(document.querySelectorAll(sel)).map((el, index) => {
3211
+ const answerText = (el.querySelector(answerSel)?.textContent ?? "").replace(/\s+/g, " ").trim();
3212
+ const expanded = el.classList.contains(expandedClass) || el.querySelector(clickTarget)?.getAttribute("aria-expanded") === "true";
3213
+ const answerReady = answerText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(answerText);
3214
+ return {
3215
+ q: el.getAttribute(dataQ) || el.getAttribute(dataInitQ) || el.querySelector(questionEl)?.innerText?.trim() || "",
3216
+ done: expanded && (questionsOnly || answerReady),
3217
+ expanded,
3218
+ index
3219
+ };
3220
+ }).filter((s) => s.q);
3120
3221
  },
3121
3222
  itemArgs
3122
3223
  );
3123
- const expandOneItemSerially = async (q, beforeCount) => {
3224
+ const expandOneItemSerially = async (q, beforeCount, itemIndex, wasExpanded) => {
3124
3225
  const clickStartedMs = Date.now();
3125
- const items = page.locator(itemArgs.sel);
3126
- const itemIndex = await items.evaluateAll(
3127
- (elements, { dataQ, dataInitQ, target }) => elements.findIndex((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ)) === target),
3128
- { dataQ: itemArgs.dataQ, dataInitQ: itemArgs.dataInitQ, target: q }
3129
- ).catch(() => -1);
3130
- const clickTarget = itemIndex >= 0 ? items.nth(itemIndex).locator(itemArgs.clickTarget) : null;
3226
+ const clickTarget = itemIndex >= 0 ? page.locator(itemArgs.sel).nth(itemIndex).locator(itemArgs.clickTarget) : null;
3131
3227
  const clicked = clickTarget ? await (async () => {
3132
- await clickTarget.scrollIntoViewIfNeeded({ timeout: 5e3 });
3133
- const box = await clickTarget.boundingBox({ timeout: 5e3 });
3134
- if (!box) return false;
3135
- await page.mouse.click(box.x + box.width / 2, box.y + box.height / 2);
3228
+ if (wasExpanded) {
3229
+ await clickTarget.click({ timeout: 2e3, force: true });
3230
+ await page.waitForTimeout(250);
3231
+ }
3232
+ await clickTarget.click({ timeout: 2e3, force: true });
3136
3233
  return true;
3137
3234
  })().catch(() => false) : false;
3138
3235
  if (!clicked) return { dispatched: false, confirmed: false };
3139
- const remainingHumanClickDelayMs = 700 - (Date.now() - clickStartedMs);
3140
- if (remainingHumanClickDelayMs > 0) await page.waitForTimeout(remainingHumanClickDelayMs);
3236
+ const remainingConfirmationMs = Math.max(100, PAA_CLICK_ATTEMPT_BUDGET_MS - (Date.now() - clickStartedMs));
3141
3237
  const confirmed = await page.waitForFunction(
3142
- ({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, target, min }) => {
3143
- const items2 = Array.from(document.querySelectorAll(sel));
3144
- const item = items2.find((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ) || element.querySelector(questionEl)?.innerText?.trim()) === target);
3145
- if (!item) return items2.length > min;
3146
- return item.classList.contains(expandedClass) || item.querySelector(clickTarget2)?.getAttribute("aria-expanded") === "true" || Boolean((item.querySelector(answerSel)?.textContent ?? "").trim()) || items2.length > min;
3238
+ ({ sel, dataQ, dataInitQ, questionEl, expandedClass, clickTarget: clickTarget2, answerSel, answerPlaceholderPattern, questionsOnly, target, min, earliestConfirmationAt }) => {
3239
+ if (Date.now() < earliestConfirmationAt) return false;
3240
+ const items = Array.from(document.querySelectorAll(sel));
3241
+ const item = items.find((element) => (element.getAttribute(dataQ) || element.getAttribute(dataInitQ) || element.querySelector(questionEl)?.innerText?.trim()) === target);
3242
+ if (!item) return false;
3243
+ const expanded = item.classList.contains(expandedClass) || item.querySelector(clickTarget2)?.getAttribute("aria-expanded") === "true";
3244
+ if (questionsOnly) return expanded || items.length > min;
3245
+ const answerText = (item.querySelector(answerSel)?.textContent ?? "").replace(/\s+/g, " ").trim();
3246
+ return answerText.length >= 10 && !new RegExp(answerPlaceholderPattern, "i").test(answerText);
3147
3247
  },
3148
- { ...itemArgs, target: q, min: beforeCount },
3149
- { timeout: 2500, polling: 100 }
3248
+ { ...itemArgs, target: q, min: beforeCount, earliestConfirmationAt: clickStartedMs + PAA_CLICK_EARLIEST_CONFIRMATION_MS },
3249
+ { timeout: remainingConfirmationMs, polling: 100 }
3150
3250
  ).catch(() => null);
3151
3251
  return { dispatched: true, confirmed: Boolean(confirmed) };
3152
3252
  };
@@ -3191,10 +3291,43 @@ var PAAExtractor = class {
3191
3291
  };
3192
3292
  break;
3193
3293
  }
3294
+ if (!options.questionsOnly && seenQs.size >= options.maxQuestions) {
3295
+ let materialChanged = false;
3296
+ const keptQuestions = new Set(orderedQs.slice(0, options.maxQuestions));
3297
+ for (const item of await this.extractVisibleItems(page)) {
3298
+ if (!keptQuestions.has(item.question)) continue;
3299
+ const existing = capturedItems.get(item.question);
3300
+ const merged = {
3301
+ ...item,
3302
+ answer: item.answer ?? existing?.answer,
3303
+ sourceTitle: item.sourceTitle ?? existing?.sourceTitle,
3304
+ sourceSite: item.sourceSite ?? existing?.sourceSite,
3305
+ sourceCite: item.sourceCite ?? existing?.sourceCite,
3306
+ sources: this.mergePaaSources(existing?.sources, item.sources)
3307
+ };
3308
+ capturedItems.set(item.question, merged);
3309
+ materialChanged = this.upsertCollectedRow(this.toFlatRow(merged, 1, null, options.query)) || materialChanged;
3310
+ }
3311
+ if (materialChanged) await this.emitProgress("answer_captured");
3312
+ if ([...keptQuestions].every((question) => {
3313
+ const item = capturedItems.get(question);
3314
+ return Boolean(item?.answer?.trim() && item.sources.length > 0);
3315
+ })) {
3316
+ failedInteractions.clear();
3317
+ this.paaLifecycle = {
3318
+ ...this.paaLifecycle,
3319
+ discoveryStatus: "target_reached",
3320
+ targetStatus: "met",
3321
+ failedInteractions: 0,
3322
+ completionReason: "The requested unique-question count and its answer/source material were observed."
3323
+ };
3324
+ break;
3325
+ }
3326
+ }
3194
3327
  const kept = new Set(orderedQs.slice(0, options.maxQuestions));
3195
3328
  const clickable = states.filter((state) => kept.has(state.q) && !state.done && (clickAttempts.get(state.q) ?? 0) < MAX_CLICK_ATTEMPTS && !abandonedNeverReclick.has(state.q));
3196
- const target = clickable[0];
3197
- if (!target) {
3329
+ const targets = clickable.slice(-PAA_FRONTIER_BATCH_SIZE);
3330
+ if (targets.length === 0) {
3198
3331
  if (seenQs.size >= options.maxQuestions) {
3199
3332
  this.paaLifecycle = {
3200
3333
  ...this.paaLifecycle,
@@ -3242,31 +3375,43 @@ var PAAExtractor = class {
3242
3375
  };
3243
3376
  break;
3244
3377
  }
3245
- this.reporter.onDepth(++round);
3246
3378
  await this.throwIfCaptcha(page, "Google PAA expansion");
3247
- clickAttempts.set(target.q, (clickAttempts.get(target.q) ?? 0) + 1);
3248
- const expansionResult = await expandOneItemSerially(target.q, states.length);
3249
- if (expansionResult.dispatched) this.clickedQuestions.add(target.q);
3250
- if (!options.questionsOnly) {
3251
- let clickedItem = (await this.extractVisibleItems(page, target.q))[0];
3252
- if (!clickedItem?.answer) {
3253
- await page.waitForTimeout(1300);
3254
- clickedItem = (await this.extractVisibleItems(page, target.q))[0];
3379
+ for (const target of targets) {
3380
+ this.reporter.onDepth(++round);
3381
+ if ((clickAttempts.get(target.q) ?? 0) > 0) {
3382
+ this.paaLifecycle = {
3383
+ ...this.paaLifecycle,
3384
+ automaticRetries: this.paaLifecycle.automaticRetries + 1
3385
+ };
3255
3386
  }
3256
- if (clickedItem) {
3257
- capturedItems.set(target.q, clickedItem);
3387
+ clickAttempts.set(target.q, (clickAttempts.get(target.q) ?? 0) + 1);
3388
+ }
3389
+ const expansionResults = await Promise.all(targets.map(async (target) => ({
3390
+ target,
3391
+ result: await expandOneItemSerially(target.q, states.length, target.index, target.expanded)
3392
+ })));
3393
+ for (const { target, result } of expansionResults) {
3394
+ if (result.dispatched) this.clickedQuestions.add(target.q);
3395
+ }
3396
+ if (!options.questionsOnly) {
3397
+ const targetQuestions = new Set(targets.map((target) => target.q));
3398
+ for (const clickedItem of await this.extractVisibleItems(page)) {
3399
+ if (!targetQuestions.has(clickedItem.question)) continue;
3400
+ capturedItems.set(clickedItem.question, clickedItem);
3258
3401
  if (this.upsertCollectedRow(this.toFlatRow(clickedItem, 1, null, options.query))) {
3259
3402
  await this.emitProgress("answer_captured");
3260
3403
  }
3261
3404
  }
3262
3405
  }
3263
- const captured = capturedItems.get(target.q);
3264
- if (expansionResult.confirmed || Boolean(captured?.answer?.trim())) {
3265
- processedQuestions.add(target.q);
3266
- failedInteractions.delete(target.q);
3267
- } else if ((clickAttempts.get(target.q) ?? 0) >= MAX_CLICK_ATTEMPTS) {
3268
- failedInteractions.add(target.q);
3269
- abandonedNeverReclick.add(target.q);
3406
+ for (const { target, result } of expansionResults) {
3407
+ const captured = capturedItems.get(target.q);
3408
+ if (result.confirmed || Boolean(captured?.answer?.trim())) {
3409
+ processedQuestions.add(target.q);
3410
+ failedInteractions.delete(target.q);
3411
+ } else if ((clickAttempts.get(target.q) ?? 0) >= MAX_CLICK_ATTEMPTS) {
3412
+ failedInteractions.add(target.q);
3413
+ abandonedNeverReclick.add(target.q);
3414
+ }
3270
3415
  }
3271
3416
  this.paaLifecycle = {
3272
3417
  ...this.paaLifecycle,
@@ -4970,6 +5115,17 @@ function stripInternalDebug(result, keepDebug) {
4970
5115
  delete diagnostics.debug;
4971
5116
  return { ...result, diagnostics };
4972
5117
  }
5118
+ async function resolveSurvivingGotoLinks(result) {
5119
+ try {
5120
+ return await resolveHarvestResultGoogleGotoLinks(result, { totalBudgetMs: 1e4 });
5121
+ } catch (error) {
5122
+ console.warn(JSON.stringify({
5123
+ event: "harvest_goto_resolution_failed",
5124
+ message: errorMessage(error)
5125
+ }));
5126
+ return result;
5127
+ }
5128
+ }
4973
5129
  async function cleanupDisposableProxy(kernelApiKey, proxyId) {
4974
5130
  if (!kernelApiKey || !proxyId) return;
4975
5131
  try {
@@ -5184,7 +5340,8 @@ async function harvest(rawOptions) {
5184
5340
  lastError = err;
5185
5341
  if (willRetry2) continue;
5186
5342
  if (attempt.result) {
5187
- return withResultQuality(stripInternalDebug(attempt.result, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
5343
+ const resolvedResult2 = await resolveSurvivingGotoLinks(attempt.result);
5344
+ return withResultQuality(stripInternalDebug(resolvedResult2, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
5188
5345
  }
5189
5346
  break;
5190
5347
  }
@@ -5219,7 +5376,8 @@ async function harvest(rawOptions) {
5219
5376
  if (willRetry2) continue;
5220
5377
  break;
5221
5378
  }
5222
- const finalResult = withResultQuality(stripInternalDebug(result, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
5379
+ const resolvedResult = await resolveSurvivingGotoLinks(result);
5380
+ const finalResult = withResultQuality(stripInternalDebug(resolvedResult, requestedDebug), attemptOptions.maxQuestions, attemptOptions.questionsOnly);
5223
5381
  const outcome = classifyAttemptResult(finalResult);
5224
5382
  const willRetry = outcome === "degraded_result" && i < maxAttempts - 1;
5225
5383
  const resultError = outcome === "degraded_result" ? degradedResultMessage(finalResult) : null;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  harvest
3
- } from "./chunk-UFJJGXVO.js";
3
+ } from "./chunk-WEEAXPWK.js";
4
4
  import "./chunk-OM7HVEJ3.js";
5
5
  import {
6
6
  recordVendorUsage
@@ -229,7 +229,7 @@ import {
229
229
  serpArrivalUrl,
230
230
  validateProviderSessionId,
231
231
  withRuntimeUserAgent
232
- } from "./chunk-UFJJGXVO.js";
232
+ } from "./chunk-WEEAXPWK.js";
233
233
  import {
234
234
  AnalyticsConfirmedPersonProjectionSchema,
235
235
  AnalyticsCrmFieldSchema,
@@ -316,7 +316,7 @@ import {
316
316
  resolveDeploymentProfile,
317
317
  resolveLocalSourcebookSchemaType,
318
318
  transcribeMediaUrl
319
- } from "./chunk-2JF4ECSJ.js";
319
+ } from "./chunk-JECPQBLZ.js";
320
320
  import {
321
321
  auditImageUrls,
322
322
  auditImages,
@@ -435,7 +435,7 @@ import {
435
435
  } from "./chunk-GGZEC22A.js";
436
436
  import {
437
437
  PACKAGE_VERSION
438
- } from "./chunk-ATIVTEML.js";
438
+ } from "./chunk-2RTELL6M.js";
439
439
  import {
440
440
  CaptchaError,
441
441
  MapsSearchIncompletePageError,
@@ -72081,6 +72081,16 @@ function personalAssistantProductionReadiness() {
72081
72081
  reasonCodes: Object.freeze([...reasonCodes])
72082
72082
  });
72083
72083
  }
72084
+ async function settlePersonalAssistantProductionStartup(startup) {
72085
+ try {
72086
+ return await startup;
72087
+ } catch (error) {
72088
+ if (!(error instanceof PersonalAssistantProductionConfigurationError)) throw error;
72089
+ const readiness = personalAssistantProductionReadiness();
72090
+ console.error("[personal-assistant] startup blocked:", error.code, readiness.reasonCodes);
72091
+ return readiness;
72092
+ }
72093
+ }
72084
72094
 
72085
72095
  // src/api/site-audit-worker.ts
72086
72096
  var MAX_CONCURRENT_SITE_AUDIT = 1;
@@ -74280,11 +74290,13 @@ async function callResendExportPage(identity, input) {
74280
74290
  }
74281
74291
 
74282
74292
  // src/api/server.ts
74283
- var personalAssistantProductionStartup = configurePersonalAssistantProduction({
74284
- variables: process.env,
74285
- bindingFactory: buildPersonalAssistantProductionBindingsFromEnvironment,
74286
- allowLocalOmission: true
74287
- });
74293
+ var personalAssistantProductionStartup = settlePersonalAssistantProductionStartup(
74294
+ configurePersonalAssistantProduction({
74295
+ variables: process.env,
74296
+ bindingFactory: buildPersonalAssistantProductionBindingsFromEnvironment,
74297
+ allowLocalOmission: true
74298
+ })
74299
+ );
74288
74300
  var secureCookies2 = process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
74289
74301
  var isProduction = secureCookies2;
74290
74302
  var sessionCookieOptions2 = {
@@ -77726,7 +77738,7 @@ app.get("/cron/tick", async (c) => {
77726
77738
  return c.json({ error: "Unauthorized" }, 401);
77727
77739
  }
77728
77740
  const startedAt = Date.now();
77729
- const { drainQueue } = await import("./worker-VU6JCCLG.js");
77741
+ const { drainQueue } = await import("./worker-2TYCYFPM.js");
77730
77742
  const budget = { maxJobs: 10, deadlineMs: startedAt + CRON_TICK_DRAIN_BUDGET_MS };
77731
77743
  const origin = `${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`;
77732
77744
  const unfinished = [];
@@ -9,7 +9,7 @@ import {
9
9
  import "./chunk-OZJMVCDK.js";
10
10
  import {
11
11
  harvest
12
- } from "./chunk-UFJJGXVO.js";
12
+ } from "./chunk-WEEAXPWK.js";
13
13
  import {
14
14
  sanitizeAttempts,
15
15
  sanitizeHarvestResult
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mcp-scraper",
3
- "version": "0.79.0",
3
+ "version": "0.79.2",
4
4
  "description": "MCP server for MCP Scraper web intelligence tools",
5
5
  "type": "module",
6
6
  "main": "./dist/index.cjs",