mcp-scraper 0.3.23 → 0.3.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/bin/api-server.cjs +8174 -5965
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +3 -3
  4. package/dist/bin/mcp-scraper-cli.cjs +320 -58
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +4 -3
  7. package/dist/bin/mcp-scraper-cli.js.map +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +3 -3
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +2 -2
  11. package/dist/bin/mcp-stdio-server.cjs +121 -24
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -5
  14. package/dist/bin/paa-harvest.cjs +284 -10
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +5 -3
  17. package/dist/bin/paa-harvest.js.map +1 -1
  18. package/dist/{chunk-ZAP7MYVO.js → chunk-3PRO376E.js} +2 -3
  19. package/dist/chunk-3PRO376E.js.map +1 -0
  20. package/dist/{chunk-LFATOGDF.js → chunk-BP27CZ5Q.js} +4 -3
  21. package/dist/chunk-BP27CZ5Q.js.map +1 -0
  22. package/dist/{chunk-DE7WP4UU.js → chunk-DOBQN3EY.js} +3 -3
  23. package/dist/chunk-DOBQN3EY.js.map +1 -0
  24. package/dist/{chunk-7KKH7LA5.js → chunk-FE2WC4JR.js} +119 -23
  25. package/dist/chunk-FE2WC4JR.js.map +1 -0
  26. package/dist/{chunk-IGOEAKC7.js → chunk-G7PQ64LM.js} +274 -9
  27. package/dist/chunk-G7PQ64LM.js.map +1 -0
  28. package/dist/chunk-K443GQY5.js +24 -0
  29. package/dist/chunk-K443GQY5.js.map +1 -0
  30. package/dist/{chunk-H2R232HK.js → chunk-NNEIXK5L.js} +307 -62
  31. package/dist/chunk-NNEIXK5L.js.map +1 -0
  32. package/dist/{chunk-3KYRG7O7.js → chunk-TRNSXR46.js} +185 -2
  33. package/dist/chunk-TRNSXR46.js.map +1 -0
  34. package/dist/chunk-WPHAMNT7.js +7 -0
  35. package/dist/chunk-WPHAMNT7.js.map +1 -0
  36. package/dist/{chunk-4CPJKKY5.js → chunk-WYCER2HW.js} +43 -20
  37. package/dist/chunk-WYCER2HW.js.map +1 -0
  38. package/dist/{db-EG5ETPTY.js → db-FP2ABUU4.js} +32 -2
  39. package/dist/index.cjs +284 -10
  40. package/dist/index.cjs.map +1 -1
  41. package/dist/index.d.cts +7 -3
  42. package/dist/index.d.ts +7 -3
  43. package/dist/index.js +5 -3
  44. package/dist/index.js.map +1 -1
  45. package/dist/{server-2IA3HUEC.js → server-RRCKVBHG.js} +1579 -271
  46. package/dist/server-RRCKVBHG.js.map +1 -0
  47. package/dist/{worker-5K7TPBPR.js → worker-UC6D2756.js} +7 -6
  48. package/dist/{worker-5K7TPBPR.js.map → worker-UC6D2756.js.map} +1 -1
  49. package/docs/kernel-proxy-future-enhancements.md +80 -0
  50. package/docs/mcp-tool-manifest.generated.json +1 -1
  51. package/docs/spec-browser-routing.md +108 -0
  52. package/docs/spec-mcp-tools.md +146 -0
  53. package/docs/specs/audit-visual-demo.md +309 -0
  54. package/package.json +5 -2
  55. package/dist/chunk-3KYRG7O7.js.map +0 -1
  56. package/dist/chunk-4CPJKKY5.js.map +0 -1
  57. package/dist/chunk-7KKH7LA5.js.map +0 -1
  58. package/dist/chunk-DE7WP4UU.js.map +0 -1
  59. package/dist/chunk-H2R232HK.js.map +0 -1
  60. package/dist/chunk-IGOEAKC7.js.map +0 -1
  61. package/dist/chunk-LFATOGDF.js.map +0 -1
  62. package/dist/chunk-YSSWPZ2B.js +0 -7
  63. package/dist/chunk-YSSWPZ2B.js.map +0 -1
  64. package/dist/chunk-ZAP7MYVO.js.map +0 -1
  65. package/dist/server-2IA3HUEC.js.map +0 -1
  66. /package/dist/{db-EG5ETPTY.js.map → db-FP2ABUU4.js.map} +0 -0
@@ -7,12 +7,206 @@ import {
7
7
  browserServiceApiKey,
8
8
  browserServiceProxyId,
9
9
  sanitizeVendorName
10
- } from "./chunk-ZAP7MYVO.js";
10
+ } from "./chunk-3PRO376E.js";
11
+ import {
12
+ postToMemoryLibrary
13
+ } from "./chunk-K443GQY5.js";
11
14
  import {
12
15
  DEFAULT_PROXY_MODE,
13
16
  HarvestOptionsSchema,
14
17
  RawPAAItemSchema
15
- } from "./chunk-LFATOGDF.js";
18
+ } from "./chunk-BP27CZ5Q.js";
19
+ import {
20
+ getDb
21
+ } from "./chunk-TRNSXR46.js";
22
+
23
+ // src/api/cost-context.ts
24
+ import { AsyncLocalStorage } from "async_hooks";
25
+ var storage = new AsyncLocalStorage();
26
+ function runWithCostContext(ctx, fn) {
27
+ return storage.run(ctx, fn);
28
+ }
29
+ function currentCostContext() {
30
+ return storage.getStore();
31
+ }
32
+
33
+ // src/api/cost-telemetry.ts
34
+ import { randomUUID } from "crypto";
35
+
36
+ // src/api/cost-rates.ts
37
+ var KERNEL_HEADLESS_USD_PER_SEC = 166667e-10;
38
+ var KERNEL_HEADFUL_USD_PER_SEC = 1333336e-10;
39
+ var HEADLESS_OPS = /* @__PURE__ */ new Set(["harvest", "maps_search", "yt_channel", "fb_search", "fb_ad", "instagram"]);
40
+ var FAL_WIZPER_USD_PER_AUDIO_MIN = 5e-3;
41
+ var DEEPINFRA_QWEN_USD_PER_1K_OUT_TOKENS = 4e-4;
42
+ function kernelCostUsd(ms, headful) {
43
+ const sec = Math.max(0, ms) / 1e3;
44
+ return sec * (headful ? KERNEL_HEADFUL_USD_PER_SEC : KERNEL_HEADLESS_USD_PER_SEC);
45
+ }
46
+ function vendorCostUsd(vendor, units) {
47
+ if (vendor === "fal_wizper") return Math.max(0, units) * FAL_WIZPER_USD_PER_AUDIO_MIN;
48
+ if (vendor === "deepinfra_qwen") return Math.max(0, units) / 1e3 * DEEPINFRA_QWEN_USD_PER_1K_OUT_TOKENS;
49
+ return 0;
50
+ }
51
+
52
+ // src/api/cost-telemetry.ts
53
+ var _ready = false;
54
+ async function migrateCostTelemetry() {
55
+ if (_ready) return;
56
+ const db = getDb();
57
+ await db.execute(`
58
+ CREATE TABLE IF NOT EXISTS kernel_session_log (
59
+ id TEXT PRIMARY KEY,
60
+ kernel_session_id TEXT,
61
+ op TEXT,
62
+ source TEXT NOT NULL,
63
+ probe_run_id TEXT,
64
+ user_id INTEGER,
65
+ stealth INTEGER,
66
+ headless_sent INTEGER,
67
+ proxy_used INTEGER,
68
+ fallback INTEGER NOT NULL DEFAULT 0,
69
+ opened_at TEXT,
70
+ closed_at TEXT,
71
+ duration_ms INTEGER NOT NULL DEFAULT 0,
72
+ est_cost_usd_headless REAL NOT NULL DEFAULT 0,
73
+ est_cost_usd_headful REAL NOT NULL DEFAULT 0,
74
+ error TEXT,
75
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
76
+ )
77
+ `);
78
+ await db.execute(`CREATE INDEX IF NOT EXISTS kernel_session_log_op ON kernel_session_log(op)`);
79
+ await db.execute(`CREATE INDEX IF NOT EXISTS kernel_session_log_probe ON kernel_session_log(probe_run_id)`);
80
+ await db.execute(`CREATE INDEX IF NOT EXISTS kernel_session_log_created ON kernel_session_log(created_at)`);
81
+ try {
82
+ await db.execute(`ALTER TABLE kernel_session_log ADD COLUMN proxy_source TEXT`);
83
+ } catch {
84
+ }
85
+ try {
86
+ await db.execute(`ALTER TABLE kernel_session_log ADD COLUMN proxy_type TEXT`);
87
+ } catch {
88
+ }
89
+ await db.execute(`
90
+ CREATE TABLE IF NOT EXISTS vendor_usage_log (
91
+ id TEXT PRIMARY KEY,
92
+ op TEXT,
93
+ probe_run_id TEXT,
94
+ user_id INTEGER,
95
+ vendor TEXT NOT NULL,
96
+ model TEXT,
97
+ units REAL NOT NULL DEFAULT 0,
98
+ unit_type TEXT,
99
+ est_cost_usd REAL NOT NULL DEFAULT 0,
100
+ error TEXT,
101
+ created_at TEXT NOT NULL DEFAULT (datetime('now'))
102
+ )
103
+ `);
104
+ await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_op ON vendor_usage_log(op)`);
105
+ await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_probe ON vendor_usage_log(probe_run_id)`);
106
+ await db.execute(`CREATE INDEX IF NOT EXISTS vendor_usage_log_created ON vendor_usage_log(created_at)`);
107
+ await db.execute(`
108
+ CREATE TABLE IF NOT EXISTS cost_probe_runs (
109
+ id TEXT PRIMARY KEY,
110
+ tool TEXT NOT NULL,
111
+ target TEXT,
112
+ started_at TEXT NOT NULL DEFAULT (datetime('now')),
113
+ wall_ms INTEGER NOT NULL DEFAULT 0,
114
+ success INTEGER NOT NULL DEFAULT 0,
115
+ error TEXT,
116
+ http_only INTEGER NOT NULL DEFAULT 0,
117
+ kernel_used INTEGER NOT NULL DEFAULT 0,
118
+ kernel_sessions INTEGER NOT NULL DEFAULT 0,
119
+ kernel_seconds_total REAL NOT NULL DEFAULT 0,
120
+ kernel_tier_observed TEXT,
121
+ est_kernel_cost_usd_headless REAL NOT NULL DEFAULT 0,
122
+ est_kernel_cost_usd_headful REAL NOT NULL DEFAULT 0,
123
+ vendor_cost_usd REAL NOT NULL DEFAULT 0,
124
+ charged_credits REAL NOT NULL DEFAULT 0,
125
+ margin_usd_headless_starter REAL NOT NULL DEFAULT 0,
126
+ margin_usd_headful_starter REAL NOT NULL DEFAULT 0,
127
+ notes TEXT
128
+ )
129
+ `);
130
+ await db.execute(`CREATE INDEX IF NOT EXISTS cost_probe_runs_tool ON cost_probe_runs(tool)`);
131
+ try {
132
+ await db.execute(`ALTER TABLE cost_probe_runs ADD COLUMN units REAL`);
133
+ } catch {
134
+ }
135
+ try {
136
+ await db.execute(`ALTER TABLE cost_probe_runs ADD COLUMN unit_type TEXT`);
137
+ } catch {
138
+ }
139
+ try {
140
+ await db.execute(`ALTER TABLE cost_probe_runs ADD COLUMN mode TEXT`);
141
+ } catch {
142
+ }
143
+ _ready = true;
144
+ }
145
+ async function recordKernelSession(r) {
146
+ try {
147
+ await migrateCostTelemetry();
148
+ const ctx = currentCostContext();
149
+ const durationMs = Math.max(0, r.closedAtMs - r.openedAtMs);
150
+ const db = getDb();
151
+ await db.execute({
152
+ sql: `INSERT INTO kernel_session_log
153
+ (id, kernel_session_id, op, source, probe_run_id, user_id, stealth, headless_sent, proxy_used, proxy_source, proxy_type, fallback, opened_at, closed_at, duration_ms, est_cost_usd_headless, est_cost_usd_headful, error)
154
+ VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?)`,
155
+ args: [
156
+ randomUUID(),
157
+ r.kernelSessionId ?? null,
158
+ ctx?.op ?? null,
159
+ r.source,
160
+ ctx?.probeRunId ?? null,
161
+ ctx?.userId ?? null,
162
+ boolToInt(r.stealth),
163
+ boolToInt(r.headlessSent),
164
+ boolToInt(r.proxyUsed),
165
+ r.proxySource ?? null,
166
+ r.proxyType ?? null,
167
+ r.fallback ? 1 : 0,
168
+ new Date(r.openedAtMs).toISOString(),
169
+ new Date(r.closedAtMs).toISOString(),
170
+ durationMs,
171
+ kernelCostUsd(durationMs, false),
172
+ kernelCostUsd(durationMs, true),
173
+ r.error ?? null
174
+ ]
175
+ });
176
+ } catch (err) {
177
+ console.warn("[cost-telemetry] recordKernelSession failed:", err instanceof Error ? err.message : String(err));
178
+ }
179
+ }
180
+ async function recordVendorUsage(r) {
181
+ try {
182
+ await migrateCostTelemetry();
183
+ const ctx = currentCostContext();
184
+ const db = getDb();
185
+ await db.execute({
186
+ sql: `INSERT INTO vendor_usage_log
187
+ (id, op, probe_run_id, user_id, vendor, model, units, unit_type, est_cost_usd, error)
188
+ VALUES (?,?,?,?,?,?,?,?,?,?)`,
189
+ args: [
190
+ randomUUID(),
191
+ ctx?.op ?? null,
192
+ ctx?.probeRunId ?? null,
193
+ ctx?.userId ?? null,
194
+ r.vendor,
195
+ r.model ?? null,
196
+ r.units,
197
+ r.unitType,
198
+ vendorCostUsd(r.vendor, r.units),
199
+ r.error ?? null
200
+ ]
201
+ });
202
+ } catch (err) {
203
+ console.warn("[cost-telemetry] recordVendorUsage failed:", err instanceof Error ? err.message : String(err));
204
+ }
205
+ }
206
+ function boolToInt(v) {
207
+ if (v === null || v === void 0) return null;
208
+ return v ? 1 : 0;
209
+ }
16
210
 
17
211
  // src/driver/BrowserDriver.ts
18
212
  import { chromium } from "playwright-extra";
@@ -206,6 +400,12 @@ var BrowserDriver = class {
206
400
  page = null;
207
401
  kernelClient = null;
208
402
  kernelSessionId = null;
403
+ kernelOpenedAtMs = null;
404
+ kernelStealth = null;
405
+ kernelProxyUsed = null;
406
+ kernelProxySource = null;
407
+ kernelProxyType = null;
408
+ kernelHeadlessSent = null;
209
409
  debugEnabled = false;
210
410
  debugSnapshot = {
211
411
  kernel: null,
@@ -237,9 +437,16 @@ var BrowserDriver = class {
237
437
  if (config.kernelProfileName && config.kernelProfileSaveChanges === true) {
238
438
  await ensureKernelProfile(this.kernelClient, config.kernelProfileName);
239
439
  }
440
+ const costCtx = currentCostContext();
441
+ let useHeadless;
442
+ if (config.headlessMode === "headful" || costCtx?.forceHeadful === true) useHeadless = false;
443
+ else if (config.headlessMode === "headless") useHeadless = true;
444
+ else useHeadless = costCtx?.forceHeadless === true || process.env.KERNEL_HEADLESS_DISABLED !== "true" && !!costCtx?.op && HEADLESS_OPS.has(costCtx.op);
445
+ this.kernelHeadlessSent = useHeadless ? true : null;
240
446
  const kernelBrowser = await this.kernelClient.browsers.create({
241
447
  stealth: true,
242
448
  timeout_seconds: timeoutSeconds,
449
+ ...useHeadless ? { headless: true } : {},
243
450
  ...config.kernelProxyId ? { proxy_id: config.kernelProxyId } : {},
244
451
  ...config.kernelProfileName ? {
245
452
  profile: {
@@ -249,9 +456,14 @@ var BrowserDriver = class {
249
456
  } : {}
250
457
  });
251
458
  this.kernelSessionId = kernelBrowser.session_id;
459
+ this.kernelOpenedAtMs = Date.now();
460
+ this.kernelStealth = typeof kernelBrowser.stealth === "boolean" ? kernelBrowser.stealth : null;
461
+ this.kernelProxyUsed = typeof kernelBrowser.proxy_id === "string" ? Boolean(kernelBrowser.proxy_id) : Boolean(config.kernelProxyId);
462
+ this.kernelProxySource = config.kernelProxyResolution?.source ?? null;
463
+ this.kernelProxyType = config.kernelProxyResolution?.proxyType ?? null;
252
464
  let defaultProxyDisabled = null;
253
465
  let defaultProxyDisableError = null;
254
- if (proxyMode === "none") {
466
+ if (!config.kernelProxyId) {
255
467
  try {
256
468
  await withTimeout(
257
469
  this.kernelClient.browsers.update(this.kernelSessionId, { disable_default_proxy: true }),
@@ -602,11 +814,24 @@ var BrowserDriver = class {
602
814
  const b = this.browser;
603
815
  const sessionId = this.kernelSessionId;
604
816
  const client = this.kernelClient;
817
+ const openedAtMs = this.kernelOpenedAtMs;
818
+ const stealth = this.kernelStealth;
819
+ const proxyUsed = this.kernelProxyUsed;
820
+ const proxySource = this.kernelProxySource;
821
+ const proxyType = this.kernelProxyType;
822
+ const headlessSent = this.kernelHeadlessSent;
605
823
  this.browser = null;
606
824
  this.context = null;
607
825
  this.page = null;
608
826
  this.kernelSessionId = null;
609
827
  this.kernelClient = null;
828
+ this.kernelOpenedAtMs = null;
829
+ this.kernelHeadlessSent = null;
830
+ this.kernelProxySource = null;
831
+ this.kernelProxyType = null;
832
+ if (client && sessionId && openedAtMs != null) {
833
+ void recordKernelSession({ kernelSessionId: sessionId, source: "browser_driver", stealth, headlessSent, proxyUsed, proxySource, proxyType, openedAtMs, closedAtMs: Date.now() });
834
+ }
610
835
  if (client && sessionId) {
611
836
  console.info(JSON.stringify({
612
837
  event: "kernel_browser_delete_started",
@@ -1321,6 +1546,7 @@ var PAAExtractor = class {
1321
1546
  );
1322
1547
  let round = 0;
1323
1548
  while (seenQs.size < options.maxQuestions) {
1549
+ if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
1324
1550
  this.throwIfAborted(signal);
1325
1551
  await this.throwIfCaptcha(page, "Google PAA expansion");
1326
1552
  const beforeQs = await readAllQs();
@@ -1330,6 +1556,7 @@ var PAAExtractor = class {
1330
1556
  if (unexpandedCount === 0) break;
1331
1557
  this.reporter.onDepth(++round);
1332
1558
  for (let ci = 0; ci < unexpandedCount; ci++) {
1559
+ if (options.softDeadlineMs && Date.now() >= options.softDeadlineMs) break;
1333
1560
  this.throwIfAborted(signal);
1334
1561
  try {
1335
1562
  const btn = page.locator(unexpandedSel).first();
@@ -2000,6 +2227,7 @@ var OutputSerializer = class {
2000
2227
  const filename = `${slug}-${Date.now()}.json`;
2001
2228
  const fullPath = path.join(outputDir, filename);
2002
2229
  await fs.writeFile(fullPath, JSON.stringify(result, null, 2), "utf8");
2230
+ await postToMemoryLibrary({ title: `${result.seed} scrape`, content: JSON.stringify(result), source: `mcp-scraper:${fullPath}` });
2003
2231
  return fullPath;
2004
2232
  }
2005
2233
  async writeCSV(rows, outputDir) {
@@ -2197,11 +2425,13 @@ function proxyIdSuffix2(proxyId) {
2197
2425
  return proxyId ? proxyId.slice(-6) : null;
2198
2426
  }
2199
2427
  function resolution(source, proxyMode, proxyId, target, error, disposable = false) {
2428
+ const proxyType = source === "disabled" ? "none" : source === "configured_fallback" ? "configured" : source === "isp_created" ? "isp" : source === "location_created" || source === "location_reused" ? "residential" : "unknown";
2200
2429
  return {
2201
2430
  kernelProxyId: proxyId,
2202
2431
  ...disposable && proxyId ? { disposableProxyId: proxyId } : {},
2203
2432
  resolution: {
2204
2433
  source,
2434
+ proxyType,
2205
2435
  proxyMode,
2206
2436
  proxyIdPresent: Boolean(proxyId),
2207
2437
  proxyIdSuffix: proxyIdSuffix2(proxyId),
@@ -2396,6 +2626,22 @@ async function createFreshLocationProxy(kernel, options, target) {
2396
2626
  createErrors.push(`${candidate.proxyName}: ${errorText2(err)}`);
2397
2627
  }
2398
2628
  }
2629
+ if (process.env.KERNEL_PROXY_TYPE !== "residential") {
2630
+ const ispTarget = stateTarget(target);
2631
+ try {
2632
+ const created = await kernel.proxies.create({
2633
+ type: "isp",
2634
+ name: freshProxyName(`mcp-serp-isp-${ispTarget.country.toLowerCase()}-${(ispTarget.state ?? "").toLowerCase()}`, options.attemptIndex),
2635
+ config: { country: ispTarget.country, ...ispTarget.state ? { state: ispTarget.state } : {} }
2636
+ });
2637
+ if (created.id) {
2638
+ return resolution("isp_created", options.proxyMode, created.id, ispTarget, null, true);
2639
+ }
2640
+ createErrors.push("isp: Kernel did not return a proxy id");
2641
+ } catch (err) {
2642
+ createErrors.push(`isp: ${errorText2(err)}`);
2643
+ }
2644
+ }
2399
2645
  return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, createErrors.join(" | "));
2400
2646
  }
2401
2647
  async function deleteKernelProxyId(kernelApiKey, proxyId) {
@@ -2679,9 +2925,11 @@ async function harvest(rawOptions) {
2679
2925
  };
2680
2926
  const requestedDebug = typeof raw.debug === "boolean" ? raw.debug : false;
2681
2927
  const needsLocationEvidence = proxyMode === "location" && Boolean(proxyOpts.location);
2682
- const maxAttempts = maxAttemptsForProxyMode(proxyMode);
2928
+ const maxAttempts = maxAttemptsForProxyMode(proxyMode) + 1;
2683
2929
  const serializer = new OutputSerializer();
2684
2930
  let lastError = null;
2931
+ let effectiveProxyMode = proxyMode;
2932
+ let proxyFailures = 0;
2685
2933
  for (let i = 0; i < maxAttempts; i++) {
2686
2934
  const attemptNumber = i + 1;
2687
2935
  const startedAtMs = Date.now();
@@ -2689,18 +2937,19 @@ async function harvest(rawOptions) {
2689
2937
  if (signal?.aborted) throw abortReason(signal);
2690
2938
  const resolution2 = await resolveKernelProxyId({
2691
2939
  ...proxyOpts,
2940
+ proxyMode: effectiveProxyMode,
2692
2941
  attemptIndex: i,
2693
- fresh: proxyMode === "location"
2942
+ fresh: effectiveProxyMode === "location"
2694
2943
  });
2695
2944
  const mergedAttempt = {
2696
2945
  ...raw,
2697
2946
  kernelApiKey,
2698
2947
  kernelProxyId: resolution2.kernelProxyId,
2699
2948
  kernelProxyResolution: resolution2.resolution,
2700
- proxyMode,
2949
+ proxyMode: effectiveProxyMode,
2701
2950
  debug: requestedDebug || needsLocationEvidence
2702
2951
  };
2703
- if (proxyMode === "none") mergedAttempt.kernelProxyId = void 0;
2952
+ if (effectiveProxyMode === "none") mergedAttempt.kernelProxyId = void 0;
2704
2953
  const attemptOptions = HarvestOptionsSchema.parse(mergedAttempt);
2705
2954
  await emitAttemptEvent(onAttemptEvent, {
2706
2955
  type: "started",
@@ -2719,10 +2968,17 @@ async function harvest(rawOptions) {
2719
2968
  location: attemptOptions.location ?? null,
2720
2969
  max_questions: attemptOptions.maxQuestions
2721
2970
  }));
2722
- const attempt = await extractOnce(attemptOptions, signal);
2971
+ const isHeadfulEscalation = i === maxAttempts - 1;
2972
+ const needHeadful = isHeadfulEscalation || !attemptOptions.serpOnly;
2973
+ const baseCtx = currentCostContext();
2974
+ const attempt = needHeadful && baseCtx ? await runWithCostContext({ ...baseCtx, forceHeadful: true }, () => extractOnce(attemptOptions, signal)) : await extractOnce(attemptOptions, signal);
2723
2975
  if (attempt.error) {
2724
2976
  const err = attempt.error;
2725
2977
  const outcome = classifyAttemptError(err);
2978
+ if ((outcome === "captcha" || outcome === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
2979
+ proxyFailures++;
2980
+ if (proxyFailures >= 2) effectiveProxyMode = "none";
2981
+ }
2726
2982
  const willRetry = i < maxAttempts - 1 && (outcome === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome));
2727
2983
  if (outcome === "captcha") {
2728
2984
  console.warn(JSON.stringify({
@@ -2824,6 +3080,10 @@ async function harvest(rawOptions) {
2824
3080
  return finalResult;
2825
3081
  } catch (err) {
2826
3082
  const outcome = classifyAttemptError(err);
3083
+ if ((outcome === "captcha" || outcome === "proxy_tunnel_failed") && effectiveProxyMode === "location") {
3084
+ proxyFailures++;
3085
+ if (proxyFailures >= 2) effectiveProxyMode = "none";
3086
+ }
2827
3087
  const willRetry = i < maxAttempts - 1 && (outcome === "captcha" || proxyMode === "location" && retryableLocationProxyError(outcome));
2828
3088
  if (outcome === "captcha") {
2829
3089
  console.warn(JSON.stringify({
@@ -2881,10 +3141,15 @@ async function harvest(rawOptions) {
2881
3141
 
2882
3142
  export {
2883
3143
  MapsSelectors,
3144
+ runWithCostContext,
3145
+ currentCostContext,
3146
+ migrateCostTelemetry,
3147
+ recordKernelSession,
3148
+ recordVendorUsage,
2884
3149
  buildYouTubeChannelVideosUrl,
2885
3150
  BrowserDriver,
2886
3151
  deleteKernelProxyId,
2887
3152
  resolveKernelProxyId,
2888
3153
  harvest
2889
3154
  };
2890
- //# sourceMappingURL=chunk-IGOEAKC7.js.map
3155
+ //# sourceMappingURL=chunk-G7PQ64LM.js.map