mcp-scraper 0.86.5 → 0.88.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.
@@ -15,7 +15,7 @@ import {
15
15
  RawPAAItemSchema,
16
16
  postToMemoryLibrary,
17
17
  recencyToTbs
18
- } from "./chunk-OWF2JJKN.js";
18
+ } from "./chunk-GXBZXWXB.js";
19
19
  import {
20
20
  CaptchaError,
21
21
  ExtractionError,
@@ -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 },
@@ -4,7 +4,7 @@ import {
4
4
  import {
5
5
  DEFAULT_MAPS_PROXY_MODE,
6
6
  postToMemoryLibrary
7
- } from "./chunk-OWF2JJKN.js";
7
+ } from "./chunk-GXBZXWXB.js";
8
8
  import {
9
9
  buildSerpEmailQuery,
10
10
  contactAttemptUrls,
@@ -1,5 +1,5 @@
1
1
  // src/version.ts
2
- var PACKAGE_VERSION = "0.86.5";
2
+ var PACKAGE_VERSION = "0.88.0";
3
3
 
4
4
  export {
5
5
  PACKAGE_VERSION
@@ -10,7 +10,7 @@ import {
10
10
  createSiteExtractContentReader,
11
11
  createSiteExtractImageArtifact,
12
12
  extractJobLimitInfo
13
- } from "./chunk-KQWVJOVR.js";
13
+ } from "./chunk-IHXAXYIS.js";
14
14
  import "./chunk-OZJMVCDK.js";
15
15
  import {
16
16
  computeIssues,
@@ -19,7 +19,7 @@ import {
19
19
  renderLinkReport
20
20
  } from "./chunk-PGJQDMC2.js";
21
21
  import "./chunk-DNM65UCK.js";
22
- import "./chunk-NKRO2SUO.js";
22
+ import "./chunk-4QMUF6XM.js";
23
23
  import "./chunk-GGZEC22A.js";
24
24
  import {
25
25
  getDb
@@ -7,10 +7,10 @@ import {
7
7
  GmailServiceError,
8
8
  normalizeGmailMessage,
9
9
  parseGmailAddresses
10
- } from "./chunk-WFGAR4BZ.js";
10
+ } from "./chunk-MZDNZQWT.js";
11
11
  import "./chunk-C5Z4OFKW.js";
12
12
  import "./chunk-T3MZISOF.js";
13
- import "./chunk-NKRO2SUO.js";
13
+ import "./chunk-4QMUF6XM.js";
14
14
  import "./chunk-OPQIGAFB.js";
15
15
  import "./chunk-YXNDOQXN.js";
16
16
  export {