mcp-scraper 0.3.0 → 0.3.2

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 (52) hide show
  1. package/README.md +4 -1
  2. package/dist/bin/api-server.cjs +2047 -831
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/browser-agent-stdio-server.cjs +3 -3
  6. package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
  7. package/dist/bin/browser-agent-stdio-server.js +2 -2
  8. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  9. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-cli.js +1 -1
  11. package/dist/bin/mcp-scraper-combined-stdio-server.cjs +350 -5
  12. package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -4
  14. package/dist/bin/mcp-scraper-install.cjs +3 -3
  15. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  16. package/dist/bin/mcp-scraper-install.js +2 -2
  17. package/dist/bin/mcp-stdio-server.cjs +346 -1
  18. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  19. package/dist/bin/mcp-stdio-server.js +2 -2
  20. package/dist/bin/paa-harvest.cjs +3 -1
  21. package/dist/bin/paa-harvest.cjs.map +1 -1
  22. package/dist/bin/paa-harvest.js +1 -1
  23. package/dist/{chunk-SXTXMFEQ.js → chunk-AZ5PKU4F.js} +34 -1
  24. package/dist/chunk-AZ5PKU4F.js.map +1 -0
  25. package/dist/{chunk-Q4STSM63.js → chunk-C3FGVJWH.js} +3 -3
  26. package/dist/chunk-C3FGVJWH.js.map +1 -0
  27. package/dist/{chunk-DH4H3F6S.js → chunk-HRTCMLZB.js} +4 -4
  28. package/dist/chunk-HRTCMLZB.js.map +1 -0
  29. package/dist/{chunk-SIXJ6EVS.js → chunk-KO223PSJ.js} +347 -2
  30. package/dist/chunk-KO223PSJ.js.map +1 -0
  31. package/dist/{chunk-IPW4LFOT.js → chunk-UWSG3C5J.js} +4 -2
  32. package/dist/chunk-UWSG3C5J.js.map +1 -0
  33. package/dist/chunk-VB65G7PS.js +7 -0
  34. package/dist/chunk-VB65G7PS.js.map +1 -0
  35. package/dist/index.cjs +3 -1
  36. package/dist/index.cjs.map +1 -1
  37. package/dist/index.js +1 -1
  38. package/dist/{server-MOP7NIUK.js → server-N5LM3PEO.js} +997 -253
  39. package/dist/server-N5LM3PEO.js.map +1 -0
  40. package/dist/{worker-SLQ375UG.js → worker-56IXWOQU.js} +3 -3
  41. package/docs/mcp-tool-craft-lint.generated.md +5 -2
  42. package/docs/mcp-tool-manifest.generated.json +85 -9
  43. package/package.json +1 -1
  44. package/dist/chunk-DH4H3F6S.js.map +0 -1
  45. package/dist/chunk-IPW4LFOT.js.map +0 -1
  46. package/dist/chunk-PGWJ2EJ3.js +0 -7
  47. package/dist/chunk-PGWJ2EJ3.js.map +0 -1
  48. package/dist/chunk-Q4STSM63.js.map +0 -1
  49. package/dist/chunk-SIXJ6EVS.js.map +0 -1
  50. package/dist/chunk-SXTXMFEQ.js.map +0 -1
  51. package/dist/server-MOP7NIUK.js.map +0 -1
  52. /package/dist/{worker-SLQ375UG.js.map → worker-56IXWOQU.js.map} +0 -0
@@ -16,7 +16,7 @@ import {
16
16
  harvestProblemResponse,
17
17
  insufficientBalanceResponse,
18
18
  serializeHarvestProblem
19
- } from "./chunk-SXTXMFEQ.js";
19
+ } from "./chunk-AZ5PKU4F.js";
20
20
  import {
21
21
  SiteAuditJobRowSchema,
22
22
  advanceWorkflowRunStep,
@@ -85,7 +85,7 @@ import {
85
85
  deleteKernelProxyId,
86
86
  harvest,
87
87
  resolveKernelProxyId
88
- } from "./chunk-IPW4LFOT.js";
88
+ } from "./chunk-UWSG3C5J.js";
89
89
  import {
90
90
  CaptureSerpPageSnapshotsInputSchema,
91
91
  CaptureSerpPageSnapshotsOutputSchema,
@@ -99,7 +99,7 @@ import {
99
99
  harvestTimeoutBudget,
100
100
  liveWebToolAnnotations,
101
101
  outputBaseDir
102
- } from "./chunk-SIXJ6EVS.js";
102
+ } from "./chunk-KO223PSJ.js";
103
103
  import {
104
104
  CaptchaError,
105
105
  RECAPTCHA_INSTRUCTIONS,
@@ -135,7 +135,12 @@ import {
135
135
  RawMapsOverviewSchema,
136
136
  RawMapsReviewStatsSchema
137
137
  } from "./chunk-LFATOGDF.js";
138
- import "./chunk-PGWJ2EJ3.js";
138
+ import {
139
+ defaultChromeExecutablePath,
140
+ loadImportedBrowserProfile,
141
+ localBrowserModeEnabled
142
+ } from "./chunk-DUEW4EOO.js";
143
+ import "./chunk-VB65G7PS.js";
139
144
 
140
145
  // src/api/outbound-sanitize.ts
141
146
  var KEY_RENAMES = {
@@ -4078,9 +4083,9 @@ async function extractBrandingFromPage(page) {
4078
4083
  colors: {
4079
4084
  primary: primary && !isTransparentOrWhite(primary) ? primary : null,
4080
4085
  accent: accent && !isTransparentOrWhite(accent) ? accent : null,
4081
- background: background && !isTransparentOrWhite(background) ? background : null,
4082
- text: text && text !== "#000000" ? text : null,
4083
- heading: heading && heading !== "#000000" ? heading : null
4086
+ background: background ?? null,
4087
+ text: text ?? null,
4088
+ heading: heading ?? null
4084
4089
  },
4085
4090
  fonts: {
4086
4091
  body: firstFont(raw.bodyFont ?? ""),
@@ -4421,8 +4426,8 @@ async function downloadAsset(url, destDir, filename) {
4421
4426
  }
4422
4427
  const writer = createWriteStream(dest);
4423
4428
  await pipeline(Readable.fromWeb(res.body), writer);
4424
- const { statSync } = await import("fs");
4425
- const sizeBytes = statSync(dest).size;
4429
+ const { statSync: statSync2 } = await import("fs");
4430
+ const sizeBytes = statSync2(dest).size;
4426
4431
  return { savedPath: dest, sizeBytes, mimeType };
4427
4432
  }
4428
4433
  async function harvestPageMedia(html, pageUrl, options = {}) {
@@ -4899,7 +4904,7 @@ async function extractSite(opts) {
4899
4904
  }
4900
4905
 
4901
4906
  // src/api/server.ts
4902
- import { Hono as Hono12 } from "hono";
4907
+ import { Hono as Hono13 } from "hono";
4903
4908
  import { serve as serveInngest } from "inngest/hono";
4904
4909
 
4905
4910
  // src/inngest/client.ts
@@ -9346,7 +9351,41 @@ async function extractFacebookOrganicVideoFromPage(page, sourceUrl, quality = "b
9346
9351
  }
9347
9352
 
9348
9353
  // src/api/facebook-ad-routes.ts
9354
+ import { fal as fal3 } from "@fal-ai/client";
9355
+
9356
+ // src/services/media-transcription.ts
9349
9357
  import { fal as fal2 } from "@fal-ai/client";
9358
+ function transcriptMarkdown(title, text, chunks, durationMs) {
9359
+ const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
9360
+ const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
9361
+ if (chunks.length) {
9362
+ lines.push("## Timestamped Segments", "");
9363
+ for (const ch of chunks) {
9364
+ lines.push(`**[${fmtTs2(ch.timestamp[0])} -> ${fmtTs2(ch.timestamp[1])}]** ${ch.text.trim()}`, "");
9365
+ }
9366
+ }
9367
+ return lines.join("\n");
9368
+ }
9369
+ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript") {
9370
+ const startMs = Date.now();
9371
+ const result = await fal2.subscribe("fal-ai/wizper", {
9372
+ input: { audio_url: mediaUrl, task: "transcribe", language: "en" },
9373
+ logs: false,
9374
+ pollInterval: 3e3
9375
+ });
9376
+ const data = result.data;
9377
+ const text = data.text ?? "";
9378
+ const chunks = data.chunks ?? [];
9379
+ const durationMs = Date.now() - startMs;
9380
+ return {
9381
+ text,
9382
+ chunks,
9383
+ durationMs,
9384
+ markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs)
9385
+ };
9386
+ }
9387
+
9388
+ // src/api/facebook-ad-routes.ts
9350
9389
  var FacebookAdBodySchema = z13.object({
9351
9390
  url: z13.string().trim().optional(),
9352
9391
  libraryId: z13.string().trim().optional(),
@@ -9384,34 +9423,8 @@ function isAllowedFacebookOrganicUrl(url) {
9384
9423
  const hostname = url.hostname.toLowerCase().replace(/^www\./, "").replace(/^m\./, "");
9385
9424
  return hostname === "facebook.com" || hostname === "fb.watch";
9386
9425
  }
9387
- function facebookTranscriptMarkdown(title, text, chunks, durationMs) {
9388
- const fmtTs2 = (s) => `${Math.floor(s / 60)}:${String(Math.floor(s % 60)).padStart(2, "0")}`;
9389
- const lines = [title, "", `*Transcribed in ${(durationMs / 1e3).toFixed(1)}s*`, "", "## Full Text", "", text, ""];
9390
- if (chunks.length) {
9391
- lines.push("## Timestamped Segments", "");
9392
- for (const ch of chunks) {
9393
- lines.push(`**[${fmtTs2(ch.timestamp[0])} -> ${fmtTs2(ch.timestamp[1])}]** ${ch.text.trim()}`, "");
9394
- }
9395
- }
9396
- return lines.join("\n");
9397
- }
9398
9426
  async function transcribeFacebookVideoUrl(videoUrl, markdownTitle = "# Facebook Video Transcript") {
9399
- const startMs = Date.now();
9400
- const result = await fal2.subscribe("fal-ai/wizper", {
9401
- input: { audio_url: videoUrl, task: "transcribe", language: "en" },
9402
- logs: false,
9403
- pollInterval: 3e3
9404
- });
9405
- const data = result.data;
9406
- const text = data.text ?? "";
9407
- const chunks = data.chunks ?? [];
9408
- const durationMs = Date.now() - startMs;
9409
- return {
9410
- text,
9411
- chunks,
9412
- durationMs,
9413
- markdown: facebookTranscriptMarkdown(markdownTitle, text, chunks, durationMs)
9414
- };
9427
+ return transcribeMediaUrl(videoUrl, markdownTitle);
9415
9428
  }
9416
9429
  async function detectSoftBlock(driver) {
9417
9430
  const page = driver.getPage();
@@ -9549,7 +9562,7 @@ facebookAdApp.post("/transcribe", createApiKeyAuth(), async (c) => {
9549
9562
  metadata: { videoUrl }
9550
9563
  });
9551
9564
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
9552
- fal2.config({ credentials: process.env.FAL_KEY });
9565
+ fal3.config({ credentials: process.env.FAL_KEY });
9553
9566
  let debited = false;
9554
9567
  try {
9555
9568
  const { ok, balance_mc } = await debitMc(fbUser.id, MC_COSTS.fb_transcribe, LedgerOperation.FB_TRANSCRIBE, videoUrl);
@@ -9588,7 +9601,7 @@ facebookAdApp.post("/video-transcribe", createApiKeyAuth(), async (c) => {
9588
9601
  metadata: { url: sourceUrl.href }
9589
9602
  });
9590
9603
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
9591
- fal2.config({ credentials: process.env.FAL_KEY });
9604
+ fal3.config({ credentials: process.env.FAL_KEY });
9592
9605
  const driver = new BrowserDriver();
9593
9606
  let debited = false;
9594
9607
  try {
@@ -9763,8 +9776,735 @@ facebookAdApp.post("/media", createApiKeyAuth(), async (c) => {
9763
9776
  });
9764
9777
  });
9765
9778
 
9766
- // src/api/maps-routes.ts
9779
+ // src/api/instagram-routes.ts
9767
9780
  import { Hono as Hono5 } from "hono";
9781
+ import { z as z14 } from "zod";
9782
+ import { createWriteStream as createWriteStream2, existsSync, mkdirSync as mkdirSync2, statSync, writeFileSync } from "fs";
9783
+ import { homedir as homedir2 } from "os";
9784
+ import { basename as basename2, join as join4 } from "path";
9785
+ import { Readable as Readable2 } from "stream";
9786
+ import { pipeline as pipeline2 } from "stream/promises";
9787
+ import { spawn } from "child_process";
9788
+
9789
+ // src/extractor/InstagramContentExtractor.ts
9790
+ var CONTENT_PATH_RE = /^\/(?:[^/]+\/)?(p|reel|tv)\/([A-Za-z0-9_-]+)\/?/i;
9791
+ var CDN_HOST_RE = /(^|\.)((cdninstagram\.com)|(scontent[^/]*\.cdninstagram\.com))$/i;
9792
+ function htmlDecode2(value) {
9793
+ return value.replace(/&amp;/g, "&").replace(/&quot;/g, '"').replace(/&#039;/g, "'").replace(/&lt;/g, "<").replace(/&gt;/g, ">");
9794
+ }
9795
+ function decodeEscapedValue2(raw) {
9796
+ let out = raw;
9797
+ try {
9798
+ out = JSON.parse(`"${raw.replace(/"/g, '\\"')}"`);
9799
+ } catch {
9800
+ out = raw.replace(/\\u0025/g, "%").replace(/\\u0026/g, "&").replace(/\\u003D/g, "=").replace(/\\u003d/g, "=").replace(/\\"/g, '"').replace(/\\\//g, "/");
9801
+ }
9802
+ return htmlDecode2(out);
9803
+ }
9804
+ function metadataContent2(html, key) {
9805
+ const escaped = key.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
9806
+ const re = new RegExp(`<meta\\s+[^>]*(?:property|name)=["']${escaped}["'][^>]*content=["']([^"']+)["'][^>]*>`, "i");
9807
+ const m = html.match(re);
9808
+ return m ? htmlDecode2(m[1]).trim() : null;
9809
+ }
9810
+ function instagramContentTypeFromPath(rawPath) {
9811
+ const match = rawPath.match(CONTENT_PATH_RE);
9812
+ if (!match) return null;
9813
+ const type = match[1].toLowerCase() === "p" ? "post" : match[1].toLowerCase();
9814
+ return { type, shortcode: match[2] };
9815
+ }
9816
+ function normalizeInstagramProfileInput(input) {
9817
+ const raw = input.trim();
9818
+ if (!raw) throw new Error("Instagram handle or profile URL is required");
9819
+ if (/^https?:\/\//i.test(raw) || /^www\.instagram\.com/i.test(raw) || /^instagram\.com/i.test(raw)) {
9820
+ const parsed = new URL(/^https?:\/\//i.test(raw) ? raw : `https://${raw}`);
9821
+ const host = parsed.hostname.toLowerCase().replace(/^www\./, "").replace(/^m\./, "");
9822
+ if (host !== "instagram.com") throw new Error("Profile URL must be on instagram.com");
9823
+ const first = parsed.pathname.split("/").filter(Boolean)[0];
9824
+ if (!first || ["p", "reel", "tv", "stories", "explore"].includes(first.toLowerCase())) {
9825
+ throw new Error("Profile URL must point to an Instagram profile, not a post, reel, story, or explore page");
9826
+ }
9827
+ return { handle: first, profileUrl: `https://www.instagram.com/${first}/` };
9828
+ }
9829
+ const handle = raw.replace(/^@/, "").replace(/^\/+|\/+$/g, "");
9830
+ if (!/^[A-Za-z0-9._]{1,30}$/.test(handle)) throw new Error("Invalid Instagram handle");
9831
+ return { handle, profileUrl: `https://www.instagram.com/${handle}/` };
9832
+ }
9833
+ function isAllowedInstagramPageUrl(url) {
9834
+ const host = url.hostname.toLowerCase().replace(/^www\./, "").replace(/^m\./, "");
9835
+ return host === "instagram.com";
9836
+ }
9837
+ function normalizeInstagramMediaUrl(raw) {
9838
+ try {
9839
+ const decoded = decodeEscapedValue2(raw);
9840
+ const parsed = new URL(decoded);
9841
+ if (!CDN_HOST_RE.test(parsed.hostname.toLowerCase())) return null;
9842
+ parsed.searchParams.delete("bytestart");
9843
+ parsed.searchParams.delete("byteend");
9844
+ return parsed.href;
9845
+ } catch {
9846
+ return null;
9847
+ }
9848
+ }
9849
+ function parseInstagramEfg(url) {
9850
+ try {
9851
+ const efg = new URL(url).searchParams.get("efg");
9852
+ if (!efg) return null;
9853
+ return JSON.parse(Buffer.from(efg, "base64").toString("utf8"));
9854
+ } catch {
9855
+ return null;
9856
+ }
9857
+ }
9858
+ function trackFromUrl(url) {
9859
+ const normalized = normalizeInstagramMediaUrl(url);
9860
+ if (!normalized) return null;
9861
+ const parsed = parseInstagramEfg(normalized);
9862
+ const tag = typeof parsed?.vencode_tag === "string" ? parsed.vencode_tag : null;
9863
+ const streamType = tag?.toLowerCase().includes("audio") ? "audio" : normalized.includes(".mp4") ? "video" : "unknown";
9864
+ return {
9865
+ url: normalized,
9866
+ streamType,
9867
+ bitrate: typeof parsed?.bitrate === "number" ? parsed.bitrate : null,
9868
+ durationSec: typeof parsed?.duration_s === "number" ? parsed.duration_s : null,
9869
+ vencodeTag: tag,
9870
+ width: typeof parsed?.width === "number" ? parsed.width : null,
9871
+ height: typeof parsed?.height === "number" ? parsed.height : null
9872
+ };
9873
+ }
9874
+ function extractInstagramMp4UrlsFromText(text) {
9875
+ const urls = [];
9876
+ const seen = /* @__PURE__ */ new Set();
9877
+ const patterns = [
9878
+ /https?:\\?\/\\?\/[^"'<>\s]+?\.mp4(?:\?[^"'<>\s]+)?/g,
9879
+ /https?:%5C?\/%5C?\/[^"'<>\s]+?\.mp4(?:\?[^"'<>\s]+)?/g
9880
+ ];
9881
+ for (const pattern of patterns) {
9882
+ let match;
9883
+ while ((match = pattern.exec(text)) !== null) {
9884
+ const normalized = normalizeInstagramMediaUrl(match[0].replace(/%5C/gi, "\\"));
9885
+ if (!normalized || seen.has(normalized)) continue;
9886
+ seen.add(normalized);
9887
+ urls.push(normalized);
9888
+ }
9889
+ }
9890
+ return urls;
9891
+ }
9892
+ function instagramTracksFromUrls(urls) {
9893
+ const seen = /* @__PURE__ */ new Set();
9894
+ const tracks = [];
9895
+ for (const raw of urls) {
9896
+ const track = trackFromUrl(raw);
9897
+ if (!track || seen.has(track.url)) continue;
9898
+ seen.add(track.url);
9899
+ tracks.push(track);
9900
+ }
9901
+ return tracks;
9902
+ }
9903
+ function selectInstagramTracks(tracks) {
9904
+ const selectedVideoTrack = tracks.filter((track) => track.streamType === "video").sort((a, b) => {
9905
+ const aPixels = (a.width ?? 0) * (a.height ?? 0);
9906
+ const bPixels = (b.width ?? 0) * (b.height ?? 0);
9907
+ if (aPixels !== bPixels) return bPixels - aPixels;
9908
+ return (b.bitrate ?? 0) - (a.bitrate ?? 0);
9909
+ })[0] ?? null;
9910
+ const selectedAudioTrack = tracks.filter((track) => track.streamType === "audio").sort((a, b) => (b.bitrate ?? 0) - (a.bitrate ?? 0))[0] ?? null;
9911
+ return { selectedVideoTrack, selectedAudioTrack };
9912
+ }
9913
+ function parseCount(text) {
9914
+ if (!text) return null;
9915
+ const match = text.match(/([\d,.]+)\s*posts?/i);
9916
+ if (!match) return null;
9917
+ const value = Number(match[1].replace(/[,.]/g, ""));
9918
+ return Number.isFinite(value) ? value : null;
9919
+ }
9920
+ function captionFromMetadata(metadata) {
9921
+ const raw = metadata.ogDescription ?? metadata.description;
9922
+ if (!raw) return null;
9923
+ const quoted = raw.match(/:\s*"([\s\S]+)"\.?\s*$/);
9924
+ if (quoted?.[1]) return quoted[1].trim();
9925
+ return raw.trim();
9926
+ }
9927
+ function ownerFromMetadata(metadata) {
9928
+ const title = metadata.ogTitle ?? metadata.twitterTitle ?? metadata.title;
9929
+ if (!title) return null;
9930
+ const match = title.match(/^(.+?)\s+on Instagram:/i) ?? title.match(/^(.+?)\s+\(@[^)]+\)\s+•\s+Instagram/i);
9931
+ return match?.[1]?.trim() || null;
9932
+ }
9933
+ async function collectInstagramProfileContentFromPage(page, opts) {
9934
+ const maxItems = Math.min(500, Math.max(1, opts.maxItems ?? 50));
9935
+ const maxScrolls = Math.min(100, Math.max(0, opts.maxScrolls ?? 10));
9936
+ const scrollDelayMs = Math.min(5e3, Math.max(250, opts.scrollDelayMs ?? 1200));
9937
+ const stableScrollLimit = Math.min(10, Math.max(1, opts.stableScrollLimit ?? 4));
9938
+ const seen = /* @__PURE__ */ new Set();
9939
+ const items = [];
9940
+ const stages = [];
9941
+ let attemptedScrolls = 0;
9942
+ let stableScrolls = 0;
9943
+ let stoppedReason = null;
9944
+ const collect = async (stage) => {
9945
+ const before = items.length;
9946
+ const links = await page.evaluate(() => Array.from(document.querySelectorAll("a[href]")).map((a) => ({
9947
+ href: a.href,
9948
+ text: (a.textContent ?? "").replace(/\s+/g, " ").trim()
9949
+ })));
9950
+ for (const link of links) {
9951
+ let parsed;
9952
+ try {
9953
+ parsed = new URL(link.href);
9954
+ } catch {
9955
+ continue;
9956
+ }
9957
+ const found = instagramContentTypeFromPath(parsed.pathname);
9958
+ if (!found) continue;
9959
+ const normalized = `https://www.instagram.com/${found.type === "post" ? "p" : found.type}/${found.shortcode}/`;
9960
+ if (seen.has(normalized)) continue;
9961
+ seen.add(normalized);
9962
+ items.push({
9963
+ url: normalized,
9964
+ type: found.type,
9965
+ shortcode: found.shortcode,
9966
+ anchorText: link.text || null,
9967
+ firstSeenStage: stage
9968
+ });
9969
+ if (items.length >= maxItems) return items.length - before;
9970
+ }
9971
+ return items.length - before;
9972
+ };
9973
+ const readBodyText = async () => page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
9974
+ const profileLines = (text) => text.split("\n").map((line) => line.trim()).filter(Boolean);
9975
+ const profilePostCountText = (text) => profileLines(text).find((line) => /\bposts?\b/i.test(line) && /[\d,.]/.test(line)) ?? null;
9976
+ const scrollMetrics = async () => page.evaluate(() => ({
9977
+ scrollY: typeof window.scrollY === "number" ? window.scrollY : null,
9978
+ scrollHeight: Math.max(
9979
+ document.body?.scrollHeight ?? 0,
9980
+ document.documentElement?.scrollHeight ?? 0
9981
+ ) || null
9982
+ })).catch(() => ({ scrollY: null, scrollHeight: null }));
9983
+ const recordStage = async (stage, addedCount) => {
9984
+ const metrics = await scrollMetrics();
9985
+ stages.push({
9986
+ stage,
9987
+ itemCount: items.length,
9988
+ addedCount,
9989
+ scrollY: metrics.scrollY,
9990
+ scrollHeight: metrics.scrollHeight
9991
+ });
9992
+ };
9993
+ const advancePagination = async () => page.evaluate(() => {
9994
+ const contentLinks = Array.from(document.querySelectorAll('a[href*="/p/"], a[href*="/reel/"], a[href*="/tv/"]'));
9995
+ const last = contentLinks[contentLinks.length - 1];
9996
+ if (last) last.scrollIntoView({ block: "end", inline: "nearest" });
9997
+ const distance = Math.max(window.innerHeight * 0.9, 700);
9998
+ window.scrollBy(0, distance);
9999
+ return {
10000
+ scrollY: typeof window.scrollY === "number" ? window.scrollY : null,
10001
+ scrollHeight: Math.max(
10002
+ document.body?.scrollHeight ?? 0,
10003
+ document.documentElement?.scrollHeight ?? 0
10004
+ ) || null
10005
+ };
10006
+ }).catch(() => ({ scrollY: null, scrollHeight: null }));
10007
+ await page.waitForLoadState("domcontentloaded", { timeout: 15e3 }).catch(() => {
10008
+ });
10009
+ await page.waitForTimeout(1500);
10010
+ await recordStage("initial", await collect("initial"));
10011
+ const initialReportedPostCount = parseCount(profilePostCountText(await readBodyText()));
10012
+ for (let i = 0; i < maxScrolls && items.length < maxItems && stableScrolls < stableScrollLimit; i++) {
10013
+ const before = items.length;
10014
+ const beforeMetrics = await scrollMetrics();
10015
+ attemptedScrolls = i + 1;
10016
+ await advancePagination();
10017
+ await page.keyboard.press("PageDown").catch(() => {
10018
+ });
10019
+ await page.waitForLoadState("networkidle", { timeout: 2500 }).catch(() => {
10020
+ });
10021
+ await page.waitForTimeout(scrollDelayMs);
10022
+ const added = await collect(`scroll_${i + 1}`);
10023
+ await recordStage(`scroll_${i + 1}`, added);
10024
+ const afterMetrics = await scrollMetrics();
10025
+ const advanced = (afterMetrics.scrollHeight ?? 0) > (beforeMetrics.scrollHeight ?? 0) || (afterMetrics.scrollY ?? 0) > (beforeMetrics.scrollY ?? 0);
10026
+ stableScrolls = items.length === before && !advanced ? stableScrolls + 1 : 0;
10027
+ if (initialReportedPostCount != null && items.length >= initialReportedPostCount) {
10028
+ stoppedReason = "reported_post_count";
10029
+ break;
10030
+ }
10031
+ if (items.length >= maxItems) {
10032
+ stoppedReason = "max_items";
10033
+ break;
10034
+ }
10035
+ if (stableScrolls >= stableScrollLimit) {
10036
+ stoppedReason = "stable_scrolls";
10037
+ break;
10038
+ }
10039
+ }
10040
+ const bodyText = await readBodyText();
10041
+ const lines = profileLines(bodyText);
10042
+ const reportedPostCountText = lines.find((line) => /\bposts?\b/i.test(line) && /[\d,.]/.test(line)) ?? null;
10043
+ const followerCountText = lines.find((line) => /\bfollowers?\b/i.test(line)) ?? null;
10044
+ const followingCountText = lines.find((line) => /\bfollowing\b/i.test(line)) ?? null;
10045
+ const profileName = lines.find((line) => line && line.toLowerCase() !== opts.handle.toLowerCase()) ?? null;
10046
+ const typeCounts = {
10047
+ post: items.filter((item) => item.type === "post").length,
10048
+ reel: items.filter((item) => item.type === "reel").length,
10049
+ tv: items.filter((item) => item.type === "tv").length
10050
+ };
10051
+ const reportedPostCount = parseCount(reportedPostCountText);
10052
+ const reachedMaxItems = items.length >= maxItems;
10053
+ const reachedReportedPostCount = reportedPostCount != null && items.length >= reportedPostCount;
10054
+ if (!stoppedReason) {
10055
+ stoppedReason = reachedMaxItems ? "max_items" : reachedReportedPostCount ? "reported_post_count" : maxScrolls === 0 ? "no_scrolls" : attemptedScrolls >= maxScrolls ? "max_scrolls" : "stable_scrolls";
10056
+ }
10057
+ const finalMetrics = await scrollMetrics();
10058
+ const pagination = {
10059
+ maxItems,
10060
+ maxScrolls,
10061
+ attemptedScrolls,
10062
+ stableScrolls,
10063
+ stableScrollLimit,
10064
+ scrollDelayMs,
10065
+ reachedMaxItems,
10066
+ reachedReportedPostCount,
10067
+ finalScrollHeight: finalMetrics.scrollHeight,
10068
+ stoppedReason,
10069
+ stages
10070
+ };
10071
+ const limitations = [];
10072
+ if (/log in|sign up/i.test(bodyText)) limitations.push("Login or sign-up prompts were visible during extraction.");
10073
+ if (reachedMaxItems && !reachedReportedPostCount) limitations.push("Collection stopped at maxItems before the full profile history was proven.");
10074
+ if (reportedPostCount != null && items.length < reportedPostCount && !reachedMaxItems) limitations.push("Collected item count is lower than the profile-reported post count.");
10075
+ if (stoppedReason === "stable_scrolls" && !reachedReportedPostCount) limitations.push("Pagination stopped after repeated stable scrolls without new content.");
10076
+ if (stoppedReason === "max_scrolls" && !reachedReportedPostCount) limitations.push("Pagination reached maxScrolls before the full profile history was proven.");
10077
+ return {
10078
+ handle: opts.handle,
10079
+ profileUrl: opts.profileUrl,
10080
+ pageUrl: page.url(),
10081
+ profileName,
10082
+ bio: null,
10083
+ reportedPostCount,
10084
+ reportedPostCountText,
10085
+ followerCountText,
10086
+ followingCountText,
10087
+ collectedContentCount: items.length,
10088
+ typeCounts,
10089
+ pagination,
10090
+ items,
10091
+ limited: limitations.length > 0,
10092
+ limitations,
10093
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString()
10094
+ };
10095
+ }
10096
+ async function extractInstagramMediaFromPage(page, sourceUrl, capturedMediaUrls = []) {
10097
+ await page.waitForLoadState("domcontentloaded", { timeout: 15e3 }).catch(() => {
10098
+ });
10099
+ await page.waitForTimeout(2500);
10100
+ const html = await page.content();
10101
+ const bodyText = await page.evaluate(() => document.body?.innerText ?? "").catch(() => "");
10102
+ const metadata = {
10103
+ title: await page.title().catch(() => null),
10104
+ description: metadataContent2(html, "description"),
10105
+ ogTitle: metadataContent2(html, "og:title"),
10106
+ ogDescription: metadataContent2(html, "og:description"),
10107
+ ogImage: metadataContent2(html, "og:image"),
10108
+ ogUrl: metadataContent2(html, "og:url"),
10109
+ twitterImage: metadataContent2(html, "twitter:image"),
10110
+ twitterTitle: metadataContent2(html, "twitter:title")
10111
+ };
10112
+ const parsedPath = (() => {
10113
+ try {
10114
+ return instagramContentTypeFromPath(new URL(page.url()).pathname) ?? instagramContentTypeFromPath(new URL(sourceUrl).pathname);
10115
+ } catch {
10116
+ return null;
10117
+ }
10118
+ })();
10119
+ const htmlUrls = extractInstagramMp4UrlsFromText(html);
10120
+ const tracks = instagramTracksFromUrls([...capturedMediaUrls, ...htmlUrls]);
10121
+ const selected = selectInstagramTracks(tracks);
10122
+ const imageUrl = metadata.ogImage ?? metadata.twitterImage;
10123
+ const limitations = [];
10124
+ if (/log in|sign up/i.test(bodyText)) limitations.push("Login or sign-up prompts were visible during extraction.");
10125
+ if (parsedPath?.type === "reel" && tracks.length === 0) limitations.push("No Instagram MP4 tracks were captured; retry with a logged-in browser profile or longer wait.");
10126
+ return {
10127
+ sourceUrl,
10128
+ pageUrl: page.url(),
10129
+ type: parsedPath?.type ?? null,
10130
+ shortcode: parsedPath?.shortcode ?? null,
10131
+ ownerName: ownerFromMetadata(metadata),
10132
+ caption: captionFromMetadata(metadata),
10133
+ bodyText,
10134
+ metadata,
10135
+ imageUrl,
10136
+ tracks,
10137
+ selectedVideoTrack: selected.selectedVideoTrack,
10138
+ selectedAudioTrack: selected.selectedAudioTrack,
10139
+ limitations,
10140
+ extractedAt: (/* @__PURE__ */ new Date()).toISOString()
10141
+ };
10142
+ }
10143
+
10144
+ // src/api/instagram-routes.ts
10145
+ var InstagramBrowserModeSchema = z14.enum(["auto", "hosted", "local"]);
10146
+ var InstagramBrowserOptionsSchema = {
10147
+ browserMode: InstagramBrowserModeSchema.default("auto"),
10148
+ browserProfile: z14.string().trim().min(1).optional()
10149
+ };
10150
+ var InstagramProfileContentBodySchema = z14.object({
10151
+ handle: z14.string().trim().optional(),
10152
+ url: z14.string().trim().optional(),
10153
+ maxItems: z14.number().int().min(1).max(500).default(50),
10154
+ maxScrolls: z14.number().int().min(0).max(100).default(10),
10155
+ scrollDelayMs: z14.number().int().min(250).max(5e3).default(1200),
10156
+ stableScrollLimit: z14.number().int().min(1).max(10).default(4),
10157
+ ...InstagramBrowserOptionsSchema
10158
+ }).refine((d) => !!d.handle || !!d.url, {
10159
+ message: "handle or url is required"
10160
+ });
10161
+ var InstagramMediaTypeSchema = z14.enum(["image", "video", "audio"]);
10162
+ var InstagramMediaDownloadBodySchema = z14.object({
10163
+ url: z14.string().trim().min(1, "url is required"),
10164
+ mediaTypes: z14.array(InstagramMediaTypeSchema).default(["image", "video", "audio"]),
10165
+ downloadMedia: z14.boolean().default(true),
10166
+ downloadAllTracks: z14.boolean().default(false),
10167
+ includeTranscript: z14.boolean().default(false),
10168
+ mux: z14.boolean().default(true),
10169
+ ...InstagramBrowserOptionsSchema
10170
+ });
10171
+ var instagramApp = new Hono5();
10172
+ function invalidRequest2(message) {
10173
+ return { error_code: "invalid_request", message };
10174
+ }
10175
+ function directProfileDir() {
10176
+ return process.env.MCP_SCRAPER_BROWSER_PROFILE_DIR?.trim() || void 0;
10177
+ }
10178
+ function configuredProfileName(profile) {
10179
+ const directDir = directProfileDir();
10180
+ return profile?.trim() || process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim() || (directDir ? basename2(directDir) : "default");
10181
+ }
10182
+ function launchExecutablePath(manifestExecutablePath) {
10183
+ const configured = process.env.MCP_SCRAPER_BROWSER_EXECUTABLE?.trim() || manifestExecutablePath || defaultChromeExecutablePath();
10184
+ return configured && existsSync(configured) ? configured : void 0;
10185
+ }
10186
+ function ignoreDefaultArgsFor(executablePath) {
10187
+ if (!executablePath) return void 0;
10188
+ if (process.platform !== "darwin") return void 0;
10189
+ if (!/Google Chrome\.app\/Contents\/MacOS\/Google Chrome$/.test(executablePath)) return void 0;
10190
+ return ["--password-store=basic", "--use-mock-keychain"];
10191
+ }
10192
+ async function kernelLaunchOptsResidential2() {
10193
+ let proxyId = browserServiceProxyId();
10194
+ try {
10195
+ const resolution = await resolveKernelProxyId({
10196
+ kernelApiKey: browserServiceApiKey(),
10197
+ proxyMode: DEFAULT_PROXY_MODE,
10198
+ configuredKernelProxyId: browserServiceProxyId(),
10199
+ gl: "us"
10200
+ });
10201
+ if (resolution.kernelProxyId) proxyId = resolution.kernelProxyId;
10202
+ } catch {
10203
+ proxyId = browserServiceProxyId();
10204
+ }
10205
+ return {
10206
+ headless: true,
10207
+ kernelApiKey: browserServiceApiKey(),
10208
+ kernelProxyId: proxyId,
10209
+ viewport: { width: 1280, height: 900 },
10210
+ locale: "en-US"
10211
+ };
10212
+ }
10213
+ async function resolveInstagramLaunch(input) {
10214
+ const requestedMode = input.browserMode ?? "auto";
10215
+ const directDir = directProfileDir();
10216
+ const useLocal = requestedMode === "local" || requestedMode === "auto" && (Boolean(input.browserProfile) || Boolean(directDir) || localBrowserModeEnabled());
10217
+ if (!useLocal) {
10218
+ return {
10219
+ config: await kernelLaunchOptsResidential2(),
10220
+ browser: {
10221
+ mode: "hosted",
10222
+ requestedMode,
10223
+ profileName: null,
10224
+ profileSource: "hosted",
10225
+ profileDirConfigured: false,
10226
+ executablePathConfigured: false
10227
+ }
10228
+ };
10229
+ }
10230
+ const requestedName = configuredProfileName(input.browserProfile);
10231
+ if (directDir && (!input.browserProfile || requestedName === process.env.MCP_SCRAPER_BROWSER_PROFILE?.trim())) {
10232
+ const executablePath2 = launchExecutablePath(null);
10233
+ return {
10234
+ config: {
10235
+ headless: false,
10236
+ profileDir: directDir,
10237
+ browserExecutablePath: executablePath2,
10238
+ ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath2),
10239
+ viewport: { width: 1280, height: 900 },
10240
+ locale: "en-US"
10241
+ },
10242
+ browser: {
10243
+ mode: "local",
10244
+ requestedMode,
10245
+ profileName: requestedName,
10246
+ profileSource: "direct_profile_dir",
10247
+ profileDirConfigured: true,
10248
+ executablePathConfigured: Boolean(executablePath2)
10249
+ }
10250
+ };
10251
+ }
10252
+ const manifest = await loadImportedBrowserProfile(requestedName);
10253
+ const executablePath = launchExecutablePath(manifest.browserExecutablePath);
10254
+ return {
10255
+ config: {
10256
+ headless: false,
10257
+ profileDir: manifest.userDataDir,
10258
+ browserExecutablePath: executablePath,
10259
+ ignoreDefaultArgs: ignoreDefaultArgsFor(executablePath),
10260
+ viewport: { width: 1280, height: 900 },
10261
+ locale: "en-US"
10262
+ },
10263
+ browser: {
10264
+ mode: "local",
10265
+ requestedMode,
10266
+ profileName: manifest.name,
10267
+ profileSource: "managed_profile",
10268
+ profileDirConfigured: true,
10269
+ executablePathConfigured: Boolean(executablePath)
10270
+ }
10271
+ };
10272
+ }
10273
+ function outputBaseDir2() {
10274
+ return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join4(homedir2(), "Downloads", "mcp-scraper");
10275
+ }
10276
+ function safeFilePart(input) {
10277
+ return input.replace(/^https?:\/\//, "").replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "instagram";
10278
+ }
10279
+ function mediaOutputDir(shortcode, sourceUrl) {
10280
+ const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
10281
+ const slug = shortcode ? `instagram-${shortcode}` : safeFilePart(sourceUrl);
10282
+ const dir = join4(outputBaseDir2(), "instagram", `${stamp}-${slug}`);
10283
+ mkdirSync2(dir, { recursive: true });
10284
+ return dir;
10285
+ }
10286
+ async function downloadToFile(url, destPath, referer) {
10287
+ const res = await fetch(url, {
10288
+ headers: {
10289
+ "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
10290
+ "Referer": referer
10291
+ },
10292
+ signal: AbortSignal.timeout(9e4)
10293
+ });
10294
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
10295
+ if (!res.body) throw new Error("Empty response body");
10296
+ await pipeline2(Readable2.fromWeb(res.body), createWriteStream2(destPath));
10297
+ return {
10298
+ savedPath: destPath,
10299
+ sizeBytes: statSync(destPath).size,
10300
+ mimeType: res.headers.get("content-type")?.split(";")[0]?.trim() ?? null
10301
+ };
10302
+ }
10303
+ function extFromMime(mimeType, fallback) {
10304
+ const map = {
10305
+ "image/jpeg": "jpg",
10306
+ "image/png": "png",
10307
+ "image/webp": "webp",
10308
+ "image/gif": "gif",
10309
+ "video/mp4": "mp4",
10310
+ "audio/mp4": "mp4",
10311
+ "audio/aac": "aac"
10312
+ };
10313
+ return mimeType ? map[mimeType] ?? fallback : fallback;
10314
+ }
10315
+ function runFfmpegMux(videoPath, audioPath, outPath) {
10316
+ return new Promise((resolve) => {
10317
+ const child = spawn("ffmpeg", ["-y", "-i", videoPath, "-i", audioPath, "-c", "copy", "-shortest", outPath], {
10318
+ stdio: ["ignore", "ignore", "pipe"]
10319
+ });
10320
+ let stderr = "";
10321
+ child.stderr.on("data", (chunk) => {
10322
+ stderr += String(chunk);
10323
+ });
10324
+ child.on("error", (err) => resolve({ ok: false, error: err.message }));
10325
+ child.on("close", (code) => resolve({ ok: code === 0, error: code === 0 ? null : stderr.trim() || `ffmpeg exited ${code}` }));
10326
+ });
10327
+ }
10328
+ function trackFilename(track, index, shortcode) {
10329
+ const label = shortcode ?? "instagram";
10330
+ const type = track.streamType === "audio" ? "audio" : track.streamType === "video" ? "video" : "track";
10331
+ const bitrate = track.bitrate ? `-${track.bitrate}` : "";
10332
+ return `${label}-${type}-${index}${bitrate}.mp4`;
10333
+ }
10334
+ instagramApp.post("/profile-content", createApiKeyAuth(), async (c) => {
10335
+ const raw = await c.req.json().catch(() => ({}));
10336
+ const parsed = InstagramProfileContentBodySchema.safeParse(raw);
10337
+ if (!parsed.success) return c.json(invalidRequest2(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
10338
+ const body = parsed.data;
10339
+ let target;
10340
+ try {
10341
+ target = normalizeInstagramProfileInput(body.url ?? body.handle ?? "");
10342
+ } catch (err) {
10343
+ return c.json(invalidRequest2(err instanceof Error ? err.message : String(err)), 400);
10344
+ }
10345
+ const user = c.get("user");
10346
+ const gate = await acquireConcurrencyGate(user, "instagram_profile_content", {
10347
+ reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
10348
+ metadata: { handle: target.handle }
10349
+ });
10350
+ if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
10351
+ const driver = new BrowserDriver();
10352
+ let debited = false;
10353
+ try {
10354
+ const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.instagram_profile, LedgerOperation.INSTAGRAM_PROFILE, target.profileUrl);
10355
+ if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.instagram_profile), 402);
10356
+ debited = true;
10357
+ const launch = await resolveInstagramLaunch(body);
10358
+ await driver.launch(launch.config);
10359
+ await driver.navigateTo(target.profileUrl);
10360
+ const page = driver.getPage();
10361
+ const result = await collectInstagramProfileContentFromPage(page, {
10362
+ ...target,
10363
+ maxItems: body.maxItems,
10364
+ maxScrolls: body.maxScrolls,
10365
+ scrollDelayMs: body.scrollDelayMs,
10366
+ stableScrollLimit: body.stableScrollLimit
10367
+ });
10368
+ const response = { ...result, browser: launch.browser };
10369
+ await logRequestEvent({ userId: user.id, source: "instagram_profile_content", status: "done", query: target.profileUrl, resultCount: result.items.length, result: response });
10370
+ return c.json(response);
10371
+ } catch (err) {
10372
+ const msg = err instanceof Error ? err.message : String(err);
10373
+ if (debited) await creditMc(user.id, MC_COSTS.instagram_profile, LedgerOperation.INSTAGRAM_PROFILE_REFUND, "failed call");
10374
+ await logRequestEvent({ userId: user.id, source: "instagram_profile_content", status: "failed", query: target.profileUrl, error: msg });
10375
+ if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) return c.json({ error: msg }, 503);
10376
+ return c.json({ error: msg }, 500);
10377
+ } finally {
10378
+ await driver.close();
10379
+ await releaseConcurrencyGate(gate.lockId);
10380
+ }
10381
+ });
10382
+ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
10383
+ const raw = await c.req.json().catch(() => ({}));
10384
+ const parsed = InstagramMediaDownloadBodySchema.safeParse(raw);
10385
+ if (!parsed.success) return c.json(invalidRequest2(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
10386
+ const body = parsed.data;
10387
+ const urlCheck = await validatePublicHttpUrl(body.url, { field: "url", requireHttps: false });
10388
+ if (urlCheck.error) return c.json(invalidRequest2(urlCheck.error), 400);
10389
+ const sourceUrl = urlCheck.parsed;
10390
+ if (!isAllowedInstagramPageUrl(sourceUrl)) return c.json(invalidRequest2("url must be an Instagram post, reel, or tv URL"), 400);
10391
+ const user = c.get("user");
10392
+ const gate = await acquireConcurrencyGate(user, "instagram_media_download", {
10393
+ reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
10394
+ metadata: { url: sourceUrl.href }
10395
+ });
10396
+ if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
10397
+ const driver = new BrowserDriver();
10398
+ let mediaDebited = false;
10399
+ let transcriptDebited = false;
10400
+ try {
10401
+ const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.instagram_media, LedgerOperation.INSTAGRAM_MEDIA, sourceUrl.href);
10402
+ if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.instagram_media), 402);
10403
+ mediaDebited = true;
10404
+ const launch = await resolveInstagramLaunch(body);
10405
+ await driver.launch(launch.config);
10406
+ const page = driver.getPage();
10407
+ const capturedMediaUrls = [];
10408
+ page.on("response", (response) => {
10409
+ const url = normalizeInstagramMediaUrl(response.url());
10410
+ if (!url) return;
10411
+ const headers = response.headers();
10412
+ const contentType = headers["content-type"] ?? "";
10413
+ if (contentType.includes("video/mp4") || url.includes(".mp4")) capturedMediaUrls.push(url);
10414
+ });
10415
+ await driver.navigateTo(sourceUrl.href);
10416
+ const extraction = await extractInstagramMediaFromPage(page, sourceUrl.href, capturedMediaUrls);
10417
+ const mediaTypes = new Set(body.mediaTypes);
10418
+ const downloads = [];
10419
+ const warnings = [];
10420
+ let outputDir = null;
10421
+ if (body.downloadMedia) {
10422
+ outputDir = mediaOutputDir(extraction.shortcode, sourceUrl.href);
10423
+ const textPath = join4(outputDir, `${extraction.shortcode ?? "instagram"}-text.txt`);
10424
+ writeFileSync(textPath, [
10425
+ `URL: ${extraction.pageUrl}`,
10426
+ extraction.caption ? `Caption: ${extraction.caption}` : "",
10427
+ "",
10428
+ extraction.bodyText
10429
+ ].filter(Boolean).join("\n"), "utf8");
10430
+ downloads.push({ kind: "text", url: null, savedPath: textPath, sizeBytes: statSync(textPath).size, mimeType: "text/plain", error: null });
10431
+ if (mediaTypes.has("image") && extraction.imageUrl) {
10432
+ const imagePathBase = join4(outputDir, `${extraction.shortcode ?? "instagram"}-image`);
10433
+ try {
10434
+ const downloaded = await downloadToFile(extraction.imageUrl, imagePathBase, extraction.pageUrl);
10435
+ const ext = extFromMime(downloaded.mimeType, "jpg");
10436
+ const finalPath = downloaded.savedPath.endsWith(`.${ext}`) ? downloaded.savedPath : `${downloaded.savedPath}.${ext}`;
10437
+ if (finalPath !== downloaded.savedPath) {
10438
+ const { renameSync } = await import("fs");
10439
+ renameSync(downloaded.savedPath, finalPath);
10440
+ }
10441
+ downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: finalPath, sizeBytes: statSync(finalPath).size, mimeType: downloaded.mimeType, error: null });
10442
+ } catch (err) {
10443
+ downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
10444
+ }
10445
+ }
10446
+ const tracksToDownload = body.downloadAllTracks ? extraction.tracks : [extraction.selectedVideoTrack, extraction.selectedAudioTrack].filter((track) => Boolean(track));
10447
+ const savedByUrl = /* @__PURE__ */ new Map();
10448
+ for (const [index, track] of tracksToDownload.entries()) {
10449
+ if (track.streamType === "video" && !mediaTypes.has("video")) continue;
10450
+ if (track.streamType === "audio" && !mediaTypes.has("audio")) continue;
10451
+ const kind = track.streamType === "audio" ? "audio" : "video";
10452
+ const target = join4(outputDir, trackFilename(track, index, extraction.shortcode));
10453
+ try {
10454
+ const downloaded = await downloadToFile(track.url, target, extraction.pageUrl);
10455
+ downloads.push({ kind, url: track.url, savedPath: downloaded.savedPath, sizeBytes: downloaded.sizeBytes, mimeType: downloaded.mimeType, error: null });
10456
+ savedByUrl.set(track.url, downloaded.savedPath);
10457
+ } catch (err) {
10458
+ downloads.push({ kind, url: track.url, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
10459
+ }
10460
+ }
10461
+ if (body.mux && mediaTypes.has("video") && mediaTypes.has("audio") && extraction.selectedVideoTrack && extraction.selectedAudioTrack && savedByUrl.has(extraction.selectedVideoTrack.url) && savedByUrl.has(extraction.selectedAudioTrack.url)) {
10462
+ const outPath = join4(outputDir, `${extraction.shortcode ?? "instagram"}-muxed.mp4`);
10463
+ const muxed = await runFfmpegMux(savedByUrl.get(extraction.selectedVideoTrack.url), savedByUrl.get(extraction.selectedAudioTrack.url), outPath);
10464
+ if (muxed.ok) {
10465
+ downloads.push({ kind: "muxed_video", url: null, savedPath: outPath, sizeBytes: statSync(outPath).size, mimeType: "video/mp4", error: null });
10466
+ } else {
10467
+ warnings.push(`Mux skipped/failed: ${muxed.error}`);
10468
+ }
10469
+ }
10470
+ }
10471
+ let transcript = null;
10472
+ if (body.includeTranscript) {
10473
+ const audioUrl = extraction.selectedAudioTrack?.url ?? extraction.selectedVideoTrack?.url;
10474
+ if (!audioUrl) {
10475
+ warnings.push("Transcript requested, but no audio/video track was available.");
10476
+ } else {
10477
+ const tx = await debitMc(user.id, MC_COSTS.instagram_transcribe, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
10478
+ if (!tx.ok) return c.json(insufficientBalanceResponse(tx.balance_mc, MC_COSTS.instagram_transcribe), 402);
10479
+ transcriptDebited = true;
10480
+ transcript = await transcribeMediaUrl(audioUrl, "# Instagram Media Transcript");
10481
+ }
10482
+ }
10483
+ const result = {
10484
+ ...extraction,
10485
+ browser: launch.browser,
10486
+ outputDir,
10487
+ downloads,
10488
+ warnings,
10489
+ transcript
10490
+ };
10491
+ await logRequestEvent({ userId: user.id, source: "instagram_media_download", status: "done", query: sourceUrl.href, resultCount: downloads.length, result });
10492
+ return c.json(result);
10493
+ } catch (err) {
10494
+ const msg = err instanceof Error ? err.message : String(err);
10495
+ if (transcriptDebited) await creditMc(user.id, MC_COSTS.instagram_transcribe, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "failed call");
10496
+ if (mediaDebited) await creditMc(user.id, MC_COSTS.instagram_media, LedgerOperation.INSTAGRAM_MEDIA_REFUND, "failed call");
10497
+ await logRequestEvent({ userId: user.id, source: "instagram_media_download", status: "failed", query: sourceUrl.href, error: msg });
10498
+ if (msg.toLowerCase().includes("blocked") || msg.toLowerCase().includes("captcha")) return c.json({ error: msg }, 503);
10499
+ return c.json({ error: msg }, 500);
10500
+ } finally {
10501
+ await driver.close();
10502
+ await releaseConcurrencyGate(gate.lockId);
10503
+ }
10504
+ });
10505
+
10506
+ // src/api/maps-routes.ts
10507
+ import { Hono as Hono6 } from "hono";
9768
10508
 
9769
10509
  // src/extractor/MapsNavigator.ts
9770
10510
  var MapsNavigator = class {
@@ -9939,7 +10679,7 @@ var MapsReviewCollector = class {
9939
10679
  };
9940
10680
 
9941
10681
  // src/extractor/MapsExtractor.ts
9942
- import { z as z14 } from "zod";
10682
+ import { z as z15 } from "zod";
9943
10683
  var MapsExtractor = class {
9944
10684
  constructor(driver) {
9945
10685
  this.driver = driver;
@@ -10125,7 +10865,7 @@ var MapsExtractor = class {
10125
10865
  });
10126
10866
  return rows;
10127
10867
  }, { hoursTable: MapsSelectors.hoursTable, hoursTableAlt: MapsSelectors.hoursTableAlt });
10128
- const result = z14.array(RawMapsHoursRowSchema).safeParse(raw);
10868
+ const result = z15.array(RawMapsHoursRowSchema).safeParse(raw);
10129
10869
  if (!result.success) {
10130
10870
  console.warn("[MapsExtractor] hours parse failed", result.error.flatten());
10131
10871
  return [];
@@ -10189,7 +10929,7 @@ var MapsExtractor = class {
10189
10929
  });
10190
10930
  return results;
10191
10931
  });
10192
- const result = z14.array(RawMapsAboutAttributeSchema).safeParse(raw);
10932
+ const result = z15.array(RawMapsAboutAttributeSchema).safeParse(raw);
10193
10933
  if (!result.success) {
10194
10934
  console.warn("[MapsExtractor] about parse failed", result.error.flatten());
10195
10935
  return [];
@@ -10562,7 +11302,7 @@ function mapsErrorResponse(c, err, errorCode) {
10562
11302
  attempts: rotationError?.attempts ?? void 0
10563
11303
  }, retryable ? 503 : 500);
10564
11304
  }
10565
- var mapsApp = new Hono5();
11305
+ var mapsApp = new Hono6();
10566
11306
  mapsApp.post("/search", createApiKeyAuth(), async (c) => {
10567
11307
  const user = c.get("user");
10568
11308
  const body = await c.req.json().catch(() => ({}));
@@ -10700,12 +11440,12 @@ mapsApp.post("/place", createApiKeyAuth(), async (c) => {
10700
11440
  });
10701
11441
 
10702
11442
  // src/api/directory-routes.ts
10703
- import { Hono as Hono6 } from "hono";
11443
+ import { Hono as Hono7 } from "hono";
10704
11444
 
10705
11445
  // src/directory/directory-workflow.ts
10706
11446
  import { mkdir as mkdir2, writeFile } from "fs/promises";
10707
- import { join as join4 } from "path";
10708
- import { z as z15 } from "zod";
11447
+ import { join as join5 } from "path";
11448
+ import { z as z16 } from "zod";
10709
11449
 
10710
11450
  // src/directory/location-db.ts
10711
11451
  import { access, readFile } from "fs/promises";
@@ -10866,24 +11606,24 @@ async function resolveDirectoryMarkets(options) {
10866
11606
  }
10867
11607
 
10868
11608
  // src/directory/directory-workflow.ts
10869
- var DirectoryWorkflowOptionsSchema = z15.object({
10870
- query: z15.string().min(1),
10871
- state: z15.string().min(2).default("TN"),
10872
- minPopulation: z15.number().int().min(0).default(1e5),
10873
- populationYear: z15.union(POPULATION_YEARS.map((year) => z15.literal(year))).default(2025),
10874
- maxCities: z15.number().int().min(1).max(100).default(25),
10875
- maxResultsPerCity: z15.number().int().min(1).max(50).default(50),
10876
- concurrency: z15.number().int().min(1).max(5).default(5),
10877
- includeZipGroups: z15.boolean().default(true),
10878
- usZipsCsvPath: z15.string().optional(),
10879
- saveCsv: z15.boolean().default(true),
10880
- gl: z15.string().length(2).default("us"),
10881
- hl: z15.string().length(2).default("en"),
10882
- proxyMode: z15.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
10883
- proxyZip: z15.string().regex(/^\d{5}$/).optional(),
10884
- debug: z15.boolean().default(false),
10885
- headless: z15.boolean().default(true),
10886
- kernelApiKey: z15.string().optional()
11609
+ var DirectoryWorkflowOptionsSchema = z16.object({
11610
+ query: z16.string().min(1),
11611
+ state: z16.string().min(2).default("TN"),
11612
+ minPopulation: z16.number().int().min(0).default(1e5),
11613
+ populationYear: z16.union(POPULATION_YEARS.map((year) => z16.literal(year))).default(2025),
11614
+ maxCities: z16.number().int().min(1).max(100).default(25),
11615
+ maxResultsPerCity: z16.number().int().min(1).max(50).default(50),
11616
+ concurrency: z16.number().int().min(1).max(5).default(5),
11617
+ includeZipGroups: z16.boolean().default(true),
11618
+ usZipsCsvPath: z16.string().optional(),
11619
+ saveCsv: z16.boolean().default(true),
11620
+ gl: z16.string().length(2).default("us"),
11621
+ hl: z16.string().length(2).default("en"),
11622
+ proxyMode: z16.enum(["location", "configured", "none"]).default(DEFAULT_MAPS_PROXY_MODE),
11623
+ proxyZip: z16.string().regex(/^\d{5}$/).optional(),
11624
+ debug: z16.boolean().default(false),
11625
+ headless: z16.boolean().default(true),
11626
+ kernelApiKey: z16.string().optional()
10887
11627
  });
10888
11628
  function errorMessage(err) {
10889
11629
  return err instanceof Error ? err.message : String(err);
@@ -11040,11 +11780,11 @@ function csvRowsFor(result) {
11040
11780
  return rows;
11041
11781
  }
11042
11782
  async function saveDirectoryCsv(result) {
11043
- const outDir = join4(outputBaseDir(), "directory-workflows");
11783
+ const outDir = join5(outputBaseDir(), "directory-workflows");
11044
11784
  await mkdir2(outDir, { recursive: true });
11045
11785
  const stamp = result.extractedAt.replace(/[:.]/g, "-");
11046
11786
  const slug = `${result.state}-${result.query}`.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
11047
- const path5 = join4(outDir, `${stamp}-${slug}-directory-workflow.csv`);
11787
+ const path5 = join5(outDir, `${stamp}-${slug}-directory-workflow.csv`);
11048
11788
  const headers = [
11049
11789
  "source_query",
11050
11790
  "source_location",
@@ -11104,7 +11844,7 @@ async function runDirectoryWorkflowFromPlan(options, plan) {
11104
11844
  }
11105
11845
 
11106
11846
  // src/api/directory-routes.ts
11107
- var directoryApp = new Hono6();
11847
+ var directoryApp = new Hono7();
11108
11848
  directoryApp.post("/run", createApiKeyAuth(), async (c) => {
11109
11849
  const user = c.get("user");
11110
11850
  const body = await c.req.json().catch(() => ({}));
@@ -11177,35 +11917,35 @@ directoryApp.post("/run", createApiKeyAuth(), async (c) => {
11177
11917
  // src/api/workflow-routes.ts
11178
11918
  import { createHmac } from "crypto";
11179
11919
  import { readFile as readFile2 } from "fs/promises";
11180
- import { Hono as Hono7 } from "hono";
11181
- import { z as z16 } from "zod";
11182
- var workflowApp = new Hono7();
11183
- var WorkflowInputSchema = z16.record(z16.unknown()).default({});
11184
- var WorkflowIdSchema = z16.string().min(1);
11185
- var CadenceSchema = z16.enum(["daily", "weekly", "monthly"]);
11186
- var ScheduleStatusSchema = z16.enum(["active", "paused"]);
11187
- var RunBodySchema = z16.object({
11920
+ import { Hono as Hono8 } from "hono";
11921
+ import { z as z17 } from "zod";
11922
+ var workflowApp = new Hono8();
11923
+ var WorkflowInputSchema = z17.record(z17.unknown()).default({});
11924
+ var WorkflowIdSchema = z17.string().min(1);
11925
+ var CadenceSchema = z17.enum(["daily", "weekly", "monthly"]);
11926
+ var ScheduleStatusSchema = z17.enum(["active", "paused"]);
11927
+ var RunBodySchema = z17.object({
11188
11928
  workflowId: WorkflowIdSchema,
11189
11929
  input: WorkflowInputSchema,
11190
- webhookUrl: z16.string().url().optional()
11930
+ webhookUrl: z17.string().url().optional()
11191
11931
  });
11192
- var ScheduleCreateSchema = z16.object({
11932
+ var ScheduleCreateSchema = z17.object({
11193
11933
  workflowId: WorkflowIdSchema,
11194
- name: z16.string().min(1).max(120).optional(),
11934
+ name: z17.string().min(1).max(120).optional(),
11195
11935
  input: WorkflowInputSchema,
11196
11936
  cadence: CadenceSchema.default("weekly"),
11197
- timezone: z16.string().min(1).max(64).default("UTC"),
11198
- webhookUrl: z16.string().url().optional(),
11199
- nextRunAt: z16.string().datetime().optional()
11937
+ timezone: z17.string().min(1).max(64).default("UTC"),
11938
+ webhookUrl: z17.string().url().optional(),
11939
+ nextRunAt: z17.string().datetime().optional()
11200
11940
  });
11201
- var SchedulePatchSchema = z16.object({
11202
- name: z16.string().min(1).max(120).optional(),
11941
+ var SchedulePatchSchema = z17.object({
11942
+ name: z17.string().min(1).max(120).optional(),
11203
11943
  status: ScheduleStatusSchema.optional(),
11204
11944
  input: WorkflowInputSchema.optional(),
11205
11945
  cadence: CadenceSchema.optional(),
11206
- timezone: z16.string().min(1).max(64).optional(),
11207
- webhookUrl: z16.string().url().nullable().optional(),
11208
- nextRunAt: z16.string().datetime().nullable().optional()
11946
+ timezone: z17.string().min(1).max(64).optional(),
11947
+ webhookUrl: z17.string().url().nullable().optional(),
11948
+ nextRunAt: z17.string().datetime().nullable().optional()
11209
11949
  });
11210
11950
  function hostedWorkflowOutputDir() {
11211
11951
  return workflowOutputBaseDir(process.env.MCP_SCRAPER_WORKFLOW_OUTPUT_DIR?.trim() || "/tmp/mcp-scraper-workflows");
@@ -11709,7 +12449,7 @@ workflowApp.post("/cron/dispatch", async (c) => {
11709
12449
  });
11710
12450
 
11711
12451
  // src/api/serp-intelligence-routes.ts
11712
- import { Hono as Hono8 } from "hono";
12452
+ import { Hono as Hono9 } from "hono";
11713
12453
 
11714
12454
  // src/serp-intelligence/page-snapshot-extractor.ts
11715
12455
  import { createHash } from "crypto";
@@ -12021,7 +12761,7 @@ async function capturePageSnapshots(targets, options = {}) {
12021
12761
  }
12022
12762
 
12023
12763
  // src/serp-intelligence/schemas.ts
12024
- import { z as z17 } from "zod";
12764
+ import { z as z18 } from "zod";
12025
12765
  var SerpIntelligenceDeviceValues = ["desktop", "mobile"];
12026
12766
  var SerpIntelligenceProxyModeValues = ["location", "configured", "none"];
12027
12767
  var SerpIntelligenceAttemptOutcomeValues = [
@@ -12083,171 +12823,171 @@ function isPublicHttpUrl(value) {
12083
12823
  return false;
12084
12824
  }
12085
12825
  }
12086
- var SerpIntelligencePublicHttpUrlSchema = z17.string().url().refine(isPublicHttpUrl, "url must be a public HTTP or HTTPS URL");
12087
- var SerpIntelligenceCaptureBodySchema = z17.object({
12088
- query: z17.string().trim().min(1, "query is required"),
12089
- location: z17.string().trim().min(1).optional(),
12090
- gl: z17.string().trim().length(2).default("us"),
12091
- hl: z17.string().trim().length(2).default("en"),
12092
- device: z17.enum(SerpIntelligenceDeviceValues).default("desktop"),
12093
- proxyMode: z17.enum(SerpIntelligenceProxyModeValues).default(DEFAULT_PROXY_MODE),
12094
- proxyZip: z17.string().regex(/^\d{5}$/).optional(),
12095
- pages: z17.number().int().min(1).max(2).default(1),
12096
- debug: z17.boolean().default(false),
12097
- includePageSnapshots: z17.boolean().default(false),
12098
- pageSnapshotLimit: z17.number().int().min(0).max(10).default(0)
12826
+ var SerpIntelligencePublicHttpUrlSchema = z18.string().url().refine(isPublicHttpUrl, "url must be a public HTTP or HTTPS URL");
12827
+ var SerpIntelligenceCaptureBodySchema = z18.object({
12828
+ query: z18.string().trim().min(1, "query is required"),
12829
+ location: z18.string().trim().min(1).optional(),
12830
+ gl: z18.string().trim().length(2).default("us"),
12831
+ hl: z18.string().trim().length(2).default("en"),
12832
+ device: z18.enum(SerpIntelligenceDeviceValues).default("desktop"),
12833
+ proxyMode: z18.enum(SerpIntelligenceProxyModeValues).default(DEFAULT_PROXY_MODE),
12834
+ proxyZip: z18.string().regex(/^\d{5}$/).optional(),
12835
+ pages: z18.number().int().min(1).max(2).default(1),
12836
+ debug: z18.boolean().default(false),
12837
+ includePageSnapshots: z18.boolean().default(false),
12838
+ pageSnapshotLimit: z18.number().int().min(0).max(10).default(0)
12099
12839
  }).strict();
12100
- var SerpIntelligencePageSnapshotRequestSchema = z17.object({
12840
+ var SerpIntelligencePageSnapshotRequestSchema = z18.object({
12101
12841
  url: SerpIntelligencePublicHttpUrlSchema,
12102
- sourceKind: z17.enum(SerpPageSnapshotSourceKindValues).default("configured_target"),
12103
- sourcePosition: z17.number().int().min(1).optional()
12842
+ sourceKind: z18.enum(SerpPageSnapshotSourceKindValues).default("configured_target"),
12843
+ sourcePosition: z18.number().int().min(1).optional()
12104
12844
  }).strict();
12105
- var SerpIntelligencePageSnapshotsBodySchema = z17.object({
12106
- urls: z17.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25),
12107
- targets: z17.array(SerpIntelligencePageSnapshotRequestSchema).min(1).max(25).optional(),
12108
- maxConcurrency: z17.number().int().min(1).max(5).default(2),
12109
- timeoutMs: z17.number().int().min(1e3).max(6e4).default(15e3),
12110
- debug: z17.boolean().default(false)
12845
+ var SerpIntelligencePageSnapshotsBodySchema = z18.object({
12846
+ urls: z18.array(SerpIntelligencePublicHttpUrlSchema).min(1).max(25),
12847
+ targets: z18.array(SerpIntelligencePageSnapshotRequestSchema).min(1).max(25).optional(),
12848
+ maxConcurrency: z18.number().int().min(1).max(5).default(2),
12849
+ timeoutMs: z18.number().int().min(1e3).max(6e4).default(15e3),
12850
+ debug: z18.boolean().default(false)
12111
12851
  }).strict();
12112
- var SerpIntelligenceAICitationSchema = z17.object({
12113
- text: z17.string(),
12114
- href: z17.string()
12852
+ var SerpIntelligenceAICitationSchema = z18.object({
12853
+ text: z18.string(),
12854
+ href: z18.string()
12115
12855
  }).strict();
12116
- var SerpIntelligenceOrganicResultSchema = z17.object({
12117
- position: z17.number().int().min(1),
12118
- title: z17.string(),
12119
- url: z17.string(),
12120
- domain: z17.string(),
12121
- cite: z17.string().nullable(),
12122
- snippet: z17.string().nullable(),
12123
- isRedditStyle: z17.boolean(),
12124
- inlineRating: z17.object({
12125
- value: z17.string(),
12126
- count: z17.string()
12856
+ var SerpIntelligenceOrganicResultSchema = z18.object({
12857
+ position: z18.number().int().min(1),
12858
+ title: z18.string(),
12859
+ url: z18.string(),
12860
+ domain: z18.string(),
12861
+ cite: z18.string().nullable(),
12862
+ snippet: z18.string().nullable(),
12863
+ isRedditStyle: z18.boolean(),
12864
+ inlineRating: z18.object({
12865
+ value: z18.string(),
12866
+ count: z18.string()
12127
12867
  }).strict().nullable()
12128
12868
  }).strict();
12129
- var SerpIntelligenceLocationEvidenceSchema = z17.object({
12130
- status: z17.enum(SerpIntelligenceLocalizationStatusValues),
12131
- expected: z17.object({
12132
- city: z17.string(),
12133
- regionCode: z17.string().nullable(),
12134
- canonicalLocation: z17.string()
12869
+ var SerpIntelligenceLocationEvidenceSchema = z18.object({
12870
+ status: z18.enum(SerpIntelligenceLocalizationStatusValues),
12871
+ expected: z18.object({
12872
+ city: z18.string(),
12873
+ regionCode: z18.string().nullable(),
12874
+ canonicalLocation: z18.string()
12135
12875
  }).strict().nullable(),
12136
- candidates: z17.array(z17.object({
12137
- city: z17.string(),
12138
- regionCode: z17.string(),
12139
- count: z17.number().int().min(0),
12140
- examples: z17.array(z17.string())
12876
+ candidates: z18.array(z18.object({
12877
+ city: z18.string(),
12878
+ regionCode: z18.string(),
12879
+ count: z18.number().int().min(0),
12880
+ examples: z18.array(z18.string())
12141
12881
  }).strict())
12142
12882
  }).strict();
12143
- var SerpIntelligenceHarvestResultSchema = z17.object({
12144
- seed: z17.string(),
12145
- location: z17.string().nullable(),
12146
- extractedAt: z17.string(),
12147
- totalQuestions: z17.number().int().min(0),
12148
- surface: z17.enum(["web", "aim", "unknown"]),
12149
- aiOverview: z17.object({
12150
- detected: z17.boolean(),
12151
- text: z17.string().nullable(),
12152
- citations: z17.array(SerpIntelligenceAICitationSchema),
12153
- expanded: z17.boolean().optional(),
12154
- fullyExpanded: z17.boolean().optional(),
12155
- sections: z17.array(z17.string()).optional()
12883
+ var SerpIntelligenceHarvestResultSchema = z18.object({
12884
+ seed: z18.string(),
12885
+ location: z18.string().nullable(),
12886
+ extractedAt: z18.string(),
12887
+ totalQuestions: z18.number().int().min(0),
12888
+ surface: z18.enum(["web", "aim", "unknown"]),
12889
+ aiOverview: z18.object({
12890
+ detected: z18.boolean(),
12891
+ text: z18.string().nullable(),
12892
+ citations: z18.array(SerpIntelligenceAICitationSchema),
12893
+ expanded: z18.boolean().optional(),
12894
+ fullyExpanded: z18.boolean().optional(),
12895
+ sections: z18.array(z18.string()).optional()
12156
12896
  }).strict(),
12157
- aiMode: z17.object({
12158
- detected: z17.boolean(),
12159
- text: z17.string().nullable(),
12160
- citations: z17.array(SerpIntelligenceAICitationSchema)
12897
+ aiMode: z18.object({
12898
+ detected: z18.boolean(),
12899
+ text: z18.string().nullable(),
12900
+ citations: z18.array(SerpIntelligenceAICitationSchema)
12161
12901
  }).strict(),
12162
- tree: z17.array(z17.unknown()),
12163
- flat: z17.array(z17.unknown()),
12164
- videos: z17.array(z17.unknown()),
12165
- forums: z17.array(z17.unknown()),
12166
- organicResults: z17.array(SerpIntelligenceOrganicResultSchema),
12167
- localPack: z17.array(z17.unknown()),
12168
- entityIds: z17.object({
12169
- entities: z17.array(z17.object({
12170
- name: z17.string(),
12171
- kgId: z17.string().nullable(),
12172
- cid: z17.string().nullable(),
12173
- gcid: z17.string().nullable()
12902
+ tree: z18.array(z18.unknown()),
12903
+ flat: z18.array(z18.unknown()),
12904
+ videos: z18.array(z18.unknown()),
12905
+ forums: z18.array(z18.unknown()),
12906
+ organicResults: z18.array(SerpIntelligenceOrganicResultSchema),
12907
+ localPack: z18.array(z18.unknown()),
12908
+ entityIds: z18.object({
12909
+ entities: z18.array(z18.object({
12910
+ name: z18.string(),
12911
+ kgId: z18.string().nullable(),
12912
+ cid: z18.string().nullable(),
12913
+ gcid: z18.string().nullable()
12174
12914
  }).strict()),
12175
- kgIds: z17.array(z17.string()),
12176
- cids: z17.array(z17.string()),
12177
- gcids: z17.array(z17.string())
12915
+ kgIds: z18.array(z18.string()),
12916
+ cids: z18.array(z18.string()),
12917
+ gcids: z18.array(z18.string())
12178
12918
  }).strict(),
12179
- stats: z17.object({
12180
- seed: z17.string(),
12181
- totalQuestions: z17.number().int().min(0),
12182
- maxDepthReached: z17.number().int().min(0),
12183
- durationMs: z17.number().min(0),
12184
- errorCount: z17.number().int().min(0)
12919
+ stats: z18.object({
12920
+ seed: z18.string(),
12921
+ totalQuestions: z18.number().int().min(0),
12922
+ maxDepthReached: z18.number().int().min(0),
12923
+ durationMs: z18.number().min(0),
12924
+ errorCount: z18.number().int().min(0)
12185
12925
  }).strict(),
12186
- diagnostics: z17.object({
12187
- completionStatus: z17.enum(["paa_found", "no_paa", "serp_only"]),
12188
- problem: z17.null(),
12189
- warnings: z17.array(z17.unknown()).optional(),
12190
- debug: z17.object({
12926
+ diagnostics: z18.object({
12927
+ completionStatus: z18.enum(["paa_found", "no_paa", "serp_only"]),
12928
+ problem: z18.null(),
12929
+ warnings: z18.array(z18.unknown()).optional(),
12930
+ debug: z18.object({
12191
12931
  locationEvidence: SerpIntelligenceLocationEvidenceSchema.optional()
12192
12932
  }).passthrough().optional()
12193
12933
  }).passthrough(),
12194
- whatPeopleSaying: z17.array(z17.unknown())
12934
+ whatPeopleSaying: z18.array(z18.unknown())
12195
12935
  }).strict();
12196
- var SerpIntelligenceCaptureAttemptSchema = z17.object({
12197
- attemptNumber: z17.number().int().min(1),
12198
- outcome: z17.enum(SerpIntelligenceAttemptOutcomeValues),
12199
- startedAt: z17.string().optional(),
12200
- completedAt: z17.string().optional(),
12201
- durationMs: z17.number().min(0).optional(),
12202
- problemCode: z17.string().optional(),
12203
- message: z17.string().optional(),
12204
- kernelSessionId: z17.string().nullable().optional(),
12205
- cleanupSucceeded: z17.boolean().nullable().optional()
12936
+ var SerpIntelligenceCaptureAttemptSchema = z18.object({
12937
+ attemptNumber: z18.number().int().min(1),
12938
+ outcome: z18.enum(SerpIntelligenceAttemptOutcomeValues),
12939
+ startedAt: z18.string().optional(),
12940
+ completedAt: z18.string().optional(),
12941
+ durationMs: z18.number().min(0).optional(),
12942
+ problemCode: z18.string().optional(),
12943
+ message: z18.string().optional(),
12944
+ kernelSessionId: z18.string().nullable().optional(),
12945
+ cleanupSucceeded: z18.boolean().nullable().optional()
12206
12946
  }).strict();
12207
- var SerpPageSnapshotCaptureSchema = z17.object({
12947
+ var SerpPageSnapshotCaptureSchema = z18.object({
12208
12948
  url: SerpIntelligencePublicHttpUrlSchema,
12209
12949
  requestedUrl: SerpIntelligencePublicHttpUrlSchema,
12210
12950
  finalUrl: SerpIntelligencePublicHttpUrlSchema.nullable(),
12211
- sourceKind: z17.enum(SerpPageSnapshotSourceKindValues),
12212
- sourcePosition: z17.number().int().min(1).nullable(),
12213
- status: z17.enum(SerpPageFetchStatusValues),
12214
- fetchedVia: z17.enum(SerpPageFetchedViaValues).nullable(),
12215
- httpStatus: z17.number().int().min(100).max(599).nullable(),
12216
- contentType: z17.string().nullable(),
12217
- title: z17.string().nullable(),
12951
+ sourceKind: z18.enum(SerpPageSnapshotSourceKindValues),
12952
+ sourcePosition: z18.number().int().min(1).nullable(),
12953
+ status: z18.enum(SerpPageFetchStatusValues),
12954
+ fetchedVia: z18.enum(SerpPageFetchedViaValues).nullable(),
12955
+ httpStatus: z18.number().int().min(100).max(599).nullable(),
12956
+ contentType: z18.string().nullable(),
12957
+ title: z18.string().nullable(),
12218
12958
  canonicalUrl: SerpIntelligencePublicHttpUrlSchema.nullable(),
12219
- metaDescription: z17.string().nullable(),
12220
- headings: z17.array(z17.object({
12221
- level: z17.number().int().min(1).max(6),
12222
- text: z17.string()
12959
+ metaDescription: z18.string().nullable(),
12960
+ headings: z18.array(z18.object({
12961
+ level: z18.number().int().min(1).max(6),
12962
+ text: z18.string()
12223
12963
  }).strict()).default([]),
12224
- artifact: z17.object({
12225
- htmlBlobUrl: z17.string().url().nullable(),
12226
- textBlobUrl: z17.string().url().nullable(),
12227
- markdownBlobUrl: z17.string().url().nullable(),
12228
- screenshotBlobUrl: z17.string().url().nullable(),
12229
- contentSha256: z17.string().nullable(),
12230
- capturedAt: z17.string().nullable()
12964
+ artifact: z18.object({
12965
+ htmlBlobUrl: z18.string().url().nullable(),
12966
+ textBlobUrl: z18.string().url().nullable(),
12967
+ markdownBlobUrl: z18.string().url().nullable(),
12968
+ screenshotBlobUrl: z18.string().url().nullable(),
12969
+ contentSha256: z18.string().nullable(),
12970
+ capturedAt: z18.string().nullable()
12231
12971
  }).strict(),
12232
- error: z17.object({
12233
- code: z17.string(),
12234
- message: z17.string()
12972
+ error: z18.object({
12973
+ code: z18.string(),
12974
+ message: z18.string()
12235
12975
  }).strict().nullable()
12236
12976
  }).strict();
12237
- var SerpIntelligenceCaptureResponseSchema = z17.object({
12977
+ var SerpIntelligenceCaptureResponseSchema = z18.object({
12238
12978
  harvestResult: SerpIntelligenceHarvestResultSchema,
12239
- attempts: z17.array(SerpIntelligenceCaptureAttemptSchema),
12979
+ attempts: z18.array(SerpIntelligenceCaptureAttemptSchema),
12240
12980
  locationEvidence: SerpIntelligenceLocationEvidenceSchema.nullable(),
12241
- pageSnapshotArtifacts: z17.array(SerpPageSnapshotCaptureSchema),
12242
- billing: z17.object({
12243
- creditsUsed: z17.number().min(0).optional(),
12244
- requestId: z17.string().optional(),
12245
- jobId: z17.string().optional()
12981
+ pageSnapshotArtifacts: z18.array(SerpPageSnapshotCaptureSchema),
12982
+ billing: z18.object({
12983
+ creditsUsed: z18.number().min(0).optional(),
12984
+ requestId: z18.string().optional(),
12985
+ jobId: z18.string().optional()
12246
12986
  }).strict().optional()
12247
12987
  }).strict();
12248
- var SerpIntelligencePageSnapshotsResponseSchema = z17.object({
12249
- pageSnapshotArtifacts: z17.array(SerpPageSnapshotCaptureSchema),
12250
- attempts: z17.array(SerpIntelligenceCaptureAttemptSchema).default([])
12988
+ var SerpIntelligencePageSnapshotsResponseSchema = z18.object({
12989
+ pageSnapshotArtifacts: z18.array(SerpPageSnapshotCaptureSchema),
12990
+ attempts: z18.array(SerpIntelligenceCaptureAttemptSchema).default([])
12251
12991
  }).strict();
12252
12992
 
12253
12993
  // src/serp-intelligence/serp-capture-service.ts
@@ -12419,7 +13159,7 @@ var SERP_INTELLIGENCE_RATE_LIMIT = 60;
12419
13159
  var SERP_INTELLIGENCE_RATE_WINDOW_SECONDS = 60;
12420
13160
  var POST_CAPTURE_ROUTE_LABEL = "POST /capture";
12421
13161
  var POST_PAGE_SNAPSHOTS_ROUTE_LABEL = "POST /page-snapshots";
12422
- var serpIntelligenceApp = new Hono8();
13162
+ var serpIntelligenceApp = new Hono9();
12423
13163
  serpIntelligenceApp.use("*", createApiKeyAuth());
12424
13164
  function structuredError(input) {
12425
13165
  return {
@@ -12588,7 +13328,7 @@ serpIntelligenceApp.post("/page-snapshots", async (c) => {
12588
13328
  });
12589
13329
 
12590
13330
  // src/mcp/mcp-routes.ts
12591
- import { Hono as Hono9 } from "hono";
13331
+ import { Hono as Hono10 } from "hono";
12592
13332
  import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
12593
13333
  configureReportSaving(false);
12594
13334
  function mcpAuthError() {
@@ -12618,7 +13358,7 @@ async function requireMcpCallerKey(c) {
12618
13358
  if (!user) return mcpAuthError();
12619
13359
  return callerKey;
12620
13360
  }
12621
- var mcpApp = new Hono9();
13361
+ var mcpApp = new Hono10();
12622
13362
  function registerSerpIntelligenceCaptureTools(server, executor) {
12623
13363
  server.registerTool("capture_serp_snapshot", {
12624
13364
  title: "SERP Intelligence Snapshot",
@@ -12656,7 +13396,7 @@ mcpApp.all("/", async (c) => {
12656
13396
  });
12657
13397
 
12658
13398
  // src/api/browser-agent-routes.ts
12659
- import { Hono as Hono10 } from "hono";
13399
+ import { Hono as Hono11 } from "hono";
12660
13400
 
12661
13401
  // src/api/browser-agent-db.ts
12662
13402
  import { randomUUID as randomUUID2 } from "crypto";
@@ -13224,7 +13964,7 @@ async function loadOpenSession(id, userId) {
13224
13964
  return row;
13225
13965
  }
13226
13966
  function buildBrowserAgentRoutes() {
13227
- const app2 = new Hono10();
13967
+ const app2 = new Hono11();
13228
13968
  app2.use("*", async (c, next) => {
13229
13969
  await migrateBrowserAgent();
13230
13970
  return next();
@@ -13781,9 +14521,9 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
13781
14521
 
13782
14522
  // src/api/stripe-routes.ts
13783
14523
  import Stripe from "stripe";
13784
- import { Hono as Hono11 } from "hono";
14524
+ import { Hono as Hono12 } from "hono";
13785
14525
  var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
13786
- var stripeApp = new Hono11();
14526
+ var stripeApp = new Hono12();
13787
14527
  stripeApp.post("/webhooks", async (c) => {
13788
14528
  const sig = c.req.header("stripe-signature");
13789
14529
  const body = await c.req.text();
@@ -13878,27 +14618,27 @@ import { getCookie, setCookie, deleteCookie } from "hono/cookie";
13878
14618
  import Stripe2 from "stripe";
13879
14619
 
13880
14620
  // src/api/billing-schemas.ts
13881
- import { z as z18 } from "zod";
13882
- var BillingCheckoutBodySchema = z18.object({
13883
- priceId: z18.string().min(1)
13884
- });
13885
- var FreeCreditBreakdownSchema = z18.object({
13886
- signup_grant_mc: z18.number().int().nonnegative(),
13887
- monthly_refresh_mc: z18.number().int().nonnegative(),
13888
- total_free_mc: z18.number().int().nonnegative(),
13889
- signup_grant_credits: z18.number().nonnegative(),
13890
- monthly_refresh_credits: z18.number().nonnegative(),
13891
- total_free_credits: z18.number().nonnegative()
13892
- });
13893
- var BillingBalanceResponseSchema = z18.object({
13894
- balance_mc: z18.number().int().nonnegative(),
13895
- balance_credits: z18.number().nonnegative(),
14621
+ import { z as z19 } from "zod";
14622
+ var BillingCheckoutBodySchema = z19.object({
14623
+ priceId: z19.string().min(1)
14624
+ });
14625
+ var FreeCreditBreakdownSchema = z19.object({
14626
+ signup_grant_mc: z19.number().int().nonnegative(),
14627
+ monthly_refresh_mc: z19.number().int().nonnegative(),
14628
+ total_free_mc: z19.number().int().nonnegative(),
14629
+ signup_grant_credits: z19.number().nonnegative(),
14630
+ monthly_refresh_credits: z19.number().nonnegative(),
14631
+ total_free_credits: z19.number().nonnegative()
14632
+ });
14633
+ var BillingBalanceResponseSchema = z19.object({
14634
+ balance_mc: z19.number().int().nonnegative(),
14635
+ balance_credits: z19.number().nonnegative(),
13896
14636
  free_credits: FreeCreditBreakdownSchema,
13897
- ledger: z18.array(z18.any())
14637
+ ledger: z19.array(z19.any())
13898
14638
  });
13899
- var MonthlyRefreshSweepResultSchema = z18.object({
13900
- usersRefreshed: z18.number().int().nonnegative(),
13901
- totalMcGranted: z18.number().int().nonnegative()
14639
+ var MonthlyRefreshSweepResultSchema = z19.object({
14640
+ usersRefreshed: z19.number().int().nonnegative(),
14641
+ totalMcGranted: z19.number().int().nonnegative()
13902
14642
  });
13903
14643
 
13904
14644
  // src/api/credit-operations.ts
@@ -14109,7 +14849,7 @@ var sessionAuth = createMiddleware3(async (c, next) => {
14109
14849
  c.set("sessionUser", { ...refreshed, balance_mc: balanceMc });
14110
14850
  return next();
14111
14851
  });
14112
- var app = new Hono12();
14852
+ var app = new Hono13();
14113
14853
  var STRIPE_API_VERSION = "2026-02-25.clover";
14114
14854
  function requireStripeSecret() {
14115
14855
  const secret2 = process.env.STRIPE_SECRET_KEY?.trim();
@@ -14547,7 +15287,10 @@ app.post("/extract-url", auth2, async (c) => {
14547
15287
  const device = screenshotDevice === "mobile" ? "mobile" : "desktop";
14548
15288
  const [result, pageData] = await Promise.all([
14549
15289
  extractKpo({ url: canonicalUrl, kernelApiKey }),
14550
- screenshot2 || extractBranding ? capturePageData(canonicalUrl, { kernelApiKey, device, screenshot: !!screenshot2, branding: !!extractBranding }).catch(() => null) : null
15290
+ screenshot2 || extractBranding ? capturePageData(canonicalUrl, { kernelApiKey, device, screenshot: !!screenshot2, branding: !!extractBranding }).catch((err) => {
15291
+ console.error("[extract-url] capturePageData failed:", err instanceof Error ? err.message : err);
15292
+ return null;
15293
+ }) : null
14551
15294
  ]);
14552
15295
  const screenshotBuf = pageData?.screenshot ?? null;
14553
15296
  const brandingData = pageData?.branding ?? null;
@@ -14555,7 +15298,7 @@ app.post("/extract-url", auth2, async (c) => {
14555
15298
  if (screenshotBuf) {
14556
15299
  screenshotMeta = { base64: screenshotBuf.toString("base64"), sizeBytes: screenshotBuf.length, device };
14557
15300
  }
14558
- const mediaMeta = downloadMedia ? await harvestPageMedia(result.bodyHtml, canonicalUrl, { types: mediaTypes ?? ["image", "video", "audio"] }) : null;
15301
+ const mediaMeta = downloadMedia ? await harvestPageMedia(result.bodyHtml, canonicalUrl, { types: mediaTypes ?? ["image", "video", "audio"], outputDir: null }) : null;
14559
15302
  await logRequestEvent({ userId: user.id, source: "extract_url", status: "done", query: canonicalUrl, resultCount: result.headings.length, result });
14560
15303
  return c.json({ ...result, screenshot: screenshotMeta, branding: brandingData, media: mediaMeta });
14561
15304
  } catch (err) {
@@ -14849,7 +15592,7 @@ app.get("/cron/tick", async (c) => {
14849
15592
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
14850
15593
  return c.json({ error: "Unauthorized" }, 401);
14851
15594
  }
14852
- const { drainQueue } = await import("./worker-SLQ375UG.js");
15595
+ const { drainQueue } = await import("./worker-56IXWOQU.js");
14853
15596
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
14854
15597
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
14855
15598
  const [results, sweepResult] = await Promise.all([
@@ -14863,6 +15606,7 @@ app.route("/api/internal/site-architecture-auditor", siteAuditApp);
14863
15606
  app.route("/youtube", youtubeApp);
14864
15607
  app.route("/screenshot", screenshotApp);
14865
15608
  app.route("/facebook", facebookAdApp);
15609
+ app.route("/instagram", instagramApp);
14866
15610
  app.route("/maps", mapsApp);
14867
15611
  app.route("/directory", directoryApp);
14868
15612
  app.route("/workflows", workflowApp);
@@ -14978,4 +15722,4 @@ app.get("/blog/:slug/", (c) => {
14978
15722
  export {
14979
15723
  app
14980
15724
  };
14981
- //# sourceMappingURL=server-MOP7NIUK.js.map
15725
+ //# sourceMappingURL=server-N5LM3PEO.js.map