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.
- package/README.md +4 -1
- package/dist/bin/api-server.cjs +2047 -831
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/browser-agent-stdio-server.cjs +3 -3
- package/dist/bin/browser-agent-stdio-server.cjs.map +1 -1
- package/dist/bin/browser-agent-stdio-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs +350 -5
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +4 -4
- package/dist/bin/mcp-scraper-install.cjs +3 -3
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +346 -1
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/bin/paa-harvest.cjs +3 -1
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +1 -1
- package/dist/{chunk-SXTXMFEQ.js → chunk-AZ5PKU4F.js} +34 -1
- package/dist/chunk-AZ5PKU4F.js.map +1 -0
- package/dist/{chunk-Q4STSM63.js → chunk-C3FGVJWH.js} +3 -3
- package/dist/chunk-C3FGVJWH.js.map +1 -0
- package/dist/{chunk-DH4H3F6S.js → chunk-HRTCMLZB.js} +4 -4
- package/dist/chunk-HRTCMLZB.js.map +1 -0
- package/dist/{chunk-SIXJ6EVS.js → chunk-KO223PSJ.js} +347 -2
- package/dist/chunk-KO223PSJ.js.map +1 -0
- package/dist/{chunk-IPW4LFOT.js → chunk-UWSG3C5J.js} +4 -2
- package/dist/chunk-UWSG3C5J.js.map +1 -0
- package/dist/chunk-VB65G7PS.js +7 -0
- package/dist/chunk-VB65G7PS.js.map +1 -0
- package/dist/index.cjs +3 -1
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +1 -1
- package/dist/{server-MOP7NIUK.js → server-N5LM3PEO.js} +997 -253
- package/dist/server-N5LM3PEO.js.map +1 -0
- package/dist/{worker-SLQ375UG.js → worker-56IXWOQU.js} +3 -3
- package/docs/mcp-tool-craft-lint.generated.md +5 -2
- package/docs/mcp-tool-manifest.generated.json +85 -9
- package/package.json +1 -1
- package/dist/chunk-DH4H3F6S.js.map +0 -1
- package/dist/chunk-IPW4LFOT.js.map +0 -1
- package/dist/chunk-PGWJ2EJ3.js +0 -7
- package/dist/chunk-PGWJ2EJ3.js.map +0 -1
- package/dist/chunk-Q4STSM63.js.map +0 -1
- package/dist/chunk-SIXJ6EVS.js.map +0 -1
- package/dist/chunk-SXTXMFEQ.js.map +0 -1
- package/dist/server-MOP7NIUK.js.map +0 -1
- /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-
|
|
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-
|
|
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-
|
|
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
|
|
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
|
|
4082
|
-
text: text
|
|
4083
|
-
heading: heading
|
|
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 =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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/
|
|
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(/&/g, "&").replace(/"/g, '"').replace(/'/g, "'").replace(/</g, "<").replace(/>/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
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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
|
|
10708
|
-
import { z as
|
|
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 =
|
|
10870
|
-
query:
|
|
10871
|
-
state:
|
|
10872
|
-
minPopulation:
|
|
10873
|
-
populationYear:
|
|
10874
|
-
maxCities:
|
|
10875
|
-
maxResultsPerCity:
|
|
10876
|
-
concurrency:
|
|
10877
|
-
includeZipGroups:
|
|
10878
|
-
usZipsCsvPath:
|
|
10879
|
-
saveCsv:
|
|
10880
|
-
gl:
|
|
10881
|
-
hl:
|
|
10882
|
-
proxyMode:
|
|
10883
|
-
proxyZip:
|
|
10884
|
-
debug:
|
|
10885
|
-
headless:
|
|
10886
|
-
kernelApiKey:
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
11181
|
-
import { z as
|
|
11182
|
-
var workflowApp = new
|
|
11183
|
-
var WorkflowInputSchema =
|
|
11184
|
-
var WorkflowIdSchema =
|
|
11185
|
-
var CadenceSchema =
|
|
11186
|
-
var ScheduleStatusSchema =
|
|
11187
|
-
var RunBodySchema =
|
|
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:
|
|
11930
|
+
webhookUrl: z17.string().url().optional()
|
|
11191
11931
|
});
|
|
11192
|
-
var ScheduleCreateSchema =
|
|
11932
|
+
var ScheduleCreateSchema = z17.object({
|
|
11193
11933
|
workflowId: WorkflowIdSchema,
|
|
11194
|
-
name:
|
|
11934
|
+
name: z17.string().min(1).max(120).optional(),
|
|
11195
11935
|
input: WorkflowInputSchema,
|
|
11196
11936
|
cadence: CadenceSchema.default("weekly"),
|
|
11197
|
-
timezone:
|
|
11198
|
-
webhookUrl:
|
|
11199
|
-
nextRunAt:
|
|
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 =
|
|
11202
|
-
name:
|
|
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:
|
|
11207
|
-
webhookUrl:
|
|
11208
|
-
nextRunAt:
|
|
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
|
|
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
|
|
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 =
|
|
12087
|
-
var SerpIntelligenceCaptureBodySchema =
|
|
12088
|
-
query:
|
|
12089
|
-
location:
|
|
12090
|
-
gl:
|
|
12091
|
-
hl:
|
|
12092
|
-
device:
|
|
12093
|
-
proxyMode:
|
|
12094
|
-
proxyZip:
|
|
12095
|
-
pages:
|
|
12096
|
-
debug:
|
|
12097
|
-
includePageSnapshots:
|
|
12098
|
-
pageSnapshotLimit:
|
|
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 =
|
|
12840
|
+
var SerpIntelligencePageSnapshotRequestSchema = z18.object({
|
|
12101
12841
|
url: SerpIntelligencePublicHttpUrlSchema,
|
|
12102
|
-
sourceKind:
|
|
12103
|
-
sourcePosition:
|
|
12842
|
+
sourceKind: z18.enum(SerpPageSnapshotSourceKindValues).default("configured_target"),
|
|
12843
|
+
sourcePosition: z18.number().int().min(1).optional()
|
|
12104
12844
|
}).strict();
|
|
12105
|
-
var SerpIntelligencePageSnapshotsBodySchema =
|
|
12106
|
-
urls:
|
|
12107
|
-
targets:
|
|
12108
|
-
maxConcurrency:
|
|
12109
|
-
timeoutMs:
|
|
12110
|
-
debug:
|
|
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 =
|
|
12113
|
-
text:
|
|
12114
|
-
href:
|
|
12852
|
+
var SerpIntelligenceAICitationSchema = z18.object({
|
|
12853
|
+
text: z18.string(),
|
|
12854
|
+
href: z18.string()
|
|
12115
12855
|
}).strict();
|
|
12116
|
-
var SerpIntelligenceOrganicResultSchema =
|
|
12117
|
-
position:
|
|
12118
|
-
title:
|
|
12119
|
-
url:
|
|
12120
|
-
domain:
|
|
12121
|
-
cite:
|
|
12122
|
-
snippet:
|
|
12123
|
-
isRedditStyle:
|
|
12124
|
-
inlineRating:
|
|
12125
|
-
value:
|
|
12126
|
-
count:
|
|
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 =
|
|
12130
|
-
status:
|
|
12131
|
-
expected:
|
|
12132
|
-
city:
|
|
12133
|
-
regionCode:
|
|
12134
|
-
canonicalLocation:
|
|
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:
|
|
12137
|
-
city:
|
|
12138
|
-
regionCode:
|
|
12139
|
-
count:
|
|
12140
|
-
examples:
|
|
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 =
|
|
12144
|
-
seed:
|
|
12145
|
-
location:
|
|
12146
|
-
extractedAt:
|
|
12147
|
-
totalQuestions:
|
|
12148
|
-
surface:
|
|
12149
|
-
aiOverview:
|
|
12150
|
-
detected:
|
|
12151
|
-
text:
|
|
12152
|
-
citations:
|
|
12153
|
-
expanded:
|
|
12154
|
-
fullyExpanded:
|
|
12155
|
-
sections:
|
|
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:
|
|
12158
|
-
detected:
|
|
12159
|
-
text:
|
|
12160
|
-
citations:
|
|
12897
|
+
aiMode: z18.object({
|
|
12898
|
+
detected: z18.boolean(),
|
|
12899
|
+
text: z18.string().nullable(),
|
|
12900
|
+
citations: z18.array(SerpIntelligenceAICitationSchema)
|
|
12161
12901
|
}).strict(),
|
|
12162
|
-
tree:
|
|
12163
|
-
flat:
|
|
12164
|
-
videos:
|
|
12165
|
-
forums:
|
|
12166
|
-
organicResults:
|
|
12167
|
-
localPack:
|
|
12168
|
-
entityIds:
|
|
12169
|
-
entities:
|
|
12170
|
-
name:
|
|
12171
|
-
kgId:
|
|
12172
|
-
cid:
|
|
12173
|
-
gcid:
|
|
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:
|
|
12176
|
-
cids:
|
|
12177
|
-
gcids:
|
|
12915
|
+
kgIds: z18.array(z18.string()),
|
|
12916
|
+
cids: z18.array(z18.string()),
|
|
12917
|
+
gcids: z18.array(z18.string())
|
|
12178
12918
|
}).strict(),
|
|
12179
|
-
stats:
|
|
12180
|
-
seed:
|
|
12181
|
-
totalQuestions:
|
|
12182
|
-
maxDepthReached:
|
|
12183
|
-
durationMs:
|
|
12184
|
-
errorCount:
|
|
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:
|
|
12187
|
-
completionStatus:
|
|
12188
|
-
problem:
|
|
12189
|
-
warnings:
|
|
12190
|
-
debug:
|
|
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:
|
|
12934
|
+
whatPeopleSaying: z18.array(z18.unknown())
|
|
12195
12935
|
}).strict();
|
|
12196
|
-
var SerpIntelligenceCaptureAttemptSchema =
|
|
12197
|
-
attemptNumber:
|
|
12198
|
-
outcome:
|
|
12199
|
-
startedAt:
|
|
12200
|
-
completedAt:
|
|
12201
|
-
durationMs:
|
|
12202
|
-
problemCode:
|
|
12203
|
-
message:
|
|
12204
|
-
kernelSessionId:
|
|
12205
|
-
cleanupSucceeded:
|
|
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 =
|
|
12947
|
+
var SerpPageSnapshotCaptureSchema = z18.object({
|
|
12208
12948
|
url: SerpIntelligencePublicHttpUrlSchema,
|
|
12209
12949
|
requestedUrl: SerpIntelligencePublicHttpUrlSchema,
|
|
12210
12950
|
finalUrl: SerpIntelligencePublicHttpUrlSchema.nullable(),
|
|
12211
|
-
sourceKind:
|
|
12212
|
-
sourcePosition:
|
|
12213
|
-
status:
|
|
12214
|
-
fetchedVia:
|
|
12215
|
-
httpStatus:
|
|
12216
|
-
contentType:
|
|
12217
|
-
title:
|
|
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:
|
|
12220
|
-
headings:
|
|
12221
|
-
level:
|
|
12222
|
-
text:
|
|
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:
|
|
12225
|
-
htmlBlobUrl:
|
|
12226
|
-
textBlobUrl:
|
|
12227
|
-
markdownBlobUrl:
|
|
12228
|
-
screenshotBlobUrl:
|
|
12229
|
-
contentSha256:
|
|
12230
|
-
capturedAt:
|
|
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:
|
|
12233
|
-
code:
|
|
12234
|
-
message:
|
|
12972
|
+
error: z18.object({
|
|
12973
|
+
code: z18.string(),
|
|
12974
|
+
message: z18.string()
|
|
12235
12975
|
}).strict().nullable()
|
|
12236
12976
|
}).strict();
|
|
12237
|
-
var SerpIntelligenceCaptureResponseSchema =
|
|
12977
|
+
var SerpIntelligenceCaptureResponseSchema = z18.object({
|
|
12238
12978
|
harvestResult: SerpIntelligenceHarvestResultSchema,
|
|
12239
|
-
attempts:
|
|
12979
|
+
attempts: z18.array(SerpIntelligenceCaptureAttemptSchema),
|
|
12240
12980
|
locationEvidence: SerpIntelligenceLocationEvidenceSchema.nullable(),
|
|
12241
|
-
pageSnapshotArtifacts:
|
|
12242
|
-
billing:
|
|
12243
|
-
creditsUsed:
|
|
12244
|
-
requestId:
|
|
12245
|
-
jobId:
|
|
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 =
|
|
12249
|
-
pageSnapshotArtifacts:
|
|
12250
|
-
attempts:
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
13882
|
-
var BillingCheckoutBodySchema =
|
|
13883
|
-
priceId:
|
|
13884
|
-
});
|
|
13885
|
-
var FreeCreditBreakdownSchema =
|
|
13886
|
-
signup_grant_mc:
|
|
13887
|
-
monthly_refresh_mc:
|
|
13888
|
-
total_free_mc:
|
|
13889
|
-
signup_grant_credits:
|
|
13890
|
-
monthly_refresh_credits:
|
|
13891
|
-
total_free_credits:
|
|
13892
|
-
});
|
|
13893
|
-
var BillingBalanceResponseSchema =
|
|
13894
|
-
balance_mc:
|
|
13895
|
-
balance_credits:
|
|
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:
|
|
14637
|
+
ledger: z19.array(z19.any())
|
|
13898
14638
|
});
|
|
13899
|
-
var MonthlyRefreshSweepResultSchema =
|
|
13900
|
-
usersRefreshed:
|
|
13901
|
-
totalMcGranted:
|
|
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
|
|
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(() =>
|
|
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-
|
|
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-
|
|
15725
|
+
//# sourceMappingURL=server-N5LM3PEO.js.map
|