mcp-scraper 0.4.13 → 0.5.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.
Files changed (33) hide show
  1. package/dist/bin/api-server.cjs +63 -157
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +2 -2
  4. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +1 -1
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +269 -3
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +3 -2
  13. package/dist/bin/mcp-stdio-server.js.map +1 -1
  14. package/dist/chunk-3RTPEQJC.js +7 -0
  15. package/dist/chunk-3RTPEQJC.js.map +1 -0
  16. package/dist/{chunk-SIPXEFTR.js → chunk-APJO2XV5.js} +6 -6
  17. package/dist/chunk-APJO2XV5.js.map +1 -0
  18. package/dist/{chunk-VQV7MB2S.js → chunk-BL7BBSYF.js} +2 -2
  19. package/dist/{chunk-EA2AANFI.js → chunk-J4T5OSCF.js} +7 -4
  20. package/dist/chunk-J4T5OSCF.js.map +1 -0
  21. package/dist/{server-HNZG36IP.js → server-Z4MEISH5.js} +59 -154
  22. package/dist/server-Z4MEISH5.js.map +1 -0
  23. package/dist/{site-extract-repository-6X2C7RLF.js → site-extract-repository-U476J44K.js} +3 -3
  24. package/dist/{worker-WCSKUAOT.js → worker-KJZ3ZN2N.js} +2 -2
  25. package/package.json +1 -1
  26. package/dist/chunk-EA2AANFI.js.map +0 -1
  27. package/dist/chunk-O2D5HGKF.js +0 -7
  28. package/dist/chunk-O2D5HGKF.js.map +0 -1
  29. package/dist/chunk-SIPXEFTR.js.map +0 -1
  30. package/dist/server-HNZG36IP.js.map +0 -1
  31. /package/dist/{chunk-VQV7MB2S.js.map → chunk-BL7BBSYF.js.map} +0 -0
  32. /package/dist/{site-extract-repository-6X2C7RLF.js.map → site-extract-repository-U476J44K.js.map} +0 -0
  33. /package/dist/{worker-WCSKUAOT.js.map → worker-KJZ3ZN2N.js.map} +0 -0
@@ -8013,16 +8013,16 @@ var init_rates = __esm({
8013
8013
  browser_minute: 12e3,
8014
8014
  reddit_thread: 3e3,
8015
8015
  reddit_comment: 200,
8016
- video_analysis: 666667,
8016
+ video_analysis: 666700,
8017
8017
  trustpilot_reviews: 500,
8018
8018
  trustpilot_review: 100,
8019
8019
  g2_reviews: 500,
8020
- g2_review: 150,
8020
+ g2_review: 200,
8021
8021
  diff_page: 100
8022
8022
  };
8023
8023
  MC_PER_BROWSER_MS = MC_COSTS.browser_minute / 6e4;
8024
8024
  BROWSER_OPEN_MIN_BALANCE_MC = 1e3;
8025
- MC_PER_CREDIT = 1e3;
8025
+ MC_PER_CREDIT = 100;
8026
8026
  CREDIT_COST_CATALOG = [
8027
8027
  {
8028
8028
  key: "serp",
@@ -8242,8 +8242,8 @@ var init_rates = __esm({
8242
8242
  CONCURRENCY_PRICE_ID = "price_1Ta1NRS8aAcsk3TGwsRnYbix";
8243
8243
  SUBSCRIPTION_TIERS = {
8244
8244
  "price_1TmiHRS8aAcsk3TGwmSNfNIa": { tier: "starter", label: "Starter", price_id: "price_1TmiHRS8aAcsk3TGwmSNfNIa", monthly_usd: 12, credits_mc: 8e6, concurrency: 3, intro_coupon: "mcp-starter-1dollar-intro-12" },
8245
- "price_1Tmg1nS8aAcsk3TGe8zcnGTM": { tier: "growth", label: "Growth", price_id: "price_1Tmg1nS8aAcsk3TGe8zcnGTM", monthly_usd: 100, credits_mc: 8e7, concurrency: 10, intro_coupon: null },
8246
- "price_1Tmg1nS8aAcsk3TGsZw34iXS": { tier: "scale", label: "Scale", price_id: "price_1Tmg1nS8aAcsk3TGsZw34iXS", monthly_usd: 250, credits_mc: 24e7, concurrency: 20, intro_coupon: null }
8245
+ "price_1TrgihS8aAcsk3TG5cglrq4D": { tier: "growth", label: "Growth", price_id: "price_1TrgihS8aAcsk3TG5cglrq4D", monthly_usd: 40, credits_mc: 26666700, concurrency: 10, intro_coupon: null },
8246
+ "price_1TrgihS8aAcsk3TG4HnG4gbY": { tier: "scale", label: "Scale", price_id: "price_1TrgihS8aAcsk3TG4HnG4gbY", monthly_usd: 100, credits_mc: 8e7, concurrency: 20, intro_coupon: null }
8247
8247
  };
8248
8248
  SUBSCRIPTION_TIER_BY_KEY = Object.fromEntries(
8249
8249
  Object.values(SUBSCRIPTION_TIERS).map((t) => [t.tier, t])
@@ -13705,13 +13705,12 @@ function parseTimedtextXml(xml) {
13705
13705
  }
13706
13706
  return results;
13707
13707
  }
13708
- function isBotBlockStatus(status) {
13709
- return status === "LOGIN_REQUIRED" || status === "AGE_VERIFICATION_REQUIRED";
13710
- }
13711
13708
  async function probePlayer(page, videoId) {
13712
13709
  return page.evaluate(
13713
13710
  async ({ vid, clients }) => {
13714
13711
  let lastStatus = null;
13712
+ let sawBotBlock = false;
13713
+ let sawAccessible = false;
13715
13714
  for (const client2 of clients) {
13716
13715
  const resp = await fetch("/youtubei/v1/player?prettyPrint=false", {
13717
13716
  method: "POST",
@@ -13727,97 +13726,24 @@ async function probePlayer(page, videoId) {
13727
13726
  }
13728
13727
  const data = await resp.json();
13729
13728
  lastStatus = data?.playabilityStatus?.status ?? null;
13730
- const audioFormats = (data?.streamingData?.adaptiveFormats ?? []).filter((f) => typeof f.url === "string" && f.url && typeof f.mimeType === "string" && f.mimeType.startsWith("audio/")).sort((a, b) => (Number(a.contentLength) || Infinity) - (Number(b.contentLength) || Infinity));
13731
- const pick = audioFormats[0] ?? null;
13732
- let captions = null;
13729
+ if (lastStatus === "LOGIN_REQUIRED" || lastStatus === "AGE_VERIFICATION_REQUIRED") sawBotBlock = true;
13730
+ else if (lastStatus === "OK") sawAccessible = true;
13733
13731
  const tracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
13734
13732
  if (tracks.length > 0) {
13735
13733
  const track = tracks.find((t) => t.languageCode === "en") ?? tracks[0];
13736
13734
  const sep = track.baseUrl.includes("?") ? "&" : "?";
13737
13735
  const xmlResp = await fetch(`${track.baseUrl}${sep}fmt=json3`).catch(() => null);
13738
- if (xmlResp?.ok) captions = await xmlResp.text();
13736
+ if (xmlResp?.ok) {
13737
+ const captions = await xmlResp.text();
13738
+ if (captions) return { captions, status: lastStatus, escalate: false };
13739
+ }
13739
13740
  }
13740
- if (!captions && !pick) continue;
13741
- return {
13742
- captions,
13743
- status: lastStatus,
13744
- audio: pick ? {
13745
- url: pick.url,
13746
- mimeType: pick.mimeType,
13747
- bitrate: pick.bitrate ?? null,
13748
- contentLength: pick.contentLength ? Number(pick.contentLength) || null : null,
13749
- approxDurationMs: pick.approxDurationMs ? Number(pick.approxDurationMs) || null : null
13750
- } : null
13751
- };
13752
13741
  }
13753
- return { captions: null, audio: null, status: lastStatus };
13742
+ return { captions: null, status: lastStatus, escalate: sawBotBlock && !sawAccessible };
13754
13743
  },
13755
13744
  { vid: videoId, clients: INNERTUBE_CLIENTS }
13756
13745
  ).catch(() => null);
13757
13746
  }
13758
- async function downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, audio) {
13759
- if (audio.contentLength && audio.contentLength > MAX_AUDIO_BYTES) {
13760
- console.warn(`[CaptionFetcher] audio stream ${audio.contentLength} bytes exceeds ${MAX_AUDIO_BYTES} byte cap`);
13761
- return null;
13762
- }
13763
- const sep = audio.url.includes("?") ? "&" : "?";
13764
- const url = audio.contentLength ? `${audio.url}${sep}range=0-${audio.contentLength - 1}` : audio.url;
13765
- const result = await page.evaluate(async (u) => {
13766
- try {
13767
- const resp = await fetch(u);
13768
- if (!resp.ok) return { ok: false, error: `http_${resp.status}` };
13769
- const blob = await resp.blob();
13770
- const dataUrl = await new Promise((resolve, reject) => {
13771
- const reader = new FileReader();
13772
- reader.onload = () => resolve(reader.result);
13773
- reader.onerror = () => reject(reader.error);
13774
- reader.readAsDataURL(blob);
13775
- });
13776
- return { ok: true, base64: dataUrl.slice(dataUrl.indexOf(",") + 1) };
13777
- } catch (e) {
13778
- return { ok: false, error: String(e) };
13779
- }
13780
- }, url).catch((e) => ({ ok: false, error: String(e) }));
13781
- if (!result.ok || !result.base64) {
13782
- console.warn(`[CaptionFetcher] audio fetch failed (${result.error ?? "unknown"}) for a ${audio.contentLength ?? 0} byte stream`);
13783
- return null;
13784
- }
13785
- const bytes = Buffer.from(result.base64, "base64");
13786
- if (audio.contentLength && bytes.length < audio.contentLength) {
13787
- console.warn(`[CaptionFetcher] partial audio: ${bytes.length} of ${audio.contentLength} bytes`);
13788
- return null;
13789
- }
13790
- return { bytes, mimeType: audio.mimeType.split(";")[0].trim() };
13791
- }
13792
- async function transcribeWithWizper(videoId, audio, startMs) {
13793
- const falKey = process.env.FAL_KEY;
13794
- if (!falKey) return null;
13795
- try {
13796
- import_client4.fal.config({ credentials: falKey });
13797
- const ext = audio.mimeType.includes("webm") ? "webm" : audio.mimeType.includes("mp4") ? "m4a" : "audio";
13798
- const audioFile = new File([new Uint8Array(audio.bytes)], `audio.${ext}`, { type: audio.mimeType });
13799
- const uploadedUrl = await import_client4.fal.storage.upload(audioFile);
13800
- const subscribeStartMs = Date.now();
13801
- const result = await import_client4.fal.subscribe("fal-ai/wizper", {
13802
- input: { audio_url: uploadedUrl, task: "transcribe", language: "en", chunk_level: "segment" },
13803
- logs: false,
13804
- pollInterval: 3e3
13805
- });
13806
- const subscribeDurationMs = Date.now() - subscribeStartMs;
13807
- const data = result.data;
13808
- const chunks = (data.chunks ?? []).map((c) => ({
13809
- timestamp: c.timestamp,
13810
- text: c.text.trim()
13811
- }));
13812
- void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: subscribeDurationMs / 1e3, unitType: "compute_sec" });
13813
- const text = data.text ?? chunks.map((c) => c.text).join(" ");
13814
- if (!text) return null;
13815
- return { videoId, text, chunks, durationMs: Date.now() - startMs, method: "browser-whisper" };
13816
- } catch (err) {
13817
- console.warn(`[CaptionFetcher] wizper transcription failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
13818
- return null;
13819
- }
13820
- }
13821
13747
  async function resolveResidentialProxy(kernelApiKey) {
13822
13748
  try {
13823
13749
  const resolved = await resolveKernelProxyId({
@@ -13833,14 +13759,13 @@ async function resolveResidentialProxy(kernelApiKey) {
13833
13759
  return {};
13834
13760
  }
13835
13761
  }
13836
- async function probeAndExtract(videoId, opts) {
13762
+ async function probeCaptions(videoId, kernelProxyIdOverride) {
13837
13763
  const kernelApiKey = browserServiceApiKey();
13838
13764
  if (!kernelApiKey) return null;
13839
- const kernelProxyId = opts?.kernelProxyId ?? browserServiceProxyId();
13765
+ const kernelProxyId = kernelProxyIdOverride ?? browserServiceProxyId();
13840
13766
  const driver = new BrowserDriver();
13841
13767
  return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "browser-innertube" }, async () => {
13842
13768
  let entries = [];
13843
- let audio = null;
13844
13769
  let retryWithFreshIp = false;
13845
13770
  try {
13846
13771
  await driver.launch({ kernelApiKey, kernelProxyId, headless: true, headlessMode: "headless", viewport: { width: 1280, height: 800 }, locale: "en-US" });
@@ -13852,69 +13777,51 @@ async function probeAndExtract(videoId, opts) {
13852
13777
  );
13853
13778
  const probe = await probePlayer(page, videoId);
13854
13779
  if (!probe) return null;
13855
- if (probe.captions && !opts?.skipCaptions) {
13780
+ if (probe.captions) {
13856
13781
  entries = parseTimedtextJson3(probe.captions);
13857
13782
  if (entries.length === 0) entries = parseTimedtextXml(probe.captions);
13858
13783
  }
13859
13784
  if (entries.length === 0) {
13860
- if (!probe.audio) {
13861
- retryWithFreshIp = isBotBlockStatus(probe.status);
13862
- console.warn(`[CaptionFetcher] no captions and no audio stream for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)})`);
13863
- return { entries, audio: null, retryWithFreshIp };
13864
- }
13865
- audio = await downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, probe.audio);
13866
- if (!audio) return { entries, audio: null, retryWithFreshIp: true };
13785
+ retryWithFreshIp = probe.escalate;
13786
+ console.warn(`[CaptionFetcher] no caption track for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)}, escalate=${probe.escalate})`);
13867
13787
  }
13868
13788
  } catch (err) {
13869
13789
  console.warn(`[CaptionFetcher] kernel session failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
13870
13790
  return null;
13871
13791
  } finally {
13872
- const servedSubOp = audio ? "browser-whisper" : "browser-innertube";
13873
- await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: servedSubOp }, () => driver.close());
13792
+ await driver.close();
13874
13793
  }
13875
- return { entries, audio, retryWithFreshIp };
13794
+ return { entries, retryWithFreshIp };
13876
13795
  });
13877
13796
  }
13878
- async function extractWithProxyEscalation(videoId, opts) {
13879
- const direct = await probeAndExtract(videoId, opts);
13880
- if (direct && (direct.entries.length > 0 || direct.audio)) return direct;
13881
- const shouldEscalate = direct === null || direct.retryWithFreshIp;
13882
- if (!shouldEscalate) return direct;
13883
- const kernelApiKey = browserServiceApiKey();
13884
- if (!kernelApiKey) return direct;
13885
- const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
13886
- if (!kernelProxyId) return direct;
13887
- try {
13888
- console.warn(`[CaptionFetcher] ${videoId}: retrying through residential proxy after bot block`);
13889
- const viaProxy = await probeAndExtract(videoId, { ...opts, kernelProxyId });
13890
- return viaProxy ?? direct;
13891
- } finally {
13892
- if (disposableProxyId) {
13893
- await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
13894
- (err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
13895
- );
13896
- }
13897
- }
13898
- }
13899
13797
  async function fetchViaKernelSession(videoId) {
13900
13798
  const start = Date.now();
13901
- const outcome = await extractWithProxyEscalation(videoId);
13902
- if (!outcome) return null;
13903
- if (outcome.entries.length > 0) {
13904
- const chunks = outcome.entries.map((e) => ({
13905
- timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
13906
- text: e.text
13907
- }));
13908
- const text = chunks.map((c) => c.text).join(" ");
13909
- return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
13910
- }
13911
- if (outcome.audio) {
13912
- return runWithCostContext(
13913
- { ...currentCostContext(), op: "yt_transcription", subOp: "browser-whisper" },
13914
- () => transcribeWithWizper(videoId, outcome.audio, start)
13915
- );
13799
+ let outcome = await probeCaptions(videoId);
13800
+ if (!outcome || outcome.entries.length === 0 && outcome.retryWithFreshIp) {
13801
+ const kernelApiKey = browserServiceApiKey();
13802
+ if (kernelApiKey) {
13803
+ const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
13804
+ if (kernelProxyId) {
13805
+ try {
13806
+ console.warn(`[CaptionFetcher] ${videoId}: retrying captions through residential proxy after bot block`);
13807
+ outcome = await probeCaptions(videoId, kernelProxyId) ?? outcome;
13808
+ } finally {
13809
+ if (disposableProxyId) {
13810
+ await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
13811
+ (err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
13812
+ );
13813
+ }
13814
+ }
13815
+ }
13816
+ }
13916
13817
  }
13917
- return null;
13818
+ if (!outcome || outcome.entries.length === 0) return null;
13819
+ const chunks = outcome.entries.map((e) => ({
13820
+ timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
13821
+ text: e.text
13822
+ }));
13823
+ const text = chunks.map((c) => c.text).join(" ");
13824
+ return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
13918
13825
  }
13919
13826
  async function fetchViaResolvedAudio(videoId) {
13920
13827
  if (!mediaResolverConfigured()) return null;
@@ -13964,7 +13871,7 @@ async function fetchCaptions(videoId) {
13964
13871
  `Could not transcribe ${videoId}: it has no caption track and its audio could not be retrieved.`
13965
13872
  );
13966
13873
  }
13967
- var import_client4, INNERTUBE_CLIENTS, MAX_AUDIO_BYTES, YT_RESIDENTIAL_LOCATION;
13874
+ var INNERTUBE_CLIENTS, YT_RESIDENTIAL_LOCATION;
13968
13875
  var init_CaptionFetcher = __esm({
13969
13876
  "src/youtube/CaptionFetcher.ts"() {
13970
13877
  "use strict";
@@ -13973,7 +13880,6 @@ var init_CaptionFetcher = __esm({
13973
13880
  init_kernel_proxy_resolver();
13974
13881
  init_audio_resolver();
13975
13882
  init_media_transcription();
13976
- import_client4 = require("@fal-ai/client");
13977
13883
  init_cost_telemetry();
13978
13884
  init_cost_context();
13979
13885
  INNERTUBE_CLIENTS = [
@@ -13981,7 +13887,6 @@ var init_CaptionFetcher = __esm({
13981
13887
  { name: "IOS", version: "20.10.4", userAgent: "com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)" },
13982
13888
  { name: "ANDROID_VR", version: "1.62.27", userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.27 (Linux; U; Android 12L) gzip" }
13983
13889
  ];
13984
- MAX_AUDIO_BYTES = 96 * 1024 * 1024;
13985
13890
  YT_RESIDENTIAL_LOCATION = process.env.YOUTUBE_PROXY_LOCATION ?? "Austin, Texas";
13986
13891
  }
13987
13892
  });
@@ -15560,7 +15465,7 @@ async function kernelLaunchOptsResidential() {
15560
15465
  }
15561
15466
  return { headless: true, kernelApiKey: browserServiceApiKey(), kernelProxyId: proxyId, viewport: { width: 1280, height: 900 }, locale: "en-US" };
15562
15467
  }
15563
- var import_hono5, import_zod16, import_client5, FacebookAdBodySchema, FacebookPageIntelBodySchema, FacebookTranscribeBodySchema, FacebookVideoTranscribeBodySchema, FacebookSearchBodySchema, FacebookMediaBodySchema, facebookAdApp, ALLOWED_MEDIA_HOSTS;
15468
+ var import_hono5, import_zod16, import_client4, FacebookAdBodySchema, FacebookPageIntelBodySchema, FacebookTranscribeBodySchema, FacebookVideoTranscribeBodySchema, FacebookSearchBodySchema, FacebookMediaBodySchema, facebookAdApp, ALLOWED_MEDIA_HOSTS;
15564
15469
  var init_facebook_ad_routes = __esm({
15565
15470
  "src/api/facebook-ad-routes.ts"() {
15566
15471
  "use strict";
@@ -15576,7 +15481,7 @@ var init_facebook_ad_routes = __esm({
15576
15481
  init_FacebookOrganicVideoExtractor();
15577
15482
  init_kernel_proxy_resolver();
15578
15483
  init_schemas3();
15579
- import_client5 = require("@fal-ai/client");
15484
+ import_client4 = require("@fal-ai/client");
15580
15485
  init_api_auth();
15581
15486
  init_url_utils();
15582
15487
  init_concurrency_gates();
@@ -15719,7 +15624,7 @@ var init_facebook_ad_routes = __esm({
15719
15624
  metadata: { videoUrl }
15720
15625
  });
15721
15626
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
15722
- import_client5.fal.config({ credentials: process.env.FAL_KEY });
15627
+ import_client4.fal.config({ credentials: process.env.FAL_KEY });
15723
15628
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
15724
15629
  let debited = false;
15725
15630
  try {
@@ -15763,7 +15668,7 @@ var init_facebook_ad_routes = __esm({
15763
15668
  metadata: { url: sourceUrl.href }
15764
15669
  });
15765
15670
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
15766
- import_client5.fal.config({ credentials: process.env.FAL_KEY });
15671
+ import_client4.fal.config({ credentials: process.env.FAL_KEY });
15767
15672
  const driver = new BrowserDriver();
15768
15673
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
15769
15674
  let debited = false;
@@ -16144,7 +16049,7 @@ async function kernelLaunchOptsResidential2() {
16144
16049
  function isBlockedMessage(msg) {
16145
16050
  return msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha");
16146
16051
  }
16147
- var import_hono6, import_zod17, import_client6, GoogleAdsSearchBodySchema, GoogleAdsPageIntelBodySchema, GoogleAdsTranscribeBodySchema, googleAdsApp;
16052
+ var import_hono6, import_zod17, import_client5, GoogleAdsSearchBodySchema, GoogleAdsPageIntelBodySchema, GoogleAdsTranscribeBodySchema, googleAdsApp;
16148
16053
  var init_google_ads_routes = __esm({
16149
16054
  "src/api/google-ads-routes.ts"() {
16150
16055
  "use strict";
@@ -16157,7 +16062,7 @@ var init_google_ads_routes = __esm({
16157
16062
  init_GoogleAdsExtractor();
16158
16063
  init_kernel_proxy_resolver();
16159
16064
  init_schemas3();
16160
- import_client6 = require("@fal-ai/client");
16065
+ import_client5 = require("@fal-ai/client");
16161
16066
  init_api_auth();
16162
16067
  init_url_utils();
16163
16068
  init_concurrency_gates();
@@ -16268,7 +16173,7 @@ var init_google_ads_routes = __esm({
16268
16173
  const user = c.get("user");
16269
16174
  const gate = await acquireConcurrencyGate(user, "google_ads_transcribe", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { videoUrl } });
16270
16175
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
16271
- import_client6.fal.config({ credentials: process.env.FAL_KEY });
16176
+ import_client5.fal.config({ credentials: process.env.FAL_KEY });
16272
16177
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
16273
16178
  let debited = false;
16274
16179
  try {
@@ -20614,7 +20519,7 @@ function formatCreditsInfo(raw, input) {
20614
20519
  return `| ${c.label} | ${c.credits} | ${c.unit}${notes} |`;
20615
20520
  }).join("\n");
20616
20521
  const ledgerRows = ledger.map((row) => {
20617
- const credits = row.amount_mc / 1e3;
20522
+ const credits = row.amount_mc / MC_PER_CREDIT;
20618
20523
  return `| ${row.created_at} | ${row.operation} | ${credits} | ${row.description ?? ""} |`;
20619
20524
  }).join("\n");
20620
20525
  const matchedSection = matched ? `
@@ -20664,7 +20569,7 @@ ${ledgerRows}` : ""
20664
20569
  ledger: ledger.map((row) => ({
20665
20570
  createdAt: String(row.created_at ?? ""),
20666
20571
  operation: String(row.operation ?? ""),
20667
- credits: row.amount_mc / 1e3,
20572
+ credits: row.amount_mc / MC_PER_CREDIT,
20668
20573
  description: row.description ?? null
20669
20574
  })),
20670
20575
  concurrency: concurrencyRaw && upgradeRaw ? {
@@ -21463,6 +21368,7 @@ var init_mcp_response_formatter = __esm({
21463
21368
  init_seo_issues();
21464
21369
  init_image_audit();
21465
21370
  init_report_artifact_offload();
21371
+ init_rates();
21466
21372
  INLINE_BUDGET_BYTES = Number(process.env.MCP_SCRAPER_INLINE_BUDGET ?? 5e4);
21467
21373
  STRUCTURED_ARRAY_CAP = 25;
21468
21374
  reportSavingEnabled = true;
@@ -27297,7 +27203,7 @@ var init_serp_intelligence_routes = __esm({
27297
27203
  kernelApiKey: browserServiceApiKey(),
27298
27204
  kernelProxyId: browserServiceProxyId(),
27299
27205
  signal: c.req.raw.signal,
27300
- billing: { creditsUsed: cost / 1e3 }
27206
+ billing: { creditsUsed: cost / MC_PER_CREDIT }
27301
27207
  });
27302
27208
  await logRequestEvent({
27303
27209
  userId: user.id,
@@ -27399,7 +27305,7 @@ var PACKAGE_VERSION;
27399
27305
  var init_version = __esm({
27400
27306
  "src/version.ts"() {
27401
27307
  "use strict";
27402
- PACKAGE_VERSION = "0.4.13";
27308
+ PACKAGE_VERSION = "0.5.0";
27403
27309
  }
27404
27310
  });
27405
27311
 
@@ -38839,7 +38745,7 @@ var init_server = __esm({
38839
38745
  api_key: user.key_active ? user.api_key : null,
38840
38746
  created_at: user.created_at,
38841
38747
  balance_mc: user.balance_mc,
38842
- balance_credits: user.balance_mc / 1e3,
38748
+ balance_credits: user.balance_mc / MC_PER_CREDIT,
38843
38749
  extra_concurrency_slots: user.extra_concurrency_slots,
38844
38750
  concurrency_limit: concurrencyLimitForUser(user),
38845
38751
  has_concurrency_sub: !!user.concurrency_stripe_sub_id,
@@ -39951,7 +39857,7 @@ var init_server = __esm({
39951
39857
  tier: tier.tier,
39952
39858
  label: tier.label,
39953
39859
  monthly_usd: tier.monthly_usd,
39954
- credits_per_month: tier.credits_mc / 1e3,
39860
+ credits_per_month: tier.credits_mc / MC_PER_CREDIT,
39955
39861
  concurrency: tier.concurrency,
39956
39862
  intro: tier.intro_coupon ? "$1 first month" : null,
39957
39863
  next_step: "Open checkout_url in a browser to complete payment."
@@ -39981,7 +39887,7 @@ var init_server = __esm({
39981
39887
  const freeCredits = await getFreeCreditBreakdown(user.id);
39982
39888
  return c.json({
39983
39889
  balance_mc: balanceMc,
39984
- balance_credits: balanceMc / 1e3,
39890
+ balance_credits: balanceMc / MC_PER_CREDIT,
39985
39891
  free_credits: freeCredits,
39986
39892
  ledger
39987
39893
  });
@@ -40003,7 +39909,7 @@ var init_server = __esm({
40003
39909
  })) : void 0;
40004
39910
  return c.json({
40005
39911
  balance_mc: balanceMc,
40006
- balance_credits: balanceMc / 1e3,
39912
+ balance_credits: balanceMc / MC_PER_CREDIT,
40007
39913
  item: body.item ?? null,
40008
39914
  matched_cost: matchedCost ? (({ aliases, ...cost }) => cost)(matchedCost) : null,
40009
39915
  costs,