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.
package/dist/index.cjs CHANGED
@@ -395,6 +395,58 @@ var LocationMismatchError = class extends Error {
395
395
  }
396
396
  };
397
397
 
398
+ // src/driver/IBrowserDriver.ts
399
+ async function withManagedTemporaryPage(context, setup, operation, signal) {
400
+ let page;
401
+ let ended = false;
402
+ let timer;
403
+ let rejectStop = () => {
404
+ };
405
+ const aborted = () => signal?.reason instanceof DOMException && signal.reason.name === "TimeoutError" ? signal.reason : new RequestAbortedError();
406
+ const onAbort = () => rejectStop(aborted());
407
+ const stop = new Promise((_, reject) => {
408
+ rejectStop = reject;
409
+ });
410
+ const close = async (target) => {
411
+ let closeTimer;
412
+ try {
413
+ await Promise.race([
414
+ target.close().catch(() => {
415
+ }),
416
+ new Promise((resolve) => {
417
+ closeTimer = setTimeout(resolve, 2e3);
418
+ })
419
+ ]);
420
+ } finally {
421
+ if (closeTimer) clearTimeout(closeTimer);
422
+ }
423
+ };
424
+ if (signal?.aborted) throw aborted();
425
+ signal?.addEventListener("abort", onAbort, { once: true });
426
+ timer = setTimeout(() => rejectStop(new DOMException("Optional pagination timed out", "TimeoutError")), 3e4);
427
+ try {
428
+ return await Promise.race([
429
+ (async () => {
430
+ const created = await context.newPage();
431
+ if (ended) {
432
+ await close(created);
433
+ throw new RequestAbortedError();
434
+ }
435
+ page = created;
436
+ await setup(created);
437
+ if (ended) throw new RequestAbortedError();
438
+ return operation(created);
439
+ })(),
440
+ stop
441
+ ]);
442
+ } finally {
443
+ ended = true;
444
+ if (timer) clearTimeout(timer);
445
+ signal?.removeEventListener("abort", onAbort);
446
+ if (page) await close(page);
447
+ }
448
+ }
449
+
398
450
  // src/api/cost-telemetry.ts
399
451
  var import_node_crypto2 = require("crypto");
400
452
 
@@ -1085,6 +1137,11 @@ var BrowserDriver = class {
1085
1137
  };
1086
1138
  });
1087
1139
  }
1140
+ async withTemporaryPage(operation, signal) {
1141
+ if (!this.context) throw new Error("Browser context is not available");
1142
+ return withManagedTemporaryPage(this.context, async () => {
1143
+ }, operation, signal);
1144
+ }
1088
1145
  async navigateToSERP(query, uule, gl, hl, options) {
1089
1146
  const params = new URLSearchParams({ q: query, gl, hl, pws: "0" });
1090
1147
  if (options?.tbs) params.set("tbs", options.tbs);
@@ -1489,7 +1546,10 @@ var BrightDataSerpDriver = class {
1489
1546
  const customCdp = this.cdpSession;
1490
1547
  const sessionResponse = await customCdp.send("Browser.getSessionId");
1491
1548
  this.providerSessionId = validateProviderSessionId(sessionResponse?.sessionId);
1492
- await this.page.route("**/*", async (route) => {
1549
+ await this.setupPage(this.page);
1550
+ }
1551
+ async setupPage(page) {
1552
+ await page.route("**/*", async (route) => {
1493
1553
  const request = route.request();
1494
1554
  const resourceType = request.resourceType();
1495
1555
  let requestUrl;
@@ -1514,6 +1574,10 @@ var BrightDataSerpDriver = class {
1514
1574
  await route.continue();
1515
1575
  });
1516
1576
  }
1577
+ async withTemporaryPage(operation, signal) {
1578
+ if (!this.context || this.closing) throw new Error("Browser context is not available");
1579
+ return withManagedTemporaryPage(this.context, (page) => this.setupPage(page), operation, signal);
1580
+ }
1517
1581
  async navigateToSERP(query, uule, gl, hl, options) {
1518
1582
  if (!this.page) throw new Error("Browser page is not available");
1519
1583
  const params = new URLSearchParams({ q: query, gl, hl, pws: "0" });
@@ -2489,6 +2553,11 @@ function parsedGoogleGoto(value) {
2489
2553
  if (!url || url.protocol !== "https:" || !isGoogleHost(url.hostname) || url.pathname !== "/goto") return null;
2490
2554
  return url.searchParams.get("url") ? url : null;
2491
2555
  }
2556
+ function hasResolvedGotoDestination(value) {
2557
+ if (value.linkType !== "google_goto_redirect" || value.resolutionStatus !== "resolved" || !parsedGoogleGoto(value.rawUrl) || !value.resolvedUrl) return false;
2558
+ const destination = safeHttpUrl(value.resolvedUrl);
2559
+ return Boolean(destination && !isGoogleHost(destination.hostname) && destination.href === value.resolvedUrl && value.url === destination.href);
2560
+ }
2492
2561
  function questionIdFor(question) {
2493
2562
  const normalized = question.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^\p{L}\p{N}\s]/gu, "").replace(/\s+/g, " ").trim();
2494
2563
  return `paa_${(0, import_node_crypto3.createHash)("sha256").update(normalized).digest("hex").slice(0, 16)}`;
@@ -2705,8 +2774,12 @@ async function resolveHarvestResultGoogleGotoLinks(result, options = {}) {
2705
2774
  }
2706
2775
  async function resolveGoogleOutboundLinks(page, values, options = {}) {
2707
2776
  const byRawUrl = /* @__PURE__ */ new Map();
2708
- for (const value of values) byRawUrl.set(value.rawUrl, value);
2709
- const inputs = [...byRawUrl.values()].filter((value) => value.linkType === "google_goto_redirect");
2777
+ for (const value of values) {
2778
+ const existing = byRawUrl.get(value.rawUrl);
2779
+ if (existing && hasResolvedGotoDestination(existing)) continue;
2780
+ byRawUrl.set(value.rawUrl, value.linkType === "google_goto_redirect" && !hasResolvedGotoDestination(value) ? { ...value, url: value.rawUrl, resolvedUrl: null, resolutionStatus: "unresolved" } : value);
2781
+ }
2782
+ const inputs = [...byRawUrl.values()].filter((value) => value.linkType === "google_goto_redirect" && !hasResolvedGotoDestination(value));
2710
2783
  if (inputs.length === 0) return byRawUrl;
2711
2784
  const deadlineMs = Date.now() + (options.totalBudgetMs ?? DEFAULT_TOTAL_BUDGET_MS);
2712
2785
  const direct = await resolveGoogleGotoUrls(inputs.map((value) => value.rawUrl), options);
@@ -2922,7 +2995,7 @@ var PAAExtractor = class {
2922
2995
  ];
2923
2996
  const unique = new Map(candidates.map((link) => [link.rawUrl, link]));
2924
2997
  let resolverNonGoogleRequestsObserved = 0;
2925
- const resolved = await resolveGoogleOutboundLinks(page, unique.values(), {
2998
+ const resolved = await resolveGoogleOutboundLinks(page, candidates, {
2926
2999
  onNetworkAudit: (audit) => {
2927
3000
  resolverNonGoogleRequestsObserved = audit.nonGoogleRequestsObserved;
2928
3001
  }
@@ -3173,6 +3246,7 @@ var PAAExtractor = class {
3173
3246
  diagnostics: {
3174
3247
  completionStatus: "paa_found",
3175
3248
  problem: null,
3249
+ ...material?.pagination ? { pagination: { ...material.pagination } } : {},
3176
3250
  resultQuality: "partial",
3177
3251
  degradedResult: false,
3178
3252
  retryRecommended: true,
@@ -4137,6 +4211,55 @@ var PAAExtractor = class {
4137
4211
  ...locationEvidence ? { locationEvidence } : {}
4138
4212
  };
4139
4213
  }
4214
+ async captureSecondOrganicPage(page, options, signal) {
4215
+ this.throwIfAborted(signal);
4216
+ if (!this.driver.withTemporaryPage) return { organic: [], status: "failed", failureCode: "unsupported_driver" };
4217
+ let guardedFailure;
4218
+ try {
4219
+ const initial = new URL(page.url());
4220
+ const valid = (raw) => {
4221
+ try {
4222
+ const url = new URL(raw, initial);
4223
+ 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));
4224
+ } catch {
4225
+ return false;
4226
+ }
4227
+ };
4228
+ const href = await page.evaluate(() => {
4229
+ const next = document.querySelector('a#pnnext, a[rel="next"]');
4230
+ return next?.getAttribute("href") ?? null;
4231
+ });
4232
+ if (!href) return { organic: [], status: "unavailable", failureCode: "missing_next" };
4233
+ if (!valid(href)) return { organic: [], status: "unavailable", failureCode: "invalid_next" };
4234
+ const organic = await this.driver.withTemporaryPage(async (second) => {
4235
+ await second.route("**/*", async (route) => {
4236
+ const request = route.request();
4237
+ if (request.isNavigationRequest() && request.frame() === second.mainFrame() && !valid(request.url())) {
4238
+ guardedFailure = /^https:\/\/www\.google\.com\/sorry(?:\/|\?)/.test(request.url()) ? "captcha" : "invalid_next";
4239
+ await route.abort("blockedbyclient");
4240
+ } else await route.fallback();
4241
+ });
4242
+ await second.goto(new URL(href, initial).href, { waitUntil: "domcontentloaded", timeout: 2e4 });
4243
+ this.throwIfAborted(signal);
4244
+ await this.throwIfCaptcha(second, "Google SERP page 2");
4245
+ if (!valid(second.url())) {
4246
+ guardedFailure = "invalid_next";
4247
+ throw new Error("Invalid pagination landing");
4248
+ }
4249
+ return this.extractOrganicResults(second);
4250
+ }, signal);
4251
+ this.throwIfAborted(signal);
4252
+ return organic.length > 0 ? { organic: organic.map((row) => ({ ...row, position: row.position + 10 })), status: "captured" } : { organic: [], status: "unavailable", failureCode: "empty_page" };
4253
+ } catch (err) {
4254
+ this.throwIfAborted(signal);
4255
+ if (err instanceof RequestAbortedError) throw err;
4256
+ return {
4257
+ organic: [],
4258
+ status: "failed",
4259
+ failureCode: guardedFailure ?? (err instanceof CaptchaError ? "captcha" : err instanceof Error && (err.name === "TimeoutError" || /timed? ?out/i.test(err.message)) ? "timeout" : "navigation_error")
4260
+ };
4261
+ }
4262
+ }
4140
4263
  async extract(options, signal) {
4141
4264
  const startMs = Date.now();
4142
4265
  this.completeness = { paaWithoutAnswer: 0, paaWithoutSource: 0, paaAnswersRecovered: 0, aioShareCaptured: null };
@@ -4340,23 +4463,49 @@ var PAAExtractor = class {
4340
4463
  const initialLocationEvidence = options.debug ? inferSerpLocationEvidence(canonicalLocation, organicResults, localPack) : void 0;
4341
4464
  this.reporter.onVideos(videos);
4342
4465
  this.reporter.onForums(forums);
4466
+ const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
4467
+ let pagination = {
4468
+ requestedPages: (options.pages ?? 1) >= 2 ? 2 : 1,
4469
+ capturedPages: 1,
4470
+ page2Status: (options.pages ?? 1) >= 2 ? "not_attempted" : "not_requested",
4471
+ page1OrganicCount: organicResults.length,
4472
+ page2OrganicCount: 0
4473
+ };
4474
+ let allOrganic = organicResults;
4475
+ this.checkpointMaterial = {
4476
+ surface: aiSurfaces.surface,
4477
+ aiOverview: aiSurfaces.aiOverview,
4478
+ aiMode: aiSurfaces.aiMode,
4479
+ whatPeopleSaying,
4480
+ videos,
4481
+ forums,
4482
+ organicResults: allOrganic,
4483
+ localPack,
4484
+ entityIds,
4485
+ pagination
4486
+ };
4487
+ await this.emitProgress("serp_captured");
4488
+ if (pagination.requestedPages === 2) {
4489
+ const second = await this.captureSecondOrganicPage(page, executionOptions, signal);
4490
+ allOrganic = [...organicResults, ...second.organic];
4491
+ pagination = {
4492
+ ...pagination,
4493
+ capturedPages: second.status === "captured" ? 2 : 1,
4494
+ page2Status: second.status,
4495
+ page2OrganicCount: second.organic.length,
4496
+ ...second.failureCode ? { failureCode: second.failureCode } : {}
4497
+ };
4498
+ this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic, pagination };
4499
+ await this.emitProgress("serp_captured");
4500
+ await this.resolveMaterialLinks(page, [], allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
4501
+ this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic };
4502
+ await this.emitProgress("serp_captured");
4503
+ }
4504
+ const locationEvidence = options.debug ? inferSerpLocationEvidence(canonicalLocation, allOrganic, localPack) : initialLocationEvidence;
4343
4505
  if (!hasPaa) {
4344
- let noPaaOrganic = organicResults;
4345
- let locationEvidence2 = initialLocationEvidence;
4346
- if ((options.pages ?? 1) >= 2) {
4347
- const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
4348
- if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
4349
- if (uule) p2params.set("uule", uule);
4350
- await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
4351
- await this.throwIfCaptcha(page, "Google SERP page 2");
4352
- const p2organic = await this.extractOrganicResults(page);
4353
- noPaaOrganic = [...organicResults, ...p2organic.map((r) => ({ ...r, position: r.position + 10 }))];
4354
- if (options.debug) {
4355
- locationEvidence2 = inferSerpLocationEvidence(canonicalLocation, noPaaOrganic, localPack);
4356
- }
4357
- }
4358
- const aiSurfaces2 = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
4359
- await this.resolveMaterialLinks(page, [], noPaaOrganic, aiSurfaces2.aiOverview, aiSurfaces2.aiMode);
4506
+ await this.resolveMaterialLinks(page, [], allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
4507
+ this.checkpointMaterial = { ...this.checkpointMaterial, organicResults: allOrganic };
4508
+ await this.emitProgress("serp_captured");
4360
4509
  const stats2 = {
4361
4510
  seed: executionOptions.query,
4362
4511
  totalQuestions: 0,
@@ -4373,39 +4522,27 @@ var PAAExtractor = class {
4373
4522
  completionStatus: "no_paa",
4374
4523
  noPaaObserved: true,
4375
4524
  problem: null,
4525
+ pagination,
4376
4526
  paaLifecycle: { ...this.paaLifecycle },
4377
4527
  completeness: { ...this.completeness },
4378
4528
  links: { ...this.linkDiagnostics },
4379
- ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence2, locationResolution) } : {}
4529
+ ...options.debug ? { debug: this.buildHarvestDebugSnapshot(executionOptions, canonicalLocation, uule, locationEvidence, locationResolution) } : {}
4380
4530
  },
4381
4531
  totalQuestions: 0,
4382
- surface: aiSurfaces2.surface,
4383
- aiOverview: aiSurfaces2.aiOverview,
4384
- aiMode: aiSurfaces2.aiMode,
4532
+ surface: aiSurfaces.surface,
4533
+ aiOverview: aiSurfaces.aiOverview,
4534
+ aiMode: aiSurfaces.aiMode,
4385
4535
  whatPeopleSaying,
4386
4536
  tree: [],
4387
4537
  flat: [],
4388
4538
  videos,
4389
4539
  forums,
4390
- organicResults: noPaaOrganic,
4540
+ organicResults: allOrganic,
4391
4541
  localPack,
4392
4542
  entityIds,
4393
4543
  stats: stats2
4394
4544
  };
4395
4545
  }
4396
- const aiSurfaces = includeAiOverview ? await this.extractAISurfaces(page, options) : emptyAiSurfaces;
4397
- this.checkpointMaterial = {
4398
- surface: aiSurfaces.surface,
4399
- aiOverview: aiSurfaces.aiOverview,
4400
- aiMode: aiSurfaces.aiMode,
4401
- whatPeopleSaying,
4402
- videos,
4403
- forums,
4404
- organicResults,
4405
- localPack,
4406
- entityIds
4407
- };
4408
- await this.emitProgress("serp_captured");
4409
4546
  const flat = await this.runBFS(page, executionOptions, signal);
4410
4547
  this.throwIfAborted(signal);
4411
4548
  const shortVidsParams = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", udm: ShortVideoSelectors.udm });
@@ -4426,20 +4563,6 @@ var PAAExtractor = class {
4426
4563
  }
4427
4564
  }
4428
4565
  this.reporter.onVideos(shortVideos);
4429
- let allOrganic = organicResults;
4430
- let locationEvidence = initialLocationEvidence;
4431
- if ((options.pages ?? 1) >= 2) {
4432
- const p2params = new URLSearchParams({ q: executionOptions.query, gl: options.gl, hl: options.hl, pws: "0", start: "10" });
4433
- if (recencyToTbs(options.recency)) p2params.set("tbs", recencyToTbs(options.recency));
4434
- if (uule) p2params.set("uule", uule);
4435
- await this.driver.navigateTo("https://www.google.com/search?" + p2params.toString());
4436
- await this.throwIfCaptcha(page, "Google SERP page 2");
4437
- const p2organic = await this.extractOrganicResults(page);
4438
- allOrganic = [...organicResults, ...p2organic.map((r) => ({ ...r, position: r.position + 10 }))];
4439
- if (options.debug) {
4440
- locationEvidence = inferSerpLocationEvidence(canonicalLocation, allOrganic, localPack);
4441
- }
4442
- }
4443
4566
  await this.resolveMaterialLinks(page, flat, allOrganic, aiSurfaces.aiOverview, aiSurfaces.aiMode);
4444
4567
  this.checkpointMaterial = {
4445
4568
  ...this.checkpointMaterial,
@@ -4449,6 +4572,7 @@ var PAAExtractor = class {
4449
4572
  videos: [...videos, ...shortVideos],
4450
4573
  organicResults: allOrganic
4451
4574
  };
4575
+ await this.emitProgress("expansion_finished");
4452
4576
  const allVideos = [...videos, ...shortVideos];
4453
4577
  const tree = this.buildTree(flat, executionOptions.query);
4454
4578
  const stats = {
@@ -4466,6 +4590,7 @@ var PAAExtractor = class {
4466
4590
  diagnostics: {
4467
4591
  completionStatus: "paa_found",
4468
4592
  problem: null,
4593
+ pagination,
4469
4594
  paaLifecycle: { ...this.paaLifecycle },
4470
4595
  completeness: { ...this.completeness },
4471
4596
  links: { ...this.linkDiagnostics },
@@ -4510,18 +4635,18 @@ var import_papaparse = __toESM(require("papaparse"), 1);
4510
4635
 
4511
4636
  // src/output/memory-library-sink.ts
4512
4637
  async function postToMemoryLibrary(args) {
4513
- const url = process.env.MCP_MEMORY_INGEST_URL;
4514
- const key = process.env.MCP_MEMORY_KEY;
4515
- if (!url || !key) return;
4638
+ const key = process.env.MCP_SCRAPER_API_KEY?.trim();
4639
+ const baseUrl = (process.env.MCP_SCRAPER_BASE_URL ?? "https://mcpscraper.dev").replace(/\/$/, "");
4640
+ if (!key) return;
4516
4641
  try {
4517
- const res = await fetch(url, {
4642
+ const res = await fetch(`${baseUrl}/memory/mcp-call`, {
4518
4643
  method: "POST",
4519
- headers: { "content-type": "application/json" },
4520
- body: JSON.stringify({ data: { apiKey: key, title: args.title, content: args.content, source: args.source } })
4644
+ headers: { "content-type": "application/json", "x-api-key": key },
4645
+ body: JSON.stringify({ toolName: "libraryIngestTool", args })
4521
4646
  });
4522
4647
  const body = await res.json().catch(() => null);
4523
- if (!res.ok || body?.result?.error) {
4524
- console.warn("[memory-library-sink] ingest not accepted:", res.status, body?.result?.message ?? "");
4648
+ if (!res.ok || body?.ok === false) {
4649
+ console.warn("[memory-library-sink] ingest not accepted:", res.status, body?.error ?? "");
4525
4650
  }
4526
4651
  } catch (err) {
4527
4652
  console.warn("[memory-library-sink] ingest failed:", err?.message);
package/dist/index.d.cts CHANGED
@@ -177,9 +177,19 @@ interface PaaInteractionObservation {
177
177
  errorCode: 'click_failed' | 'click_ack_timeout' | 'control_missing' | 'confirmation_timeout' | null;
178
178
  }
179
179
  type PaaProgressPhase = 'serp_captured' | 'discovered' | 'click_observed' | 'answer_captured' | 'fill_incomplete' | 'expansion_finished' | 'attempt_ending';
180
+ /** Observed organic pagination outcome; page two never expands a separate PAA graph. */
181
+ interface HarvestPaginationDiagnostics {
182
+ requestedPages: 1 | 2;
183
+ capturedPages: 1 | 2;
184
+ page2Status: 'not_requested' | 'not_attempted' | 'captured' | 'unavailable' | 'failed';
185
+ page1OrganicCount: number;
186
+ page2OrganicCount: number;
187
+ failureCode?: 'missing_next' | 'invalid_next' | 'empty_page' | 'captcha' | 'timeout' | 'navigation_error' | 'unsupported_driver';
188
+ }
180
189
  /** Material already observed before PAA expansion completes. It is checkpointed
181
190
  * so a worker interruption cannot discard the SERP that led to the PAA graph. */
182
191
  interface PaaProgressMaterial {
192
+ pagination?: HarvestPaginationDiagnostics;
183
193
  surface: GoogleSurface;
184
194
  aiOverview: AIOverviewResult;
185
195
  aiMode: AIModeResult;
@@ -201,6 +211,7 @@ interface PaaProgressSnapshot {
201
211
  interactions?: PaaInteractionObservation[];
202
212
  }
203
213
  interface HarvestDiagnostics {
214
+ pagination?: HarvestPaginationDiagnostics;
204
215
  completionStatus: HarvestCompletionStatus;
205
216
  /** Set only when the extractor directly observed a readable SERP without a PAA block. */
206
217
  noPaaObserved?: boolean;
package/dist/index.d.ts CHANGED
@@ -177,9 +177,19 @@ interface PaaInteractionObservation {
177
177
  errorCode: 'click_failed' | 'click_ack_timeout' | 'control_missing' | 'confirmation_timeout' | null;
178
178
  }
179
179
  type PaaProgressPhase = 'serp_captured' | 'discovered' | 'click_observed' | 'answer_captured' | 'fill_incomplete' | 'expansion_finished' | 'attempt_ending';
180
+ /** Observed organic pagination outcome; page two never expands a separate PAA graph. */
181
+ interface HarvestPaginationDiagnostics {
182
+ requestedPages: 1 | 2;
183
+ capturedPages: 1 | 2;
184
+ page2Status: 'not_requested' | 'not_attempted' | 'captured' | 'unavailable' | 'failed';
185
+ page1OrganicCount: number;
186
+ page2OrganicCount: number;
187
+ failureCode?: 'missing_next' | 'invalid_next' | 'empty_page' | 'captcha' | 'timeout' | 'navigation_error' | 'unsupported_driver';
188
+ }
180
189
  /** Material already observed before PAA expansion completes. It is checkpointed
181
190
  * so a worker interruption cannot discard the SERP that led to the PAA graph. */
182
191
  interface PaaProgressMaterial {
192
+ pagination?: HarvestPaginationDiagnostics;
183
193
  surface: GoogleSurface;
184
194
  aiOverview: AIOverviewResult;
185
195
  aiMode: AIModeResult;
@@ -201,6 +211,7 @@ interface PaaProgressSnapshot {
201
211
  interactions?: PaaInteractionObservation[];
202
212
  }
203
213
  interface HarvestDiagnostics {
214
+ pagination?: HarvestPaginationDiagnostics;
204
215
  completionStatus: HarvestCompletionStatus;
205
216
  /** Set only when the extractor directly observed a readable SERP without a PAA block. */
206
217
  noPaaObserved?: boolean;
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  harvest
3
- } from "./chunk-NH4QAH3X.js";
3
+ } from "./chunk-S24LFPL7.js";
4
4
  import "./chunk-OM7HVEJ3.js";
5
5
  import {
6
6
  recordVendorUsage
7
7
  } from "./chunk-OPQIGAFB.js";
8
- import "./chunk-OWF2JJKN.js";
8
+ import "./chunk-GXBZXWXB.js";
9
9
  import "./chunk-YXNDOQXN.js";
10
10
 
11
11
  // src/video/VideoGenerator.ts