mcp-scraper 0.18.0 → 0.20.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 (59) hide show
  1. package/README.md +2 -2
  2. package/dist/bin/api-server.cjs +635 -109
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  5. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +2 -2
  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 +2228 -1783
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -4
  14. package/dist/bin/mcp-stdio-server.js.map +1 -1
  15. package/dist/bin/paa-harvest.cjs.map +1 -1
  16. package/dist/bin/paa-harvest.js +3 -3
  17. package/dist/{chunk-GL4BW4CP.js → chunk-22VEGGTW.js} +21 -9
  18. package/dist/{chunk-GL4BW4CP.js.map → chunk-22VEGGTW.js.map} +1 -1
  19. package/dist/{chunk-RUGJE5EB.js → chunk-EJK25QOW.js} +2 -2
  20. package/dist/chunk-JWIE5NCR.js +284 -0
  21. package/dist/chunk-JWIE5NCR.js.map +1 -0
  22. package/dist/{chunk-FMEDPBIV.js → chunk-KE7KE2Q2.js} +482 -35
  23. package/dist/chunk-KE7KE2Q2.js.map +1 -0
  24. package/dist/chunk-PZB3TJWK.js +7 -0
  25. package/dist/chunk-PZB3TJWK.js.map +1 -0
  26. package/dist/{chunk-BX5RCOG5.js → chunk-Q44DN6T2.js} +2 -2
  27. package/dist/{chunk-BX5RCOG5.js.map → chunk-Q44DN6T2.js.map} +1 -1
  28. package/dist/chunk-UN7VMHZL.js +244 -0
  29. package/dist/chunk-UN7VMHZL.js.map +1 -0
  30. package/dist/{chunk-RMPPYKUV.js → chunk-XDFSLSSH.js} +7 -218
  31. package/dist/chunk-XDFSLSSH.js.map +1 -0
  32. package/dist/{chunk-HE2LQPJ2.js → chunk-ZRKFW5FB.js} +2 -2
  33. package/dist/{db-LIOTIWVN.js → db-YHZYG7D2.js} +2 -2
  34. package/dist/{extract-bundle-U4D5LW5W.js → extract-bundle-OSUPAHCE.js} +58 -5
  35. package/dist/extract-bundle-OSUPAHCE.js.map +1 -0
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +5 -4
  38. package/dist/index.js.map +1 -1
  39. package/dist/{server-MQDCAR6I.js → server-VDWIPV7F.js} +100 -363
  40. package/dist/server-VDWIPV7F.js.map +1 -0
  41. package/dist/{site-extract-repository-NVSZH35Y.js → site-extract-repository-GWKGK46Z.js} +3 -3
  42. package/dist/{worker-JQTS437L.js → worker-O5PZTTPY.js} +8 -8
  43. package/docs/mcp-tool-manifest.generated.json +564 -30
  44. package/docs/specs/meta-ad-creative-media-resolution-spec.md +31 -0
  45. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +1007 -0
  46. package/package.json +1 -1
  47. package/dist/chunk-FMEDPBIV.js.map +0 -1
  48. package/dist/chunk-HPV4VOQX.js +0 -27
  49. package/dist/chunk-HPV4VOQX.js.map +0 -1
  50. package/dist/chunk-RLWCHLV3.js +0 -7
  51. package/dist/chunk-RLWCHLV3.js.map +0 -1
  52. package/dist/chunk-RMPPYKUV.js.map +0 -1
  53. package/dist/extract-bundle-U4D5LW5W.js.map +0 -1
  54. package/dist/server-MQDCAR6I.js.map +0 -1
  55. /package/dist/{chunk-RUGJE5EB.js.map → chunk-EJK25QOW.js.map} +0 -0
  56. /package/dist/{chunk-HE2LQPJ2.js.map → chunk-ZRKFW5FB.js.map} +0 -0
  57. /package/dist/{db-LIOTIWVN.js.map → db-YHZYG7D2.js.map} +0 -0
  58. /package/dist/{site-extract-repository-NVSZH35Y.js.map → site-extract-repository-GWKGK46Z.js.map} +0 -0
  59. /package/dist/{worker-JQTS437L.js.map → worker-O5PZTTPY.js.map} +0 -0
@@ -1,27 +1,24 @@
1
+ import {
2
+ harvestPageMedia
3
+ } from "./chunk-JWIE5NCR.js";
1
4
  import {
2
5
  classifyHarvestProblem,
3
6
  createHarvestAttemptRecorder,
4
7
  harvestProblemResponse,
5
8
  serializeHarvestProblem
6
- } from "./chunk-HE2LQPJ2.js";
9
+ } from "./chunk-ZRKFW5FB.js";
7
10
  import {
8
11
  BrowserDriver,
9
12
  MapsSelectors,
10
13
  buildYouTubeChannelVideosUrl,
11
- currentCostContext,
12
14
  deleteKernelProxyId,
13
15
  harvest,
14
- kernelCostUsd,
15
- migrateCostTelemetry,
16
- recordKernelSession,
17
- recordVendorUsage,
18
- resolveKernelProxyId,
19
- runWithCostContext,
20
- vendorCostUsd
21
- } from "./chunk-RMPPYKUV.js";
16
+ resolveKernelProxyId
17
+ } from "./chunk-XDFSLSSH.js";
22
18
  import {
23
19
  HttpMcpToolExecutor,
24
20
  MemoryMcpToolExecutor,
21
+ assessTranscriptSignal,
25
22
  buildLinkGraph,
26
23
  buildPaaExtractorMcpServer,
27
24
  cleanupExpiredConnectedDataArtifacts,
@@ -36,8 +33,9 @@ import {
36
33
  registerSerpIntelligenceCaptureTools,
37
34
  renewConnectedDataArtifactDownload,
38
35
  sanitizeAttempts,
39
- sanitizeHarvestResult
40
- } from "./chunk-FMEDPBIV.js";
36
+ sanitizeHarvestResult,
37
+ transcribeMediaUrl
38
+ } from "./chunk-KE7KE2Q2.js";
41
39
  import {
42
40
  auditImages,
43
41
  buildLinkReport,
@@ -48,8 +46,14 @@ import {
48
46
  browserServiceApiKey,
49
47
  browserServiceProfileName,
50
48
  browserServiceProfileSaveChanges,
51
- browserServiceProxyId
52
- } from "./chunk-HPV4VOQX.js";
49
+ browserServiceProxyId,
50
+ currentCostContext,
51
+ kernelCostUsd,
52
+ migrateCostTelemetry,
53
+ recordKernelSession,
54
+ recordVendorUsage,
55
+ runWithCostContext
56
+ } from "./chunk-UN7VMHZL.js";
53
57
  import {
54
58
  csvRecords,
55
59
  listWorkflowDefinitions,
@@ -73,7 +77,7 @@ import {
73
77
  RawMapsOverviewSchema,
74
78
  RawMapsReviewStatsSchema
75
79
  } from "./chunk-XGIPATLV.js";
76
- import "./chunk-RLWCHLV3.js";
80
+ import "./chunk-PZB3TJWK.js";
77
81
  import {
78
82
  completeExtractJob,
79
83
  countSuccessfulPages,
@@ -84,7 +88,7 @@ import {
84
88
  saveExtractPages,
85
89
  setExtractJobTotal,
86
90
  settleExtractJob
87
- } from "./chunk-RUGJE5EB.js";
91
+ } from "./chunk-EJK25QOW.js";
88
92
  import {
89
93
  BROWSER_OPEN_MIN_BALANCE_MC,
90
94
  CONCURRENCY_PRICE_ID,
@@ -190,7 +194,7 @@ import {
190
194
  setUserMemoryCreds,
191
195
  upsertPageSnapshot,
192
196
  verifyPassword
193
- } from "./chunk-GL4BW4CP.js";
197
+ } from "./chunk-22VEGGTW.js";
194
198
 
195
199
  // src/blog/registry.ts
196
200
  var posts = [
@@ -4225,285 +4229,6 @@ async function capturePageData(url, opts = {}) {
4225
4229
  }
4226
4230
  }
4227
4231
 
4228
- // src/lib/media-extractor.ts
4229
- import { createWriteStream, mkdirSync } from "fs";
4230
- import { homedir } from "os";
4231
- import { join as join2, extname, basename } from "path";
4232
- import { pipeline } from "stream/promises";
4233
- import { Readable } from "stream";
4234
- var AD_PATTERNS = [
4235
- "doubleclick.net",
4236
- "googlesyndication.com",
4237
- "googletagmanager.com",
4238
- "google-analytics.com",
4239
- "googletagservices.com",
4240
- "adservice.google",
4241
- "googletag.",
4242
- "pagead2.googlesyndication",
4243
- "facebook.net/tr",
4244
- "connect.facebook.net",
4245
- "fbcdn.net/rsrc",
4246
- "analytics.twitter.com",
4247
- "static.ads-twitter.com",
4248
- "ads.twitter.com",
4249
- "t.co/i/adsct",
4250
- "pixel.advertising.com",
4251
- "hotjar.com",
4252
- "clarity.ms",
4253
- "quantserve.com",
4254
- "scorecardresearch.com",
4255
- "newrelic.com",
4256
- "nr-data.net",
4257
- "segment.io",
4258
- "segment.com",
4259
- "amplitude.com",
4260
- "mixpanel.com",
4261
- "heap.io",
4262
- "fullstory.com",
4263
- "moatads.com",
4264
- "criteo.com",
4265
- "adsrvr.org",
4266
- "rubiconproject.com",
4267
- "pubmatic.com",
4268
- "openx.net",
4269
- "appnexus.com",
4270
- "amazon-adsystem.com",
4271
- "media.net",
4272
- "yieldmo.com",
4273
- "triplelift.com",
4274
- "sharethrough.com",
4275
- "prebid.",
4276
- "smaato.net",
4277
- "indexworm.com",
4278
- "casalemedia.com",
4279
- "outbrain.com",
4280
- "taboola.com",
4281
- "revcontent.com",
4282
- "mgid.com",
4283
- "tawk.to",
4284
- "intercom.io",
4285
- "drift.com",
4286
- "hs-scripts.com",
4287
- "zopim.com",
4288
- "livechatinc.com",
4289
- "userlike.com",
4290
- "onetrust.com",
4291
- "cookielaw.org",
4292
- "cookieinformation.com",
4293
- "trustarc.com",
4294
- "/ads/",
4295
- "/ad/",
4296
- "/banner/",
4297
- "/banners/",
4298
- "/pixel/",
4299
- "/beacon/",
4300
- "/tracking/",
4301
- "/tracker/",
4302
- "/remarketing/",
4303
- "/conversion/",
4304
- "1x1.gif",
4305
- "spacer.gif",
4306
- "blank.gif",
4307
- "transparent.gif"
4308
- ];
4309
- var IMAGE_EXTS = /* @__PURE__ */ new Set([".jpg", ".jpeg", ".png", ".webp", ".gif", ".avif", ".svg", ".tiff"]);
4310
- var VIDEO_EXTS = /* @__PURE__ */ new Set([".mp4", ".webm", ".mov", ".avi", ".m4v", ".ogv", ".mkv"]);
4311
- var AUDIO_EXTS = /* @__PURE__ */ new Set([".mp3", ".wav", ".ogg", ".aac", ".m4a", ".flac", ".opus"]);
4312
- function isAdUrl(url) {
4313
- const lower = url.toLowerCase();
4314
- return AD_PATTERNS.some((p) => lower.includes(p));
4315
- }
4316
- function isDataUri(url) {
4317
- return url.startsWith("data:");
4318
- }
4319
- function typeFromUrl(url) {
4320
- try {
4321
- const ext = extname(new URL(url).pathname).toLowerCase();
4322
- if (IMAGE_EXTS.has(ext)) return "image";
4323
- if (VIDEO_EXTS.has(ext)) return "video";
4324
- if (AUDIO_EXTS.has(ext)) return "audio";
4325
- } catch {
4326
- }
4327
- return null;
4328
- }
4329
- function typeFromMime(mime) {
4330
- const lower = mime.toLowerCase();
4331
- if (lower.startsWith("image/")) return "image";
4332
- if (lower.startsWith("video/")) return "video";
4333
- if (lower.startsWith("audio/")) return "audio";
4334
- return null;
4335
- }
4336
- function resolveUrl(raw, base) {
4337
- if (!raw || isDataUri(raw)) return null;
4338
- try {
4339
- return new URL(raw, base).href;
4340
- } catch {
4341
- return null;
4342
- }
4343
- }
4344
- function safeFilename(url, index) {
4345
- try {
4346
- const u = new URL(url);
4347
- const base = basename(u.pathname).replace(/[^a-zA-Z0-9._-]/g, "_").slice(0, 80);
4348
- return base || `asset-${index}`;
4349
- } catch {
4350
- return `asset-${index}`;
4351
- }
4352
- }
4353
- function extractMediaUrls(html, baseUrl) {
4354
- const seen = /* @__PURE__ */ new Set();
4355
- const urls = [];
4356
- const add = (raw) => {
4357
- const resolved = resolveUrl(raw.trim(), baseUrl);
4358
- if (!resolved || seen.has(resolved) || isAdUrl(resolved)) return;
4359
- seen.add(resolved);
4360
- urls.push(resolved);
4361
- };
4362
- for (const m of html.matchAll(/<img\s[^>]*>/gi)) {
4363
- const tag = m[0];
4364
- for (const attr of ["src", "data-src", "data-lazy-src", "data-original"]) {
4365
- const v = (tag.match(new RegExp(`\\b${attr}\\s*=\\s*["']([^"']+)["']`, "i")) ?? [])[1];
4366
- if (v) add(v);
4367
- }
4368
- const srcset = (tag.match(/\bsrcset\s*=\s*["']([^"']+)["']/i) ?? [])[1];
4369
- if (srcset) {
4370
- for (const part of srcset.split(",")) {
4371
- const u = part.trim().split(/\s+/)[0];
4372
- if (u) add(u);
4373
- }
4374
- }
4375
- }
4376
- for (const m of html.matchAll(/<source\s[^>]*>/gi)) {
4377
- const tag = m[0];
4378
- const src = (tag.match(/\bsrc\s*=\s*["']([^"']+)["']/i) ?? [])[1];
4379
- if (src) add(src);
4380
- const srcset = (tag.match(/\bsrcset\s*=\s*["']([^"']+)["']/i) ?? [])[1];
4381
- if (srcset) {
4382
- for (const part of srcset.split(",")) {
4383
- const u = part.trim().split(/\s+/)[0];
4384
- if (u) add(u);
4385
- }
4386
- }
4387
- }
4388
- for (const m of html.matchAll(/<video\s[^>]*>/gi)) {
4389
- const tag = m[0];
4390
- for (const attr of ["src", "poster"]) {
4391
- const v = (tag.match(new RegExp(`\\b${attr}\\s*=\\s*["']([^"']+)["']`, "i")) ?? [])[1];
4392
- if (v) add(v);
4393
- }
4394
- }
4395
- for (const m of html.matchAll(/<audio\s[^>]*>/gi)) {
4396
- const tag = m[0];
4397
- const v = (tag.match(/\bsrc\s*=\s*["']([^"']+)["']/i) ?? [])[1];
4398
- if (v) add(v);
4399
- }
4400
- for (const m of html.matchAll(/<meta\s[^>]*>/gi)) {
4401
- const tag = m[0];
4402
- const prop = (tag.match(/\b(?:property|name)\s*=\s*["']([^"']+)["']/i) ?? [])[1]?.toLowerCase();
4403
- if (prop === "og:image" || prop === "twitter:image") {
4404
- const content = (tag.match(/\bcontent\s*=\s*["']([^"']+)["']/i) ?? [])[1];
4405
- if (content) add(content);
4406
- }
4407
- }
4408
- for (const m of html.matchAll(/background(?:-image)?\s*:\s*url\(\s*["']?([^"')]+)["']?\s*\)/gi)) {
4409
- add(m[1]);
4410
- }
4411
- return urls;
4412
- }
4413
- async function downloadAsset(url, destDir, filename) {
4414
- const res = await fetch(url, {
4415
- headers: { "User-Agent": "Mozilla/5.0 (compatible; ThorbitBot/1.0)" },
4416
- signal: AbortSignal.timeout(15e3)
4417
- });
4418
- if (!res.ok) throw new Error(`HTTP ${res.status}`);
4419
- if (!res.body) throw new Error("Empty response body");
4420
- const mimeType = res.headers.get("content-type")?.split(";")[0].trim() ?? null;
4421
- let dest = join2(destDir, filename);
4422
- if (mimeType && !extname(filename)) {
4423
- const mimeExt = {
4424
- "image/jpeg": ".jpg",
4425
- "image/png": ".png",
4426
- "image/webp": ".webp",
4427
- "image/gif": ".gif",
4428
- "image/svg+xml": ".svg",
4429
- "image/avif": ".avif",
4430
- "video/mp4": ".mp4",
4431
- "video/webm": ".webm",
4432
- "audio/mpeg": ".mp3",
4433
- "audio/ogg": ".ogg",
4434
- "audio/wav": ".wav"
4435
- };
4436
- const ext = mimeExt[mimeType];
4437
- if (ext) dest = dest + ext;
4438
- }
4439
- const writer = createWriteStream(dest);
4440
- await pipeline(Readable.fromWeb(res.body), writer);
4441
- const { statSync } = await import("fs");
4442
- const sizeBytes = statSync(dest).size;
4443
- return { savedPath: dest, sizeBytes, mimeType };
4444
- }
4445
- async function harvestPageMedia(html, pageUrl, options = {}) {
4446
- const types = options.types ?? ["image", "video", "audio"];
4447
- const typesSet = new Set(types);
4448
- const rawUrls = extractMediaUrls(html, pageUrl);
4449
- const totalFound = rawUrls.length;
4450
- const filteredUrls = [];
4451
- const kept = [];
4452
- for (const url of rawUrls) {
4453
- const type = typeFromUrl(url);
4454
- if (!type || !typesSet.has(type)) {
4455
- filteredUrls.push(url);
4456
- continue;
4457
- }
4458
- kept.push({ url, type });
4459
- }
4460
- if (options.outputDir === null) {
4461
- return {
4462
- pageUrl,
4463
- outputDir: null,
4464
- assets: kept.map(({ url, type }, i) => ({
4465
- url,
4466
- type,
4467
- mimeType: null,
4468
- filename: safeFilename(url, i),
4469
- savedPath: null,
4470
- sizeBytes: null
4471
- })),
4472
- filteredCount: filteredUrls.length,
4473
- totalFound
4474
- };
4475
- }
4476
- const domain = (() => {
4477
- try {
4478
- return new URL(pageUrl).hostname.replace(/^www\./, "");
4479
- } catch {
4480
- return "unknown";
4481
- }
4482
- })();
4483
- const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
4484
- const outDir = options.outputDir ?? join2(homedir(), "Downloads", "mcp-scraper", "media", `${stamp}-${domain}`);
4485
- mkdirSync(outDir, { recursive: true });
4486
- const assets = [];
4487
- await Promise.allSettled(
4488
- kept.map(async ({ url, type }, i) => {
4489
- const filename = safeFilename(url, i);
4490
- try {
4491
- const { savedPath, sizeBytes, mimeType } = await downloadAsset(url, outDir, filename);
4492
- const resolvedType = mimeType ? typeFromMime(mimeType) ?? type : type;
4493
- assets.push({ url, type: resolvedType, mimeType, filename: basename(savedPath), savedPath, sizeBytes });
4494
- } catch {
4495
- assets.push({ url, type, mimeType: null, filename, savedPath: null, sizeBytes: null });
4496
- }
4497
- })
4498
- );
4499
- assets.sort((a, b) => {
4500
- if (a.savedPath && !b.savedPath) return -1;
4501
- if (!a.savedPath && b.savedPath) return 1;
4502
- return a.url.localeCompare(b.url);
4503
- });
4504
- return { pageUrl, outputDir: outDir, assets, filteredCount: filteredUrls.length, totalFound };
4505
- }
4506
-
4507
4232
  // src/api/site-mapper.ts
4508
4233
  var SKIP_EXTENSIONS = /* @__PURE__ */ new Set([
4509
4234
  "jpg",
@@ -7627,7 +7352,7 @@ var siteExtractFn = inngest.createFunction(
7627
7352
  return buildSeoAudit(pages, job.startUrl);
7628
7353
  }) : null;
7629
7354
  const artifacts = await step.run("finalize", async () => {
7630
- const { assembleExtractArtifacts } = await import("./extract-bundle-U4D5LW5W.js");
7355
+ const { assembleExtractArtifacts } = await import("./extract-bundle-OSUPAHCE.js");
7631
7356
  const stored = await assembleExtractArtifacts(job, { branding, imageAudit, seoAudit });
7632
7357
  await completeExtractJob(jobId, stored);
7633
7358
  return stored;
@@ -8797,41 +8522,6 @@ async function resolveYouTubeAudio(videoId) {
8797
8522
  }
8798
8523
  }
8799
8524
 
8800
- // src/services/media-transcription.ts
8801
- import { fal } from "@fal-ai/client";
8802
- function transcriptMarkdown(title, text, chunks, durationMs) {
8803
- const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
8804
- const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
8805
- if (chunks.length) {
8806
- lines.push("## Timestamped Segments", "");
8807
- for (const ch of chunks) {
8808
- lines.push(`**[${fmtTs2(ch.timestamp[0])} -> ${fmtTs2(ch.timestamp[1])}]** ${ch.text.trim()}`, "");
8809
- }
8810
- }
8811
- return lines.join("\n");
8812
- }
8813
- async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript") {
8814
- const startMs = Date.now();
8815
- const result = await fal.subscribe("fal-ai/wizper", {
8816
- input: { audio_url: mediaUrl, task: "transcribe", language: "en" },
8817
- logs: false,
8818
- pollInterval: 3e3
8819
- });
8820
- const data = result.data;
8821
- const text = data.text ?? "";
8822
- const chunks = data.chunks ?? [];
8823
- const durationMs = Date.now() - startMs;
8824
- void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: durationMs / 1e3, unitType: "compute_sec" });
8825
- const costUsd = vendorCostUsd("fal_wizper", durationMs / 1e3);
8826
- return {
8827
- text,
8828
- chunks,
8829
- durationMs,
8830
- costUsd,
8831
- markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs)
8832
- };
8833
- }
8834
-
8835
8525
  // src/youtube/CaptionFetcher.ts
8836
8526
  async function fetchViaYoutubeTranscript(videoId) {
8837
8527
  try {
@@ -9134,6 +8824,7 @@ var ExtractSiteBodySchema = z11.object({
9134
8824
  rotateProxies: z11.boolean().optional(),
9135
8825
  rotateProxyEvery: z11.number().int().min(1).max(100).optional(),
9136
8826
  background: z11.boolean().optional(),
8827
+ downloadImages: z11.boolean().optional(),
9137
8828
  formats: z11.array(z11.enum(["markdown", "links", "json", "images", "branding", "issues"])).optional()
9138
8829
  });
9139
8830
  var YoutubeHarvestBodySchema = z11.object({
@@ -10047,11 +9738,13 @@ var FacebookAdExtractor = class {
10047
9738
  if (n) nameFreq.set(n, (nameFreq.get(n) ?? 0) + 1);
10048
9739
  }
10049
9740
  const advertiserName = nameFreq.size ? [...nameFreq.entries()].sort((a, b) => b[1] - a[1])[0][0] : null;
9741
+ const matchedAdvertisers = [...nameFreq.entries()].map(([name, adCount]) => ({ name, adCount })).sort((a, b) => b.adCount - a.adCount).slice(0, 20);
10050
9742
  return {
10051
9743
  listingUrl,
10052
9744
  scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
10053
9745
  advertiserPageId,
10054
9746
  advertiserName,
9747
+ matchedAdvertisers,
10055
9748
  summary: {
10056
9749
  totalAds,
10057
9750
  activeCount,
@@ -10435,7 +10128,7 @@ async function extractFacebookOrganicVideoFromPage(page, sourceUrl, quality = "b
10435
10128
  }
10436
10129
 
10437
10130
  // src/api/facebook-ad-routes.ts
10438
- import { fal as fal2 } from "@fal-ai/client";
10131
+ import { fal } from "@fal-ai/client";
10439
10132
  var FacebookAdBodySchema = z13.object({
10440
10133
  url: z13.string().trim().optional(),
10441
10134
  libraryId: z13.string().trim().optional(),
@@ -10486,6 +10179,45 @@ function buildPageIntelUrl(body, country) {
10486
10179
  if (body.pageId?.trim()) return `https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=${country}&is_targeted_country=false&media_type=all&search_type=page&view_all_page_id=${body.pageId.trim()}`;
10487
10180
  return `https://www.facebook.com/ads/library/?active_status=all&ad_type=all&country=${country}&q=${encodeURIComponent(body.query.trim())}&search_type=keyword_unordered`;
10488
10181
  }
10182
+ function normalizeAdvertiserName(value) {
10183
+ return value.toLowerCase().normalize("NFKD").replace(/[^a-z0-9]+/g, " ").trim().replace(/\s+/g, " ");
10184
+ }
10185
+ function advertiserTokenOverlap(query, candidate) {
10186
+ const queryTokens = new Set(normalizeAdvertiserName(query).split(" ").filter(Boolean));
10187
+ const candidateTokens = new Set(normalizeAdvertiserName(candidate).split(" ").filter(Boolean));
10188
+ if (queryTokens.size === 0 || candidateTokens.size === 0) return 0;
10189
+ let overlap = 0;
10190
+ for (const token of queryTokens) if (candidateTokens.has(token)) overlap++;
10191
+ return overlap / queryTokens.size;
10192
+ }
10193
+ function assessPageIntelMatch(body, result) {
10194
+ if (body.pageId) return { inputMode: "pageId", matchConfidence: "high", matchReason: "Exact Facebook page ID filter.", warnings: [] };
10195
+ if (body.libraryId) return { inputMode: "libraryId", matchConfidence: "high", matchReason: "Exact Ad Library archive ID filter.", warnings: [] };
10196
+ const query = body.query?.trim() ?? "";
10197
+ const normalizedQuery = normalizeAdvertiserName(query);
10198
+ const matches = result.matchedAdvertisers ?? [];
10199
+ const exact = matches.find((match) => normalizeAdvertiserName(match.name) === normalizedQuery);
10200
+ const top = matches[0];
10201
+ const topShare = top && result.summary.totalAds > 0 ? top.adCount / result.summary.totalAds : 0;
10202
+ const overlap = top ? advertiserTokenOverlap(query, top.name) : 0;
10203
+ const warnings = [];
10204
+ let matchConfidence;
10205
+ let matchReason;
10206
+ if (exact && exact.adCount / Math.max(1, result.summary.totalAds) >= 0.7) {
10207
+ matchConfidence = "high";
10208
+ matchReason = "The exact advertiser name accounts for most returned ads.";
10209
+ } else if (top && overlap >= 0.75 && topShare >= 0.5) {
10210
+ matchConfidence = "medium";
10211
+ matchReason = "The leading advertiser overlaps strongly with the query, but keyword search is not page-scoped.";
10212
+ } else {
10213
+ matchConfidence = "low";
10214
+ matchReason = "Keyword search did not identify one dominant exact advertiser match.";
10215
+ }
10216
+ if (matches.length > 1) warnings.push(`Keyword search mixed ${matches.length} advertiser names. Confirm the intended pageId before using these ads for performance or creative analysis.`);
10217
+ if (matchConfidence === "low") warnings.push("Low-specificity Ad Library match: treat these results as discovery candidates, not as verified ads from the requested brand.");
10218
+ warnings.push("Paused, cancelled, or otherwise inactive non-political ads may be absent from Ad Library. Use meta_ad_creative_media for connected ad-account creatives.");
10219
+ return { inputMode: "query", matchConfidence, matchReason, warnings };
10220
+ }
10489
10221
  function configuredKernelProxy() {
10490
10222
  return (process.env.BROWSER_SERVICE_PROXY_ID ?? process.env.KERNEL_PROXY_ID)?.trim() || void 0;
10491
10223
  }
@@ -10582,8 +10314,10 @@ facebookAdApp.post("/page-intel", createApiKeyAuth(), async (c) => {
10582
10314
  await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "failed", query: body.pageId ?? body.query ?? body.libraryId ?? "", error: "soft-block: empty result refunded" });
10583
10315
  return c.json({ error: "soft-block: no ads returned (refunded)" }, 503);
10584
10316
  }
10585
- await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: body.pageId ?? body.query ?? body.libraryId ?? "", resultCount: result.ads.length, result });
10586
- return c.json(result);
10317
+ const match = assessPageIntelMatch(body, result);
10318
+ const output = { ...result, ...match };
10319
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_page_intel", status: "done", query: body.pageId ?? body.query ?? body.libraryId ?? "", resultCount: result.ads.length, result: output });
10320
+ return c.json(output);
10587
10321
  } catch (err) {
10588
10322
  const msg = err instanceof Error ? err.message : String(err);
10589
10323
  if (debited && !refunded) await creditMc(fbUser.id, MC_COSTS.fb_ad, LedgerOperation.FB_AD_REFUND, "failed call");
@@ -10615,7 +10349,7 @@ facebookAdApp.post("/transcribe", createApiKeyAuth(), async (c) => {
10615
10349
  metadata: { videoUrl }
10616
10350
  });
10617
10351
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
10618
- fal2.config({ credentials: process.env.FAL_KEY });
10352
+ fal.config({ credentials: process.env.FAL_KEY });
10619
10353
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
10620
10354
  let debited = false;
10621
10355
  try {
@@ -10659,7 +10393,7 @@ facebookAdApp.post("/video-transcribe", createApiKeyAuth(), async (c) => {
10659
10393
  metadata: { url: sourceUrl.href }
10660
10394
  });
10661
10395
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
10662
- fal2.config({ credentials: process.env.FAL_KEY });
10396
+ fal.config({ credentials: process.env.FAL_KEY });
10663
10397
  const driver = new BrowserDriver();
10664
10398
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
10665
10399
  let debited = false;
@@ -10684,7 +10418,8 @@ facebookAdApp.post("/video-transcribe", createApiKeyAuth(), async (c) => {
10684
10418
  text: transcript.text,
10685
10419
  chunks: transcript.chunks,
10686
10420
  durationMs: transcript.durationMs,
10687
- markdown: transcript.markdown
10421
+ markdown: transcript.markdown,
10422
+ transcriptSignal: assessTranscriptSignal(transcript.text, transcript.chunks, video.durationSec)
10688
10423
  };
10689
10424
  await logRequestEvent({ userId: fbUser.id, source: "facebook_video_transcribe", status: "done", query: sourceUrl.href, resultCount: transcript.chunks.length, result });
10690
10425
  return c.json(result);
@@ -11018,7 +10753,7 @@ var GoogleAdsExtractor = class {
11018
10753
  };
11019
10754
 
11020
10755
  // src/api/google-ads-routes.ts
11021
- import { fal as fal3 } from "@fal-ai/client";
10756
+ import { fal as fal2 } from "@fal-ai/client";
11022
10757
  var GoogleAdsSearchBodySchema = z14.object({
11023
10758
  query: z14.string().trim().min(1, "query is required"),
11024
10759
  region: z14.string().trim().toUpperCase().length(2).optional(),
@@ -11148,7 +10883,7 @@ googleAdsApp.post("/transcribe", createApiKeyAuth(), async (c) => {
11148
10883
  const user = c.get("user");
11149
10884
  const gate = await acquireConcurrencyGate(user, "google_ads_transcribe", { reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"), metadata: { videoUrl } });
11150
10885
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
11151
- fal3.config({ credentials: process.env.FAL_KEY });
10886
+ fal2.config({ credentials: process.env.FAL_KEY });
11152
10887
  const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
11153
10888
  let debited = false;
11154
10889
  try {
@@ -11177,7 +10912,7 @@ import { Hono as Hono7 } from "hono";
11177
10912
  import { z as z15 } from "zod";
11178
10913
  import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
11179
10914
  import { tmpdir } from "os";
11180
- import { join as join4 } from "path";
10915
+ import { join as join3 } from "path";
11181
10916
  import { spawn } from "child_process";
11182
10917
 
11183
10918
  // src/extractor/InstagramContentExtractor.ts
@@ -11775,8 +11510,8 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
11775
11510
  downloads.push({ kind, url: track.url, savedPath: stored.url, sizeBytes: stored.bytes, mimeType, error: null });
11776
11511
  const isMuxInput = muxEligible && (track.url === extraction.selectedVideoTrack.url || track.url === extraction.selectedAudioTrack.url);
11777
11512
  if (isMuxInput) {
11778
- if (!muxWorkDir) muxWorkDir = mkdtempSync(join4(tmpdir(), "ig-mux-"));
11779
- const localPath = join4(muxWorkDir, trackFilename(track, index, extraction.shortcode));
11513
+ if (!muxWorkDir) muxWorkDir = mkdtempSync(join3(tmpdir(), "ig-mux-"));
11514
+ const localPath = join3(muxWorkDir, trackFilename(track, index, extraction.shortcode));
11780
11515
  writeFileSync(localPath, bytes);
11781
11516
  muxInputs.set(track.url, localPath);
11782
11517
  }
@@ -11785,7 +11520,7 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
11785
11520
  }
11786
11521
  }
11787
11522
  if (muxEligible && muxWorkDir && muxInputs.has(extraction.selectedVideoTrack.url) && muxInputs.has(extraction.selectedAudioTrack.url)) {
11788
- const outPath = join4(muxWorkDir, "muxed.mp4");
11523
+ const outPath = join3(muxWorkDir, "muxed.mp4");
11789
11524
  const muxed = await runFfmpegMux(muxInputs.get(extraction.selectedVideoTrack.url), muxInputs.get(extraction.selectedAudioTrack.url), outPath);
11790
11525
  if (muxed.ok) {
11791
11526
  const stored = await store.put(`${prefix}/muxed.mp4`, readFileSync2(outPath), "video/mp4");
@@ -13696,7 +13431,7 @@ import { Hono as Hono13 } from "hono";
13696
13431
 
13697
13432
  // src/directory/directory-workflow.ts
13698
13433
  import { mkdir as mkdir2, writeFile } from "fs/promises";
13699
- import { join as join5 } from "path";
13434
+ import { join as join4 } from "path";
13700
13435
  import { z as z21 } from "zod";
13701
13436
 
13702
13437
  // src/directory/location-db.ts
@@ -14032,11 +13767,11 @@ function csvRowsFor(result) {
14032
13767
  return rows;
14033
13768
  }
14034
13769
  async function saveDirectoryCsv(result) {
14035
- const outDir = join5(outputBaseDir(), "directory-workflows");
13770
+ const outDir = join4(outputBaseDir(), "directory-workflows");
14036
13771
  await mkdir2(outDir, { recursive: true });
14037
13772
  const stamp = result.extractedAt.replace(/[:.]/g, "-");
14038
13773
  const slug = `${result.state}-${result.query}`.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
14039
- const path5 = join5(outDir, `${stamp}-${slug}-directory-workflow.csv`);
13774
+ const path5 = join4(outDir, `${stamp}-${slug}-directory-workflow.csv`);
14040
13775
  const headers = [
14041
13776
  "source_query",
14042
13777
  "source_location",
@@ -20608,8 +20343,8 @@ async function importServiceConnectionToMemory(identity, input, dependencies) {
20608
20343
 
20609
20344
  // src/api/scrape-blob-cleanup.ts
20610
20345
  import { readdir, stat, unlink } from "fs/promises";
20611
- import { homedir as homedir2 } from "os";
20612
- import { join as join6 } from "path";
20346
+ import { homedir } from "os";
20347
+ import { join as join5 } from "path";
20613
20348
  async function cleanupVercel(token, cutoff) {
20614
20349
  const { list, del } = await import("@vercel/blob");
20615
20350
  let cursor;
@@ -20626,8 +20361,8 @@ async function cleanupVercel(token, cutoff) {
20626
20361
  return { deleted, store: "vercel-blob" };
20627
20362
  }
20628
20363
  async function cleanupLocal(cutoff) {
20629
- const baseDir = process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join6(homedir2(), "Downloads", "mcp-scraper");
20630
- const dir = join6(baseDir, "blobs", SCRAPE_FALLBACK_PREFIX.replace(/\/$/, ""));
20364
+ const baseDir = process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join5(homedir(), "Downloads", "mcp-scraper");
20365
+ const dir = join5(baseDir, "blobs", SCRAPE_FALLBACK_PREFIX.replace(/\/$/, ""));
20631
20366
  let deleted = 0;
20632
20367
  let entries;
20633
20368
  try {
@@ -20636,7 +20371,7 @@ async function cleanupLocal(cutoff) {
20636
20371
  return { deleted: 0, store: "local" };
20637
20372
  }
20638
20373
  for (const name of entries) {
20639
- const path5 = join6(dir, name);
20374
+ const path5 = join5(dir, name);
20640
20375
  try {
20641
20376
  const s = await stat(path5);
20642
20377
  if (s.isFile() && s.mtimeMs < cutoff) {
@@ -22209,7 +21944,7 @@ var auth2 = createMiddleware3(async (c, next) => {
22209
21944
  const user = await getUserByApiKey(key);
22210
21945
  if (!user) return c.json({ error: "Invalid or inactive API key" }, 401);
22211
21946
  const refreshed = await applyMonthlyFreeRefresh(user);
22212
- const balanceMc = await reconcileBalanceMc(refreshed.id);
21947
+ const balanceMc = await reconcileBalanceMc(refreshed.id, refreshed.balance_mc);
22213
21948
  c.set("user", { ...refreshed, balance_mc: balanceMc });
22214
21949
  return next();
22215
21950
  });
@@ -22228,7 +21963,7 @@ var sessionAuth = createMiddleware3(async (c, next) => {
22228
21963
  const user = await getUserById(userId);
22229
21964
  if (!user) return c.json({ error: "User not found" }, 401);
22230
21965
  const refreshed = await applyMonthlyFreeRefresh(user);
22231
- const balanceMc = await reconcileBalanceMc(refreshed.id);
21966
+ const balanceMc = await reconcileBalanceMc(refreshed.id, refreshed.balance_mc);
22232
21967
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
22233
21968
  return next();
22234
21969
  });
@@ -22468,7 +22203,7 @@ app.get("/me", async (c) => {
22468
22203
  }
22469
22204
  const refreshed = await applyMonthlyFreeRefresh(foundUser);
22470
22205
  const [balanceMc, stats] = await Promise.all([
22471
- reconcileBalanceMc(refreshed.id),
22206
+ reconcileBalanceMc(refreshed.id, refreshed.balance_mc),
22472
22207
  getUserStats(refreshed.id)
22473
22208
  ]);
22474
22209
  const user = { ...refreshed, balance_mc: balanceMc };
@@ -23892,7 +23627,8 @@ app.post("/extract-site", auth2, async (c) => {
23892
23627
  if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
23893
23628
  const parsed = checked.parsed;
23894
23629
  const user = c.get("user");
23895
- if (body.background === true) {
23630
+ const downloadImages = body.downloadImages === true;
23631
+ if (body.background === true || downloadImages) {
23896
23632
  const affordablePages = Math.floor(user.balance_mc / MC_COSTS.page_scrape);
23897
23633
  if (affordablePages < 1) return c.json(insufficientBalanceResponse(user.balance_mc, MC_COSTS.page_scrape), 402);
23898
23634
  const maxPages = Math.min(Math.min(1e4, Math.max(1, body.maxPages ?? 1e4)), affordablePages);
@@ -23907,6 +23643,7 @@ app.post("/extract-site", auth2, async (c) => {
23907
23643
  concurrency: concurrencyLimitForUser(user),
23908
23644
  urlsPerBrowser: body.rotateProxyEvery ?? 10,
23909
23645
  formats: body.formats,
23646
+ downloadImages,
23910
23647
  heldMc
23911
23648
  });
23912
23649
  jobCreated = true;
@@ -24215,7 +23952,7 @@ app.post("/billing/concurrency/cancel", requireAllowedOrigin, sessionAuth, async
24215
23952
  });
24216
23953
  app.get("/billing/balance", auth2, async (c) => {
24217
23954
  const user = c.get("user");
24218
- const balanceMc = await reconcileBalanceMc(user.id);
23955
+ const balanceMc = user.balance_mc;
24219
23956
  const ledger = await getLedger(user.id, 20);
24220
23957
  const freeCredits = await getFreeCreditBreakdown(user.id);
24221
23958
  return c.json({
@@ -24227,7 +23964,7 @@ app.get("/billing/balance", auth2, async (c) => {
24227
23964
  });
24228
23965
  app.post("/billing/credits", auth2, async (c) => {
24229
23966
  const user = c.get("user");
24230
- const balanceMc = await reconcileBalanceMc(user.id);
23967
+ const balanceMc = user.balance_mc;
24231
23968
  const body = await c.req.json().catch(() => ({}));
24232
23969
  const query = body.item?.trim().toLowerCase();
24233
23970
  const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => cost);
@@ -24260,7 +23997,7 @@ app.get("/cron/tick", async (c) => {
24260
23997
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
24261
23998
  return c.json({ error: "Unauthorized" }, 401);
24262
23999
  }
24263
- const { drainQueue } = await import("./worker-JQTS437L.js");
24000
+ const { drainQueue } = await import("./worker-O5PZTTPY.js");
24264
24001
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
24265
24002
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
24266
24003
  const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
@@ -24288,15 +24025,15 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
24288
24025
  return c.json({ error: "Unauthorized" }, 401);
24289
24026
  }
24290
24027
  const jobId = c.req.param("id");
24291
- const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-NVSZH35Y.js");
24292
- const { assembleExtractArtifacts } = await import("./extract-bundle-U4D5LW5W.js");
24028
+ const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-GWKGK46Z.js");
24029
+ const { assembleExtractArtifacts } = await import("./extract-bundle-OSUPAHCE.js");
24293
24030
  const job = await getExtractJob2(jobId);
24294
24031
  if (!job) return c.json({ error: "job not found" }, 404);
24295
24032
  const stored = await assembleExtractArtifacts(job, {});
24296
24033
  await completeExtractJob2(jobId, stored);
24297
24034
  let settlement = "already_settled_or_refunded";
24298
24035
  if (job.billedMc == null && job.userId != null) {
24299
- const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-NVSZH35Y.js");
24036
+ const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-GWKGK46Z.js");
24300
24037
  const heldMc = Number(job.options.heldMc ?? 0);
24301
24038
  const successful = await countSuccessfulPages2(jobId);
24302
24039
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -24461,4 +24198,4 @@ app.get("/blog/:slug/", (c) => {
24461
24198
  export {
24462
24199
  app
24463
24200
  };
24464
- //# sourceMappingURL=server-MQDCAR6I.js.map
24201
+ //# sourceMappingURL=server-VDWIPV7F.js.map