mcp-scraper 0.3.14 → 0.3.15
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/dist/bin/api-server.cjs +1991 -508
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/browser-agent-stdio-server.cjs +6 -6
- 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 +425 -44
- package/dist/bin/mcp-scraper-combined-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-scraper-combined-stdio-server.js +6 -5
- package/dist/bin/mcp-scraper-combined-stdio-server.js.map +1 -1
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- 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 +419 -38
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/chunk-2MX5WOII.js +7 -0
- package/dist/chunk-2MX5WOII.js.map +1 -0
- package/dist/{chunk-DBQDG7EH.js → chunk-5H22TOXQ.js} +28 -1
- package/dist/chunk-5H22TOXQ.js.map +1 -0
- package/dist/{chunk-KPXMPAJ3.js → chunk-7Y6JDBML.js} +2 -2
- package/dist/chunk-7Y6JDBML.js.map +1 -0
- package/dist/{chunk-AX7UBYLG.js → chunk-WJ2LWHPA.js} +7 -7
- package/dist/chunk-WJ2LWHPA.js.map +1 -0
- package/dist/{chunk-HM6FHV5U.js → chunk-WQAIOY2D.js} +424 -39
- package/dist/chunk-WQAIOY2D.js.map +1 -0
- package/dist/{chunk-AZ5PKU4F.js → chunk-ZAUMSBV3.js} +2 -2
- package/dist/{db-BE4JVB3V.js → db-P76GVIFN.js} +2 -2
- package/dist/{server-ILIVSPNY.js → server-UOQ2JIUQ.js} +1106 -93
- package/dist/server-UOQ2JIUQ.js.map +1 -0
- package/dist/{worker-FG7ZWEGA.js → worker-2WVKKCC7.js} +3 -3
- package/docs/final-tooling-spec.md +206 -0
- package/docs/mcp-tool-design-guide.md +225 -0
- package/docs/mcp-tool-manifest.generated.json +13 -13
- package/docs/seo-crawl-report-spec.md +287 -0
- package/docs/tool-catalog-spec.md +388 -0
- package/package.json +2 -1
- package/dist/chunk-3QHZPR4U.js +0 -7
- package/dist/chunk-3QHZPR4U.js.map +0 -1
- package/dist/chunk-AX7UBYLG.js.map +0 -1
- package/dist/chunk-DBQDG7EH.js.map +0 -1
- package/dist/chunk-HM6FHV5U.js.map +0 -1
- package/dist/chunk-KPXMPAJ3.js.map +0 -1
- package/dist/server-ILIVSPNY.js.map +0 -1
- /package/dist/{chunk-AZ5PKU4F.js.map → chunk-ZAUMSBV3.js.map} +0 -0
- /package/dist/{db-BE4JVB3V.js.map → db-P76GVIFN.js.map} +0 -0
- /package/dist/{worker-FG7ZWEGA.js.map → worker-2WVKKCC7.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -4214,6 +4214,10 @@ async function captureScreenshot(url, kernelApiKey, device = "desktop") {
|
|
|
4214
4214
|
const result = await capturePageData(url, { kernelApiKey, device, screenshot: true, branding: false });
|
|
4215
4215
|
return result.screenshot;
|
|
4216
4216
|
}
|
|
4217
|
+
async function extractBranding(url, kernelApiKey, device = "desktop") {
|
|
4218
|
+
const result = await capturePageData(url, { kernelApiKey, device, screenshot: false, branding: true });
|
|
4219
|
+
return result.branding;
|
|
4220
|
+
}
|
|
4217
4221
|
async function capturePageData(url, opts = {}) {
|
|
4218
4222
|
const device = opts.device ?? "desktop";
|
|
4219
4223
|
const wantShot = opts.screenshot ?? false;
|
|
@@ -4646,9 +4650,10 @@ function baseDomain(url) {
|
|
|
4646
4650
|
}
|
|
4647
4651
|
async function spiderSite(opts) {
|
|
4648
4652
|
const startMs = Date.now();
|
|
4649
|
-
const maxUrls = Math.min(opts.maxUrls ?? 500,
|
|
4653
|
+
const maxUrls = Math.min(opts.maxUrls ?? 500, 1e4);
|
|
4650
4654
|
const concurrency = Math.min(opts.concurrency ?? 12, 20);
|
|
4651
4655
|
const timeoutMs = opts.timeoutMs ?? 8e3;
|
|
4656
|
+
const budgetMs = opts.budgetMs ?? 24e4;
|
|
4652
4657
|
const startNorm = normalizeUrl(opts.startUrl, opts.startUrl) ?? opts.startUrl;
|
|
4653
4658
|
const startDomain = baseDomain(opts.startUrl);
|
|
4654
4659
|
const visited = /* @__PURE__ */ new Set();
|
|
@@ -4662,7 +4667,7 @@ async function spiderSite(opts) {
|
|
|
4662
4667
|
queue.push({ url: loc, from: "sitemap" });
|
|
4663
4668
|
}
|
|
4664
4669
|
}
|
|
4665
|
-
while (queue.length > 0 && results.length < maxUrls) {
|
|
4670
|
+
while (queue.length > 0 && results.length < maxUrls && Date.now() - startMs < budgetMs) {
|
|
4666
4671
|
const batch = queue.splice(0, concurrency);
|
|
4667
4672
|
const settled = await Promise.all(
|
|
4668
4673
|
batch.map(async ({ url, from }) => {
|
|
@@ -4819,9 +4824,193 @@ var init_site_mapper = __esm({
|
|
|
4819
4824
|
}
|
|
4820
4825
|
});
|
|
4821
4826
|
|
|
4827
|
+
// src/api/rotating-proxy-crawl.ts
|
|
4828
|
+
async function discoverSitemapUrls2(startUrl, maxUrls) {
|
|
4829
|
+
const domain = new URL(startUrl).hostname.replace(/^www\./, "");
|
|
4830
|
+
const urls = await discoverSitemapUrls(startUrl, domain);
|
|
4831
|
+
return urls.slice(0, maxUrls);
|
|
4832
|
+
}
|
|
4833
|
+
function withTimeout(p, ms) {
|
|
4834
|
+
return Promise.race([p, new Promise((_, reject) => setTimeout(() => reject(new Error("timeout")), ms))]);
|
|
4835
|
+
}
|
|
4836
|
+
async function createBrowserWithRetry(client2, headless) {
|
|
4837
|
+
for (let attempt = 0; attempt < 3; attempt++) {
|
|
4838
|
+
try {
|
|
4839
|
+
return await client2.browsers.create({ stealth: true, headless, timeout_seconds: 120 });
|
|
4840
|
+
} catch (err) {
|
|
4841
|
+
if (attempt === 2) throw err;
|
|
4842
|
+
await new Promise((r) => setTimeout(r, 5e3));
|
|
4843
|
+
}
|
|
4844
|
+
}
|
|
4845
|
+
throw new Error("unreachable");
|
|
4846
|
+
}
|
|
4847
|
+
async function crawlPage(context, url) {
|
|
4848
|
+
const startedAt = Date.now();
|
|
4849
|
+
let page = null;
|
|
4850
|
+
try {
|
|
4851
|
+
page = await context.newPage();
|
|
4852
|
+
const work = (async (p) => {
|
|
4853
|
+
const resp = await p.goto(url, { waitUntil: "domcontentloaded", timeout: 3e4 });
|
|
4854
|
+
const html = await p.content();
|
|
4855
|
+
const headers = resp ? Object.fromEntries(Object.entries(await resp.headers()).map(([k, v]) => [k.toLowerCase(), v])) : void 0;
|
|
4856
|
+
return { url, html, status: resp?.status() ?? 200, via: "browser", headers, responseTimeMs: Date.now() - startedAt, redirectUrl: headers?.["location"] ?? null };
|
|
4857
|
+
})(page);
|
|
4858
|
+
const timeout = new Promise((_, reject) => setTimeout(() => reject(new Error("page-hard-timeout")), PAGE_HARD_TIMEOUT_MS));
|
|
4859
|
+
return await Promise.race([work, timeout]);
|
|
4860
|
+
} catch {
|
|
4861
|
+
return { url, html: null, status: 0, via: "browser", responseTimeMs: Date.now() - startedAt };
|
|
4862
|
+
} finally {
|
|
4863
|
+
if (page) void page.close().catch(() => {
|
|
4864
|
+
});
|
|
4865
|
+
}
|
|
4866
|
+
}
|
|
4867
|
+
async function crawlWithRotation(urls, opts) {
|
|
4868
|
+
const client2 = new import_sdk3.default({ apiKey: opts.apiKey });
|
|
4869
|
+
const concurrency = Math.max(1, opts.concurrency ?? DEFAULT_CONCURRENCY);
|
|
4870
|
+
const urlsPerBrowser = Math.max(1, opts.urlsPerBrowser ?? DEFAULT_URLS_PER_BROWSER);
|
|
4871
|
+
const headless = opts.headless ?? DEFAULT_HEADLESS;
|
|
4872
|
+
const uniqueUrls = [...new Set(urls)];
|
|
4873
|
+
const byUrl = /* @__PURE__ */ new Map();
|
|
4874
|
+
const runPass = async (targets, phase) => {
|
|
4875
|
+
if (targets.length === 0) return;
|
|
4876
|
+
let next = 0;
|
|
4877
|
+
let done = 0;
|
|
4878
|
+
const workers = Math.min(concurrency, targets.length);
|
|
4879
|
+
const worker = async () => {
|
|
4880
|
+
let kb = null;
|
|
4881
|
+
let browser = null;
|
|
4882
|
+
let inBrowser = 0;
|
|
4883
|
+
const retire = () => {
|
|
4884
|
+
const b = browser, k = kb;
|
|
4885
|
+
browser = null;
|
|
4886
|
+
kb = null;
|
|
4887
|
+
if (b) void b.close().catch(() => {
|
|
4888
|
+
});
|
|
4889
|
+
if (k) void client2.browsers.deleteByID(k.session_id).catch(() => {
|
|
4890
|
+
});
|
|
4891
|
+
};
|
|
4892
|
+
try {
|
|
4893
|
+
while (true) {
|
|
4894
|
+
const i = next++;
|
|
4895
|
+
if (i >= targets.length) break;
|
|
4896
|
+
const url = targets[i];
|
|
4897
|
+
let r;
|
|
4898
|
+
try {
|
|
4899
|
+
if (!browser || inBrowser >= urlsPerBrowser) {
|
|
4900
|
+
retire();
|
|
4901
|
+
kb = await createBrowserWithRetry(client2, headless);
|
|
4902
|
+
browser = await withTimeout(import_playwright3.chromium.connectOverCDP(kb.cdp_ws_url), 3e4);
|
|
4903
|
+
inBrowser = 0;
|
|
4904
|
+
}
|
|
4905
|
+
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
4906
|
+
r = await crawlPage(context, url);
|
|
4907
|
+
inBrowser++;
|
|
4908
|
+
} catch {
|
|
4909
|
+
r = { url, html: null, status: 0, via: "browser" };
|
|
4910
|
+
retire();
|
|
4911
|
+
}
|
|
4912
|
+
byUrl.set(url, r);
|
|
4913
|
+
done++;
|
|
4914
|
+
opts.onPage?.({ url, status: r.status, ok: !!(r.html && r.status >= 200 && r.status < 300), ms: r.responseTimeMs ?? 0, done, total: targets.length, phase });
|
|
4915
|
+
}
|
|
4916
|
+
} finally {
|
|
4917
|
+
retire();
|
|
4918
|
+
}
|
|
4919
|
+
};
|
|
4920
|
+
await Promise.all(Array.from({ length: workers }, () => worker()));
|
|
4921
|
+
};
|
|
4922
|
+
await runPass(uniqueUrls, "main");
|
|
4923
|
+
const failed = uniqueUrls.filter((u) => {
|
|
4924
|
+
const r = byUrl.get(u);
|
|
4925
|
+
return !r || !r.html || r.status < 200 || r.status >= 300;
|
|
4926
|
+
});
|
|
4927
|
+
if (failed.length > 0) await runPass(failed, "retry");
|
|
4928
|
+
return urls.map((u) => byUrl.get(u) ?? { url: u, html: null, status: 0, via: "browser" });
|
|
4929
|
+
}
|
|
4930
|
+
var import_sdk3, import_playwright3, DEFAULT_CONCURRENCY, DEFAULT_URLS_PER_BROWSER, DEFAULT_HEADLESS, PAGE_HARD_TIMEOUT_MS;
|
|
4931
|
+
var init_rotating_proxy_crawl = __esm({
|
|
4932
|
+
"src/api/rotating-proxy-crawl.ts"() {
|
|
4933
|
+
"use strict";
|
|
4934
|
+
import_sdk3 = __toESM(require("@onkernel/sdk"), 1);
|
|
4935
|
+
import_playwright3 = require("playwright");
|
|
4936
|
+
init_site_mapper();
|
|
4937
|
+
DEFAULT_CONCURRENCY = 1;
|
|
4938
|
+
DEFAULT_URLS_PER_BROWSER = 10;
|
|
4939
|
+
DEFAULT_HEADLESS = true;
|
|
4940
|
+
PAGE_HARD_TIMEOUT_MS = 35e3;
|
|
4941
|
+
}
|
|
4942
|
+
});
|
|
4943
|
+
|
|
4822
4944
|
// src/api/site-extractor.ts
|
|
4823
|
-
function
|
|
4945
|
+
function estimatePixels(s) {
|
|
4946
|
+
let px = 0;
|
|
4947
|
+
for (const ch of s) px += PIXEL_WIDTHS[ch] ?? (ch === ch.toUpperCase() && ch !== ch.toLowerCase() ? 10 : 8);
|
|
4948
|
+
return px;
|
|
4949
|
+
}
|
|
4950
|
+
function emptyPageData(url, status, via) {
|
|
4951
|
+
return {
|
|
4952
|
+
url,
|
|
4953
|
+
status,
|
|
4954
|
+
via,
|
|
4955
|
+
contentType: null,
|
|
4956
|
+
responseTimeMs: null,
|
|
4957
|
+
sizeBytes: null,
|
|
4958
|
+
redirectUrl: null,
|
|
4959
|
+
lastModified: null,
|
|
4960
|
+
xRobotsTag: null,
|
|
4961
|
+
title: null,
|
|
4962
|
+
titleLength: null,
|
|
4963
|
+
titlePixels: null,
|
|
4964
|
+
metaDescription: null,
|
|
4965
|
+
metaDescLength: null,
|
|
4966
|
+
metaKeywords: null,
|
|
4967
|
+
h1: null,
|
|
4968
|
+
h1_2: null,
|
|
4969
|
+
h2Count: 0,
|
|
4970
|
+
headings: [],
|
|
4971
|
+
wordCount: 0,
|
|
4972
|
+
textRatio: 0,
|
|
4973
|
+
contentHash: "",
|
|
4974
|
+
metaRobots: null,
|
|
4975
|
+
relNext: null,
|
|
4976
|
+
relPrev: null,
|
|
4977
|
+
hreflang: [],
|
|
4978
|
+
og: null,
|
|
4979
|
+
twitter: null,
|
|
4980
|
+
ampHref: null,
|
|
4981
|
+
imageCount: 0,
|
|
4982
|
+
imagesMissingAlt: 0,
|
|
4983
|
+
imageLinks: [],
|
|
4984
|
+
indexable: status === 200,
|
|
4985
|
+
indexabilityReason: status === 200 ? null : "non-200",
|
|
4986
|
+
schemaTypes: [],
|
|
4987
|
+
canonicalUrl: null,
|
|
4988
|
+
internalLinks: 0,
|
|
4989
|
+
externalLinks: 0,
|
|
4990
|
+
outlinks: [],
|
|
4991
|
+
bodyMarkdown: "",
|
|
4992
|
+
schema: []
|
|
4993
|
+
};
|
|
4994
|
+
}
|
|
4995
|
+
function makeSeedSpider(startUrl, urls) {
|
|
4996
|
+
return {
|
|
4997
|
+
startUrl,
|
|
4998
|
+
urls: urls.map((url) => ({ url, status: 200, linkedFrom: "seed", via: "fetch" })),
|
|
4999
|
+
totalFound: urls.length,
|
|
5000
|
+
durationMs: 0,
|
|
5001
|
+
truncated: false,
|
|
5002
|
+
browserRetries: 0
|
|
5003
|
+
};
|
|
5004
|
+
}
|
|
5005
|
+
function parsePageData(url, html, status, via, resp) {
|
|
4824
5006
|
const origin = new URL(url).origin;
|
|
5007
|
+
const headers = resp?.headers ?? {};
|
|
5008
|
+
const contentType = headers["content-type"] ?? null;
|
|
5009
|
+
const lastModified = headers["last-modified"] ?? null;
|
|
5010
|
+
const xRobotsTag = headers["x-robots-tag"] ?? null;
|
|
5011
|
+
const responseTimeMs = resp?.responseTimeMs ?? null;
|
|
5012
|
+
const redirectUrl = resp?.redirectUrl ?? headers["location"] ?? null;
|
|
5013
|
+
const sizeBytes = headers["content-length"] ? Number(headers["content-length"]) : Buffer.byteLength(html);
|
|
4825
5014
|
const title = html.match(/<title[^>]*>([^<]*)<\/title>/i)?.[1]?.trim() ?? null;
|
|
4826
5015
|
const metaDescription = html.match(/<meta[^>]+name=["']description["'][^>]+content=["']([^"']*)["']/i)?.[1]?.trim() ?? html.match(/<meta[^>]+content=["']([^"']*)["'][^>]+name=["']description["']/i)?.[1]?.trim() ?? null;
|
|
4827
5016
|
const canonicalUrl = html.match(/<link[^>]+rel=["']canonical["'][^>]+href=["']([^"']*)["']/i)?.[1]?.trim() ?? html.match(/<link[^>]+href=["']([^"']*)["'][^>]+rel=["']canonical["']/i)?.[1]?.trim() ?? null;
|
|
@@ -4858,38 +5047,170 @@ function parsePageData(url, html, status, via) {
|
|
|
4858
5047
|
} catch {
|
|
4859
5048
|
bodyMarkdown = bodyText.slice(0, MAX_PAGE_MARKDOWN);
|
|
4860
5049
|
}
|
|
4861
|
-
|
|
4862
|
-
|
|
4863
|
-
|
|
5050
|
+
const outlinks = [];
|
|
5051
|
+
for (const m of html.matchAll(/<a\b([^>]*?)href\s*=\s*["']([^"'#][^"']*)["']([^>]*)>([\s\S]*?)<\/a>/gi)) {
|
|
5052
|
+
try {
|
|
5053
|
+
const abs = new URL(m[2], url);
|
|
5054
|
+
if (abs.protocol !== "http:" && abs.protocol !== "https:") continue;
|
|
5055
|
+
const attrs = `${m[1]} ${m[3]}`;
|
|
5056
|
+
const rel = attrs.match(/\brel\s*=\s*["']([^"']*)["']/i)?.[1]?.toLowerCase() ?? null;
|
|
5057
|
+
const anchor = m[4].replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").trim().slice(0, 160);
|
|
5058
|
+
outlinks.push({ href: abs.href, anchor, rel, internal: abs.origin === origin });
|
|
5059
|
+
} catch {
|
|
5060
|
+
}
|
|
5061
|
+
}
|
|
5062
|
+
const internalLinks = outlinks.filter((l) => l.internal).length;
|
|
5063
|
+
const externalLinks = outlinks.length - internalLinks;
|
|
5064
|
+
const titleLength = title?.length ?? null;
|
|
5065
|
+
const titlePixels = title ? estimatePixels(title) : null;
|
|
5066
|
+
const metaDescLength = metaDescription?.length ?? null;
|
|
5067
|
+
const metaKeywords = html.match(/<meta[^>]+name=["']keywords["'][^>]+content=["']([^"']*)["']/i)?.[1]?.trim() ?? null;
|
|
5068
|
+
const h1List = headings.filter((h) => h.level === 1);
|
|
5069
|
+
const h1_2 = h1List[1]?.text ?? null;
|
|
5070
|
+
const h2Count = headings.filter((h) => h.level === 2).length;
|
|
5071
|
+
const textRatio = Number((bodyText.length / Math.max(1, html.length)).toFixed(3));
|
|
5072
|
+
const contentHash = (0, import_node_crypto.createHash)("sha1").update(bodyText).digest("hex");
|
|
5073
|
+
const metaRobots = html.match(/<meta[^>]+name=["']robots["'][^>]+content=["']([^"']*)["']/i)?.[1]?.trim().toLowerCase() ?? null;
|
|
5074
|
+
const relNext = html.match(/<link[^>]+rel=["']next["'][^>]+href=["']([^"']*)["']/i)?.[1]?.trim() ?? null;
|
|
5075
|
+
const relPrev = html.match(/<link[^>]+rel=["']prev["'][^>]+href=["']([^"']*)["']/i)?.[1]?.trim() ?? null;
|
|
5076
|
+
const hreflang = [];
|
|
5077
|
+
for (const m of html.matchAll(/<link[^>]+rel=["']alternate["'][^>]*>/gi)) {
|
|
5078
|
+
const lang = m[0].match(/hreflang=["']([^"']*)["']/i)?.[1];
|
|
5079
|
+
const href = m[0].match(/href=["']([^"']*)["']/i)?.[1];
|
|
5080
|
+
if (lang && href) hreflang.push({ lang, href });
|
|
5081
|
+
}
|
|
5082
|
+
const ogTag = (p) => html.match(new RegExp(`<meta[^>]+property=["']og:${p}["'][^>]+content=["']([^"']*)["']`, "i"))?.[1]?.trim();
|
|
5083
|
+
const og = ogTag("title") || ogTag("description") || ogTag("image") || ogTag("type") ? { title: ogTag("title"), description: ogTag("description"), image: ogTag("image"), type: ogTag("type") } : null;
|
|
5084
|
+
const twTag = (p) => html.match(new RegExp(`<meta[^>]+name=["']twitter:${p}["'][^>]+content=["']([^"']*)["']`, "i"))?.[1]?.trim();
|
|
5085
|
+
const twitter = twTag("card") || twTag("title") || twTag("description") ? { card: twTag("card"), title: twTag("title"), description: twTag("description") } : null;
|
|
5086
|
+
const ampHref = html.match(/<link[^>]+rel=["']amphtml["'][^>]+href=["']([^"']*)["']/i)?.[1]?.trim() ?? null;
|
|
5087
|
+
let imageCount = 0;
|
|
5088
|
+
let imagesMissingAlt = 0;
|
|
5089
|
+
const imageLinkSet = /* @__PURE__ */ new Set();
|
|
5090
|
+
for (const m of html.matchAll(/<img\b([^>]*)>/gi)) {
|
|
5091
|
+
imageCount++;
|
|
5092
|
+
const alt = m[1].match(/\balt\s*=\s*["']([^"']*)["']/i)?.[1];
|
|
5093
|
+
if (!alt || !alt.trim()) imagesMissingAlt++;
|
|
5094
|
+
const src = m[1].match(/\bsrc\s*=\s*["']([^"']+)["']/i)?.[1];
|
|
5095
|
+
if (src) {
|
|
5096
|
+
try {
|
|
5097
|
+
const abs = new URL(src, url);
|
|
5098
|
+
if (abs.protocol === "http:" || abs.protocol === "https:") imageLinkSet.add(abs.href);
|
|
5099
|
+
} catch {
|
|
5100
|
+
}
|
|
5101
|
+
}
|
|
5102
|
+
}
|
|
5103
|
+
if (og?.image) {
|
|
5104
|
+
try {
|
|
5105
|
+
imageLinkSet.add(new URL(og.image, url).href);
|
|
5106
|
+
} catch {
|
|
5107
|
+
}
|
|
5108
|
+
}
|
|
5109
|
+
const imageLinks = [...imageLinkSet].slice(0, 300);
|
|
5110
|
+
const canonNorm = (u) => {
|
|
4864
5111
|
try {
|
|
4865
|
-
const
|
|
4866
|
-
|
|
4867
|
-
else externalLinks++;
|
|
5112
|
+
const x = new URL(u, url);
|
|
5113
|
+
return (x.origin + x.pathname).replace(/\/+$/, "");
|
|
4868
5114
|
} catch {
|
|
5115
|
+
return u.replace(/\/+$/, "");
|
|
4869
5116
|
}
|
|
5117
|
+
};
|
|
5118
|
+
const canonicalSelf = canonicalUrl ? canonNorm(canonicalUrl) === canonNorm(url) : true;
|
|
5119
|
+
let indexable = true;
|
|
5120
|
+
let indexabilityReason = null;
|
|
5121
|
+
if (status !== 200) {
|
|
5122
|
+
indexable = false;
|
|
5123
|
+
indexabilityReason = "non-200";
|
|
5124
|
+
} else if (metaRobots?.includes("noindex")) {
|
|
5125
|
+
indexable = false;
|
|
5126
|
+
indexabilityReason = "noindex";
|
|
5127
|
+
} else if (xRobotsTag?.toLowerCase().includes("noindex")) {
|
|
5128
|
+
indexable = false;
|
|
5129
|
+
indexabilityReason = "x-robots-noindex";
|
|
5130
|
+
} else if (canonicalUrl && !canonicalSelf) {
|
|
5131
|
+
indexable = false;
|
|
5132
|
+
indexabilityReason = "canonicalised";
|
|
4870
5133
|
}
|
|
4871
|
-
return {
|
|
5134
|
+
return {
|
|
5135
|
+
url,
|
|
5136
|
+
status,
|
|
5137
|
+
via,
|
|
5138
|
+
contentType,
|
|
5139
|
+
responseTimeMs,
|
|
5140
|
+
sizeBytes,
|
|
5141
|
+
redirectUrl,
|
|
5142
|
+
lastModified,
|
|
5143
|
+
xRobotsTag,
|
|
5144
|
+
title,
|
|
5145
|
+
titleLength,
|
|
5146
|
+
titlePixels,
|
|
5147
|
+
metaDescription,
|
|
5148
|
+
metaDescLength,
|
|
5149
|
+
metaKeywords,
|
|
5150
|
+
h1,
|
|
5151
|
+
h1_2,
|
|
5152
|
+
h2Count,
|
|
5153
|
+
headings,
|
|
5154
|
+
wordCount: wordCount2,
|
|
5155
|
+
textRatio,
|
|
5156
|
+
contentHash,
|
|
5157
|
+
metaRobots,
|
|
5158
|
+
relNext,
|
|
5159
|
+
relPrev,
|
|
5160
|
+
hreflang,
|
|
5161
|
+
og,
|
|
5162
|
+
twitter,
|
|
5163
|
+
ampHref,
|
|
5164
|
+
imageCount,
|
|
5165
|
+
imagesMissingAlt,
|
|
5166
|
+
imageLinks,
|
|
5167
|
+
indexable,
|
|
5168
|
+
indexabilityReason,
|
|
5169
|
+
schemaTypes,
|
|
5170
|
+
canonicalUrl,
|
|
5171
|
+
internalLinks,
|
|
5172
|
+
externalLinks,
|
|
5173
|
+
outlinks,
|
|
5174
|
+
bodyMarkdown,
|
|
5175
|
+
schema
|
|
5176
|
+
};
|
|
5177
|
+
}
|
|
5178
|
+
async function extractPagesRotating(urls, opts) {
|
|
5179
|
+
const fetched = await crawlWithRotation(urls, { apiKey: opts.kernelApiKey, concurrency: opts.concurrency, urlsPerBrowser: opts.urlsPerBrowser });
|
|
5180
|
+
return fetched.map((r) => r.html ? parsePageData(r.url, r.html, r.status || 200, "browser", { headers: r.headers, responseTimeMs: r.responseTimeMs, redirectUrl: r.redirectUrl }) : emptyPageData(r.url, r.status || null, "browser"));
|
|
5181
|
+
}
|
|
5182
|
+
async function discoverUrls(startUrl, maxPages, kernelApiKey) {
|
|
5183
|
+
const sitemapUrls = await discoverSitemapUrls2(startUrl, maxPages);
|
|
5184
|
+
if (sitemapUrls.length > 0) return sitemapUrls;
|
|
5185
|
+
const spider = await spiderSite({ startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey });
|
|
5186
|
+
return spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
|
|
4872
5187
|
}
|
|
4873
5188
|
async function fetchAndParse(url, kernelApiKey) {
|
|
5189
|
+
const startedAt = Date.now();
|
|
4874
5190
|
try {
|
|
4875
5191
|
const res = await fetch(url, {
|
|
4876
5192
|
headers: { "User-Agent": UA2, "Accept": "text/html,application/xhtml+xml" },
|
|
4877
5193
|
signal: AbortSignal.timeout(1e4),
|
|
4878
5194
|
redirect: "manual"
|
|
4879
5195
|
});
|
|
5196
|
+
const meta = {
|
|
5197
|
+
headers: Object.fromEntries([...res.headers.entries()].map(([k, v]) => [k.toLowerCase(), v])),
|
|
5198
|
+
responseTimeMs: Date.now() - startedAt,
|
|
5199
|
+
redirectUrl: res.headers.get("location")
|
|
5200
|
+
};
|
|
4880
5201
|
if (res.ok) {
|
|
4881
5202
|
const ct = res.headers.get("content-type") ?? "";
|
|
4882
5203
|
if (ct.includes("html")) {
|
|
4883
5204
|
const html = await res.text();
|
|
4884
|
-
return parsePageData(url, html, res.status, "fetch");
|
|
5205
|
+
return parsePageData(url, html, res.status, "fetch", meta);
|
|
4885
5206
|
}
|
|
4886
|
-
return { url,
|
|
5207
|
+
return { ...emptyPageData(url, res.status, "fetch"), contentType: ct || null, responseTimeMs: meta.responseTimeMs ?? null };
|
|
4887
5208
|
}
|
|
4888
5209
|
if ((res.status === 403 || res.status === 429) && kernelApiKey) {
|
|
4889
5210
|
const html = await fetchWithKernel(url);
|
|
4890
5211
|
return parsePageData(url, html, 200, "browser");
|
|
4891
5212
|
}
|
|
4892
|
-
return { url,
|
|
5213
|
+
return { ...emptyPageData(url, res.status, "fetch"), responseTimeMs: meta.responseTimeMs ?? null, redirectUrl: meta.redirectUrl ?? null };
|
|
4893
5214
|
} catch {
|
|
4894
5215
|
if (kernelApiKey) {
|
|
4895
5216
|
try {
|
|
@@ -4898,7 +5219,7 @@ async function fetchAndParse(url, kernelApiKey) {
|
|
|
4898
5219
|
} catch {
|
|
4899
5220
|
}
|
|
4900
5221
|
}
|
|
4901
|
-
return
|
|
5222
|
+
return emptyPageData(url, null, "fetch");
|
|
4902
5223
|
}
|
|
4903
5224
|
}
|
|
4904
5225
|
async function runWithConcurrency(items, concurrency, fn) {
|
|
@@ -4913,39 +5234,95 @@ async function runWithConcurrency(items, concurrency, fn) {
|
|
|
4913
5234
|
}
|
|
4914
5235
|
async function extractSite(opts) {
|
|
4915
5236
|
const startMs = Date.now();
|
|
4916
|
-
const maxPages = Math.min(opts.maxPages ?? 100,
|
|
5237
|
+
const maxPages = Math.min(opts.maxPages ?? 100, 1e4);
|
|
4917
5238
|
const concurrency = Math.min(opts.concurrency ?? EXTRACT_CONCURRENCY, 10);
|
|
4918
|
-
const
|
|
4919
|
-
|
|
4920
|
-
maxUrls: maxPages,
|
|
4921
|
-
concurrency: 12,
|
|
4922
|
-
kernelApiKey: opts.kernelApiKey
|
|
4923
|
-
});
|
|
4924
|
-
const urlsToExtract = spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
|
|
5239
|
+
const rotating = Boolean(opts.rotateProxyEvery && opts.kernelApiKey);
|
|
5240
|
+
const seeded = opts.seedUrls && opts.seedUrls.length > 0;
|
|
4925
5241
|
const pages = [];
|
|
4926
|
-
let browserRetries =
|
|
4927
|
-
|
|
4928
|
-
|
|
4929
|
-
|
|
4930
|
-
|
|
4931
|
-
|
|
5242
|
+
let browserRetries = 0;
|
|
5243
|
+
let truncated = false;
|
|
5244
|
+
let spider;
|
|
5245
|
+
if (rotating) {
|
|
5246
|
+
const browserConcurrency = Math.max(1, Math.min(opts.parallelism ?? 1, 8));
|
|
5247
|
+
const urlsPerBrowser = opts.rotateProxyEvery ?? 10;
|
|
5248
|
+
const followLinks = !seeded;
|
|
5249
|
+
const seen = /* @__PURE__ */ new Set();
|
|
5250
|
+
const frontier = [];
|
|
5251
|
+
const enqueue = (u) => {
|
|
5252
|
+
const n = normalizeUrl(u, opts.startUrl);
|
|
5253
|
+
if (n && !seen.has(n)) {
|
|
5254
|
+
seen.add(n);
|
|
5255
|
+
frontier.push(n);
|
|
5256
|
+
}
|
|
5257
|
+
};
|
|
5258
|
+
if (seeded) {
|
|
5259
|
+
for (const u of opts.seedUrls) enqueue(u);
|
|
5260
|
+
} else {
|
|
5261
|
+
enqueue(opts.startUrl);
|
|
5262
|
+
for (const u of await discoverSitemapUrls2(opts.startUrl, maxPages)) enqueue(u);
|
|
5263
|
+
}
|
|
5264
|
+
const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
|
|
5265
|
+
let done = 0;
|
|
5266
|
+
while (frontier.length > 0 && pages.length < maxPages) {
|
|
5267
|
+
const capacity = maxPages - pages.length;
|
|
5268
|
+
const wave = frontier.splice(0, Math.min(capacity, waveCap));
|
|
5269
|
+
const fetched = await crawlWithRotation(wave, {
|
|
5270
|
+
apiKey: opts.kernelApiKey,
|
|
5271
|
+
concurrency: browserConcurrency,
|
|
5272
|
+
urlsPerBrowser,
|
|
5273
|
+
onPage: (p) => {
|
|
5274
|
+
done++;
|
|
5275
|
+
opts.onProgress?.({ done, discovered: seen.size, frontier: frontier.length, lastOk: p.ok ? 1 : 0, lastFail: p.ok ? 0 : 1, phase: p.phase });
|
|
5276
|
+
}
|
|
5277
|
+
});
|
|
5278
|
+
for (const r of fetched) {
|
|
5279
|
+
const page = r.html ? parsePageData(r.url, r.html, r.status || 200, "browser", { headers: r.headers, responseTimeMs: r.responseTimeMs, redirectUrl: r.redirectUrl }) : emptyPageData(r.url, r.status || null, "browser");
|
|
5280
|
+
pages.push(page);
|
|
5281
|
+
browserRetries++;
|
|
5282
|
+
if (followLinks && r.html) {
|
|
5283
|
+
for (const l of page.outlinks) if (l.internal) enqueue(l.href);
|
|
5284
|
+
}
|
|
5285
|
+
}
|
|
5286
|
+
}
|
|
5287
|
+
truncated = frontier.length > 0;
|
|
5288
|
+
spider = makeSeedSpider(opts.startUrl, [...seen]);
|
|
5289
|
+
} else {
|
|
5290
|
+
spider = seeded ? makeSeedSpider(opts.startUrl, opts.seedUrls) : await spiderSite({ startUrl: opts.startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey: opts.kernelApiKey });
|
|
5291
|
+
const urlsToExtract = spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
|
|
5292
|
+
browserRetries = spider.browserRetries;
|
|
5293
|
+
await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
|
|
5294
|
+
const data = await fetchAndParse(url, opts.kernelApiKey);
|
|
5295
|
+
if (data.via === "browser") browserRetries++;
|
|
5296
|
+
pages.push(data);
|
|
5297
|
+
});
|
|
5298
|
+
truncated = spider.truncated || pages.length >= maxPages;
|
|
5299
|
+
}
|
|
5300
|
+
let branding = null;
|
|
5301
|
+
if (opts.formats?.includes("branding") && opts.kernelApiKey) {
|
|
5302
|
+
branding = await extractBranding(opts.startUrl, opts.kernelApiKey).catch(() => null);
|
|
5303
|
+
}
|
|
4932
5304
|
return {
|
|
4933
5305
|
startUrl: opts.startUrl,
|
|
4934
5306
|
pages,
|
|
4935
5307
|
totalPages: pages.length,
|
|
4936
5308
|
spider,
|
|
4937
5309
|
durationMs: Date.now() - startMs,
|
|
4938
|
-
truncated
|
|
4939
|
-
browserRetries
|
|
5310
|
+
truncated,
|
|
5311
|
+
browserRetries,
|
|
5312
|
+
branding
|
|
4940
5313
|
};
|
|
4941
5314
|
}
|
|
4942
|
-
var import_turndown2, UA2, EXTRACT_CONCURRENCY, MAX_PAGE_MARKDOWN, turndown;
|
|
5315
|
+
var import_turndown2, import_node_crypto, PIXEL_WIDTHS, UA2, EXTRACT_CONCURRENCY, MAX_PAGE_MARKDOWN, turndown;
|
|
4943
5316
|
var init_site_extractor = __esm({
|
|
4944
5317
|
"src/api/site-extractor.ts"() {
|
|
4945
5318
|
"use strict";
|
|
4946
5319
|
import_turndown2 = __toESM(require("turndown"), 1);
|
|
5320
|
+
import_node_crypto = require("crypto");
|
|
4947
5321
|
init_site_mapper();
|
|
4948
5322
|
init_kernel_fetch();
|
|
5323
|
+
init_rotating_proxy_crawl();
|
|
5324
|
+
init_screenshot();
|
|
5325
|
+
PIXEL_WIDTHS = { i: 4, l: 4, j: 4, ".": 4, ",": 4, "'": 4, t: 6, f: 6, r: 6, " ": 4, m: 14, w: 13, W: 16, M: 16 };
|
|
4949
5326
|
UA2 = "Mozilla/5.0 (compatible; ThorbitBot/1.0; +https://thorbit.ai)";
|
|
4950
5327
|
EXTRACT_CONCURRENCY = 6;
|
|
4951
5328
|
MAX_PAGE_MARKDOWN = 4e4;
|
|
@@ -5033,11 +5410,11 @@ async function llmParseWithRetry(schema, promptFn, llm) {
|
|
|
5033
5410
|
const detail = lastError?.message?.slice(0, 200) ?? "unknown error";
|
|
5034
5411
|
throw new Error(`LLM failed to produce valid output after 3 attempts: ${detail}`);
|
|
5035
5412
|
}
|
|
5036
|
-
var
|
|
5413
|
+
var import_sdk4, DeepInfraLlmClient, OpenRouterLlmClient, FallbackLlmClient;
|
|
5037
5414
|
var init_llm_parse_with_retry = __esm({
|
|
5038
5415
|
"src/lib/llm-parse-with-retry.ts"() {
|
|
5039
5416
|
"use strict";
|
|
5040
|
-
|
|
5417
|
+
import_sdk4 = __toESM(require("@anthropic-ai/sdk"), 1);
|
|
5041
5418
|
DeepInfraLlmClient = class {
|
|
5042
5419
|
apiKey;
|
|
5043
5420
|
model;
|
|
@@ -7185,6 +7562,33 @@ async function migrate() {
|
|
|
7185
7562
|
await db.execute(`CREATE INDEX IF NOT EXISTS site_audit_jobs_user_id ON site_audit_jobs(user_id)`);
|
|
7186
7563
|
await db.execute(`CREATE INDEX IF NOT EXISTS site_audit_jobs_status ON site_audit_jobs(status)`);
|
|
7187
7564
|
await db.execute(`CREATE INDEX IF NOT EXISTS site_audit_phase_log_job_id ON site_audit_phase_log(job_id)`);
|
|
7565
|
+
await db.execute(`
|
|
7566
|
+
CREATE TABLE IF NOT EXISTS site_extract_jobs (
|
|
7567
|
+
id TEXT PRIMARY KEY,
|
|
7568
|
+
user_id INTEGER,
|
|
7569
|
+
status TEXT DEFAULT 'pending',
|
|
7570
|
+
start_url TEXT,
|
|
7571
|
+
options TEXT,
|
|
7572
|
+
total_urls INTEGER DEFAULT 0,
|
|
7573
|
+
done_urls INTEGER DEFAULT 0,
|
|
7574
|
+
artifacts TEXT,
|
|
7575
|
+
error TEXT,
|
|
7576
|
+
billed_mc INTEGER,
|
|
7577
|
+
created_at TEXT DEFAULT (datetime('now')),
|
|
7578
|
+
updated_at TEXT DEFAULT (datetime('now'))
|
|
7579
|
+
)
|
|
7580
|
+
`);
|
|
7581
|
+
await db.execute(`
|
|
7582
|
+
CREATE TABLE IF NOT EXISTS site_extract_pages (
|
|
7583
|
+
job_id TEXT,
|
|
7584
|
+
url TEXT,
|
|
7585
|
+
page TEXT,
|
|
7586
|
+
PRIMARY KEY (job_id, url)
|
|
7587
|
+
)
|
|
7588
|
+
`);
|
|
7589
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS site_extract_jobs_user_id ON site_extract_jobs(user_id)`);
|
|
7590
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS site_extract_jobs_status ON site_extract_jobs(status)`);
|
|
7591
|
+
await db.execute(`CREATE INDEX IF NOT EXISTS site_extract_pages_job_id ON site_extract_pages(job_id)`);
|
|
7188
7592
|
await db.execute(`
|
|
7189
7593
|
CREATE TABLE IF NOT EXISTS password_reset_tokens (
|
|
7190
7594
|
token TEXT PRIMARY KEY,
|
|
@@ -7341,18 +7745,18 @@ async function stripeEventAlreadyProcessed(eventId) {
|
|
|
7341
7745
|
return res.rows.length > 0;
|
|
7342
7746
|
}
|
|
7343
7747
|
function generateApiKey() {
|
|
7344
|
-
return "sk_" + (0,
|
|
7748
|
+
return "sk_" + (0, import_node_crypto2.randomBytes)(24).toString("hex");
|
|
7345
7749
|
}
|
|
7346
7750
|
function hashPassword(password) {
|
|
7347
|
-
const salt = (0,
|
|
7348
|
-
const hash = (0,
|
|
7751
|
+
const salt = (0, import_node_crypto2.randomBytes)(16).toString("hex");
|
|
7752
|
+
const hash = (0, import_node_crypto2.scryptSync)(password, salt, 64);
|
|
7349
7753
|
return `${salt}:${hash.toString("hex")}`;
|
|
7350
7754
|
}
|
|
7351
7755
|
function verifyPassword(password, stored) {
|
|
7352
7756
|
const [salt, hash] = stored.split(":");
|
|
7353
7757
|
const hashBuf = Buffer.from(hash, "hex");
|
|
7354
|
-
const derived = (0,
|
|
7355
|
-
return (0,
|
|
7758
|
+
const derived = (0, import_node_crypto2.scryptSync)(password, salt, 64);
|
|
7759
|
+
return (0, import_node_crypto2.timingSafeEqual)(hashBuf, derived);
|
|
7356
7760
|
}
|
|
7357
7761
|
function rowToUser(row) {
|
|
7358
7762
|
return {
|
|
@@ -7405,7 +7809,7 @@ async function getUserStats(userId) {
|
|
|
7405
7809
|
async function createUser(email, name, password, stripeCustomerId) {
|
|
7406
7810
|
const db = getDb();
|
|
7407
7811
|
const api_key = generateApiKey();
|
|
7408
|
-
const plainPassword = password ?? (0,
|
|
7812
|
+
const plainPassword = password ?? (0, import_node_crypto2.randomBytes)(6).toString("hex");
|
|
7409
7813
|
const password_hash = hashPassword(plainPassword);
|
|
7410
7814
|
const result = await db.execute({
|
|
7411
7815
|
sql: "INSERT INTO users (email, name, api_key, password_hash, stripe_customer_id) VALUES (?, ?, ?, ?, ?)",
|
|
@@ -7416,7 +7820,7 @@ async function createUser(email, name, password, stripeCustomerId) {
|
|
|
7416
7820
|
async function checkRateLimit(scope, key, limit, windowSeconds) {
|
|
7417
7821
|
const nowSeconds = Math.floor(Date.now() / 1e3);
|
|
7418
7822
|
const windowStart = Math.floor(nowSeconds / windowSeconds) * windowSeconds;
|
|
7419
|
-
const keyHash = (0,
|
|
7823
|
+
const keyHash = (0, import_node_crypto2.createHash)("sha256").update(key).digest("hex");
|
|
7420
7824
|
const db = getDb();
|
|
7421
7825
|
if (!_rateLimitSchemaReady) {
|
|
7422
7826
|
await db.execute(`
|
|
@@ -7453,8 +7857,8 @@ async function checkRateLimit(scope, key, limit, windowSeconds) {
|
|
|
7453
7857
|
};
|
|
7454
7858
|
}
|
|
7455
7859
|
async function createPasswordResetToken(userId) {
|
|
7456
|
-
const token = (0,
|
|
7457
|
-
const tokenHash = (0,
|
|
7860
|
+
const token = (0, import_node_crypto2.randomBytes)(32).toString("hex");
|
|
7861
|
+
const tokenHash = (0, import_node_crypto2.createHash)("sha256").update(token).digest("hex");
|
|
7458
7862
|
const expiresAt = new Date(Date.now() + 60 * 60 * 1e3).toISOString();
|
|
7459
7863
|
await getDb().execute({
|
|
7460
7864
|
sql: "INSERT INTO password_reset_tokens (token, user_id, expires_at) VALUES (?, ?, ?)",
|
|
@@ -7464,7 +7868,7 @@ async function createPasswordResetToken(userId) {
|
|
|
7464
7868
|
}
|
|
7465
7869
|
async function consumePasswordResetToken(token) {
|
|
7466
7870
|
const db = getDb();
|
|
7467
|
-
const tokenHash = (0,
|
|
7871
|
+
const tokenHash = (0, import_node_crypto2.createHash)("sha256").update(token).digest("hex");
|
|
7468
7872
|
const res = await db.execute({
|
|
7469
7873
|
sql: "SELECT user_id, expires_at, used FROM password_reset_tokens WHERE token = ?",
|
|
7470
7874
|
args: [tokenHash]
|
|
@@ -7552,7 +7956,7 @@ function deserialize(raw) {
|
|
|
7552
7956
|
};
|
|
7553
7957
|
}
|
|
7554
7958
|
async function createJob(userId, query, options, callbackUrl) {
|
|
7555
|
-
const id = (0,
|
|
7959
|
+
const id = (0, import_node_crypto2.randomUUID)();
|
|
7556
7960
|
await getDb().execute({
|
|
7557
7961
|
sql: "INSERT INTO jobs (id, user_id, query, options, callback_url) VALUES (?, ?, ?, ?, ?)",
|
|
7558
7962
|
args: [id, userId, query, JSON.stringify(options), callbackUrl ?? null]
|
|
@@ -7560,7 +7964,7 @@ async function createJob(userId, query, options, callbackUrl) {
|
|
|
7560
7964
|
return id;
|
|
7561
7965
|
}
|
|
7562
7966
|
async function createRunningJob(userId, query, options) {
|
|
7563
|
-
const id = (0,
|
|
7967
|
+
const id = (0, import_node_crypto2.randomUUID)();
|
|
7564
7968
|
await getDb().execute({
|
|
7565
7969
|
sql: `INSERT INTO jobs (id, user_id, query, options, status, started_at) VALUES (?, ?, ?, ?, 'running', datetime('now'))`,
|
|
7566
7970
|
args: [id, userId, query, JSON.stringify(options)]
|
|
@@ -7583,7 +7987,7 @@ async function startHarvestAttempt(input) {
|
|
|
7583
7987
|
debug_json = NULL
|
|
7584
7988
|
`,
|
|
7585
7989
|
args: [
|
|
7586
|
-
(0,
|
|
7990
|
+
(0, import_node_crypto2.randomUUID)(),
|
|
7587
7991
|
input.jobId,
|
|
7588
7992
|
input.userId,
|
|
7589
7993
|
input.attemptNumber,
|
|
@@ -7675,7 +8079,7 @@ function rowToRequestEvent(row) {
|
|
|
7675
8079
|
};
|
|
7676
8080
|
}
|
|
7677
8081
|
async function logRequestEvent(input) {
|
|
7678
|
-
const id = (0,
|
|
8082
|
+
const id = (0, import_node_crypto2.randomUUID)();
|
|
7679
8083
|
await getDb().execute({
|
|
7680
8084
|
sql: `
|
|
7681
8085
|
INSERT INTO request_events (id, user_id, source, status, query, location, result_count, result, error)
|
|
@@ -7780,7 +8184,7 @@ function rowToWorkflowArtifact(row) {
|
|
|
7780
8184
|
};
|
|
7781
8185
|
}
|
|
7782
8186
|
async function createWorkflowSchedule(input) {
|
|
7783
|
-
const id = (0,
|
|
8187
|
+
const id = (0, import_node_crypto2.randomUUID)();
|
|
7784
8188
|
await getDb().execute({
|
|
7785
8189
|
sql: `
|
|
7786
8190
|
INSERT INTO workflow_schedules (
|
|
@@ -7880,7 +8284,7 @@ async function markWorkflowScheduleRan(id, lastRunAt, nextRunAt) {
|
|
|
7880
8284
|
});
|
|
7881
8285
|
}
|
|
7882
8286
|
async function createWorkflowRun(input) {
|
|
7883
|
-
const id = input.id ?? (0,
|
|
8287
|
+
const id = input.id ?? (0, import_node_crypto2.randomUUID)();
|
|
7884
8288
|
const args = [
|
|
7885
8289
|
id,
|
|
7886
8290
|
input.userId,
|
|
@@ -7956,7 +8360,7 @@ async function insertWorkflowArtifact(runId, userId, artifact) {
|
|
|
7956
8360
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
7957
8361
|
`,
|
|
7958
8362
|
args: [
|
|
7959
|
-
(0,
|
|
8363
|
+
(0, import_node_crypto2.randomUUID)(),
|
|
7960
8364
|
runId,
|
|
7961
8365
|
userId,
|
|
7962
8366
|
artifact.kind,
|
|
@@ -8019,7 +8423,7 @@ async function recordWorkflowWebhookDelivery(input) {
|
|
|
8019
8423
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
8020
8424
|
`,
|
|
8021
8425
|
args: [
|
|
8022
|
-
(0,
|
|
8426
|
+
(0, import_node_crypto2.randomUUID)(),
|
|
8023
8427
|
input.runId,
|
|
8024
8428
|
input.scheduleId ?? null,
|
|
8025
8429
|
input.userId,
|
|
@@ -8227,12 +8631,12 @@ async function reconcileBalanceMc(userId) {
|
|
|
8227
8631
|
});
|
|
8228
8632
|
return balanceMc;
|
|
8229
8633
|
}
|
|
8230
|
-
var import_http,
|
|
8634
|
+
var import_http, import_node_crypto2, import_zod10, DB_URL, DB_TOKEN, _db, _rateLimitSchemaReady, SiteAuditJobRowSchema, SiteAuditPhaseLogRowSchema;
|
|
8231
8635
|
var init_db = __esm({
|
|
8232
8636
|
"src/api/db.ts"() {
|
|
8233
8637
|
"use strict";
|
|
8234
8638
|
import_http = require("@libsql/client/http");
|
|
8235
|
-
|
|
8639
|
+
import_node_crypto2 = require("crypto");
|
|
8236
8640
|
import_zod10 = require("zod");
|
|
8237
8641
|
DB_URL = process.env.TURSO_DATABASE_URL ?? "file:./paa-api.db";
|
|
8238
8642
|
DB_TOKEN = process.env.TURSO_AUTH_TOKEN;
|
|
@@ -8568,212 +8972,257 @@ var init_site_audit = __esm({
|
|
|
8568
8972
|
}
|
|
8569
8973
|
});
|
|
8570
8974
|
|
|
8571
|
-
// src/api/
|
|
8572
|
-
|
|
8573
|
-
|
|
8574
|
-
|
|
8575
|
-
|
|
8576
|
-
|
|
8577
|
-
|
|
8578
|
-
|
|
8579
|
-
|
|
8580
|
-
|
|
8581
|
-
|
|
8582
|
-
|
|
8583
|
-
|
|
8584
|
-
|
|
8975
|
+
// src/api/seo-link-graph.ts
|
|
8976
|
+
function normalize(u) {
|
|
8977
|
+
return u.split("#")[0].replace(/\/$/, "");
|
|
8978
|
+
}
|
|
8979
|
+
function buildLinkGraph(pages, startUrl) {
|
|
8980
|
+
const statusByUrl = /* @__PURE__ */ new Map();
|
|
8981
|
+
for (const p of pages) statusByUrl.set(normalize(p.url), p.status);
|
|
8982
|
+
const edges = [];
|
|
8983
|
+
for (const p of pages) {
|
|
8984
|
+
for (const l of p.outlinks ?? []) {
|
|
8985
|
+
const nofollow = (l.rel ?? "").split(/\s+/).includes("nofollow");
|
|
8986
|
+
edges.push({
|
|
8987
|
+
from: p.url,
|
|
8988
|
+
to: l.href,
|
|
8989
|
+
anchor: l.anchor,
|
|
8990
|
+
rel: l.rel,
|
|
8991
|
+
internal: l.internal,
|
|
8992
|
+
nofollow,
|
|
8993
|
+
targetStatus: statusByUrl.get(normalize(l.href)) ?? null
|
|
8994
|
+
});
|
|
8995
|
+
}
|
|
8996
|
+
}
|
|
8997
|
+
const inboundFrom = /* @__PURE__ */ new Map();
|
|
8998
|
+
const inboundAnchors = /* @__PURE__ */ new Map();
|
|
8999
|
+
const adjacency = /* @__PURE__ */ new Map();
|
|
9000
|
+
for (const e of edges) {
|
|
9001
|
+
if (!e.internal) continue;
|
|
9002
|
+
const to = normalize(e.to);
|
|
9003
|
+
const from = normalize(e.from);
|
|
9004
|
+
if (!inboundFrom.has(to)) inboundFrom.set(to, /* @__PURE__ */ new Set());
|
|
9005
|
+
inboundFrom.get(to).add(from);
|
|
9006
|
+
if (e.anchor) {
|
|
9007
|
+
if (!inboundAnchors.has(to)) inboundAnchors.set(to, []);
|
|
9008
|
+
inboundAnchors.get(to).push(e.anchor);
|
|
9009
|
+
}
|
|
9010
|
+
if (!e.nofollow && (statusByUrl.get(to) === 200 || !statusByUrl.has(to))) {
|
|
9011
|
+
if (!adjacency.has(from)) adjacency.set(from, /* @__PURE__ */ new Set());
|
|
9012
|
+
adjacency.get(from).add(to);
|
|
9013
|
+
}
|
|
9014
|
+
}
|
|
9015
|
+
const depth = /* @__PURE__ */ new Map();
|
|
9016
|
+
const start = normalize(startUrl);
|
|
9017
|
+
const queue = [start];
|
|
9018
|
+
depth.set(start, 0);
|
|
9019
|
+
while (queue.length > 0) {
|
|
9020
|
+
const cur = queue.shift();
|
|
9021
|
+
const d = depth.get(cur);
|
|
9022
|
+
for (const next of adjacency.get(cur) ?? []) {
|
|
9023
|
+
if (!depth.has(next)) {
|
|
9024
|
+
depth.set(next, d + 1);
|
|
9025
|
+
queue.push(next);
|
|
8585
9026
|
}
|
|
8586
|
-
|
|
9027
|
+
}
|
|
9028
|
+
}
|
|
9029
|
+
const topAnchorsFor = (url) => {
|
|
9030
|
+
const counts = /* @__PURE__ */ new Map();
|
|
9031
|
+
for (const a of inboundAnchors.get(url) ?? []) counts.set(a, (counts.get(a) ?? 0) + 1);
|
|
9032
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1]).slice(0, 3).map((e) => e[0]);
|
|
9033
|
+
};
|
|
9034
|
+
const metrics = /* @__PURE__ */ new Map();
|
|
9035
|
+
for (const p of pages) {
|
|
9036
|
+
const key = normalize(p.url);
|
|
9037
|
+
const inbound = inboundFrom.get(key) ?? /* @__PURE__ */ new Set();
|
|
9038
|
+
metrics.set(p.url, {
|
|
9039
|
+
url: p.url,
|
|
9040
|
+
inlinks: inbound.size,
|
|
9041
|
+
uniqueInlinks: inbound.size,
|
|
9042
|
+
outlinksInternal: (p.outlinks ?? []).filter((l) => l.internal).length,
|
|
9043
|
+
outlinksExternal: (p.outlinks ?? []).filter((l) => !l.internal).length,
|
|
9044
|
+
crawlDepth: depth.has(key) ? depth.get(key) : null,
|
|
9045
|
+
orphan: inbound.size === 0 && key !== start,
|
|
9046
|
+
topAnchors: topAnchorsFor(key)
|
|
8587
9047
|
});
|
|
8588
9048
|
}
|
|
9049
|
+
return { edges, metrics };
|
|
9050
|
+
}
|
|
9051
|
+
var init_seo_link_graph = __esm({
|
|
9052
|
+
"src/api/seo-link-graph.ts"() {
|
|
9053
|
+
"use strict";
|
|
9054
|
+
}
|
|
8589
9055
|
});
|
|
8590
9056
|
|
|
8591
|
-
// src/api/
|
|
8592
|
-
function
|
|
8593
|
-
const
|
|
8594
|
-
|
|
9057
|
+
// src/api/seo-issues.ts
|
|
9058
|
+
function dupes(pages, key) {
|
|
9059
|
+
const groups = /* @__PURE__ */ new Map();
|
|
9060
|
+
for (const p of pages) {
|
|
9061
|
+
const k = key(p);
|
|
9062
|
+
if (k === null || k === void 0 || k === "") continue;
|
|
9063
|
+
const ks = String(k);
|
|
9064
|
+
if (!groups.has(ks)) groups.set(ks, []);
|
|
9065
|
+
groups.get(ks).push(p.url);
|
|
9066
|
+
}
|
|
9067
|
+
return [...groups.values()].filter((g) => g.length > 1).flat();
|
|
9068
|
+
}
|
|
9069
|
+
function computeIssues(pages, metrics) {
|
|
9070
|
+
const group = (urls) => ({ count: urls.length, urls: urls.slice(0, 200) });
|
|
9071
|
+
const where = (fn) => pages.filter(fn).map((p) => p.url);
|
|
9072
|
+
const ok = (p) => p.status === 200;
|
|
9073
|
+
const pathname = (u) => {
|
|
9074
|
+
try {
|
|
9075
|
+
return new URL(u).pathname;
|
|
9076
|
+
} catch {
|
|
9077
|
+
return "";
|
|
9078
|
+
}
|
|
9079
|
+
};
|
|
9080
|
+
const report = {
|
|
9081
|
+
"title.missing": group(where((p) => ok(p) && !p.title)),
|
|
9082
|
+
"title.duplicate": group(dupes(pages.filter(ok), (p) => p.title)),
|
|
9083
|
+
"title.tooLong": group(where((p) => (p.titleLength ?? 0) > TITLE_MAX || (p.titlePixels ?? 0) > TITLE_PX_MAX)),
|
|
9084
|
+
"title.tooShort": group(where((p) => p.title != null && (p.titleLength ?? 0) < TITLE_MIN)),
|
|
9085
|
+
"title.sameAsH1": group(where((p) => !!p.title && !!p.h1 && p.title.trim() === p.h1.trim())),
|
|
9086
|
+
"meta.missing": group(where((p) => ok(p) && !p.metaDescription)),
|
|
9087
|
+
"meta.duplicate": group(dupes(pages.filter(ok), (p) => p.metaDescription)),
|
|
9088
|
+
"meta.tooLong": group(where((p) => (p.metaDescLength ?? 0) > META_MAX)),
|
|
9089
|
+
"meta.tooShort": group(where((p) => p.metaDescription != null && (p.metaDescLength ?? 0) < META_MIN)),
|
|
9090
|
+
"h1.missing": group(where((p) => ok(p) && !p.h1)),
|
|
9091
|
+
"h1.multiple": group(where((p) => !!p.h1_2)),
|
|
9092
|
+
"h1.duplicate": group(dupes(pages.filter(ok), (p) => p.h1)),
|
|
9093
|
+
"h1.tooLong": group(where((p) => (p.h1?.length ?? 0) > H1_MAX)),
|
|
9094
|
+
"h2.missing": group(where((p) => ok(p) && p.h2Count === 0)),
|
|
9095
|
+
"indexability.nonIndexable": group(where((p) => !p.indexable)),
|
|
9096
|
+
"indexability.noindex": group(where((p) => p.indexabilityReason === "noindex")),
|
|
9097
|
+
"canonical.missing": group(where((p) => ok(p) && !p.canonicalUrl)),
|
|
9098
|
+
"canonical.canonicalised": group(where((p) => p.indexabilityReason === "canonicalised")),
|
|
9099
|
+
"response.broken4xx": group(where((p) => (p.status ?? 0) >= 400 && (p.status ?? 0) < 500)),
|
|
9100
|
+
"response.error5xx": group(where((p) => (p.status ?? 0) >= 500)),
|
|
9101
|
+
"response.redirect3xx": group(where((p) => (p.status ?? 0) >= 300 && (p.status ?? 0) < 400)),
|
|
9102
|
+
"response.noResponse": group(where((p) => p.status === null)),
|
|
9103
|
+
"content.thin": group(where((p) => ok(p) && p.wordCount > 0 && p.wordCount < THIN_WORDS)),
|
|
9104
|
+
"content.exactDuplicate": group(dupes(pages.filter((p) => ok(p) && p.contentHash), (p) => p.contentHash)),
|
|
9105
|
+
"images.missingAlt": group(where((p) => p.imagesMissingAlt > 0)),
|
|
9106
|
+
"schema.missing": group(where((p) => ok(p) && p.schemaTypes.length === 0)),
|
|
9107
|
+
"url.tooLong": group(where((p) => p.url.length > URL_MAX)),
|
|
9108
|
+
"url.uppercase": group(where((p) => /[A-Z]/.test(pathname(p.url)))),
|
|
9109
|
+
"url.underscores": group(where((p) => pathname(p.url).includes("_"))),
|
|
9110
|
+
"links.orphan": group([...metrics.values()].filter((m) => m.orphan).map((m) => m.url))
|
|
9111
|
+
};
|
|
9112
|
+
const normUrl2 = (u) => {
|
|
9113
|
+
try {
|
|
9114
|
+
const x = new URL(u);
|
|
9115
|
+
x.hash = "";
|
|
9116
|
+
return (x.origin + x.pathname).replace(/\/+$/, "") + x.search;
|
|
9117
|
+
} catch {
|
|
9118
|
+
return u.replace(/#.*$/, "").replace(/\/+$/, "");
|
|
9119
|
+
}
|
|
9120
|
+
};
|
|
9121
|
+
const statusByUrl = new Map(pages.map((p) => [normUrl2(p.url), p.status]));
|
|
9122
|
+
const brokenLinkPages = /* @__PURE__ */ new Set();
|
|
9123
|
+
for (const p of pages) {
|
|
9124
|
+
for (const l of p.outlinks ?? []) {
|
|
9125
|
+
if (!l.internal) continue;
|
|
9126
|
+
const st = statusByUrl.get(normUrl2(l.href));
|
|
9127
|
+
if (st != null && st >= 400) brokenLinkPages.add(p.url);
|
|
9128
|
+
}
|
|
9129
|
+
}
|
|
9130
|
+
report["links.brokenInternal"] = group([...brokenLinkPages]);
|
|
9131
|
+
return report;
|
|
9132
|
+
}
|
|
9133
|
+
function renderIssueReport(siteUrl, pages, report, metrics) {
|
|
9134
|
+
const total = pages.length;
|
|
9135
|
+
const sev = (key) => key.startsWith("response.broken") || key.startsWith("response.error") || key.startsWith("links.broken") ? "\u{1F534}" : key.startsWith("title.missing") || key.startsWith("h1.missing") || key.startsWith("indexability") || key.startsWith("canonical.missing") ? "\u{1F7E0}" : "\u{1F7E1}";
|
|
9136
|
+
const rows = Object.entries(report).filter(([, g]) => g.count > 0).sort((a, b) => b[1].count - a[1].count).map(([k, g]) => `| ${sev(k)} | \`${k}\` | ${g.count} | ${g.urls.slice(0, 3).join(" \xB7 ")}${g.urls.length > 3 ? " \u2026" : ""} |`);
|
|
9137
|
+
const depths = [...metrics.values()].map((m) => m.crawlDepth).filter((d) => d != null);
|
|
9138
|
+
const maxDepth = depths.length ? Math.max(...depths) : 0;
|
|
9139
|
+
const orphans = [...metrics.values()].filter((m) => m.orphan).length;
|
|
9140
|
+
const topLinked = [...metrics.values()].sort((a, b) => b.inlinks - a.inlinks).slice(0, 5);
|
|
9141
|
+
return [
|
|
9142
|
+
`# SEO Crawl Report: ${siteUrl}`,
|
|
9143
|
+
`**${total} pages crawled** \xB7 max crawl depth ${maxDepth} \xB7 ${orphans} orphan page(s)`,
|
|
9144
|
+
`
|
|
9145
|
+
## Issues
|
|
9146
|
+
| | Issue | Count | Examples |
|
|
9147
|
+
|---|-------|-------|----------|
|
|
9148
|
+
${rows.join("\n") || "| \u2705 | none | 0 | \u2014 |"}`,
|
|
9149
|
+
`
|
|
9150
|
+
## Most-linked pages
|
|
9151
|
+
${topLinked.map((m) => `- ${m.inlinks} inlinks \xB7 depth ${m.crawlDepth ?? "\u2014"} \xB7 ${m.url}`).join("\n")}`,
|
|
9152
|
+
`
|
|
9153
|
+
_Thresholds: title ${TITLE_MAX}ch/${TITLE_PX_MAX}px, meta ${META_MAX}ch, H1 ${H1_MAX}ch, thin <${THIN_WORDS} words, URL ${URL_MAX}ch. Pixel widths are estimates._`
|
|
9154
|
+
].join("\n");
|
|
8595
9155
|
}
|
|
8596
|
-
var
|
|
8597
|
-
var
|
|
8598
|
-
"src/api/
|
|
9156
|
+
var THIN_WORDS, TITLE_MAX, TITLE_PX_MAX, TITLE_MIN, META_MAX, META_MIN, H1_MAX, URL_MAX;
|
|
9157
|
+
var init_seo_issues = __esm({
|
|
9158
|
+
"src/api/seo-issues.ts"() {
|
|
8599
9159
|
"use strict";
|
|
8600
|
-
|
|
8601
|
-
|
|
8602
|
-
|
|
8603
|
-
|
|
8604
|
-
|
|
8605
|
-
|
|
8606
|
-
|
|
8607
|
-
|
|
8608
|
-
|
|
8609
|
-
|
|
8610
|
-
|
|
8611
|
-
|
|
9160
|
+
THIN_WORDS = 200;
|
|
9161
|
+
TITLE_MAX = 60;
|
|
9162
|
+
TITLE_PX_MAX = 561;
|
|
9163
|
+
TITLE_MIN = 30;
|
|
9164
|
+
META_MAX = 155;
|
|
9165
|
+
META_MIN = 70;
|
|
9166
|
+
H1_MAX = 70;
|
|
9167
|
+
URL_MAX = 115;
|
|
9168
|
+
}
|
|
9169
|
+
});
|
|
9170
|
+
|
|
9171
|
+
// src/api/blob-store.ts
|
|
9172
|
+
function byteLength(data) {
|
|
9173
|
+
return Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);
|
|
9174
|
+
}
|
|
9175
|
+
function localBaseDir() {
|
|
9176
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), "Downloads", "mcp-scraper");
|
|
9177
|
+
}
|
|
9178
|
+
function getBlobStore() {
|
|
9179
|
+
if (cached) return cached;
|
|
9180
|
+
if (process.env.BLOB_READ_WRITE_TOKEN) {
|
|
9181
|
+
cached = new VercelBlobStore(process.env.BLOB_READ_WRITE_TOKEN);
|
|
9182
|
+
} else {
|
|
9183
|
+
cached = new LocalBlobStore(localBaseDir());
|
|
9184
|
+
}
|
|
9185
|
+
return cached;
|
|
9186
|
+
}
|
|
9187
|
+
var import_node_fs2, import_node_os2, import_node_path2, LocalBlobStore, cached, VercelBlobStore;
|
|
9188
|
+
var init_blob_store = __esm({
|
|
9189
|
+
"src/api/blob-store.ts"() {
|
|
9190
|
+
"use strict";
|
|
9191
|
+
import_node_fs2 = require("fs");
|
|
9192
|
+
import_node_os2 = require("os");
|
|
9193
|
+
import_node_path2 = require("path");
|
|
9194
|
+
LocalBlobStore = class {
|
|
9195
|
+
constructor(baseDir) {
|
|
9196
|
+
this.baseDir = baseDir;
|
|
8612
9197
|
}
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
const sessionPath = import_node_path2.default.resolve(body.sessionPath);
|
|
8621
|
-
if (!isPathInside(allowedRoot, sessionPath)) {
|
|
8622
|
-
return c.json({ error: "sessionPath must be inside the configured session root" }, 400);
|
|
9198
|
+
baseDir;
|
|
9199
|
+
kind = "local";
|
|
9200
|
+
async put(key, data, contentType = "application/octet-stream") {
|
|
9201
|
+
const path6 = (0, import_node_path2.join)(this.baseDir, "blobs", key);
|
|
9202
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path6), { recursive: true });
|
|
9203
|
+
(0, import_node_fs2.writeFileSync)(path6, data);
|
|
9204
|
+
return { key, url: `file://${path6}`, bytes: byteLength(data), contentType };
|
|
8623
9205
|
}
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
|
|
8632
|
-
|
|
8633
|
-
|
|
8634
|
-
body.
|
|
8635
|
-
|
|
8636
|
-
|
|
8637
|
-
|
|
8638
|
-
|
|
8639
|
-
|
|
9206
|
+
};
|
|
9207
|
+
cached = null;
|
|
9208
|
+
VercelBlobStore = class {
|
|
9209
|
+
constructor(token) {
|
|
9210
|
+
this.token = token;
|
|
9211
|
+
}
|
|
9212
|
+
token;
|
|
9213
|
+
kind = "vercel-blob";
|
|
9214
|
+
async put(key, data, contentType = "application/octet-stream") {
|
|
9215
|
+
const { put } = await import("@vercel/blob");
|
|
9216
|
+
const body = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
9217
|
+
const result = await put(key, body, {
|
|
9218
|
+
access: "public",
|
|
9219
|
+
token: this.token,
|
|
9220
|
+
contentType,
|
|
9221
|
+
addRandomSuffix: true
|
|
9222
|
+
});
|
|
9223
|
+
return { key, url: result.url, bytes: byteLength(data), contentType };
|
|
8640
9224
|
}
|
|
8641
|
-
|
|
8642
|
-
const service = makeSiteAuditService();
|
|
8643
|
-
const jobId = await service.startAudit(userId, body);
|
|
8644
|
-
void dispatchSiteAuditPhase(service, "phase1-ingest", { jobId, request: body }).catch(
|
|
8645
|
-
(err) => {
|
|
8646
|
-
console.error(
|
|
8647
|
-
"[site-audit-routes] phase1-ingest dispatch failed for job",
|
|
8648
|
-
jobId,
|
|
8649
|
-
err instanceof Error ? err.message : String(err)
|
|
8650
|
-
);
|
|
8651
|
-
}
|
|
8652
|
-
);
|
|
8653
|
-
return c.json({ jobId }, 202);
|
|
8654
|
-
});
|
|
8655
|
-
siteAuditApp.post("/ingest", async (c) => {
|
|
8656
|
-
const body = SiteAuditIngestRequestSchema.parse(await c.req.json());
|
|
8657
|
-
const service = makeSiteAuditService();
|
|
8658
|
-
const job = await service.getJob(body.jobId);
|
|
8659
|
-
if (!job) return c.json({ error: "Not found" }, 404);
|
|
8660
|
-
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
8661
|
-
void dispatchSiteAuditPhase(service, "phase1-ingest", {
|
|
8662
|
-
jobId: body.jobId,
|
|
8663
|
-
request: JSON.parse(job.request)
|
|
8664
|
-
}).catch((err) => {
|
|
8665
|
-
console.error(
|
|
8666
|
-
"[site-audit-routes] phase1-ingest re-dispatch failed for job",
|
|
8667
|
-
body.jobId,
|
|
8668
|
-
err instanceof Error ? err.message : String(err)
|
|
8669
|
-
);
|
|
8670
|
-
});
|
|
8671
|
-
return c.json({ jobId: body.jobId }, 202);
|
|
8672
|
-
});
|
|
8673
|
-
siteAuditApp.post("/build-graph", async (c) => {
|
|
8674
|
-
const body = SiteAuditBuildGraphRequestSchema.parse(await c.req.json());
|
|
8675
|
-
const service = makeSiteAuditService();
|
|
8676
|
-
const job = await service.getJob(body.jobId);
|
|
8677
|
-
if (!job) return c.json({ error: "Not found" }, 404);
|
|
8678
|
-
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
8679
|
-
void dispatchSiteAuditPhase(service, "phase2-build-graph", { jobId: body.jobId }).catch(
|
|
8680
|
-
(err) => {
|
|
8681
|
-
console.error(
|
|
8682
|
-
"[site-audit-routes] phase2-build-graph failed for job",
|
|
8683
|
-
body.jobId,
|
|
8684
|
-
err instanceof Error ? err.message : String(err)
|
|
8685
|
-
);
|
|
8686
|
-
}
|
|
8687
|
-
);
|
|
8688
|
-
return c.json({ jobId: body.jobId }, 202);
|
|
8689
|
-
});
|
|
8690
|
-
siteAuditApp.post("/classify", async (c) => {
|
|
8691
|
-
const body = SiteAuditClassifyRequestSchema.parse(await c.req.json());
|
|
8692
|
-
const service = makeSiteAuditService();
|
|
8693
|
-
const job = await service.getJob(body.jobId);
|
|
8694
|
-
if (!job) return c.json({ error: "Not found" }, 404);
|
|
8695
|
-
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
8696
|
-
void dispatchSiteAuditPhase(service, "phase3-classify", { jobId: body.jobId }).catch(
|
|
8697
|
-
(err) => {
|
|
8698
|
-
console.error(
|
|
8699
|
-
"[site-audit-routes] phase3-classify failed for job",
|
|
8700
|
-
body.jobId,
|
|
8701
|
-
err instanceof Error ? err.message : String(err)
|
|
8702
|
-
);
|
|
8703
|
-
}
|
|
8704
|
-
);
|
|
8705
|
-
return c.json({ jobId: body.jobId }, 202);
|
|
8706
|
-
});
|
|
8707
|
-
siteAuditApp.post("/compare", async (c) => {
|
|
8708
|
-
const body = SiteAuditCompareRequestSchema.parse(await c.req.json());
|
|
8709
|
-
const service = makeSiteAuditService();
|
|
8710
|
-
const job = await service.getJob(body.jobId);
|
|
8711
|
-
if (!job) return c.json({ error: "Not found" }, 404);
|
|
8712
|
-
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
8713
|
-
void dispatchSiteAuditPhase(service, "phase4-compare", { jobId: body.jobId }).catch(
|
|
8714
|
-
(err) => {
|
|
8715
|
-
console.error(
|
|
8716
|
-
"[site-audit-routes] phase4-compare failed for job",
|
|
8717
|
-
body.jobId,
|
|
8718
|
-
err instanceof Error ? err.message : String(err)
|
|
8719
|
-
);
|
|
8720
|
-
}
|
|
8721
|
-
);
|
|
8722
|
-
return c.json({ jobId: body.jobId }, 202);
|
|
8723
|
-
});
|
|
8724
|
-
siteAuditApp.post("/score", async (c) => {
|
|
8725
|
-
const body = SiteAuditScoreRequestSchema.parse(await c.req.json());
|
|
8726
|
-
const service = makeSiteAuditService();
|
|
8727
|
-
const job = await service.getJob(body.jobId);
|
|
8728
|
-
if (!job) return c.json({ error: "Not found" }, 404);
|
|
8729
|
-
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
8730
|
-
void dispatchSiteAuditPhase(service, "phase5-synthesize", {
|
|
8731
|
-
jobId: body.jobId,
|
|
8732
|
-
scoringOnly: true,
|
|
8733
|
-
reportOnly: false
|
|
8734
|
-
}).catch((err) => {
|
|
8735
|
-
console.error(
|
|
8736
|
-
"[site-audit-routes] phase5-synthesize(scoringOnly) failed for job",
|
|
8737
|
-
body.jobId,
|
|
8738
|
-
err instanceof Error ? err.message : String(err)
|
|
8739
|
-
);
|
|
8740
|
-
});
|
|
8741
|
-
return c.json({ jobId: body.jobId }, 202);
|
|
8742
|
-
});
|
|
8743
|
-
siteAuditApp.post("/report", async (c) => {
|
|
8744
|
-
const body = SiteAuditReportRequestSchema.parse(await c.req.json());
|
|
8745
|
-
const service = makeSiteAuditService();
|
|
8746
|
-
const job = await service.getJob(body.jobId);
|
|
8747
|
-
if (!job) return c.json({ error: "Not found" }, 404);
|
|
8748
|
-
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
8749
|
-
void dispatchSiteAuditPhase(service, "phase5-synthesize", {
|
|
8750
|
-
jobId: body.jobId,
|
|
8751
|
-
scoringOnly: false,
|
|
8752
|
-
reportOnly: true
|
|
8753
|
-
}).catch((err) => {
|
|
8754
|
-
console.error(
|
|
8755
|
-
"[site-audit-routes] phase5-synthesize(reportOnly) failed for job",
|
|
8756
|
-
body.jobId,
|
|
8757
|
-
err instanceof Error ? err.message : String(err)
|
|
8758
|
-
);
|
|
8759
|
-
});
|
|
8760
|
-
return c.json({ jobId: body.jobId }, 202);
|
|
8761
|
-
});
|
|
8762
|
-
siteAuditApp.get("/jobs/:id", async (c) => {
|
|
8763
|
-
const jobId = c.req.param("id");
|
|
8764
|
-
const userId = c.get("siteAuditUserId");
|
|
8765
|
-
const service = makeSiteAuditService();
|
|
8766
|
-
const job = await service.getJob(jobId);
|
|
8767
|
-
if (!job) return c.json({ error: "Not found" }, 404);
|
|
8768
|
-
if (job.user_id !== userId) return c.json({ error: "Not found" }, 404);
|
|
8769
|
-
return c.json(job);
|
|
8770
|
-
});
|
|
8771
|
-
siteAuditApp.get("/jobs", async (c) => {
|
|
8772
|
-
const userId = c.get("siteAuditUserId");
|
|
8773
|
-
const service = makeSiteAuditService();
|
|
8774
|
-
const jobs = await service.getJobsForUser(userId);
|
|
8775
|
-
return c.json(jobs);
|
|
8776
|
-
});
|
|
9225
|
+
};
|
|
8777
9226
|
}
|
|
8778
9227
|
});
|
|
8779
9228
|
|
|
@@ -9023,6 +9472,766 @@ var init_rates = __esm({
|
|
|
9023
9472
|
}
|
|
9024
9473
|
});
|
|
9025
9474
|
|
|
9475
|
+
// src/api/site-extract-repository.ts
|
|
9476
|
+
function rowToJob(r) {
|
|
9477
|
+
return {
|
|
9478
|
+
id: String(r.id),
|
|
9479
|
+
userId: r.user_id != null ? Number(r.user_id) : null,
|
|
9480
|
+
status: r.status != null ? String(r.status) : "pending",
|
|
9481
|
+
startUrl: String(r.start_url ?? ""),
|
|
9482
|
+
options: r.options ? JSON.parse(String(r.options)) : {},
|
|
9483
|
+
totalUrls: Number(r.total_urls ?? 0),
|
|
9484
|
+
doneUrls: Number(r.done_urls ?? 0),
|
|
9485
|
+
artifacts: r.artifacts ? JSON.parse(String(r.artifacts)) : null,
|
|
9486
|
+
error: r.error != null ? String(r.error) : null,
|
|
9487
|
+
billedMc: r.billed_mc != null ? Number(r.billed_mc) : null,
|
|
9488
|
+
createdAt: String(r.created_at ?? ""),
|
|
9489
|
+
updatedAt: String(r.updated_at ?? "")
|
|
9490
|
+
};
|
|
9491
|
+
}
|
|
9492
|
+
async function createExtractJob(jobId, userId, startUrl, options) {
|
|
9493
|
+
const db = getDb();
|
|
9494
|
+
await db.execute({
|
|
9495
|
+
sql: `INSERT INTO site_extract_jobs (id, user_id, status, start_url, options, created_at, updated_at)
|
|
9496
|
+
VALUES (?, ?, 'pending', ?, ?, datetime('now'), datetime('now'))`,
|
|
9497
|
+
args: [jobId, userId, startUrl, JSON.stringify(options)]
|
|
9498
|
+
});
|
|
9499
|
+
}
|
|
9500
|
+
async function getExtractJob(jobId) {
|
|
9501
|
+
const db = getDb();
|
|
9502
|
+
const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE id = ?`, args: [jobId] });
|
|
9503
|
+
return res.rows[0] ? rowToJob(res.rows[0]) : null;
|
|
9504
|
+
}
|
|
9505
|
+
async function setExtractJobTotal(jobId, totalUrls) {
|
|
9506
|
+
const db = getDb();
|
|
9507
|
+
await db.execute({
|
|
9508
|
+
sql: `UPDATE site_extract_jobs SET status = 'running', total_urls = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
9509
|
+
args: [totalUrls, jobId]
|
|
9510
|
+
});
|
|
9511
|
+
}
|
|
9512
|
+
async function saveExtractPages(jobId, pages) {
|
|
9513
|
+
if (pages.length === 0) return;
|
|
9514
|
+
const db = getDb();
|
|
9515
|
+
await db.batch(
|
|
9516
|
+
pages.map((p) => ({
|
|
9517
|
+
sql: `INSERT OR REPLACE INTO site_extract_pages (job_id, url, page) VALUES (?, ?, ?)`,
|
|
9518
|
+
args: [jobId, p.url, JSON.stringify({ ...p, bodyMarkdown: "", schema: [] })]
|
|
9519
|
+
}))
|
|
9520
|
+
);
|
|
9521
|
+
await db.execute({
|
|
9522
|
+
sql: `UPDATE site_extract_jobs
|
|
9523
|
+
SET done_urls = (SELECT COUNT(*) FROM site_extract_pages WHERE job_id = ?), updated_at = datetime('now')
|
|
9524
|
+
WHERE id = ?`,
|
|
9525
|
+
args: [jobId, jobId]
|
|
9526
|
+
});
|
|
9527
|
+
}
|
|
9528
|
+
async function getExtractedPages(jobId) {
|
|
9529
|
+
const db = getDb();
|
|
9530
|
+
const res = await db.execute({ sql: `SELECT page FROM site_extract_pages WHERE job_id = ?`, args: [jobId] });
|
|
9531
|
+
return res.rows.map((r) => JSON.parse(String(r.page)));
|
|
9532
|
+
}
|
|
9533
|
+
async function countSuccessfulPages(jobId) {
|
|
9534
|
+
const db = getDb();
|
|
9535
|
+
const res = await db.execute({
|
|
9536
|
+
sql: `SELECT COUNT(*) AS n FROM site_extract_pages
|
|
9537
|
+
WHERE job_id = ? AND json_extract(page, '$.status') = 200 AND json_extract(page, '$.wordCount') > 0`,
|
|
9538
|
+
args: [jobId]
|
|
9539
|
+
});
|
|
9540
|
+
return Number(res.rows[0]?.n ?? 0);
|
|
9541
|
+
}
|
|
9542
|
+
async function completeExtractJob(jobId, artifacts) {
|
|
9543
|
+
const db = getDb();
|
|
9544
|
+
await db.execute({
|
|
9545
|
+
sql: `UPDATE site_extract_jobs SET status = 'complete', artifacts = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
9546
|
+
args: [JSON.stringify(artifacts ?? []), jobId]
|
|
9547
|
+
});
|
|
9548
|
+
}
|
|
9549
|
+
async function failExtractJob(jobId, error) {
|
|
9550
|
+
const db = getDb();
|
|
9551
|
+
await db.execute({
|
|
9552
|
+
sql: `UPDATE site_extract_jobs SET status = 'failed', error = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
9553
|
+
args: [sanitizeVendorName(error).slice(0, 2e3), jobId]
|
|
9554
|
+
});
|
|
9555
|
+
}
|
|
9556
|
+
async function settleExtractJob(jobId, userId, refundMc, netChargeMc, reference) {
|
|
9557
|
+
const db = getDb();
|
|
9558
|
+
const job = await getExtractJob(jobId);
|
|
9559
|
+
if (!job || job.billedMc != null) return;
|
|
9560
|
+
if (refundMc <= 0) {
|
|
9561
|
+
await db.execute({ sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now') WHERE id = ? AND billed_mc IS NULL`, args: [Math.max(0, netChargeMc), jobId] });
|
|
9562
|
+
return;
|
|
9563
|
+
}
|
|
9564
|
+
await db.batch([
|
|
9565
|
+
{ sql: "UPDATE users SET balance_mc = balance_mc + ? WHERE id = ?", args: [refundMc, userId] },
|
|
9566
|
+
{ sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) VALUES (?, ?, ?, ?)", args: [userId, refundMc, LedgerOperation.EXTRACT_SITE_REFUND, reference] },
|
|
9567
|
+
{ sql: `UPDATE site_extract_jobs SET billed_mc = ?, updated_at = datetime('now') WHERE id = ? AND billed_mc IS NULL`, args: [Math.max(0, netChargeMc), jobId] }
|
|
9568
|
+
]);
|
|
9569
|
+
}
|
|
9570
|
+
var init_site_extract_repository = __esm({
|
|
9571
|
+
"src/api/site-extract-repository.ts"() {
|
|
9572
|
+
"use strict";
|
|
9573
|
+
init_db();
|
|
9574
|
+
init_errors();
|
|
9575
|
+
init_rates();
|
|
9576
|
+
}
|
|
9577
|
+
});
|
|
9578
|
+
|
|
9579
|
+
// src/inngest/functions/site-extract.ts
|
|
9580
|
+
function chunk(items, size) {
|
|
9581
|
+
const out = [];
|
|
9582
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
9583
|
+
return out;
|
|
9584
|
+
}
|
|
9585
|
+
function buildArtifacts(job, pages, branding) {
|
|
9586
|
+
const { edges, metrics } = buildLinkGraph(pages, job.startUrl);
|
|
9587
|
+
const issues = computeIssues(pages, metrics);
|
|
9588
|
+
const reportMd = renderIssueReport(job.startUrl, pages, issues, metrics);
|
|
9589
|
+
const pageRows = pages.map((p) => {
|
|
9590
|
+
const { bodyMarkdown: _b, schema: _s, outlinks: _o, ...rest } = p;
|
|
9591
|
+
const m = metrics.get(p.url);
|
|
9592
|
+
return { ...rest, inlinks: m?.inlinks ?? 0, crawlDepth: m?.crawlDepth ?? null, orphan: m?.orphan ?? false };
|
|
9593
|
+
});
|
|
9594
|
+
const jsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n");
|
|
9595
|
+
const files = [
|
|
9596
|
+
{ name: "report.md", body: reportMd, type: "text/markdown" },
|
|
9597
|
+
{ name: "issues.json", body: JSON.stringify(issues, null, 2), type: "application/json" },
|
|
9598
|
+
{ name: "pages.jsonl", body: jsonl(pageRows), type: "application/x-ndjson" },
|
|
9599
|
+
{ name: "links.jsonl", body: jsonl(edges), type: "application/x-ndjson" },
|
|
9600
|
+
{ name: "link-metrics.jsonl", body: jsonl([...metrics.values()]), type: "application/x-ndjson" }
|
|
9601
|
+
];
|
|
9602
|
+
if (branding) files.push({ name: "branding.json", body: JSON.stringify(branding, null, 2), type: "application/json" });
|
|
9603
|
+
return files;
|
|
9604
|
+
}
|
|
9605
|
+
var BATCH, siteExtractFn;
|
|
9606
|
+
var init_site_extract = __esm({
|
|
9607
|
+
"src/inngest/functions/site-extract.ts"() {
|
|
9608
|
+
"use strict";
|
|
9609
|
+
init_client();
|
|
9610
|
+
init_site_extractor();
|
|
9611
|
+
init_seo_link_graph();
|
|
9612
|
+
init_seo_issues();
|
|
9613
|
+
init_blob_store();
|
|
9614
|
+
init_screenshot();
|
|
9615
|
+
init_browser_service_env();
|
|
9616
|
+
init_rates();
|
|
9617
|
+
init_site_extract_repository();
|
|
9618
|
+
BATCH = 30;
|
|
9619
|
+
siteExtractFn = inngest.createFunction(
|
|
9620
|
+
{
|
|
9621
|
+
id: "site-extract",
|
|
9622
|
+
retries: 2,
|
|
9623
|
+
triggers: [{ event: "mcp-scraper/extract.requested" }],
|
|
9624
|
+
onFailure: async ({ event }) => {
|
|
9625
|
+
const jobId = event?.data?.event?.data?.jobId;
|
|
9626
|
+
if (!jobId) return;
|
|
9627
|
+
const current = await getExtractJob(jobId);
|
|
9628
|
+
if (current && current.userId != null && current.billedMc == null) {
|
|
9629
|
+
const heldMc = Number(current.options.heldMc ?? 0);
|
|
9630
|
+
await settleExtractJob(jobId, current.userId, heldMc, 0, "crawl failed refund").catch(() => {
|
|
9631
|
+
});
|
|
9632
|
+
}
|
|
9633
|
+
await failExtractJob(jobId, String(event?.data?.error?.message ?? "crawl failed")).catch(() => {
|
|
9634
|
+
});
|
|
9635
|
+
}
|
|
9636
|
+
},
|
|
9637
|
+
async ({ event, step }) => {
|
|
9638
|
+
const jobId = event.data.jobId;
|
|
9639
|
+
const job = await step.run("load-job", () => getExtractJob(jobId));
|
|
9640
|
+
if (!job) return { jobId, status: "missing" };
|
|
9641
|
+
const key = browserServiceApiKey();
|
|
9642
|
+
if (!key) throw new Error("browser service key not configured");
|
|
9643
|
+
const maxPages = Number(job.options.maxPages ?? 1e4);
|
|
9644
|
+
const concurrency = Number(job.options.concurrency ?? 1);
|
|
9645
|
+
const urlsPerBrowser = Number(job.options.urlsPerBrowser ?? job.options.rotateProxyEvery ?? 10);
|
|
9646
|
+
const urls = await step.run("discover", async () => {
|
|
9647
|
+
const discovered = await discoverUrls(job.startUrl, maxPages, key);
|
|
9648
|
+
await setExtractJobTotal(jobId, discovered.length);
|
|
9649
|
+
return discovered;
|
|
9650
|
+
});
|
|
9651
|
+
const batches = chunk(urls, BATCH);
|
|
9652
|
+
for (let i = 0; i < batches.length; i++) {
|
|
9653
|
+
await step.run(`crawl-batch-${i}`, async () => {
|
|
9654
|
+
const pages = await extractPagesRotating(batches[i], { kernelApiKey: key, concurrency, urlsPerBrowser });
|
|
9655
|
+
await saveExtractPages(jobId, pages);
|
|
9656
|
+
return pages.length;
|
|
9657
|
+
});
|
|
9658
|
+
}
|
|
9659
|
+
const branding = Array.isArray(job.options.formats) && job.options.formats.includes("branding") ? await step.run("branding", () => extractBranding(job.startUrl, key).catch(() => null)) : null;
|
|
9660
|
+
const artifacts = await step.run("finalize", async () => {
|
|
9661
|
+
const pages = await getExtractedPages(jobId);
|
|
9662
|
+
const store = getBlobStore();
|
|
9663
|
+
const files = buildArtifacts(job, pages, branding);
|
|
9664
|
+
const stored = [];
|
|
9665
|
+
for (const f of files) stored.push(await store.put(`extract/${jobId}/${f.name}`, f.body, f.type));
|
|
9666
|
+
await completeExtractJob(jobId, stored);
|
|
9667
|
+
return stored;
|
|
9668
|
+
});
|
|
9669
|
+
await step.run("settle", async () => {
|
|
9670
|
+
const current = await getExtractJob(jobId);
|
|
9671
|
+
if (!current || current.billedMc != null || current.userId == null) return;
|
|
9672
|
+
const heldMc = Number(current.options.heldMc ?? 0);
|
|
9673
|
+
const successful = await countSuccessfulPages(jobId);
|
|
9674
|
+
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
9675
|
+
await settleExtractJob(jobId, current.userId, heldMc - usedMc, usedMc, new URL(job.startUrl).hostname);
|
|
9676
|
+
});
|
|
9677
|
+
return { jobId, status: "complete", artifacts };
|
|
9678
|
+
}
|
|
9679
|
+
);
|
|
9680
|
+
}
|
|
9681
|
+
});
|
|
9682
|
+
|
|
9683
|
+
// src/api/catalog-seed.ts
|
|
9684
|
+
var URL_PARAM, FORMATS_PARAM, CATALOG;
|
|
9685
|
+
var init_catalog_seed = __esm({
|
|
9686
|
+
"src/api/catalog-seed.ts"() {
|
|
9687
|
+
"use strict";
|
|
9688
|
+
URL_PARAM = { key: "url", label: "URL", type: "url", required: true, placeholder: "https://example.com" };
|
|
9689
|
+
FORMATS_PARAM = {
|
|
9690
|
+
key: "formats",
|
|
9691
|
+
label: "Formats",
|
|
9692
|
+
type: "multi_enum",
|
|
9693
|
+
default: ["markdown", "links"],
|
|
9694
|
+
enumValues: [
|
|
9695
|
+
{ value: "markdown", label: "Markdown" },
|
|
9696
|
+
{ value: "links", label: "Links" },
|
|
9697
|
+
{ value: "json", label: "JSON / schema" },
|
|
9698
|
+
{ value: "images", label: "Image links" },
|
|
9699
|
+
{ value: "branding", label: "Branding" }
|
|
9700
|
+
]
|
|
9701
|
+
};
|
|
9702
|
+
CATALOG = {
|
|
9703
|
+
groups: [
|
|
9704
|
+
{
|
|
9705
|
+
key: "youtube",
|
|
9706
|
+
label: "YouTube",
|
|
9707
|
+
icon: "youtube",
|
|
9708
|
+
tools: [
|
|
9709
|
+
{
|
|
9710
|
+
key: "youtube_harvest",
|
|
9711
|
+
label: "Search",
|
|
9712
|
+
kind: "simple",
|
|
9713
|
+
endpoint: "/youtube/harvest",
|
|
9714
|
+
mcpName: "youtube_harvest",
|
|
9715
|
+
tagline: "Find or list videos by topic or channel",
|
|
9716
|
+
params: [
|
|
9717
|
+
{ key: "mode", label: "Mode", type: "enum", required: true, default: "search", enumValues: [{ value: "search", label: "Search" }, { value: "channel", label: "Channel" }] },
|
|
9718
|
+
{ key: "query", label: "Query", type: "string", placeholder: "best local SEO 2026" },
|
|
9719
|
+
{ key: "channelHandle", label: "Channel handle", type: "string", placeholder: "@mkbhd" },
|
|
9720
|
+
{ key: "maxVideos", label: "Max videos", type: "number", min: 1, max: 500, default: 50, advanced: true }
|
|
9721
|
+
],
|
|
9722
|
+
examples: ["Find YouTube videos about local SEO", "Harvest videos from @mkbhd"]
|
|
9723
|
+
},
|
|
9724
|
+
{
|
|
9725
|
+
key: "youtube_transcribe",
|
|
9726
|
+
label: "Transcribe",
|
|
9727
|
+
kind: "simple",
|
|
9728
|
+
endpoint: "/youtube/transcribe",
|
|
9729
|
+
mcpName: "youtube_transcribe",
|
|
9730
|
+
tagline: "Transcribe one video to text",
|
|
9731
|
+
params: [{ key: "url", label: "Video URL", type: "url", required: true, placeholder: "https://www.youtube.com/watch?v=\u2026" }],
|
|
9732
|
+
examples: ["Transcribe this YouTube URL"]
|
|
9733
|
+
}
|
|
9734
|
+
]
|
|
9735
|
+
},
|
|
9736
|
+
{
|
|
9737
|
+
key: "facebook",
|
|
9738
|
+
label: "Facebook",
|
|
9739
|
+
icon: "facebook",
|
|
9740
|
+
tools: [
|
|
9741
|
+
{
|
|
9742
|
+
key: "facebook_ad_search",
|
|
9743
|
+
label: "Ad Search",
|
|
9744
|
+
kind: "simple",
|
|
9745
|
+
endpoint: "/facebook/search",
|
|
9746
|
+
mcpName: "facebook_ad_search",
|
|
9747
|
+
tagline: "Find ads in the Ad Library",
|
|
9748
|
+
params: [
|
|
9749
|
+
{ key: "query", label: "Advertiser / query", type: "string", required: true },
|
|
9750
|
+
{ key: "country", label: "Country", type: "string", default: "US" },
|
|
9751
|
+
{ key: "maxResults", label: "Max results", type: "number", min: 1, max: 20, default: 10, advanced: true }
|
|
9752
|
+
],
|
|
9753
|
+
examples: ["Find Facebook ads for this brand"]
|
|
9754
|
+
},
|
|
9755
|
+
{
|
|
9756
|
+
key: "facebook_video_transcribe",
|
|
9757
|
+
label: "Video Transcribe",
|
|
9758
|
+
kind: "simple",
|
|
9759
|
+
endpoint: "/facebook/video-transcribe",
|
|
9760
|
+
mcpName: "facebook_video_transcribe",
|
|
9761
|
+
tagline: "Transcribe a Facebook video/reel",
|
|
9762
|
+
params: [{ ...URL_PARAM, label: "Video URL", placeholder: "https://www.facebook.com/share/v/\u2026" }]
|
|
9763
|
+
},
|
|
9764
|
+
{
|
|
9765
|
+
key: "facebook_ad_transcribe",
|
|
9766
|
+
label: "Ad Transcribe",
|
|
9767
|
+
kind: "simple",
|
|
9768
|
+
endpoint: "/facebook/transcribe",
|
|
9769
|
+
mcpName: "facebook_ad_transcribe",
|
|
9770
|
+
tagline: "Transcribe an ad creative",
|
|
9771
|
+
params: [{ ...URL_PARAM, label: "Ad video URL" }]
|
|
9772
|
+
},
|
|
9773
|
+
{
|
|
9774
|
+
key: "facebook_page_intel",
|
|
9775
|
+
label: "Page Intel",
|
|
9776
|
+
kind: "simple",
|
|
9777
|
+
endpoint: "/facebook/page-intel",
|
|
9778
|
+
mcpName: "facebook_page_intel",
|
|
9779
|
+
tagline: "Advertiser page profile + ads",
|
|
9780
|
+
params: [{ key: "libraryId", label: "Page / Library ID or URL", type: "string", required: true }]
|
|
9781
|
+
}
|
|
9782
|
+
]
|
|
9783
|
+
},
|
|
9784
|
+
{
|
|
9785
|
+
key: "instagram",
|
|
9786
|
+
label: "Instagram",
|
|
9787
|
+
icon: "instagram",
|
|
9788
|
+
tools: [
|
|
9789
|
+
{
|
|
9790
|
+
key: "instagram_profile_content",
|
|
9791
|
+
label: "Profile",
|
|
9792
|
+
kind: "simple",
|
|
9793
|
+
endpoint: "/instagram/profile-content",
|
|
9794
|
+
mcpName: "instagram_profile_content",
|
|
9795
|
+
tagline: "Inventory a profile grid",
|
|
9796
|
+
params: [
|
|
9797
|
+
{ key: "url", label: "Profile URL or handle", type: "string", required: true, placeholder: "https://instagram.com/nasa" },
|
|
9798
|
+
{ key: "maxItems", label: "Max items", type: "number", min: 1, max: 2e3, default: 50, advanced: true },
|
|
9799
|
+
{ key: "maxScrolls", label: "Max scrolls", type: "number", min: 0, max: 250, default: 10, advanced: true }
|
|
9800
|
+
]
|
|
9801
|
+
},
|
|
9802
|
+
{
|
|
9803
|
+
key: "instagram_media_download",
|
|
9804
|
+
label: "Post / Reel",
|
|
9805
|
+
kind: "simple",
|
|
9806
|
+
endpoint: "/instagram/media-download",
|
|
9807
|
+
mcpName: "instagram_media_download",
|
|
9808
|
+
tagline: "Download a post or reel (+ transcript)",
|
|
9809
|
+
params: [
|
|
9810
|
+
{ ...URL_PARAM, label: "Post / reel URL", placeholder: "https://www.instagram.com/reel/\u2026" },
|
|
9811
|
+
{ key: "includeTranscript", label: "Include transcript", type: "boolean", default: true }
|
|
9812
|
+
]
|
|
9813
|
+
}
|
|
9814
|
+
]
|
|
9815
|
+
},
|
|
9816
|
+
{
|
|
9817
|
+
key: "reddit",
|
|
9818
|
+
label: "Reddit",
|
|
9819
|
+
icon: "message-circle",
|
|
9820
|
+
tools: [
|
|
9821
|
+
{
|
|
9822
|
+
key: "reddit_browser",
|
|
9823
|
+
label: "Research (Browser)",
|
|
9824
|
+
kind: "simple",
|
|
9825
|
+
endpoint: "/agent",
|
|
9826
|
+
mcpName: "browser_open",
|
|
9827
|
+
tagline: "No dedicated tool yet \u2014 driven by the Browser agent",
|
|
9828
|
+
params: [{ key: "query", label: "What to research", type: "string", required: true, placeholder: "ICP pain points in [niche]" }]
|
|
9829
|
+
}
|
|
9830
|
+
]
|
|
9831
|
+
},
|
|
9832
|
+
{
|
|
9833
|
+
key: "google_search",
|
|
9834
|
+
label: "Google Search",
|
|
9835
|
+
icon: "search",
|
|
9836
|
+
tools: [
|
|
9837
|
+
{
|
|
9838
|
+
key: "search_serp",
|
|
9839
|
+
label: "SERP",
|
|
9840
|
+
kind: "simple",
|
|
9841
|
+
endpoint: "/harvest/sync",
|
|
9842
|
+
mcpName: "search_serp",
|
|
9843
|
+
tagline: "Organic results only",
|
|
9844
|
+
params: [
|
|
9845
|
+
{ key: "query", label: "Query", type: "string", required: true },
|
|
9846
|
+
{ key: "location", label: "Location", type: "string", placeholder: "Denver, CO" }
|
|
9847
|
+
],
|
|
9848
|
+
examples: ["Top restaurants in San Francisco"]
|
|
9849
|
+
},
|
|
9850
|
+
{
|
|
9851
|
+
key: "harvest_paa",
|
|
9852
|
+
label: "PAA + SERP Detail",
|
|
9853
|
+
kind: "simple",
|
|
9854
|
+
endpoint: "/harvest/sync",
|
|
9855
|
+
mcpName: "harvest_paa",
|
|
9856
|
+
tagline: "People-Also-Ask, AI Overview, full SERP features",
|
|
9857
|
+
params: [
|
|
9858
|
+
{ key: "query", label: "Query", type: "string", required: true },
|
|
9859
|
+
{ key: "maxQuestions", label: "Max questions", type: "number", min: 1, max: 200, default: 30, advanced: true },
|
|
9860
|
+
{ key: "location", label: "Location", type: "string" }
|
|
9861
|
+
]
|
|
9862
|
+
}
|
|
9863
|
+
]
|
|
9864
|
+
},
|
|
9865
|
+
{
|
|
9866
|
+
key: "google_maps",
|
|
9867
|
+
label: "Google Maps",
|
|
9868
|
+
icon: "map-pin",
|
|
9869
|
+
tools: [
|
|
9870
|
+
{
|
|
9871
|
+
key: "maps_search",
|
|
9872
|
+
label: "Search",
|
|
9873
|
+
kind: "simple",
|
|
9874
|
+
endpoint: "/maps/search",
|
|
9875
|
+
mcpName: "maps_search",
|
|
9876
|
+
tagline: "Find local businesses",
|
|
9877
|
+
params: [
|
|
9878
|
+
{ key: "query", label: "Query", type: "string", required: true, placeholder: "roofers" },
|
|
9879
|
+
{ key: "location", label: "Location", type: "string", required: true, placeholder: "Denver, CO" },
|
|
9880
|
+
{ key: "maxResults", label: "Max results", type: "number", min: 1, max: 20, default: 10, advanced: true }
|
|
9881
|
+
]
|
|
9882
|
+
},
|
|
9883
|
+
{
|
|
9884
|
+
key: "maps_place_intel",
|
|
9885
|
+
label: "Place Intel",
|
|
9886
|
+
kind: "simple",
|
|
9887
|
+
endpoint: "/maps/place",
|
|
9888
|
+
mcpName: "maps_place_intel",
|
|
9889
|
+
tagline: "One place: details + reviews",
|
|
9890
|
+
params: [
|
|
9891
|
+
{ key: "businessName", label: "Business name", type: "string", required: true },
|
|
9892
|
+
{ key: "location", label: "Location", type: "string", required: true },
|
|
9893
|
+
{ key: "includeReviews", label: "Include reviews", type: "boolean", default: false },
|
|
9894
|
+
{ key: "maxReviews", label: "Max reviews", type: "number", min: 1, max: 500, default: 50, advanced: true }
|
|
9895
|
+
]
|
|
9896
|
+
}
|
|
9897
|
+
]
|
|
9898
|
+
},
|
|
9899
|
+
{
|
|
9900
|
+
key: "websites",
|
|
9901
|
+
label: "Websites",
|
|
9902
|
+
icon: "globe",
|
|
9903
|
+
tools: [
|
|
9904
|
+
{
|
|
9905
|
+
key: "scrape",
|
|
9906
|
+
label: "Scrape",
|
|
9907
|
+
kind: "modal",
|
|
9908
|
+
endpoint: "/extract-url",
|
|
9909
|
+
mcpName: "extract_url",
|
|
9910
|
+
tagline: "Headless single or bulk page extraction",
|
|
9911
|
+
modes: [
|
|
9912
|
+
{ groupKey: "scope", groupLabel: "Scope", optionKey: "single", optionLabel: "Headless Single", isDefault: true, endpointOverride: "/extract-url", mcpNameOverride: "extract_url" },
|
|
9913
|
+
{ groupKey: "scope", groupLabel: "Scope", optionKey: "bulk", optionLabel: "Headless Bulk", endpointOverride: "/extract-site", mcpNameOverride: "extract_site", paramOverrides: { rotateProxies: true } }
|
|
9914
|
+
],
|
|
9915
|
+
params: [
|
|
9916
|
+
URL_PARAM,
|
|
9917
|
+
FORMATS_PARAM,
|
|
9918
|
+
{ key: "maxPages", label: "Max pages", type: "number", min: 1, max: 1e4, default: 100, advanced: true, sourceKey: "scope:bulk" },
|
|
9919
|
+
{ key: "screenshot", label: "Screenshot", type: "boolean", advanced: true }
|
|
9920
|
+
],
|
|
9921
|
+
examples: ["https://steenshoney.com/"]
|
|
9922
|
+
},
|
|
9923
|
+
{
|
|
9924
|
+
key: "map_site_urls",
|
|
9925
|
+
label: "Map",
|
|
9926
|
+
kind: "simple",
|
|
9927
|
+
endpoint: "/map-urls",
|
|
9928
|
+
mcpName: "map_site_urls",
|
|
9929
|
+
tagline: "URL inventory only",
|
|
9930
|
+
params: [URL_PARAM, { key: "maxUrls", label: "Max URLs", type: "number", min: 1, max: 1e4, default: 100, advanced: true }]
|
|
9931
|
+
},
|
|
9932
|
+
{
|
|
9933
|
+
key: "extract_site",
|
|
9934
|
+
label: "Crawl",
|
|
9935
|
+
kind: "simple",
|
|
9936
|
+
endpoint: "/extract-site",
|
|
9937
|
+
mcpName: "extract_site",
|
|
9938
|
+
tagline: "Map + bulk scrape + SEO report",
|
|
9939
|
+
params: [
|
|
9940
|
+
URL_PARAM,
|
|
9941
|
+
{ key: "maxPages", label: "Max pages", type: "number", min: 1, max: 1e4, default: 100 },
|
|
9942
|
+
FORMATS_PARAM,
|
|
9943
|
+
{ key: "rotateProxies", label: "Rotate proxies (blocked sites)", type: "boolean", default: true },
|
|
9944
|
+
{ key: "background", label: "Run in background", type: "boolean", default: true, advanced: true }
|
|
9945
|
+
],
|
|
9946
|
+
examples: ["https://steenshoney.com/ (full-site SEO crawl)"]
|
|
9947
|
+
}
|
|
9948
|
+
]
|
|
9949
|
+
},
|
|
9950
|
+
{
|
|
9951
|
+
key: "browser",
|
|
9952
|
+
label: "Browser",
|
|
9953
|
+
icon: "monitor",
|
|
9954
|
+
tools: [
|
|
9955
|
+
{
|
|
9956
|
+
key: "browser_agent",
|
|
9957
|
+
label: "Browser Agent",
|
|
9958
|
+
kind: "simple",
|
|
9959
|
+
endpoint: "/agent",
|
|
9960
|
+
mcpName: "browser_open",
|
|
9961
|
+
tagline: "Drive a live browser (open, navigate, read, click)",
|
|
9962
|
+
params: [
|
|
9963
|
+
{ key: "url", label: "Start URL", type: "url" },
|
|
9964
|
+
{ key: "profile", label: "Use Chrome profile", type: "string", placeholder: "saved profile name", advanced: true }
|
|
9965
|
+
]
|
|
9966
|
+
},
|
|
9967
|
+
{
|
|
9968
|
+
key: "query_fanout_workflow",
|
|
9969
|
+
label: "AI Visibility (Fan-out)",
|
|
9970
|
+
kind: "simple",
|
|
9971
|
+
mcpName: "query_fanout_workflow",
|
|
9972
|
+
tagline: "Capture what ChatGPT/Claude search & cite (AEO)",
|
|
9973
|
+
params: [
|
|
9974
|
+
{ key: "prompt", label: "Prompt", type: "string" },
|
|
9975
|
+
{ key: "first_party_domain", label: "Your domain", type: "string", advanced: true },
|
|
9976
|
+
{ key: "export", label: "Export artifacts", type: "boolean", advanced: true }
|
|
9977
|
+
]
|
|
9978
|
+
}
|
|
9979
|
+
]
|
|
9980
|
+
},
|
|
9981
|
+
{
|
|
9982
|
+
key: "workflows",
|
|
9983
|
+
label: "Workflows",
|
|
9984
|
+
icon: "workflow",
|
|
9985
|
+
tools: [
|
|
9986
|
+
{
|
|
9987
|
+
key: "directory_workflow",
|
|
9988
|
+
label: "Directory",
|
|
9989
|
+
kind: "simple",
|
|
9990
|
+
endpoint: "/directory/run",
|
|
9991
|
+
mcpName: "directory_workflow",
|
|
9992
|
+
isSpecial: true,
|
|
9993
|
+
tagline: "Build a local business directory",
|
|
9994
|
+
params: [
|
|
9995
|
+
{ key: "query", label: "Business type", type: "string", required: true, placeholder: "roofers" },
|
|
9996
|
+
{ key: "state", label: "State", type: "string", required: true, placeholder: "TN" },
|
|
9997
|
+
{ key: "minPopulation", label: "Min city population", type: "number", default: 1e5, advanced: true }
|
|
9998
|
+
]
|
|
9999
|
+
},
|
|
10000
|
+
{
|
|
10001
|
+
key: "rank_tracker_workflow",
|
|
10002
|
+
label: "Rank Tracker",
|
|
10003
|
+
kind: "simple",
|
|
10004
|
+
mcpName: "rank_tracker_workflow",
|
|
10005
|
+
isSpecial: true,
|
|
10006
|
+
tagline: "Generate a rank-tracker blueprint",
|
|
10007
|
+
params: [{ key: "domain", label: "Domain", type: "string", required: true }]
|
|
10008
|
+
},
|
|
10009
|
+
{
|
|
10010
|
+
key: "deep_research_workflow",
|
|
10011
|
+
label: "Deep Research",
|
|
10012
|
+
kind: "simple",
|
|
10013
|
+
endpoint: "/workflows/run",
|
|
10014
|
+
mcpName: "workflow_run",
|
|
10015
|
+
isSpecial: true,
|
|
10016
|
+
tagline: "Multi-source, fact-checked research report",
|
|
10017
|
+
params: [{ key: "question", label: "Question", type: "string", required: true }]
|
|
10018
|
+
}
|
|
10019
|
+
]
|
|
10020
|
+
}
|
|
10021
|
+
]
|
|
10022
|
+
};
|
|
10023
|
+
}
|
|
10024
|
+
});
|
|
10025
|
+
|
|
10026
|
+
// src/api/site-audit-middleware.ts
|
|
10027
|
+
var import_factory2, siteAuditAuth;
|
|
10028
|
+
var init_site_audit_middleware = __esm({
|
|
10029
|
+
"src/api/site-audit-middleware.ts"() {
|
|
10030
|
+
"use strict";
|
|
10031
|
+
import_factory2 = require("hono/factory");
|
|
10032
|
+
init_db();
|
|
10033
|
+
siteAuditAuth = (0, import_factory2.createMiddleware)(async (c, next) => {
|
|
10034
|
+
const apiKey = c.req.header("x-api-key");
|
|
10035
|
+
if (apiKey) {
|
|
10036
|
+
const user = await getUserByApiKey(apiKey);
|
|
10037
|
+
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
10038
|
+
c.set("siteAuditUserId", user.id);
|
|
10039
|
+
return next();
|
|
10040
|
+
}
|
|
10041
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
10042
|
+
});
|
|
10043
|
+
}
|
|
10044
|
+
});
|
|
10045
|
+
|
|
10046
|
+
// src/api/site-audit-routes.ts
|
|
10047
|
+
function isPathInside(root, target) {
|
|
10048
|
+
const relative = import_node_path3.default.relative(root, target);
|
|
10049
|
+
return relative === "" || !relative.startsWith("..") && !import_node_path3.default.isAbsolute(relative);
|
|
10050
|
+
}
|
|
10051
|
+
var import_node_path3, import_node_os3, import_hono, import_zod11, siteAuditApp;
|
|
10052
|
+
var init_site_audit_routes = __esm({
|
|
10053
|
+
"src/api/site-audit-routes.ts"() {
|
|
10054
|
+
"use strict";
|
|
10055
|
+
import_node_path3 = __toESM(require("path"), 1);
|
|
10056
|
+
import_node_os3 = __toESM(require("os"), 1);
|
|
10057
|
+
import_hono = require("hono");
|
|
10058
|
+
import_zod11 = require("zod");
|
|
10059
|
+
init_site_audit_middleware();
|
|
10060
|
+
init_factory();
|
|
10061
|
+
init_phases();
|
|
10062
|
+
init_schemas();
|
|
10063
|
+
siteAuditApp = new import_hono.Hono();
|
|
10064
|
+
siteAuditApp.onError((err, c) => {
|
|
10065
|
+
if (err instanceof import_zod11.ZodError) {
|
|
10066
|
+
return c.json({ error: "Invalid request body", issues: err.issues }, 400);
|
|
10067
|
+
}
|
|
10068
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10069
|
+
return c.json({ error: msg }, 500);
|
|
10070
|
+
});
|
|
10071
|
+
siteAuditApp.use("*", siteAuditAuth);
|
|
10072
|
+
siteAuditApp.post("/audit", async (c) => {
|
|
10073
|
+
const body = SiteAuditStartRequestSchema.parse(await c.req.json());
|
|
10074
|
+
const allowedRoot = import_node_path3.default.resolve(process.env["SESSION_ROOT"] ?? import_node_os3.default.tmpdir());
|
|
10075
|
+
const sessionPath = import_node_path3.default.resolve(body.sessionPath);
|
|
10076
|
+
if (!isPathInside(allowedRoot, sessionPath)) {
|
|
10077
|
+
return c.json({ error: "sessionPath must be inside the configured session root" }, 400);
|
|
10078
|
+
}
|
|
10079
|
+
const pathFields = [
|
|
10080
|
+
body.sessionPath,
|
|
10081
|
+
body.sfInternalPath,
|
|
10082
|
+
body.sfInlinksPath,
|
|
10083
|
+
body.sfOutlinksPath,
|
|
10084
|
+
body.sitemapPath,
|
|
10085
|
+
body.competitorDomainsCsvPath,
|
|
10086
|
+
body.visualSitemapsPath,
|
|
10087
|
+
body.gscPath,
|
|
10088
|
+
body.ga4Path,
|
|
10089
|
+
body.ahrefsPath
|
|
10090
|
+
];
|
|
10091
|
+
for (const filePath of pathFields) {
|
|
10092
|
+
if (filePath != null && !isPathInside(allowedRoot, import_node_path3.default.resolve(filePath))) {
|
|
10093
|
+
return c.json({ error: "Path traversal attempt" }, 400);
|
|
10094
|
+
}
|
|
10095
|
+
}
|
|
10096
|
+
const userId = c.get("siteAuditUserId");
|
|
10097
|
+
const service = makeSiteAuditService();
|
|
10098
|
+
const jobId = await service.startAudit(userId, body);
|
|
10099
|
+
void dispatchSiteAuditPhase(service, "phase1-ingest", { jobId, request: body }).catch(
|
|
10100
|
+
(err) => {
|
|
10101
|
+
console.error(
|
|
10102
|
+
"[site-audit-routes] phase1-ingest dispatch failed for job",
|
|
10103
|
+
jobId,
|
|
10104
|
+
err instanceof Error ? err.message : String(err)
|
|
10105
|
+
);
|
|
10106
|
+
}
|
|
10107
|
+
);
|
|
10108
|
+
return c.json({ jobId }, 202);
|
|
10109
|
+
});
|
|
10110
|
+
siteAuditApp.post("/ingest", async (c) => {
|
|
10111
|
+
const body = SiteAuditIngestRequestSchema.parse(await c.req.json());
|
|
10112
|
+
const service = makeSiteAuditService();
|
|
10113
|
+
const job = await service.getJob(body.jobId);
|
|
10114
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10115
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10116
|
+
void dispatchSiteAuditPhase(service, "phase1-ingest", {
|
|
10117
|
+
jobId: body.jobId,
|
|
10118
|
+
request: JSON.parse(job.request)
|
|
10119
|
+
}).catch((err) => {
|
|
10120
|
+
console.error(
|
|
10121
|
+
"[site-audit-routes] phase1-ingest re-dispatch failed for job",
|
|
10122
|
+
body.jobId,
|
|
10123
|
+
err instanceof Error ? err.message : String(err)
|
|
10124
|
+
);
|
|
10125
|
+
});
|
|
10126
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10127
|
+
});
|
|
10128
|
+
siteAuditApp.post("/build-graph", async (c) => {
|
|
10129
|
+
const body = SiteAuditBuildGraphRequestSchema.parse(await c.req.json());
|
|
10130
|
+
const service = makeSiteAuditService();
|
|
10131
|
+
const job = await service.getJob(body.jobId);
|
|
10132
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10133
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10134
|
+
void dispatchSiteAuditPhase(service, "phase2-build-graph", { jobId: body.jobId }).catch(
|
|
10135
|
+
(err) => {
|
|
10136
|
+
console.error(
|
|
10137
|
+
"[site-audit-routes] phase2-build-graph failed for job",
|
|
10138
|
+
body.jobId,
|
|
10139
|
+
err instanceof Error ? err.message : String(err)
|
|
10140
|
+
);
|
|
10141
|
+
}
|
|
10142
|
+
);
|
|
10143
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10144
|
+
});
|
|
10145
|
+
siteAuditApp.post("/classify", async (c) => {
|
|
10146
|
+
const body = SiteAuditClassifyRequestSchema.parse(await c.req.json());
|
|
10147
|
+
const service = makeSiteAuditService();
|
|
10148
|
+
const job = await service.getJob(body.jobId);
|
|
10149
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10150
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10151
|
+
void dispatchSiteAuditPhase(service, "phase3-classify", { jobId: body.jobId }).catch(
|
|
10152
|
+
(err) => {
|
|
10153
|
+
console.error(
|
|
10154
|
+
"[site-audit-routes] phase3-classify failed for job",
|
|
10155
|
+
body.jobId,
|
|
10156
|
+
err instanceof Error ? err.message : String(err)
|
|
10157
|
+
);
|
|
10158
|
+
}
|
|
10159
|
+
);
|
|
10160
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10161
|
+
});
|
|
10162
|
+
siteAuditApp.post("/compare", async (c) => {
|
|
10163
|
+
const body = SiteAuditCompareRequestSchema.parse(await c.req.json());
|
|
10164
|
+
const service = makeSiteAuditService();
|
|
10165
|
+
const job = await service.getJob(body.jobId);
|
|
10166
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10167
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10168
|
+
void dispatchSiteAuditPhase(service, "phase4-compare", { jobId: body.jobId }).catch(
|
|
10169
|
+
(err) => {
|
|
10170
|
+
console.error(
|
|
10171
|
+
"[site-audit-routes] phase4-compare failed for job",
|
|
10172
|
+
body.jobId,
|
|
10173
|
+
err instanceof Error ? err.message : String(err)
|
|
10174
|
+
);
|
|
10175
|
+
}
|
|
10176
|
+
);
|
|
10177
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10178
|
+
});
|
|
10179
|
+
siteAuditApp.post("/score", async (c) => {
|
|
10180
|
+
const body = SiteAuditScoreRequestSchema.parse(await c.req.json());
|
|
10181
|
+
const service = makeSiteAuditService();
|
|
10182
|
+
const job = await service.getJob(body.jobId);
|
|
10183
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10184
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10185
|
+
void dispatchSiteAuditPhase(service, "phase5-synthesize", {
|
|
10186
|
+
jobId: body.jobId,
|
|
10187
|
+
scoringOnly: true,
|
|
10188
|
+
reportOnly: false
|
|
10189
|
+
}).catch((err) => {
|
|
10190
|
+
console.error(
|
|
10191
|
+
"[site-audit-routes] phase5-synthesize(scoringOnly) failed for job",
|
|
10192
|
+
body.jobId,
|
|
10193
|
+
err instanceof Error ? err.message : String(err)
|
|
10194
|
+
);
|
|
10195
|
+
});
|
|
10196
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10197
|
+
});
|
|
10198
|
+
siteAuditApp.post("/report", async (c) => {
|
|
10199
|
+
const body = SiteAuditReportRequestSchema.parse(await c.req.json());
|
|
10200
|
+
const service = makeSiteAuditService();
|
|
10201
|
+
const job = await service.getJob(body.jobId);
|
|
10202
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10203
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10204
|
+
void dispatchSiteAuditPhase(service, "phase5-synthesize", {
|
|
10205
|
+
jobId: body.jobId,
|
|
10206
|
+
scoringOnly: false,
|
|
10207
|
+
reportOnly: true
|
|
10208
|
+
}).catch((err) => {
|
|
10209
|
+
console.error(
|
|
10210
|
+
"[site-audit-routes] phase5-synthesize(reportOnly) failed for job",
|
|
10211
|
+
body.jobId,
|
|
10212
|
+
err instanceof Error ? err.message : String(err)
|
|
10213
|
+
);
|
|
10214
|
+
});
|
|
10215
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10216
|
+
});
|
|
10217
|
+
siteAuditApp.get("/jobs/:id", async (c) => {
|
|
10218
|
+
const jobId = c.req.param("id");
|
|
10219
|
+
const userId = c.get("siteAuditUserId");
|
|
10220
|
+
const service = makeSiteAuditService();
|
|
10221
|
+
const job = await service.getJob(jobId);
|
|
10222
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10223
|
+
if (job.user_id !== userId) return c.json({ error: "Not found" }, 404);
|
|
10224
|
+
return c.json(job);
|
|
10225
|
+
});
|
|
10226
|
+
siteAuditApp.get("/jobs", async (c) => {
|
|
10227
|
+
const userId = c.get("siteAuditUserId");
|
|
10228
|
+
const service = makeSiteAuditService();
|
|
10229
|
+
const jobs = await service.getJobsForUser(userId);
|
|
10230
|
+
return c.json(jobs);
|
|
10231
|
+
});
|
|
10232
|
+
}
|
|
10233
|
+
});
|
|
10234
|
+
|
|
9026
10235
|
// src/youtube/schemas.ts
|
|
9027
10236
|
var import_zod12, YouTubeHarvestOptionsSchema;
|
|
9028
10237
|
var init_schemas2 = __esm({
|
|
@@ -9184,7 +10393,7 @@ function rankCheckContextOptions(config) {
|
|
|
9184
10393
|
...config.hasTouch !== void 0 ? { hasTouch: config.hasTouch } : {}
|
|
9185
10394
|
};
|
|
9186
10395
|
}
|
|
9187
|
-
async function
|
|
10396
|
+
async function withTimeout2(promise, timeoutMs, label) {
|
|
9188
10397
|
let timeout;
|
|
9189
10398
|
try {
|
|
9190
10399
|
return await Promise.race([
|
|
@@ -9224,14 +10433,14 @@ function buildYouTubeChannelVideosUrl(channelInput) {
|
|
|
9224
10433
|
}
|
|
9225
10434
|
return `https://www.youtube.com/${handle}/videos`;
|
|
9226
10435
|
}
|
|
9227
|
-
var import_playwright_extra, import_puppeteer_extra_plugin_stealth,
|
|
10436
|
+
var import_playwright_extra, import_puppeteer_extra_plugin_stealth, import_playwright4, import_sdk5, DESKTOP_USER_AGENT, MOBILE_USER_AGENT, DEFAULT_KERNEL_BROWSER_TIMEOUT_SECONDS, KERNEL_BROWSER_CLOSE_TIMEOUT_MS, KERNEL_SESSION_DELETE_TIMEOUT_MS, DEFAULT_SERP_CHALLENGE_SOLVE_WAIT_MS, SERP_CHALLENGE_SOLVE_POLL_MS, BrowserDriver;
|
|
9228
10437
|
var init_BrowserDriver = __esm({
|
|
9229
10438
|
"src/driver/BrowserDriver.ts"() {
|
|
9230
10439
|
"use strict";
|
|
9231
10440
|
import_playwright_extra = require("playwright-extra");
|
|
9232
10441
|
import_puppeteer_extra_plugin_stealth = __toESM(require("puppeteer-extra-plugin-stealth"), 1);
|
|
9233
|
-
|
|
9234
|
-
|
|
10442
|
+
import_playwright4 = require("playwright");
|
|
10443
|
+
import_sdk5 = __toESM(require("@onkernel/sdk"), 1);
|
|
9235
10444
|
init_selectors();
|
|
9236
10445
|
init_errors();
|
|
9237
10446
|
import_playwright_extra.chromium.use((0, import_puppeteer_extra_plugin_stealth.default)());
|
|
@@ -9274,7 +10483,7 @@ var init_BrowserDriver = __esm({
|
|
|
9274
10483
|
serpNavigation: null
|
|
9275
10484
|
};
|
|
9276
10485
|
if (config.kernelApiKey) {
|
|
9277
|
-
this.kernelClient = new
|
|
10486
|
+
this.kernelClient = new import_sdk5.default({ apiKey: config.kernelApiKey });
|
|
9278
10487
|
const timeoutSeconds = positiveIntFromEnv("KERNEL_BROWSER_TIMEOUT_SECONDS", DEFAULT_KERNEL_BROWSER_TIMEOUT_SECONDS);
|
|
9279
10488
|
if (config.kernelProfileName && config.kernelProfileSaveChanges === true) {
|
|
9280
10489
|
await ensureKernelProfile(this.kernelClient, config.kernelProfileName);
|
|
@@ -9295,7 +10504,7 @@ var init_BrowserDriver = __esm({
|
|
|
9295
10504
|
let defaultProxyDisableError = null;
|
|
9296
10505
|
if (proxyMode === "none") {
|
|
9297
10506
|
try {
|
|
9298
|
-
await
|
|
10507
|
+
await withTimeout2(
|
|
9299
10508
|
this.kernelClient.browsers.update(this.kernelSessionId, { disable_default_proxy: true }),
|
|
9300
10509
|
5e3,
|
|
9301
10510
|
`Kernel session ${this.kernelSessionId} disable default proxy`
|
|
@@ -9338,7 +10547,7 @@ var init_BrowserDriver = __esm({
|
|
|
9338
10547
|
if (this.debugEnabled) {
|
|
9339
10548
|
await this.populateKernelRetrieveDebug(kernelDebug, config.kernelProxyId);
|
|
9340
10549
|
}
|
|
9341
|
-
this.browser = await
|
|
10550
|
+
this.browser = await import_playwright4.chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
|
|
9342
10551
|
this.context = await this.browser.newContext(rankCheckContextOptions(config));
|
|
9343
10552
|
await this.installEsbuildHelperShims(this.context);
|
|
9344
10553
|
this.page = await this.context.newPage();
|
|
@@ -9375,7 +10584,7 @@ var init_BrowserDriver = __esm({
|
|
|
9375
10584
|
async populateKernelRetrieveDebug(kernelDebug, requestedProxyId) {
|
|
9376
10585
|
if (!this.kernelClient || !this.kernelSessionId) return;
|
|
9377
10586
|
try {
|
|
9378
|
-
const retrieved = await
|
|
10587
|
+
const retrieved = await withTimeout2(
|
|
9379
10588
|
this.kernelClient.browsers.retrieve(this.kernelSessionId),
|
|
9380
10589
|
5e3,
|
|
9381
10590
|
`Kernel session ${this.kernelSessionId} retrieve`
|
|
@@ -9433,7 +10642,7 @@ var init_BrowserDriver = __esm({
|
|
|
9433
10642
|
error: null
|
|
9434
10643
|
};
|
|
9435
10644
|
}
|
|
9436
|
-
await
|
|
10645
|
+
await withTimeout2(
|
|
9437
10646
|
debugPage.goto("https://ipapi.co/json/", { waitUntil: "domcontentloaded", timeout: 7e3 }),
|
|
9438
10647
|
8e3,
|
|
9439
10648
|
"browser network location navigation"
|
|
@@ -9459,7 +10668,7 @@ var init_BrowserDriver = __esm({
|
|
|
9459
10668
|
}
|
|
9460
10669
|
async loadJsonInDebugPage(debugPage, url) {
|
|
9461
10670
|
try {
|
|
9462
|
-
await
|
|
10671
|
+
await withTimeout2(
|
|
9463
10672
|
debugPage.goto(url, { waitUntil: "domcontentloaded", timeout: 7e3 }),
|
|
9464
10673
|
8e3,
|
|
9465
10674
|
`browser network location navigation ${url}`
|
|
@@ -9654,12 +10863,12 @@ var init_BrowserDriver = __esm({
|
|
|
9654
10863
|
event: "kernel_browser_delete_started",
|
|
9655
10864
|
kernel_session_id: sessionId
|
|
9656
10865
|
}));
|
|
9657
|
-
const deleteSession =
|
|
10866
|
+
const deleteSession = withTimeout2(
|
|
9658
10867
|
client2.browsers.deleteByID(sessionId),
|
|
9659
10868
|
KERNEL_SESSION_DELETE_TIMEOUT_MS,
|
|
9660
10869
|
`Kernel session ${sessionId} delete`
|
|
9661
10870
|
);
|
|
9662
|
-
const closeBrowser =
|
|
10871
|
+
const closeBrowser = withTimeout2(
|
|
9663
10872
|
b.close(),
|
|
9664
10873
|
KERNEL_BROWSER_CLOSE_TIMEOUT_MS,
|
|
9665
10874
|
`Kernel browser ${sessionId} close`
|
|
@@ -10060,14 +11269,14 @@ var init_YouTubeExtractor = __esm({
|
|
|
10060
11269
|
});
|
|
10061
11270
|
|
|
10062
11271
|
// src/youtube/MP3Downloader.ts
|
|
10063
|
-
var import_node_child_process, import_node_util, import_promises3,
|
|
11272
|
+
var import_node_child_process, import_node_util, import_promises3, import_node_path4, execFileAsync, MP3Downloader;
|
|
10064
11273
|
var init_MP3Downloader = __esm({
|
|
10065
11274
|
"src/youtube/MP3Downloader.ts"() {
|
|
10066
11275
|
"use strict";
|
|
10067
11276
|
import_node_child_process = require("child_process");
|
|
10068
11277
|
import_node_util = require("util");
|
|
10069
11278
|
import_promises3 = require("fs/promises");
|
|
10070
|
-
|
|
11279
|
+
import_node_path4 = __toESM(require("path"), 1);
|
|
10071
11280
|
execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
10072
11281
|
MP3Downloader = class {
|
|
10073
11282
|
constructor(outputDir, reporter, concurrency = 3) {
|
|
@@ -10089,7 +11298,7 @@ var init_MP3Downloader = __esm({
|
|
|
10089
11298
|
}
|
|
10090
11299
|
async downloadOne(video) {
|
|
10091
11300
|
const url = video.url;
|
|
10092
|
-
const outputTemplate =
|
|
11301
|
+
const outputTemplate = import_node_path4.default.join(this.outputDir, "%(title)s.%(ext)s");
|
|
10093
11302
|
try {
|
|
10094
11303
|
const { stdout } = await execFileAsync("yt-dlp", [
|
|
10095
11304
|
"--extract-audio",
|
|
@@ -10167,24 +11376,24 @@ async function extractOnce(options) {
|
|
|
10167
11376
|
return extractor.extract(options);
|
|
10168
11377
|
}
|
|
10169
11378
|
async function writeOutputs(result, outputDir) {
|
|
10170
|
-
await
|
|
11379
|
+
await import_node_fs3.promises.mkdir(outputDir, { recursive: true });
|
|
10171
11380
|
const slug = result.target.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
10172
11381
|
const ts = Date.now();
|
|
10173
|
-
await
|
|
10174
|
-
|
|
11382
|
+
await import_node_fs3.promises.writeFile(
|
|
11383
|
+
import_node_path5.default.join(outputDir, `${slug}-${ts}.json`),
|
|
10175
11384
|
JSON.stringify(result, null, 2),
|
|
10176
11385
|
"utf8"
|
|
10177
11386
|
);
|
|
10178
11387
|
if (result.videos.length > 0) {
|
|
10179
|
-
await
|
|
10180
|
-
|
|
11388
|
+
await import_node_fs3.promises.writeFile(
|
|
11389
|
+
import_node_path5.default.join(outputDir, `${slug}-videos-${ts}.csv`),
|
|
10181
11390
|
import_papaparse.default.unparse(result.videos, { header: true }),
|
|
10182
11391
|
"utf8"
|
|
10183
11392
|
);
|
|
10184
11393
|
}
|
|
10185
11394
|
if (result.downloads.length > 0) {
|
|
10186
|
-
await
|
|
10187
|
-
|
|
11395
|
+
await import_node_fs3.promises.writeFile(
|
|
11396
|
+
import_node_path5.default.join(outputDir, `${slug}-downloads-${ts}.csv`),
|
|
10188
11397
|
import_papaparse.default.unparse(result.downloads, { header: true }),
|
|
10189
11398
|
"utf8"
|
|
10190
11399
|
);
|
|
@@ -10224,13 +11433,13 @@ async function ytHarvest(rawOptions) {
|
|
|
10224
11433
|
`YouTube blocked all ${MAX_ATTEMPTS} attempts. Try again in a few minutes.`
|
|
10225
11434
|
);
|
|
10226
11435
|
}
|
|
10227
|
-
var
|
|
11436
|
+
var import_node_fs3, import_node_path5, import_papaparse, MAX_ATTEMPTS, RETRY_DELAY_MS;
|
|
10228
11437
|
var init_youtube_harvest = __esm({
|
|
10229
11438
|
"src/youtube/youtube-harvest.ts"() {
|
|
10230
11439
|
"use strict";
|
|
10231
|
-
|
|
11440
|
+
import_node_fs3 = require("fs");
|
|
10232
11441
|
init_browser_service_env();
|
|
10233
|
-
|
|
11442
|
+
import_node_path5 = __toESM(require("path"), 1);
|
|
10234
11443
|
import_papaparse = __toESM(require("papaparse"), 1);
|
|
10235
11444
|
init_schemas2();
|
|
10236
11445
|
init_BrowserDriver();
|
|
@@ -10428,11 +11637,11 @@ async function attemptKernelWhisper(videoId, kernelApiKey, kernelProxyId, falKey
|
|
|
10428
11637
|
const audioInfo = await captureAudioViaMediaRecorder(page);
|
|
10429
11638
|
await driver.close();
|
|
10430
11639
|
if (!audioInfo || audioInfo.bytes.length < 1e4) return null;
|
|
10431
|
-
|
|
11640
|
+
import_client3.fal.config({ credentials: falKey });
|
|
10432
11641
|
const ext = audioInfo.mimeType.includes("ogg") ? "ogg" : "webm";
|
|
10433
11642
|
const audioFile = new File([new Uint8Array(audioInfo.bytes)], `audio.${ext}`, { type: audioInfo.mimeType });
|
|
10434
|
-
const uploadedUrl = await
|
|
10435
|
-
const result = await
|
|
11643
|
+
const uploadedUrl = await import_client3.fal.storage.upload(audioFile);
|
|
11644
|
+
const result = await import_client3.fal.subscribe("fal-ai/wizper", {
|
|
10436
11645
|
input: { audio_url: uploadedUrl, task: "transcribe", language: "en" },
|
|
10437
11646
|
logs: false,
|
|
10438
11647
|
pollInterval: 3e3
|
|
@@ -10487,13 +11696,13 @@ async function fetchCaptions(videoId) {
|
|
|
10487
11696
|
if (whisper) return finalizeCaptions(whisper);
|
|
10488
11697
|
throw new Error(`No captions available for ${videoId} \u2014 all three tiers failed`);
|
|
10489
11698
|
}
|
|
10490
|
-
var
|
|
11699
|
+
var import_client3, WHISPER_RECORD_SECONDS;
|
|
10491
11700
|
var init_CaptionFetcher = __esm({
|
|
10492
11701
|
"src/youtube/CaptionFetcher.ts"() {
|
|
10493
11702
|
"use strict";
|
|
10494
11703
|
init_BrowserDriver();
|
|
10495
11704
|
init_browser_service_env();
|
|
10496
|
-
|
|
11705
|
+
import_client3 = require("@fal-ai/client");
|
|
10497
11706
|
WHISPER_RECORD_SECONDS = 90;
|
|
10498
11707
|
}
|
|
10499
11708
|
});
|
|
@@ -10557,9 +11766,13 @@ var init_server_schemas = __esm({
|
|
|
10557
11766
|
});
|
|
10558
11767
|
ExtractSiteBodySchema = import_zod13.z.object({
|
|
10559
11768
|
url: import_zod13.z.string().min(1, "url is required"),
|
|
10560
|
-
maxPages: import_zod13.z.number().int().min(1).max(
|
|
11769
|
+
maxPages: import_zod13.z.number().int().min(1).max(1e4).optional(),
|
|
10561
11770
|
browserFallback: import_zod13.z.boolean().optional(),
|
|
10562
|
-
kernelFallback: import_zod13.z.boolean().optional()
|
|
11771
|
+
kernelFallback: import_zod13.z.boolean().optional(),
|
|
11772
|
+
rotateProxies: import_zod13.z.boolean().optional(),
|
|
11773
|
+
rotateProxyEvery: import_zod13.z.number().int().min(1).max(100).optional(),
|
|
11774
|
+
background: import_zod13.z.boolean().optional(),
|
|
11775
|
+
formats: import_zod13.z.array(import_zod13.z.enum(["markdown", "links", "json", "images", "branding"])).optional()
|
|
10563
11776
|
});
|
|
10564
11777
|
YoutubeHarvestBodySchema = import_zod13.z.object({
|
|
10565
11778
|
mode: import_zod13.z.enum(["search", "channel"]),
|
|
@@ -10686,7 +11899,7 @@ async function acquireConcurrencyGate(user, operation, options = {}) {
|
|
|
10686
11899
|
return { ok: true, lockId: null, active: await countActiveUsageForUser(user.id), limit, operation, reused: true };
|
|
10687
11900
|
}
|
|
10688
11901
|
await expireConcurrencyLocksForUser(user.id);
|
|
10689
|
-
const lockId = `cl_${(0,
|
|
11902
|
+
const lockId = `cl_${(0, import_node_crypto3.randomUUID)().replace(/-/g, "").slice(0, 24)}`;
|
|
10690
11903
|
const res = await getDb().execute({
|
|
10691
11904
|
sql: `INSERT INTO concurrency_locks (id, user_id, operation, status, expires_at, metadata)
|
|
10692
11905
|
SELECT ?, ?, ?, 'active', datetime('now', ?), ?
|
|
@@ -10740,11 +11953,11 @@ async function extendConcurrencyGate(lockId, ttlSeconds = DEFAULT_LOCK_TTL_SECON
|
|
|
10740
11953
|
args: [lockTtlModifier(ttlSeconds), lockId]
|
|
10741
11954
|
});
|
|
10742
11955
|
}
|
|
10743
|
-
var
|
|
11956
|
+
var import_node_crypto3, DEFAULT_LOCK_TTL_SECONDS, DEFAULT_RETRY_AFTER_SECONDS, MAX_LOCK_TTL_SECONDS, schemaReady;
|
|
10744
11957
|
var init_concurrency_gates = __esm({
|
|
10745
11958
|
"src/api/concurrency-gates.ts"() {
|
|
10746
11959
|
"use strict";
|
|
10747
|
-
|
|
11960
|
+
import_node_crypto3 = require("crypto");
|
|
10748
11961
|
init_db();
|
|
10749
11962
|
init_rates();
|
|
10750
11963
|
DEFAULT_LOCK_TTL_SECONDS = 15 * 60;
|
|
@@ -10768,10 +11981,10 @@ function buildTranscriptMarkdown(result) {
|
|
|
10768
11981
|
if (result.chunks?.length) {
|
|
10769
11982
|
lines.push("## Timestamped Segments");
|
|
10770
11983
|
lines.push("");
|
|
10771
|
-
for (const
|
|
10772
|
-
const start = fmtTs(
|
|
10773
|
-
const end = fmtTs(
|
|
10774
|
-
lines.push(`**[${start} \u2192 ${end}]** ${
|
|
11984
|
+
for (const chunk2 of result.chunks) {
|
|
11985
|
+
const start = fmtTs(chunk2.timestamp[0]);
|
|
11986
|
+
const end = fmtTs(chunk2.timestamp[1]);
|
|
11987
|
+
lines.push(`**[${start} \u2192 ${end}]** ${chunk2.text.trim()}`);
|
|
10775
11988
|
lines.push("");
|
|
10776
11989
|
}
|
|
10777
11990
|
}
|
|
@@ -11409,20 +12622,20 @@ var init_FacebookAdExtractor = __esm({
|
|
|
11409
12622
|
if (splitRe.lastIndex === m.index) splitRe.lastIndex++;
|
|
11410
12623
|
}
|
|
11411
12624
|
if (last < bodyText.length) adChunks.push(bodyText.slice(last));
|
|
11412
|
-
const ads = adChunks.filter((c) => /Library ID[:\s]+\d{10,20}/i.test(c)).slice(0, maxAds).map((
|
|
11413
|
-
const lidM =
|
|
12625
|
+
const ads = adChunks.filter((c) => /Library ID[:\s]+\d{10,20}/i.test(c)).slice(0, maxAds).map((chunk2) => {
|
|
12626
|
+
const lidM = chunk2.match(/Library ID[:\s]+([0-9]{10,20})/i);
|
|
11414
12627
|
const libraryId = lidM ? lidM[1] : null;
|
|
11415
|
-
const statusM =
|
|
12628
|
+
const statusM = chunk2.match(/^(Active|Inactive)/i);
|
|
11416
12629
|
const status = statusM ? statusM[1] : null;
|
|
11417
|
-
const startM =
|
|
12630
|
+
const startM = chunk2.match(/Started running on\s+([A-Za-z]+\.?\s+\d{1,2},?\s*\d{4})/i);
|
|
11418
12631
|
const started = startM ? startM[1].replace(/\s+/g, " ").trim() : null;
|
|
11419
12632
|
const visual = adVisuals[libraryId ?? ""] ?? { imageSrc: null, videoSrc: null, videoPoster: null };
|
|
11420
|
-
const hasVideoText = /0:00\s*\/\s*0:00|\d+:\d+\s*\/\s*\d+:\d+/.test(
|
|
12633
|
+
const hasVideoText = /0:00\s*\/\s*0:00|\d+:\d+\s*\/\s*\d+:\d+/.test(chunk2);
|
|
11421
12634
|
const creativeType = visual.videoSrc || hasVideoText ? "video" : visual.imageSrc ? "image" : "unknown";
|
|
11422
12635
|
let domain = null;
|
|
11423
12636
|
const domainRe = /\b([A-Z0-9]{2,}(?:\.[A-Z]{2,})+)\b/g;
|
|
11424
12637
|
let dm;
|
|
11425
|
-
while ((dm = domainRe.exec(
|
|
12638
|
+
while ((dm = domainRe.exec(chunk2)) !== null) {
|
|
11426
12639
|
const c = dm[1];
|
|
11427
12640
|
if (c.length >= 6 && c !== "LIBRARY") {
|
|
11428
12641
|
domain = c;
|
|
@@ -11431,9 +12644,9 @@ var init_FacebookAdExtractor = __esm({
|
|
|
11431
12644
|
}
|
|
11432
12645
|
let primaryText = null;
|
|
11433
12646
|
let headline = null;
|
|
11434
|
-
const sponsoredIdx =
|
|
12647
|
+
const sponsoredIdx = chunk2.search(/\bSponsored\b/i);
|
|
11435
12648
|
if (sponsoredIdx >= 0) {
|
|
11436
|
-
let afterSponsored =
|
|
12649
|
+
let afterSponsored = chunk2.slice(sponsoredIdx + "Sponsored".length).trim();
|
|
11437
12650
|
afterSponsored = afterSponsored.replace(/\s*\d*:?\d+\s*\/\s*\d*:?\d+.*$/, "");
|
|
11438
12651
|
if (domain) {
|
|
11439
12652
|
const di = afterSponsored.indexOf(domain);
|
|
@@ -11444,18 +12657,18 @@ var init_FacebookAdExtractor = __esm({
|
|
|
11444
12657
|
}
|
|
11445
12658
|
const ctaRe2 = new RegExp("\\b(" + CTA_LABELS.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") + ")\\b", "i");
|
|
11446
12659
|
if (domain) {
|
|
11447
|
-
const di =
|
|
12660
|
+
const di = chunk2.indexOf(domain);
|
|
11448
12661
|
if (di >= 0) {
|
|
11449
|
-
let afterDomain =
|
|
12662
|
+
let afterDomain = chunk2.slice(di + domain.length).trim();
|
|
11450
12663
|
const ctaMatch = afterDomain.search(ctaRe2);
|
|
11451
12664
|
if (ctaMatch >= 0) afterDomain = afterDomain.slice(0, ctaMatch).trim();
|
|
11452
12665
|
if (afterDomain.length > 3 && afterDomain.length < 120) headline = afterDomain;
|
|
11453
12666
|
}
|
|
11454
12667
|
}
|
|
11455
12668
|
if (!headline) {
|
|
11456
|
-
const ctaIdx =
|
|
12669
|
+
const ctaIdx = chunk2.search(ctaRe2);
|
|
11457
12670
|
if (ctaIdx > 0) {
|
|
11458
|
-
const beforeCta =
|
|
12671
|
+
const beforeCta = chunk2.slice(0, ctaIdx).trimEnd();
|
|
11459
12672
|
const sentenceEnd = Math.max(beforeCta.lastIndexOf(". "), beforeCta.lastIndexOf("! "), beforeCta.lastIndexOf("? "));
|
|
11460
12673
|
if (sentenceEnd >= 0) {
|
|
11461
12674
|
const candidate = beforeCta.slice(sentenceEnd + 2).trim();
|
|
@@ -11467,12 +12680,12 @@ var init_FacebookAdExtractor = __esm({
|
|
|
11467
12680
|
}
|
|
11468
12681
|
let cta = null;
|
|
11469
12682
|
for (const lbl of CTA_LABELS) {
|
|
11470
|
-
if (
|
|
12683
|
+
if (chunk2.toLowerCase().includes(lbl.toLowerCase())) {
|
|
11471
12684
|
cta = lbl;
|
|
11472
12685
|
break;
|
|
11473
12686
|
}
|
|
11474
12687
|
}
|
|
11475
|
-
const multiM =
|
|
12688
|
+
const multiM = chunk2.match(/(\d+)\s+ad[s]?\s+use[s]?\s+this/i);
|
|
11476
12689
|
const clusterCount = multiM ? parseInt(multiM[1]) : null;
|
|
11477
12690
|
return {
|
|
11478
12691
|
libraryId,
|
|
@@ -11495,8 +12708,8 @@ var init_FacebookAdExtractor = __esm({
|
|
|
11495
12708
|
const unknownCreativeCount = ads.filter((a) => a.creativeType === "unknown").length;
|
|
11496
12709
|
const advertiserPageId = new URL(page.url()).searchParams.get("view_all_page_id") ?? null;
|
|
11497
12710
|
const nameFreq = /* @__PURE__ */ new Map();
|
|
11498
|
-
for (const
|
|
11499
|
-
const detailsParts =
|
|
12711
|
+
for (const chunk2 of adChunks) {
|
|
12712
|
+
const detailsParts = chunk2.split("See ad details ");
|
|
11500
12713
|
if (detailsParts.length < 2) continue;
|
|
11501
12714
|
const sponsoredIdx = detailsParts[1].indexOf(" Sponsored");
|
|
11502
12715
|
if (sponsoredIdx < 0) continue;
|
|
@@ -12221,7 +13434,7 @@ async function createFreshLocationProxy(kernel, options, target) {
|
|
|
12221
13434
|
}
|
|
12222
13435
|
async function deleteKernelProxyId(kernelApiKey, proxyId) {
|
|
12223
13436
|
if (!kernelApiKey || !proxyId) return;
|
|
12224
|
-
const kernel = new
|
|
13437
|
+
const kernel = new import_sdk6.default({ apiKey: kernelApiKey });
|
|
12225
13438
|
await kernel.proxies.delete(proxyId);
|
|
12226
13439
|
}
|
|
12227
13440
|
async function resolveKernelProxyId(options) {
|
|
@@ -12235,7 +13448,7 @@ async function resolveKernelProxyId(options) {
|
|
|
12235
13448
|
if (!target || !options.kernelApiKey) {
|
|
12236
13449
|
return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, target ? null : "location could not be normalized to a US city/state proxy target");
|
|
12237
13450
|
}
|
|
12238
|
-
const kernel = new
|
|
13451
|
+
const kernel = new import_sdk6.default({ apiKey: options.kernelApiKey });
|
|
12239
13452
|
try {
|
|
12240
13453
|
const attemptIndex = options.attemptIndex ?? 0;
|
|
12241
13454
|
if (options.fresh) {
|
|
@@ -12341,11 +13554,11 @@ async function resolveKernelProxyId(options) {
|
|
|
12341
13554
|
return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, errorText2(err));
|
|
12342
13555
|
}
|
|
12343
13556
|
}
|
|
12344
|
-
var
|
|
13557
|
+
var import_sdk6, US_STATE_CODES, US_CITY_CENTER_ZIPS;
|
|
12345
13558
|
var init_kernel_proxy_resolver = __esm({
|
|
12346
13559
|
"src/kernel-proxy-resolver.ts"() {
|
|
12347
13560
|
"use strict";
|
|
12348
|
-
|
|
13561
|
+
import_sdk6 = __toESM(require("@onkernel/sdk"), 1);
|
|
12349
13562
|
init_uule();
|
|
12350
13563
|
US_STATE_CODES = {
|
|
12351
13564
|
alabama: "AL",
|
|
@@ -12561,7 +13774,7 @@ function transcriptMarkdown(title, text, chunks, durationMs) {
|
|
|
12561
13774
|
}
|
|
12562
13775
|
async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript") {
|
|
12563
13776
|
const startMs = Date.now();
|
|
12564
|
-
const result = await
|
|
13777
|
+
const result = await import_client4.fal.subscribe("fal-ai/wizper", {
|
|
12565
13778
|
input: { audio_url: mediaUrl, task: "transcribe", language: "en" },
|
|
12566
13779
|
logs: false,
|
|
12567
13780
|
pollInterval: 3e3
|
|
@@ -12577,11 +13790,11 @@ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript"
|
|
|
12577
13790
|
markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs)
|
|
12578
13791
|
};
|
|
12579
13792
|
}
|
|
12580
|
-
var
|
|
13793
|
+
var import_client4;
|
|
12581
13794
|
var init_media_transcription = __esm({
|
|
12582
13795
|
"src/services/media-transcription.ts"() {
|
|
12583
13796
|
"use strict";
|
|
12584
|
-
|
|
13797
|
+
import_client4 = require("@fal-ai/client");
|
|
12585
13798
|
}
|
|
12586
13799
|
});
|
|
12587
13800
|
|
|
@@ -12624,7 +13837,7 @@ async function kernelLaunchOptsResidential() {
|
|
|
12624
13837
|
}
|
|
12625
13838
|
return { headless: true, kernelApiKey: browserServiceApiKey(), kernelProxyId: proxyId, viewport: { width: 1280, height: 900 }, locale: "en-US" };
|
|
12626
13839
|
}
|
|
12627
|
-
var import_hono4, import_zod16,
|
|
13840
|
+
var import_hono4, import_zod16, import_client5, FacebookAdBodySchema, FacebookPageIntelBodySchema, FacebookTranscribeBodySchema, FacebookVideoTranscribeBodySchema, FacebookSearchBodySchema, FacebookMediaBodySchema, facebookAdApp, ALLOWED_MEDIA_HOSTS;
|
|
12628
13841
|
var init_facebook_ad_routes = __esm({
|
|
12629
13842
|
"src/api/facebook-ad-routes.ts"() {
|
|
12630
13843
|
"use strict";
|
|
@@ -12639,7 +13852,7 @@ var init_facebook_ad_routes = __esm({
|
|
|
12639
13852
|
init_FacebookOrganicVideoExtractor();
|
|
12640
13853
|
init_kernel_proxy_resolver();
|
|
12641
13854
|
init_schemas3();
|
|
12642
|
-
|
|
13855
|
+
import_client5 = require("@fal-ai/client");
|
|
12643
13856
|
init_api_auth();
|
|
12644
13857
|
init_url_utils();
|
|
12645
13858
|
init_concurrency_gates();
|
|
@@ -12782,7 +13995,7 @@ var init_facebook_ad_routes = __esm({
|
|
|
12782
13995
|
metadata: { videoUrl }
|
|
12783
13996
|
});
|
|
12784
13997
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
12785
|
-
|
|
13998
|
+
import_client5.fal.config({ credentials: process.env.FAL_KEY });
|
|
12786
13999
|
let debited = false;
|
|
12787
14000
|
try {
|
|
12788
14001
|
const { ok, balance_mc } = await debitMc(fbUser.id, MC_COSTS.fb_transcribe, LedgerOperation.FB_TRANSCRIBE, videoUrl);
|
|
@@ -12821,7 +14034,7 @@ var init_facebook_ad_routes = __esm({
|
|
|
12821
14034
|
metadata: { url: sourceUrl.href }
|
|
12822
14035
|
});
|
|
12823
14036
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
12824
|
-
|
|
14037
|
+
import_client5.fal.config({ credentials: process.env.FAL_KEY });
|
|
12825
14038
|
const driver = new BrowserDriver();
|
|
12826
14039
|
let debited = false;
|
|
12827
14040
|
try {
|
|
@@ -12915,11 +14128,11 @@ var init_facebook_ad_routes = __esm({
|
|
|
12915
14128
|
}
|
|
12916
14129
|
if (last < bodyText.length) adChunks.push(bodyText.slice(last));
|
|
12917
14130
|
const advertiserMap = /* @__PURE__ */ new Map();
|
|
12918
|
-
for (const
|
|
12919
|
-
const lidM =
|
|
14131
|
+
for (const chunk2 of adChunks) {
|
|
14132
|
+
const lidM = chunk2.match(/Library ID[:\s]+([0-9]{10,20})/i);
|
|
12920
14133
|
if (!lidM) continue;
|
|
12921
14134
|
const libraryId = lidM[1];
|
|
12922
|
-
const detailsParts =
|
|
14135
|
+
const detailsParts = chunk2.split("See ad details ");
|
|
12923
14136
|
if (detailsParts.length < 2) continue;
|
|
12924
14137
|
const afterDetails = detailsParts[1];
|
|
12925
14138
|
const sponsoredIdx = afterDetails.indexOf(" Sponsored");
|
|
@@ -13396,7 +14609,7 @@ async function resolveInstagramLaunch(body) {
|
|
|
13396
14609
|
};
|
|
13397
14610
|
}
|
|
13398
14611
|
function outputBaseDir() {
|
|
13399
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
14612
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Downloads", "mcp-scraper");
|
|
13400
14613
|
}
|
|
13401
14614
|
function safeFilePart(input) {
|
|
13402
14615
|
return input.replace(/^https?:\/\//, "").replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "instagram";
|
|
@@ -13404,8 +14617,8 @@ function safeFilePart(input) {
|
|
|
13404
14617
|
function mediaOutputDir(shortcode, sourceUrl) {
|
|
13405
14618
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
13406
14619
|
const slug = shortcode ? `instagram-${shortcode}` : safeFilePart(sourceUrl);
|
|
13407
|
-
const dir = (0,
|
|
13408
|
-
(0,
|
|
14620
|
+
const dir = (0, import_node_path6.join)(outputBaseDir(), "instagram", `${stamp}-${slug}`);
|
|
14621
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
13409
14622
|
return dir;
|
|
13410
14623
|
}
|
|
13411
14624
|
async function downloadToFile(url, destPath, referer) {
|
|
@@ -13418,10 +14631,10 @@ async function downloadToFile(url, destPath, referer) {
|
|
|
13418
14631
|
});
|
|
13419
14632
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
13420
14633
|
if (!res.body) throw new Error("Empty response body");
|
|
13421
|
-
await (0, import_promises4.pipeline)(import_node_stream2.Readable.fromWeb(res.body), (0,
|
|
14634
|
+
await (0, import_promises4.pipeline)(import_node_stream2.Readable.fromWeb(res.body), (0, import_node_fs4.createWriteStream)(destPath));
|
|
13422
14635
|
return {
|
|
13423
14636
|
savedPath: destPath,
|
|
13424
|
-
sizeBytes: (0,
|
|
14637
|
+
sizeBytes: (0, import_node_fs4.statSync)(destPath).size,
|
|
13425
14638
|
mimeType: res.headers.get("content-type")?.split(";")[0]?.trim() ?? null
|
|
13426
14639
|
};
|
|
13427
14640
|
}
|
|
@@ -13443,8 +14656,8 @@ function runFfmpegMux(videoPath, audioPath, outPath) {
|
|
|
13443
14656
|
stdio: ["ignore", "ignore", "pipe"]
|
|
13444
14657
|
});
|
|
13445
14658
|
let stderr = "";
|
|
13446
|
-
child.stderr.on("data", (
|
|
13447
|
-
stderr += String(
|
|
14659
|
+
child.stderr.on("data", (chunk2) => {
|
|
14660
|
+
stderr += String(chunk2);
|
|
13448
14661
|
});
|
|
13449
14662
|
child.on("error", (err) => resolve({ ok: false, error: err.message }));
|
|
13450
14663
|
child.on("close", (code) => resolve({ ok: code === 0, error: code === 0 ? null : stderr.trim() || `ffmpeg exited ${code}` }));
|
|
@@ -13456,15 +14669,15 @@ function trackFilename(track, index, shortcode) {
|
|
|
13456
14669
|
const bitrate = track.bitrate ? `-${track.bitrate}` : "";
|
|
13457
14670
|
return `${label}-${type}-${index}${bitrate}.mp4`;
|
|
13458
14671
|
}
|
|
13459
|
-
var import_hono5, import_zod17,
|
|
14672
|
+
var import_hono5, import_zod17, import_node_fs4, import_node_os4, import_node_path6, import_node_stream2, import_promises4, import_node_child_process2, InstagramProfileContentBodySchema, InstagramMediaTypeSchema, InstagramMediaDownloadBodySchema, instagramApp;
|
|
13460
14673
|
var init_instagram_routes = __esm({
|
|
13461
14674
|
"src/api/instagram-routes.ts"() {
|
|
13462
14675
|
"use strict";
|
|
13463
14676
|
import_hono5 = require("hono");
|
|
13464
14677
|
import_zod17 = require("zod");
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
14678
|
+
import_node_fs4 = require("fs");
|
|
14679
|
+
import_node_os4 = require("os");
|
|
14680
|
+
import_node_path6 = require("path");
|
|
13468
14681
|
import_node_stream2 = require("stream");
|
|
13469
14682
|
import_promises4 = require("stream/promises");
|
|
13470
14683
|
import_node_child_process2 = require("child_process");
|
|
@@ -13592,16 +14805,16 @@ var init_instagram_routes = __esm({
|
|
|
13592
14805
|
let outputDir = null;
|
|
13593
14806
|
if (body.downloadMedia) {
|
|
13594
14807
|
outputDir = mediaOutputDir(extraction.shortcode, sourceUrl.href);
|
|
13595
|
-
const textPath = (0,
|
|
13596
|
-
(0,
|
|
14808
|
+
const textPath = (0, import_node_path6.join)(outputDir, `${extraction.shortcode ?? "instagram"}-text.txt`);
|
|
14809
|
+
(0, import_node_fs4.writeFileSync)(textPath, [
|
|
13597
14810
|
`URL: ${extraction.pageUrl}`,
|
|
13598
14811
|
extraction.caption ? `Caption: ${extraction.caption}` : "",
|
|
13599
14812
|
"",
|
|
13600
14813
|
extraction.bodyText
|
|
13601
14814
|
].filter(Boolean).join("\n"), "utf8");
|
|
13602
|
-
downloads.push({ kind: "text", url: null, savedPath: textPath, sizeBytes: (0,
|
|
14815
|
+
downloads.push({ kind: "text", url: null, savedPath: textPath, sizeBytes: (0, import_node_fs4.statSync)(textPath).size, mimeType: "text/plain", error: null });
|
|
13603
14816
|
if (mediaTypes.has("image") && extraction.imageUrl) {
|
|
13604
|
-
const imagePathBase = (0,
|
|
14817
|
+
const imagePathBase = (0, import_node_path6.join)(outputDir, `${extraction.shortcode ?? "instagram"}-image`);
|
|
13605
14818
|
try {
|
|
13606
14819
|
const downloaded = await downloadToFile(extraction.imageUrl, imagePathBase, extraction.pageUrl);
|
|
13607
14820
|
const ext = extFromMime(downloaded.mimeType, "jpg");
|
|
@@ -13610,7 +14823,7 @@ var init_instagram_routes = __esm({
|
|
|
13610
14823
|
const { renameSync } = await import("fs");
|
|
13611
14824
|
renameSync(downloaded.savedPath, finalPath);
|
|
13612
14825
|
}
|
|
13613
|
-
downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: finalPath, sizeBytes: (0,
|
|
14826
|
+
downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: finalPath, sizeBytes: (0, import_node_fs4.statSync)(finalPath).size, mimeType: downloaded.mimeType, error: null });
|
|
13614
14827
|
} catch (err) {
|
|
13615
14828
|
downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
|
|
13616
14829
|
}
|
|
@@ -13621,7 +14834,7 @@ var init_instagram_routes = __esm({
|
|
|
13621
14834
|
if (track.streamType === "video" && !mediaTypes.has("video")) continue;
|
|
13622
14835
|
if (track.streamType === "audio" && !mediaTypes.has("audio")) continue;
|
|
13623
14836
|
const kind = track.streamType === "audio" ? "audio" : "video";
|
|
13624
|
-
const target = (0,
|
|
14837
|
+
const target = (0, import_node_path6.join)(outputDir, trackFilename(track, index, extraction.shortcode));
|
|
13625
14838
|
try {
|
|
13626
14839
|
const downloaded = await downloadToFile(track.url, target, extraction.pageUrl);
|
|
13627
14840
|
downloads.push({ kind, url: track.url, savedPath: downloaded.savedPath, sizeBytes: downloaded.sizeBytes, mimeType: downloaded.mimeType, error: null });
|
|
@@ -13631,10 +14844,10 @@ var init_instagram_routes = __esm({
|
|
|
13631
14844
|
}
|
|
13632
14845
|
}
|
|
13633
14846
|
if (body.mux && mediaTypes.has("video") && mediaTypes.has("audio") && extraction.selectedVideoTrack && extraction.selectedAudioTrack && savedByUrl.has(extraction.selectedVideoTrack.url) && savedByUrl.has(extraction.selectedAudioTrack.url)) {
|
|
13634
|
-
const outPath = (0,
|
|
14847
|
+
const outPath = (0, import_node_path6.join)(outputDir, `${extraction.shortcode ?? "instagram"}-muxed.mp4`);
|
|
13635
14848
|
const muxed = await runFfmpegMux(savedByUrl.get(extraction.selectedVideoTrack.url), savedByUrl.get(extraction.selectedAudioTrack.url), outPath);
|
|
13636
14849
|
if (muxed.ok) {
|
|
13637
|
-
downloads.push({ kind: "muxed_video", url: null, savedPath: outPath, sizeBytes: (0,
|
|
14850
|
+
downloads.push({ kind: "muxed_video", url: null, savedPath: outPath, sizeBytes: (0, import_node_fs4.statSync)(outPath).size, mimeType: "video/mp4", error: null });
|
|
13638
14851
|
} else {
|
|
13639
14852
|
warnings.push(`Mux skipped/failed: ${muxed.error}`);
|
|
13640
14853
|
}
|
|
@@ -14669,13 +15882,13 @@ var init_maps_routes = __esm({
|
|
|
14669
15882
|
});
|
|
14670
15883
|
|
|
14671
15884
|
// src/mcp/workflow-catalog.ts
|
|
14672
|
-
function
|
|
15885
|
+
function normalize2(value) {
|
|
14673
15886
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
14674
15887
|
}
|
|
14675
15888
|
function suggestWorkflowRecipes(goal, limit = 3) {
|
|
14676
|
-
const normalized =
|
|
15889
|
+
const normalized = normalize2(goal);
|
|
14677
15890
|
const scored = WORKFLOW_RECIPES.map((recipe) => {
|
|
14678
|
-
const haystack =
|
|
15891
|
+
const haystack = normalize2([
|
|
14679
15892
|
recipe.id,
|
|
14680
15893
|
recipe.title,
|
|
14681
15894
|
recipe.description,
|
|
@@ -14806,16 +16019,93 @@ function reportTitle(full) {
|
|
|
14806
16019
|
return title?.replace(/^#\s+/, "").trim() || "MCP Scraper Report";
|
|
14807
16020
|
}
|
|
14808
16021
|
function outputBaseDir2() {
|
|
14809
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
16022
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path7.join)((0, import_node_os5.homedir)(), "Downloads", "mcp-scraper");
|
|
14810
16023
|
}
|
|
14811
16024
|
function saveFullReport(full) {
|
|
14812
16025
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
14813
16026
|
const outDir = outputBaseDir2();
|
|
14814
16027
|
try {
|
|
14815
|
-
(0,
|
|
16028
|
+
(0, import_node_fs5.mkdirSync)(outDir, { recursive: true });
|
|
16029
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
16030
|
+
const file = (0, import_node_path7.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
|
|
16031
|
+
(0, import_node_fs5.writeFileSync)(file, full, "utf8");
|
|
16032
|
+
return file;
|
|
16033
|
+
} catch {
|
|
16034
|
+
return null;
|
|
16035
|
+
}
|
|
16036
|
+
}
|
|
16037
|
+
function reportSavingActive() {
|
|
16038
|
+
return reportSavingEnabled && process.env.MCP_SCRAPER_SAVE_REPORTS !== "false";
|
|
16039
|
+
}
|
|
16040
|
+
function saveBulkSite(siteUrl, pages, seo) {
|
|
16041
|
+
if (!reportSavingActive()) return null;
|
|
16042
|
+
try {
|
|
16043
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
16044
|
+
const dir = (0, import_node_path7.join)(outputBaseDir2(), `extract-${slugifyReportName(siteUrl)}-${stamp}`);
|
|
16045
|
+
const pagesDir = (0, import_node_path7.join)(dir, "pages");
|
|
16046
|
+
(0, import_node_fs5.mkdirSync)(pagesDir, { recursive: true });
|
|
16047
|
+
const indexRows = pages.map((p, i) => {
|
|
16048
|
+
const num = String(i + 1).padStart(4, "0");
|
|
16049
|
+
const slug = slugifyReportName(p.url.replace(/^https?:\/\//, "")).slice(0, 60) || "page";
|
|
16050
|
+
const fname = `${num}-${slug}.md`;
|
|
16051
|
+
const body = (p.bodyMarkdown ?? "").trim();
|
|
16052
|
+
const content = [
|
|
16053
|
+
`# ${p.title ?? "Untitled"}`,
|
|
16054
|
+
`- **URL:** ${p.url}`,
|
|
16055
|
+
p.metaDescription ? `- **Description:** ${p.metaDescription}` : "",
|
|
16056
|
+
p.schemaTypes?.length ? `- **Schema:** ${p.schemaTypes.join(", ")}` : "",
|
|
16057
|
+
"",
|
|
16058
|
+
body || "_(no content extracted)_"
|
|
16059
|
+
].filter(Boolean).join("\n");
|
|
16060
|
+
(0, import_node_fs5.writeFileSync)((0, import_node_path7.join)(pagesDir, fname), content, "utf8");
|
|
16061
|
+
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | pages/${fname} |`;
|
|
16062
|
+
});
|
|
16063
|
+
const index = [
|
|
16064
|
+
`# Site Extract: ${siteUrl}`,
|
|
16065
|
+
`**${pages.length} pages** \u2014 each saved as an individual Markdown file under \`pages/\`.`,
|
|
16066
|
+
`
|
|
16067
|
+
## Pages
|
|
16068
|
+
| # | Title | URL | File |
|
|
16069
|
+
|---|-------|-----|------|
|
|
16070
|
+
${indexRows.join("\n")}`
|
|
16071
|
+
].join("\n");
|
|
16072
|
+
const indexFile = (0, import_node_path7.join)(dir, "index.md");
|
|
16073
|
+
(0, import_node_fs5.writeFileSync)(indexFile, index, "utf8");
|
|
16074
|
+
let seoFiles;
|
|
16075
|
+
if (seo) {
|
|
16076
|
+
const toJsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n");
|
|
16077
|
+
const pagesJsonl = (0, import_node_path7.join)(dir, "pages.jsonl");
|
|
16078
|
+
const linksJsonl = (0, import_node_path7.join)(dir, "links.jsonl");
|
|
16079
|
+
const metricsJsonl = (0, import_node_path7.join)(dir, "link-metrics.jsonl");
|
|
16080
|
+
const issuesFile = (0, import_node_path7.join)(dir, "issues.json");
|
|
16081
|
+
const reportFile = (0, import_node_path7.join)(dir, "report.md");
|
|
16082
|
+
(0, import_node_fs5.writeFileSync)(pagesJsonl, toJsonl(seo.pageRows), "utf8");
|
|
16083
|
+
(0, import_node_fs5.writeFileSync)(linksJsonl, toJsonl(seo.edges), "utf8");
|
|
16084
|
+
(0, import_node_fs5.writeFileSync)(metricsJsonl, toJsonl(seo.metrics), "utf8");
|
|
16085
|
+
(0, import_node_fs5.writeFileSync)(issuesFile, JSON.stringify(seo.issues, null, 2), "utf8");
|
|
16086
|
+
(0, import_node_fs5.writeFileSync)(reportFile, seo.reportMd, "utf8");
|
|
16087
|
+
seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile];
|
|
16088
|
+
if (seo.branding) {
|
|
16089
|
+
const brandingFile = (0, import_node_path7.join)(dir, "branding.json");
|
|
16090
|
+
(0, import_node_fs5.writeFileSync)(brandingFile, JSON.stringify(seo.branding, null, 2), "utf8");
|
|
16091
|
+
seoFiles.push(brandingFile);
|
|
16092
|
+
}
|
|
16093
|
+
}
|
|
16094
|
+
return { dir, indexFile, fileCount: pages.length, seoFiles };
|
|
16095
|
+
} catch {
|
|
16096
|
+
return null;
|
|
16097
|
+
}
|
|
16098
|
+
}
|
|
16099
|
+
function saveUrlInventory(siteUrl, urls) {
|
|
16100
|
+
if (!reportSavingActive()) return null;
|
|
16101
|
+
try {
|
|
16102
|
+
const outDir = outputBaseDir2();
|
|
16103
|
+
(0, import_node_fs5.mkdirSync)(outDir, { recursive: true });
|
|
14816
16104
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
14817
|
-
const file = (0,
|
|
14818
|
-
(
|
|
16105
|
+
const file = (0, import_node_path7.join)(outDir, `${stamp}-urlmap-${slugifyReportName(siteUrl.replace(/^https?:\/\//, ""))}.csv`);
|
|
16106
|
+
const csv = (v) => /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
|
|
16107
|
+
const rows = ["url,status", ...urls.map((u) => `${csv(u.url)},${u.status ?? ""}`)];
|
|
16108
|
+
(0, import_node_fs5.writeFileSync)(file, rows.join("\n"), "utf8");
|
|
14819
16109
|
return file;
|
|
14820
16110
|
} catch {
|
|
14821
16111
|
return null;
|
|
@@ -14824,19 +16114,19 @@ function saveFullReport(full) {
|
|
|
14824
16114
|
function persistScreenshotLocally(base64, url) {
|
|
14825
16115
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
14826
16116
|
try {
|
|
14827
|
-
const dir = (0,
|
|
14828
|
-
(0,
|
|
16117
|
+
const dir = (0, import_node_path7.join)(outputBaseDir2(), "screenshots");
|
|
16118
|
+
(0, import_node_fs5.mkdirSync)(dir, { recursive: true });
|
|
14829
16119
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
14830
16120
|
const slug = url.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
14831
|
-
const filePath = (0,
|
|
14832
|
-
(0,
|
|
16121
|
+
const filePath = (0, import_node_path7.join)(dir, `${stamp}-${slug}.png`);
|
|
16122
|
+
(0, import_node_fs5.writeFileSync)(filePath, Buffer.from(base64, "base64"));
|
|
14833
16123
|
return filePath;
|
|
14834
16124
|
} catch {
|
|
14835
16125
|
return null;
|
|
14836
16126
|
}
|
|
14837
16127
|
}
|
|
14838
|
-
function oneBlock(content) {
|
|
14839
|
-
const filePath = saveFullReport(content);
|
|
16128
|
+
function oneBlock(content, diskContent) {
|
|
16129
|
+
const filePath = saveFullReport(diskContent ?? content);
|
|
14840
16130
|
const text = filePath ? `${content}
|
|
14841
16131
|
|
|
14842
16132
|
\u{1F4C4} Saved: \`${filePath}\`` : content;
|
|
@@ -15105,7 +16395,12 @@ ${[h1Lines, h2Lines].filter(Boolean).join("\n")}` : "";
|
|
|
15105
16395
|
].filter(Boolean).join("\n") : "";
|
|
15106
16396
|
const bodySection = bodyMd ? `
|
|
15107
16397
|
## Page Content
|
|
15108
|
-
${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ?
|
|
16398
|
+
${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? `
|
|
16399
|
+
|
|
16400
|
+
*(truncated to 3,000 of ${bodyMd.length.toLocaleString()} chars \u2014 full content in the saved report)*` : ""}` : "";
|
|
16401
|
+
const bodySectionFull = bodyMd ? `
|
|
16402
|
+
## Page Content
|
|
16403
|
+
${bodyMd}` : "";
|
|
15109
16404
|
const screenshotSection = screenshotMeta ? `
|
|
15110
16405
|
## Screenshot
|
|
15111
16406
|
- **File:** ${screenshotPath ?? "(returned inline only \u2014 disk write unavailable in this environment)"}
|
|
@@ -15136,7 +16431,10 @@ ${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? "\n\n*(truncated)*" : ""}` : "";
|
|
|
15136
16431
|
const full = `# URL Extract: ${url}
|
|
15137
16432
|
**${title}**
|
|
15138
16433
|
${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSection}${mediaSection}${tips}`;
|
|
15139
|
-
const
|
|
16434
|
+
const diskReport = `# URL Extract: ${url}
|
|
16435
|
+
**${title}**
|
|
16436
|
+
${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${screenshotSection}${mediaSection}${tips}`;
|
|
16437
|
+
const textResult = oneBlock(full, diskReport);
|
|
15140
16438
|
const structuredContent = {
|
|
15141
16439
|
url,
|
|
15142
16440
|
title: d.title ?? null,
|
|
@@ -15169,7 +16467,16 @@ function formatMapSiteUrls(raw, input) {
|
|
|
15169
16467
|
const ok = urls.filter((u) => (u.status ?? 0) >= 200 && (u.status ?? 0) < 300);
|
|
15170
16468
|
const broken = urls.filter((u) => u.status !== null && u.status >= 400);
|
|
15171
16469
|
const redirects = urls.filter((u) => u.status !== null && u.status >= 300 && u.status < 400);
|
|
15172
|
-
const
|
|
16470
|
+
const isBulk = urls.length > BULK_URL_THRESHOLD;
|
|
16471
|
+
const inventoryFile = isBulk ? saveUrlInventory(input.url, urls.map((u) => ({ url: u.url, status: u.status ?? null }))) : null;
|
|
16472
|
+
const inlineCount = isBulk ? BULK_URL_THRESHOLD : 200;
|
|
16473
|
+
const urlRows = urls.slice(0, inlineCount).map((u, i) => `| ${i + 1} | ${u.url} | ${u.status ?? "\u2014"} |`).join("\n");
|
|
16474
|
+
const inventoryNote = isBulk ? inventoryFile ? `
|
|
16475
|
+
## \u{1F4C1} Full inventory saved
|
|
16476
|
+
- **File:** \`${inventoryFile}\` (CSV: url, status \u2014 all ${urls.length} URLs)
|
|
16477
|
+
Showing the first ${inlineCount} below; read the file for the complete list.` : `
|
|
16478
|
+
## \u26A0\uFE0F Full inventory not saved
|
|
16479
|
+
Report saving is disabled, so only the first ${inlineCount} of ${urls.length} URLs are shown. Enable \`MCP_SCRAPER_SAVE_REPORTS\` to capture the full inventory.` : "";
|
|
15173
16480
|
const full = [
|
|
15174
16481
|
`# URL Map: ${input.url}`,
|
|
15175
16482
|
`**${d.totalFound} URLs** \xB7 ${(d.durationMs / 1e3).toFixed(1)}s${d.truncated ? " \xB7 *truncated*" : ""}`,
|
|
@@ -15178,12 +16485,13 @@ function formatMapSiteUrls(raw, input) {
|
|
|
15178
16485
|
- \u2705 2xx: ${ok.length}
|
|
15179
16486
|
- \u{1F500} 3xx: ${redirects.length}
|
|
15180
16487
|
- \u274C 4xx+: ${broken.length}`,
|
|
16488
|
+
inventoryNote,
|
|
15181
16489
|
`
|
|
15182
|
-
## URL Inventory
|
|
16490
|
+
## URL Inventory${isBulk ? ` (first ${inlineCount} of ${urls.length})` : ""}
|
|
15183
16491
|
| # | URL | Status |
|
|
15184
16492
|
|---|-----|--------|
|
|
15185
16493
|
${urlRows}`,
|
|
15186
|
-
broken.length ? `
|
|
16494
|
+
!isBulk && broken.length ? `
|
|
15187
16495
|
## Broken URLs
|
|
15188
16496
|
${broken.map((u) => `- ${u.url} (${u.status})`).join("\n")}` : "",
|
|
15189
16497
|
`
|
|
@@ -15201,47 +16509,116 @@ ${broken.map((u) => `- ${u.url} (${u.status})`).join("\n")}` : "",
|
|
|
15201
16509
|
okCount: ok.length,
|
|
15202
16510
|
redirectCount: redirects.length,
|
|
15203
16511
|
brokenCount: broken.length,
|
|
16512
|
+
inventoryFile: inventoryFile ?? null,
|
|
15204
16513
|
urls: urls.map((u) => ({ url: u.url, status: u.status ?? null })),
|
|
15205
16514
|
durationMs: d.durationMs ?? 0
|
|
15206
16515
|
}
|
|
15207
16516
|
};
|
|
15208
16517
|
}
|
|
16518
|
+
function buildSeoExport(siteUrl, pages, branding) {
|
|
16519
|
+
const { edges, metrics } = buildLinkGraph(pages, siteUrl);
|
|
16520
|
+
const pageRows = pages.map((p) => {
|
|
16521
|
+
const { bodyMarkdown: _b, schema: _s, outlinks: _o, ...rest } = p;
|
|
16522
|
+
const m = metrics.get(p.url);
|
|
16523
|
+
return { ...rest, inlinks: m?.inlinks ?? 0, crawlDepth: m?.crawlDepth ?? null, orphan: m?.orphan ?? false };
|
|
16524
|
+
});
|
|
16525
|
+
const issues = computeIssues(pages, metrics);
|
|
16526
|
+
return {
|
|
16527
|
+
pageRows,
|
|
16528
|
+
edges,
|
|
16529
|
+
metrics: [...metrics.values()],
|
|
16530
|
+
issues,
|
|
16531
|
+
reportMd: renderIssueReport(siteUrl, pages, issues, metrics),
|
|
16532
|
+
branding: branding ?? void 0
|
|
16533
|
+
};
|
|
16534
|
+
}
|
|
15209
16535
|
function formatExtractSite(raw, input) {
|
|
15210
16536
|
const parsed = parseData(raw);
|
|
15211
16537
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
15212
16538
|
const d = parsed.data;
|
|
15213
16539
|
const pages = d.pages ?? [];
|
|
15214
|
-
const
|
|
15215
|
-
|
|
16540
|
+
const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
|
|
16541
|
+
const pageRow = (p, i) => {
|
|
16542
|
+
const schemaInfo = schemaTypesOf(p).join(", ") || "\u2014";
|
|
15216
16543
|
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | ${schemaInfo} |`;
|
|
15217
|
-
}
|
|
15218
|
-
const
|
|
16544
|
+
};
|
|
16545
|
+
const tips = `
|
|
16546
|
+
---
|
|
16547
|
+
\u{1F4A1} **Tips**
|
|
16548
|
+
- Map URLs first: use \`map_site_urls\`
|
|
16549
|
+
- Inspect a single page: use \`extract_url\``;
|
|
16550
|
+
const durationLine = `**${pages.length} pages** \xB7 ${((d.durationMs ?? 0) / 1e3).toFixed(1)}s`;
|
|
16551
|
+
const structuredContent = {
|
|
16552
|
+
url: input.url,
|
|
16553
|
+
pageCount: pages.length,
|
|
16554
|
+
pages: pages.map((p) => ({
|
|
16555
|
+
url: String(p.url ?? ""),
|
|
16556
|
+
title: p.title ?? null,
|
|
16557
|
+
schemaTypes: schemaTypesOf(p)
|
|
16558
|
+
})),
|
|
16559
|
+
durationMs: d.durationMs ?? 0
|
|
16560
|
+
};
|
|
16561
|
+
if (pages.length > BULK_PAGE_THRESHOLD) {
|
|
16562
|
+
const seo = buildSeoExport(input.url, pages, d.branding);
|
|
16563
|
+
const bulk = saveBulkSite(input.url, pages.map((p) => ({
|
|
16564
|
+
url: String(p.url ?? ""),
|
|
16565
|
+
title: p.title ?? null,
|
|
16566
|
+
bodyMarkdown: p.bodyMarkdown ?? null,
|
|
16567
|
+
metaDescription: p.metaDescription ?? null,
|
|
16568
|
+
schemaTypes: schemaTypesOf(p)
|
|
16569
|
+
})), seo);
|
|
16570
|
+
const preview = pages.slice(0, BULK_PAGE_THRESHOLD).map(pageRow).join("\n");
|
|
16571
|
+
const seoLine = bulk?.seoFiles?.length ? `
|
|
16572
|
+
- **SEO data:** \`report.md\` (issues summary), \`issues.json\` (findings + offender URLs), \`pages.jsonl\` (per-page fields), \`links.jsonl\` (link edges), \`link-metrics.jsonl\` (inlinks / crawl depth / orphans)` : "";
|
|
16573
|
+
const location2 = bulk ? `
|
|
16574
|
+
## \u{1F4C1} Bulk scrape saved
|
|
16575
|
+
- **Folder:** \`${bulk.dir}\`
|
|
16576
|
+
- **Index:** \`${bulk.indexFile}\`
|
|
16577
|
+
- **Files:** ${bulk.fileCount} page files under \`pages/\`${seoLine}
|
|
16578
|
+
|
|
16579
|
+
Read \`index.md\` for the full page list, open files in \`pages/\` for full content, or read \`pages.jsonl\` / \`link-metrics.jsonl\` for the SEO crawl data. Page bodies are not inlined here to keep the context window clear.` : `
|
|
16580
|
+
## \u26A0\uFE0F Bulk scrape not saved
|
|
16581
|
+
Report saving is disabled in this environment, so the ${pages.length} pages of content could not be written to disk and are omitted to protect the context window. Enable \`MCP_SCRAPER_SAVE_REPORTS\` to capture bulk crawls.`;
|
|
16582
|
+
const full2 = [
|
|
16583
|
+
`# Site Extract: ${input.url}`,
|
|
16584
|
+
durationLine,
|
|
16585
|
+
location2,
|
|
16586
|
+
`
|
|
16587
|
+
## Pages (first ${Math.min(BULK_PAGE_THRESHOLD, pages.length)} of ${pages.length})
|
|
16588
|
+
| # | Title | URL | Schema |
|
|
16589
|
+
|---|-------|-----|--------|
|
|
16590
|
+
${preview}`,
|
|
16591
|
+
tips
|
|
16592
|
+
].join("\n");
|
|
16593
|
+
return { content: [{ type: "text", text: full2 }], structuredContent: { ...structuredContent, bulkFolder: bulk?.dir ?? null } };
|
|
16594
|
+
}
|
|
16595
|
+
const header = [
|
|
15219
16596
|
`# Site Extract: ${input.url}`,
|
|
15220
|
-
|
|
16597
|
+
durationLine,
|
|
15221
16598
|
`
|
|
15222
16599
|
## Pages
|
|
15223
16600
|
| # | Title | URL | Schema |
|
|
15224
16601
|
|---|-------|-----|--------|
|
|
15225
|
-
${
|
|
15226
|
-
`
|
|
15227
|
-
---
|
|
15228
|
-
\u{1F4A1} **Tips**
|
|
15229
|
-
- Map URLs first: use \`map_site_urls\`
|
|
15230
|
-
- Inspect a single page: use \`extract_url\``
|
|
16602
|
+
${pages.map(pageRow).join("\n")}`
|
|
15231
16603
|
].join("\n");
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
15244
|
-
}
|
|
16604
|
+
const full = `${header}${tips}`;
|
|
16605
|
+
const pageDetails = pages.map((p, i) => {
|
|
16606
|
+
const body = (p.bodyMarkdown ?? "").trim();
|
|
16607
|
+
return [
|
|
16608
|
+
`
|
|
16609
|
+
## ${i + 1}. ${p.title ?? "Untitled"}`,
|
|
16610
|
+
`- **URL:** ${p.url}`,
|
|
16611
|
+
p.metaDescription ? `- **Description:** ${p.metaDescription}` : "",
|
|
16612
|
+
body ? `
|
|
16613
|
+
${body}` : "_(no content extracted)_"
|
|
16614
|
+
].filter(Boolean).join("\n");
|
|
16615
|
+
}).join("\n");
|
|
16616
|
+
const diskReport = `${header}
|
|
16617
|
+
|
|
16618
|
+
---
|
|
16619
|
+
# Full Page Content
|
|
16620
|
+
${pageDetails}${tips}`;
|
|
16621
|
+
return { ...oneBlock(full, diskReport), structuredContent };
|
|
15245
16622
|
}
|
|
15246
16623
|
function formatYoutubeHarvest(raw, input) {
|
|
15247
16624
|
const parsed = parseData(raw);
|
|
@@ -16417,16 +17794,20 @@ ${rows}` : ""
|
|
|
16417
17794
|
}
|
|
16418
17795
|
};
|
|
16419
17796
|
}
|
|
16420
|
-
var
|
|
17797
|
+
var import_node_fs5, import_node_os5, import_node_path7, reportSavingEnabled, BULK_PAGE_THRESHOLD, BULK_URL_THRESHOLD;
|
|
16421
17798
|
var init_mcp_response_formatter = __esm({
|
|
16422
17799
|
"src/mcp/mcp-response-formatter.ts"() {
|
|
16423
17800
|
"use strict";
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
17801
|
+
import_node_fs5 = require("fs");
|
|
17802
|
+
import_node_os5 = require("os");
|
|
17803
|
+
import_node_path7 = require("path");
|
|
16427
17804
|
init_errors();
|
|
16428
17805
|
init_workflow_catalog();
|
|
17806
|
+
init_seo_link_graph();
|
|
17807
|
+
init_seo_issues();
|
|
16429
17808
|
reportSavingEnabled = true;
|
|
17809
|
+
BULK_PAGE_THRESHOLD = 25;
|
|
17810
|
+
BULK_URL_THRESHOLD = 500;
|
|
16430
17811
|
}
|
|
16431
17812
|
});
|
|
16432
17813
|
|
|
@@ -16819,11 +18200,11 @@ function csvRowsFor(result) {
|
|
|
16819
18200
|
return rows;
|
|
16820
18201
|
}
|
|
16821
18202
|
async function saveDirectoryCsv(result) {
|
|
16822
|
-
const outDir = (0,
|
|
18203
|
+
const outDir = (0, import_node_path8.join)(outputBaseDir2(), "directory-workflows");
|
|
16823
18204
|
await (0, import_promises6.mkdir)(outDir, { recursive: true });
|
|
16824
18205
|
const stamp = result.extractedAt.replace(/[:.]/g, "-");
|
|
16825
18206
|
const slug = `${result.state}-${result.query}`.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
16826
|
-
const path6 = (0,
|
|
18207
|
+
const path6 = (0, import_node_path8.join)(outDir, `${stamp}-${slug}-directory-workflow.csv`);
|
|
16827
18208
|
const headers = [
|
|
16828
18209
|
"source_query",
|
|
16829
18210
|
"source_location",
|
|
@@ -16881,12 +18262,12 @@ async function runDirectoryWorkflowFromPlan(options, plan) {
|
|
|
16881
18262
|
const csvPath = options.saveCsv ? await saveDirectoryCsv(base) : null;
|
|
16882
18263
|
return { ...base, csvPath };
|
|
16883
18264
|
}
|
|
16884
|
-
var import_promises6,
|
|
18265
|
+
var import_promises6, import_node_path8, import_zod19, DirectoryWorkflowOptionsSchema;
|
|
16885
18266
|
var init_directory_workflow = __esm({
|
|
16886
18267
|
"src/directory/directory-workflow.ts"() {
|
|
16887
18268
|
"use strict";
|
|
16888
18269
|
import_promises6 = require("fs/promises");
|
|
16889
|
-
|
|
18270
|
+
import_node_path8 = require("path");
|
|
16890
18271
|
import_zod19 = require("zod");
|
|
16891
18272
|
init_mcp_response_formatter();
|
|
16892
18273
|
init_browser_service_env();
|
|
@@ -17013,7 +18394,7 @@ var init_slugify = __esm({
|
|
|
17013
18394
|
|
|
17014
18395
|
// src/workflows/artifact-writer.ts
|
|
17015
18396
|
function workflowOutputBaseDir(outputDir) {
|
|
17016
|
-
return outputDir?.trim() || process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
18397
|
+
return outputDir?.trim() || process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path9.join)((0, import_node_os6.homedir)(), "Downloads", "mcp-scraper");
|
|
17017
18398
|
}
|
|
17018
18399
|
function timestamp() {
|
|
17019
18400
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -17022,11 +18403,11 @@ function safeSlug(value) {
|
|
|
17022
18403
|
return slugify(value).replace(/^-+|-+$/g, "").slice(0, 80) || "run";
|
|
17023
18404
|
}
|
|
17024
18405
|
function indexPath(baseDir = workflowOutputBaseDir()) {
|
|
17025
|
-
return (0,
|
|
18406
|
+
return (0, import_node_path9.join)(baseDir, "workflows", "index.json");
|
|
17026
18407
|
}
|
|
17027
18408
|
async function readIndex(baseDir) {
|
|
17028
18409
|
const path6 = indexPath(baseDir);
|
|
17029
|
-
if (!(0,
|
|
18410
|
+
if (!(0, import_node_fs6.existsSync)(path6)) return { runs: [] };
|
|
17030
18411
|
try {
|
|
17031
18412
|
return JSON.parse(await (0, import_promises7.readFile)(path6, "utf8"));
|
|
17032
18413
|
} catch {
|
|
@@ -17048,17 +18429,17 @@ async function updateWorkflowIndex(manifest, manifestPath, baseDir) {
|
|
|
17048
18429
|
summary: `${manifest.title} (${manifest.status})`
|
|
17049
18430
|
};
|
|
17050
18431
|
index.runs = [entry, ...index.runs.filter((r) => r.runId !== manifest.runId)].slice(0, 200);
|
|
17051
|
-
await (0, import_promises7.mkdir)((0,
|
|
18432
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path6), { recursive: true });
|
|
17052
18433
|
await (0, import_promises7.writeFile)(path6, JSON.stringify(index, null, 2), "utf8");
|
|
17053
18434
|
}
|
|
17054
|
-
var import_promises7,
|
|
18435
|
+
var import_promises7, import_node_fs6, import_node_os6, import_node_path9, import_node_child_process3, ArtifactWriter;
|
|
17055
18436
|
var init_artifact_writer = __esm({
|
|
17056
18437
|
"src/workflows/artifact-writer.ts"() {
|
|
17057
18438
|
"use strict";
|
|
17058
18439
|
import_promises7 = require("fs/promises");
|
|
17059
|
-
|
|
17060
|
-
|
|
17061
|
-
|
|
18440
|
+
import_node_fs6 = require("fs");
|
|
18441
|
+
import_node_os6 = require("os");
|
|
18442
|
+
import_node_path9 = require("path");
|
|
17062
18443
|
import_node_child_process3 = require("child_process");
|
|
17063
18444
|
init_csv();
|
|
17064
18445
|
init_slugify();
|
|
@@ -17085,7 +18466,7 @@ var init_artifact_writer = __esm({
|
|
|
17085
18466
|
const runId = forcedRunId?.trim() || `${timestamp()}-${safeSlug(workflowId)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
17086
18467
|
const nameSource = String(input.keyword ?? input.query ?? input.domain ?? input.state ?? workflowId);
|
|
17087
18468
|
const baseDir = workflowOutputBaseDir(outputDir);
|
|
17088
|
-
const runDir = (0,
|
|
18469
|
+
const runDir = (0, import_node_path9.join)(baseDir, "workflows", workflowId, `${timestamp()}-${safeSlug(nameSource)}-${safeSlug(runId).slice(0, 20)}`);
|
|
17089
18470
|
await (0, import_promises7.mkdir)(runDir, { recursive: true });
|
|
17090
18471
|
return new _ArtifactWriter(workflowId, title, runId, baseDir, runDir, startedAt, input);
|
|
17091
18472
|
}
|
|
@@ -17095,20 +18476,20 @@ var init_artifact_writer = __esm({
|
|
|
17095
18476
|
return path6;
|
|
17096
18477
|
}
|
|
17097
18478
|
async writeJson(label, relativePath, data) {
|
|
17098
|
-
const path6 = (0,
|
|
17099
|
-
await (0, import_promises7.mkdir)((0,
|
|
18479
|
+
const path6 = (0, import_node_path9.join)(this.runDir, relativePath);
|
|
18480
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path6), { recursive: true });
|
|
17100
18481
|
await (0, import_promises7.writeFile)(path6, JSON.stringify(data, null, 2), "utf8");
|
|
17101
18482
|
return this.remember("json", label, path6);
|
|
17102
18483
|
}
|
|
17103
18484
|
async writeText(label, relativePath, text, kind = "markdown") {
|
|
17104
|
-
const path6 = (0,
|
|
17105
|
-
await (0, import_promises7.mkdir)((0,
|
|
18485
|
+
const path6 = (0, import_node_path9.join)(this.runDir, relativePath);
|
|
18486
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path6), { recursive: true });
|
|
17106
18487
|
await (0, import_promises7.writeFile)(path6, text, "utf8");
|
|
17107
18488
|
return this.remember(kind, label, path6);
|
|
17108
18489
|
}
|
|
17109
18490
|
async writeCsv(label, relativePath, headers, rows) {
|
|
17110
|
-
const path6 = (0,
|
|
17111
|
-
await (0, import_promises7.mkdir)((0,
|
|
18491
|
+
const path6 = (0, import_node_path9.join)(this.runDir, relativePath);
|
|
18492
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path6), { recursive: true });
|
|
17112
18493
|
await (0, import_promises7.writeFile)(path6, rowsToCsv(headers, rows), "utf8");
|
|
17113
18494
|
return this.remember("csv", label, path6, rows.length);
|
|
17114
18495
|
}
|
|
@@ -17129,7 +18510,7 @@ var init_artifact_writer = __esm({
|
|
|
17129
18510
|
errors,
|
|
17130
18511
|
counts
|
|
17131
18512
|
};
|
|
17132
|
-
const path6 = (0,
|
|
18513
|
+
const path6 = (0, import_node_path9.join)(this.runDir, "manifest.json");
|
|
17133
18514
|
await (0, import_promises7.writeFile)(path6, JSON.stringify(manifest, null, 2), "utf8");
|
|
17134
18515
|
await updateWorkflowIndex(manifest, path6, this.baseDir);
|
|
17135
18516
|
return path6;
|
|
@@ -18709,7 +20090,7 @@ async function readManifestFromSummary(summary) {
|
|
|
18709
20090
|
function webhookSignature(body, timestamp2) {
|
|
18710
20091
|
const secret2 = process.env.MCP_SCRAPER_WEBHOOK_SECRET?.trim();
|
|
18711
20092
|
if (!secret2) return null;
|
|
18712
|
-
return (0,
|
|
20093
|
+
return (0, import_node_crypto4.createHmac)("sha256", secret2).update(`${timestamp2}.${body}`).digest("hex");
|
|
18713
20094
|
}
|
|
18714
20095
|
async function deliverWorkflowWebhook(input) {
|
|
18715
20096
|
if (!input.webhookUrl) return;
|
|
@@ -18916,11 +20297,11 @@ async function dispatchDueWorkflowSchedules(apiUrl, limit = 3) {
|
|
|
18916
20297
|
}
|
|
18917
20298
|
return { dispatched: results.length, results };
|
|
18918
20299
|
}
|
|
18919
|
-
var
|
|
20300
|
+
var import_node_crypto4, import_promises9, import_hono8, import_zod25, workflowApp, WorkflowInputSchema, WorkflowIdSchema, CadenceSchema, ScheduleStatusSchema, RunBodySchema, ScheduleCreateSchema, SchedulePatchSchema, TERMINAL_RUN_STATUSES;
|
|
18920
20301
|
var init_workflow_routes = __esm({
|
|
18921
20302
|
"src/api/workflow-routes.ts"() {
|
|
18922
20303
|
"use strict";
|
|
18923
|
-
|
|
20304
|
+
import_node_crypto4 = require("crypto");
|
|
18924
20305
|
import_promises9 = require("fs/promises");
|
|
18925
20306
|
import_hono8 = require("hono");
|
|
18926
20307
|
import_zod25 = require("zod");
|
|
@@ -19201,7 +20582,7 @@ var init_workflow_routes = __esm({
|
|
|
19201
20582
|
// src/serp-intelligence/page-snapshot-extractor.ts
|
|
19202
20583
|
function sha256(value) {
|
|
19203
20584
|
if (!value) return null;
|
|
19204
|
-
return (0,
|
|
20585
|
+
return (0, import_node_crypto5.createHash)("sha256").update(value).digest("hex");
|
|
19205
20586
|
}
|
|
19206
20587
|
function countWords(markdown) {
|
|
19207
20588
|
const matches = markdown.trim().match(/\b[\p{L}\p{N}][\p{L}\p{N}'-]*\b/gu);
|
|
@@ -19250,7 +20631,7 @@ function countLinks(html, baseUrl) {
|
|
|
19250
20631
|
totalLinkCount: internalLinkCount + externalLinkCount
|
|
19251
20632
|
};
|
|
19252
20633
|
}
|
|
19253
|
-
function
|
|
20634
|
+
function withTimeout3(operation, timeoutMs) {
|
|
19254
20635
|
return new Promise((resolve, reject) => {
|
|
19255
20636
|
const timer = setTimeout(() => reject(new Error(`Page snapshot timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
19256
20637
|
operation.then(
|
|
@@ -19367,7 +20748,7 @@ async function capturePageSnapshot(target, options = {}) {
|
|
|
19367
20748
|
});
|
|
19368
20749
|
}
|
|
19369
20750
|
try {
|
|
19370
|
-
const extraction = await
|
|
20751
|
+
const extraction = await withTimeout3(
|
|
19371
20752
|
extractKpo({ url: checked.parsed.href, kernelApiKey }),
|
|
19372
20753
|
timeoutMs
|
|
19373
20754
|
);
|
|
@@ -19501,11 +20882,11 @@ async function capturePageSnapshots(targets, options = {}) {
|
|
|
19501
20882
|
}
|
|
19502
20883
|
};
|
|
19503
20884
|
}
|
|
19504
|
-
var
|
|
20885
|
+
var import_node_crypto5, import_p_limit3, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_CONCURRENCY, DEFAULT_MAX_CONTENT_CHARS;
|
|
19505
20886
|
var init_page_snapshot_extractor = __esm({
|
|
19506
20887
|
"src/serp-intelligence/page-snapshot-extractor.ts"() {
|
|
19507
20888
|
"use strict";
|
|
19508
|
-
|
|
20889
|
+
import_node_crypto5 = require("crypto");
|
|
19509
20890
|
import_p_limit3 = __toESM(require("p-limit"), 1);
|
|
19510
20891
|
init_kpo_extractor();
|
|
19511
20892
|
init_url_utils();
|
|
@@ -20750,52 +22131,52 @@ var init_PAAExtractor = __esm({
|
|
|
20750
22131
|
});
|
|
20751
22132
|
|
|
20752
22133
|
// src/output/OutputSerializer.ts
|
|
20753
|
-
var
|
|
22134
|
+
var import_node_fs7, import_node_path10, import_papaparse2, OutputSerializer;
|
|
20754
22135
|
var init_OutputSerializer = __esm({
|
|
20755
22136
|
"src/output/OutputSerializer.ts"() {
|
|
20756
22137
|
"use strict";
|
|
20757
|
-
|
|
20758
|
-
|
|
22138
|
+
import_node_fs7 = require("fs");
|
|
22139
|
+
import_node_path10 = __toESM(require("path"), 1);
|
|
20759
22140
|
import_papaparse2 = __toESM(require("papaparse"), 1);
|
|
20760
22141
|
OutputSerializer = class {
|
|
20761
22142
|
async writeJSON(result, outputDir) {
|
|
20762
|
-
await
|
|
22143
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20763
22144
|
const slug = result.seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20764
22145
|
const filename = `${slug}-${Date.now()}.json`;
|
|
20765
|
-
const fullPath =
|
|
20766
|
-
await
|
|
22146
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22147
|
+
await import_node_fs7.promises.writeFile(fullPath, JSON.stringify(result, null, 2), "utf8");
|
|
20767
22148
|
return fullPath;
|
|
20768
22149
|
}
|
|
20769
22150
|
async writeCSV(rows, outputDir) {
|
|
20770
|
-
await
|
|
22151
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20771
22152
|
const seedRaw = rows[0]?.seed_query ?? "paa";
|
|
20772
22153
|
const slug = seedRaw.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20773
22154
|
const csv = import_papaparse2.default.unparse(rows, { header: true });
|
|
20774
22155
|
const filename = `${slug}-${Date.now()}.csv`;
|
|
20775
|
-
const fullPath =
|
|
20776
|
-
await
|
|
22156
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22157
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20777
22158
|
return fullPath;
|
|
20778
22159
|
}
|
|
20779
22160
|
async writeVideoCSV(videos, seed, outputDir) {
|
|
20780
|
-
await
|
|
22161
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20781
22162
|
const slug = seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20782
22163
|
const csv = import_papaparse2.default.unparse(videos, { header: true });
|
|
20783
22164
|
const filename = `${slug}-videos-${Date.now()}.csv`;
|
|
20784
|
-
const fullPath =
|
|
20785
|
-
await
|
|
22165
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22166
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20786
22167
|
return fullPath;
|
|
20787
22168
|
}
|
|
20788
22169
|
async writeForumCSV(forums, seed, outputDir) {
|
|
20789
|
-
await
|
|
22170
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20790
22171
|
const slug = seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20791
22172
|
const csv = import_papaparse2.default.unparse(forums, { header: true });
|
|
20792
22173
|
const filename = `${slug}-forums-${Date.now()}.csv`;
|
|
20793
|
-
const fullPath =
|
|
20794
|
-
await
|
|
22174
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22175
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20795
22176
|
return fullPath;
|
|
20796
22177
|
}
|
|
20797
22178
|
async writeAIOverviewCSV(citations, text, seed, outputDir) {
|
|
20798
|
-
await
|
|
22179
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20799
22180
|
const slug = seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20800
22181
|
const rows = citations.map((c, i) => ({
|
|
20801
22182
|
seed_query: seed,
|
|
@@ -20805,12 +22186,12 @@ var init_OutputSerializer = __esm({
|
|
|
20805
22186
|
}));
|
|
20806
22187
|
const csv = import_papaparse2.default.unparse(rows, { header: true });
|
|
20807
22188
|
const filename = `${slug}-ai-overview-${Date.now()}.csv`;
|
|
20808
|
-
const fullPath =
|
|
20809
|
-
await
|
|
22189
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22190
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20810
22191
|
return fullPath;
|
|
20811
22192
|
}
|
|
20812
22193
|
async writeAIModeCSV(citations, text, seed, outputDir) {
|
|
20813
|
-
await
|
|
22194
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20814
22195
|
const slug = seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20815
22196
|
const rows = citations.map((c, i) => ({
|
|
20816
22197
|
seed_query: seed,
|
|
@@ -20820,18 +22201,18 @@ var init_OutputSerializer = __esm({
|
|
|
20820
22201
|
}));
|
|
20821
22202
|
const csv = import_papaparse2.default.unparse(rows, { header: true });
|
|
20822
22203
|
const filename = `${slug}-ai-mode-${Date.now()}.csv`;
|
|
20823
|
-
const fullPath =
|
|
20824
|
-
await
|
|
22204
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22205
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20825
22206
|
return fullPath;
|
|
20826
22207
|
}
|
|
20827
22208
|
async writeWhatPeopleSayingCSV(cards, seed, outputDir) {
|
|
20828
|
-
await
|
|
22209
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20829
22210
|
const slug = seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20830
22211
|
const rows = cards.map((c) => ({ seed_query: seed, ...c }));
|
|
20831
22212
|
const csv = import_papaparse2.default.unparse(rows, { header: true });
|
|
20832
22213
|
const filename = `${slug}-what-people-saying-${Date.now()}.csv`;
|
|
20833
|
-
const fullPath =
|
|
20834
|
-
await
|
|
22214
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22215
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20835
22216
|
return fullPath;
|
|
20836
22217
|
}
|
|
20837
22218
|
};
|
|
@@ -21958,7 +23339,40 @@ var PACKAGE_VERSION;
|
|
|
21958
23339
|
var init_version = __esm({
|
|
21959
23340
|
"src/version.ts"() {
|
|
21960
23341
|
"use strict";
|
|
21961
|
-
PACKAGE_VERSION = "0.3.
|
|
23342
|
+
PACKAGE_VERSION = "0.3.15";
|
|
23343
|
+
}
|
|
23344
|
+
});
|
|
23345
|
+
|
|
23346
|
+
// src/mcp/server-instructions.ts
|
|
23347
|
+
var SERVER_INSTRUCTIONS;
|
|
23348
|
+
var init_server_instructions = __esm({
|
|
23349
|
+
"src/mcp/server-instructions.ts"() {
|
|
23350
|
+
"use strict";
|
|
23351
|
+
SERVER_INSTRUCTIONS = `
|
|
23352
|
+
This server scrapes and analyzes web, social, search, maps, and site data. Pick a tool by NAME using
|
|
23353
|
+
this routing map, then load its schema before calling.
|
|
23354
|
+
|
|
23355
|
+
ROUTING
|
|
23356
|
+
- One web page -> extract_url. Whole site (crawl + SEO report) -> extract_site. Just the URL list -> map_site_urls.
|
|
23357
|
+
- Web search (organic SERP) -> search_serp. Full SERP + People-Also-Ask / AI-Overview detail -> harvest_paa.
|
|
23358
|
+
- Google Maps: find places -> maps_search; one place deep-dive + reviews -> maps_place_intel.
|
|
23359
|
+
- YouTube: find or list videos -> youtube_harvest; transcribe one video -> youtube_transcribe.
|
|
23360
|
+
- Facebook: find ads -> facebook_ad_search; transcribe an ad -> facebook_ad_transcribe; transcribe a
|
|
23361
|
+
video -> facebook_video_transcribe; page profile/intel -> facebook_page_intel.
|
|
23362
|
+
- Instagram: profile inventory -> instagram_profile_content; one post or reel -> instagram_media_download.
|
|
23363
|
+
- Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
|
|
23364
|
+
AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
|
|
23365
|
+
- Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
|
|
23366
|
+
(browser_open, then navigate/read); save/reuse logins via browser_profile_*.
|
|
23367
|
+
|
|
23368
|
+
NOTES
|
|
23369
|
+
- Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
|
|
23370
|
+
It discovers URLs and returns a saved folder/artifact plus a summary, not the full content inline.
|
|
23371
|
+
- Tools that open a browser session or transcribe media cost credits and take longer. Prefer the
|
|
23372
|
+
cheapest tool that answers the question (plain search/extract before browser agents).
|
|
23373
|
+
- Large results are saved to disk or an artifact and returned as a summary plus a path; read the path
|
|
23374
|
+
for full detail rather than expecting the whole payload inline.
|
|
23375
|
+
`.trim();
|
|
21962
23376
|
}
|
|
21963
23377
|
});
|
|
21964
23378
|
|
|
@@ -21991,11 +23405,14 @@ var init_mcp_tool_schemas = __esm({
|
|
|
21991
23405
|
};
|
|
21992
23406
|
MapSiteUrlsInputSchema = {
|
|
21993
23407
|
url: import_zod27.z.string().url().describe("Public website URL or domain to crawl for internal URLs. Use before extract_site when the user asks to audit/map/crawl a site."),
|
|
21994
|
-
maxUrls: import_zod27.z.number().int().min(1).max(
|
|
23408
|
+
maxUrls: import_zod27.z.number().int().min(1).max(1e4).optional().describe("Maximum URLs to discover. Use 100 for normal maps, up to 10000 for a full inventory. Large maps (over 500 URLs) write the complete inventory to a local file and return only a summary plus the file path instead of the full list inline.")
|
|
21995
23409
|
};
|
|
21996
23410
|
ExtractSiteInputSchema = {
|
|
21997
23411
|
url: import_zod27.z.string().url().describe("Public website URL or domain to extract across multiple pages. Use when the user asks for a site audit, website crawl, or full-site content/schema extraction."),
|
|
21998
|
-
maxPages: import_zod27.z.number().int().min(1).max(
|
|
23412
|
+
maxPages: import_zod27.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to extract. Use 50 for a normal crawl, up to 10000 for a full-site bulk scrape. Bulk crawls (over 25 pages) switch to folder mode: every page is saved as its own Markdown file in a local folder and the response returns only a summary plus the folder path, so the full content never floods the context window."),
|
|
23413
|
+
rotateProxies: import_zod27.z.boolean().optional().describe("Route page fetches through rotating residential proxies in headful browsers to defeat rate-limiting and bot blocks (403/429). Discovers URLs from the sitemap, fetches in batches with a fresh proxy per batch, retries failures on a new proxy, and automatically parallelizes across the account's concurrency slots. Slower and pricier than the default fetch path, so use only when a site blocks normal crawling."),
|
|
23414
|
+
rotateProxyEvery: import_zod27.z.number().int().min(1).max(100).optional().describe("When rotateProxies is on, how many pages to fetch per browser/proxy before discarding it and minting a fresh one. Default 30. Lower values rotate IPs more aggressively against strict rate limits."),
|
|
23415
|
+
formats: import_zod27.z.array(import_zod27.z.enum(["markdown", "links", "json", "images", "branding"])).optional().describe("Which data formats to include in the per-page output. markdown=page content as Markdown, links=internal/external link graph, json=structured data/schema, images=per-page image URLs (links only, no downloads), branding=site-level logo/colors/fonts captured once from the homepage (requires a browser, adds time). markdown, links, json, and images are always captured cheaply from the HTML; branding is the only opt-in capture. Defaults to markdown+links when omitted.")
|
|
21999
23416
|
};
|
|
22000
23417
|
YoutubeHarvestInputSchema = {
|
|
22001
23418
|
mode: import_zod27.z.enum(["search", "channel"]).describe("Use search for topic/keyword requests. Use channel when the user provides @handle, channel ID, or channel URL."),
|
|
@@ -23076,7 +24493,7 @@ function localPlanningToolAnnotations(title) {
|
|
|
23076
24493
|
function listSavedReports() {
|
|
23077
24494
|
try {
|
|
23078
24495
|
const dir = outputBaseDir2();
|
|
23079
|
-
return (0,
|
|
24496
|
+
return (0, import_node_fs8.readdirSync)(dir).filter((f) => f.endsWith(".md")).map((f) => ({ filename: f, mtimeMs: (0, import_node_fs8.statSync)((0, import_node_path11.join)(dir, f)).mtimeMs })).sort((a, b) => b.mtimeMs - a.mtimeMs).slice(0, 100);
|
|
23080
24497
|
} catch {
|
|
23081
24498
|
return [];
|
|
23082
24499
|
}
|
|
@@ -23100,15 +24517,15 @@ function registerSavedReportResources(server) {
|
|
|
23100
24517
|
},
|
|
23101
24518
|
async (uri, variables) => {
|
|
23102
24519
|
const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
|
|
23103
|
-
const filename = (0,
|
|
24520
|
+
const filename = (0, import_node_path11.basename)(decodeURIComponent(String(requested ?? "")));
|
|
23104
24521
|
if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
|
|
23105
|
-
const text = (0,
|
|
24522
|
+
const text = (0, import_node_fs8.readFileSync)((0, import_node_path11.join)(outputBaseDir2(), filename), "utf8");
|
|
23106
24523
|
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
|
|
23107
24524
|
}
|
|
23108
24525
|
);
|
|
23109
24526
|
}
|
|
23110
24527
|
function buildPaaExtractorMcpServer(executor, options = {}) {
|
|
23111
|
-
const server = new import_mcp.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION });
|
|
24528
|
+
const server = new import_mcp.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
23112
24529
|
registerPaaExtractorMcpTools(server, executor, options);
|
|
23113
24530
|
return server;
|
|
23114
24531
|
}
|
|
@@ -23140,14 +24557,14 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
23140
24557
|
}, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
|
|
23141
24558
|
server.registerTool("map_site_urls", {
|
|
23142
24559
|
title: "Site URL Map",
|
|
23143
|
-
description: withReportNote("Map/crawl a public website when the user asks for a sitemap, URL inventory, broken-link scan, redirect scan, or crawl planning. Returns internal URLs with HTTP status and truncation metadata. Use this before extract_site when choosing pages for an audit; use extract_url for one known page."),
|
|
24560
|
+
description: withReportNote("Map/crawl a public website when the user asks for a sitemap, URL inventory, broken-link scan, redirect scan, or crawl planning. Returns internal URLs with HTTP status and truncation metadata. Supports up to maxUrls=10000; maps over 500 URLs write the complete inventory to a local CSV file and return a summary plus the file path instead of the full list inline. Use this before extract_site when choosing pages for an audit; use extract_url for one known page."),
|
|
23144
24561
|
inputSchema: MapSiteUrlsInputSchema,
|
|
23145
24562
|
outputSchema: MapSiteUrlsOutputSchema,
|
|
23146
24563
|
annotations: liveWebToolAnnotations("Site URL Map")
|
|
23147
24564
|
}, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
|
|
23148
24565
|
server.registerTool("extract_site", {
|
|
23149
24566
|
title: "Multi-Page Site Extract",
|
|
23150
|
-
description: withReportNote("Run multi-page extraction across a public website when the user asks for a website audit, competitor audit, full-site content crawl, schema inventory, or page metadata review. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Use map_site_urls first when URL selection matters; use extract_url for one page."),
|
|
24567
|
+
description: withReportNote("Run multi-page extraction across a public website when the user asks for a website audit, competitor audit, full-site content crawl, schema inventory, or page metadata review. Returns per-page titles, H1s, metadata, headings, schema/entity data, canonical URLs, and content. Supports up to maxPages=10000; bulk crawls over 25 pages switch to folder mode: each page is saved as its own Markdown file in a local folder and the response returns a summary plus the folder path instead of inlining all page content. Use map_site_urls first when URL selection matters; use extract_url for one page."),
|
|
23151
24568
|
inputSchema: ExtractSiteInputSchema,
|
|
23152
24569
|
outputSchema: ExtractSiteOutputSchema,
|
|
23153
24570
|
annotations: liveWebToolAnnotations("Multi-Page Site Extract")
|
|
@@ -23271,7 +24688,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
23271
24688
|
outputSchema: WorkflowArtifactReadOutputSchema,
|
|
23272
24689
|
annotations: liveWebToolAnnotations("Read Workflow Artifact")
|
|
23273
24690
|
}, async (input) => formatWorkflowArtifactRead(await executor.workflowArtifactRead(input), input));
|
|
23274
|
-
server.registerTool("
|
|
24691
|
+
server.registerTool("rank_tracker_workflow", {
|
|
23275
24692
|
title: "Rank Tracker Blueprint Builder",
|
|
23276
24693
|
description: "Generate a build-ready database, cron/heartbeat, ingestion, metrics, and AI implementation prompt for a rank tracker powered by MCP Scraper. Supports Maps rankings through directory_workflow/maps_search, organic rankings through search_serp, AI Overview citation tracking, and People Also Ask source presence tracking. This tool is local planning only; it does not call the web or spend credits.",
|
|
23277
24694
|
inputSchema: RankTrackerBlueprintInputSchema,
|
|
@@ -23292,15 +24709,16 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
23292
24709
|
}
|
|
23293
24710
|
}, async (input) => formatCreditsInfo(await executor.creditsInfo(input), input));
|
|
23294
24711
|
}
|
|
23295
|
-
var import_mcp,
|
|
24712
|
+
var import_mcp, import_node_fs8, import_node_path11;
|
|
23296
24713
|
var init_paa_mcp_server = __esm({
|
|
23297
24714
|
"src/mcp/paa-mcp-server.ts"() {
|
|
23298
24715
|
"use strict";
|
|
23299
24716
|
import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
23300
|
-
|
|
23301
|
-
|
|
24717
|
+
import_node_fs8 = require("fs");
|
|
24718
|
+
import_node_path11 = require("path");
|
|
23302
24719
|
init_version();
|
|
23303
24720
|
init_mcp_response_formatter();
|
|
24721
|
+
init_server_instructions();
|
|
23304
24722
|
init_mcp_tool_schemas();
|
|
23305
24723
|
init_rank_tracker_blueprint();
|
|
23306
24724
|
init_mcp_response_formatter();
|
|
@@ -23709,7 +25127,7 @@ async function migrateBrowserAgent() {
|
|
|
23709
25127
|
}
|
|
23710
25128
|
async function createSessionRow(input) {
|
|
23711
25129
|
const db = getDb();
|
|
23712
|
-
const id = `bas_${(0,
|
|
25130
|
+
const id = `bas_${(0, import_node_crypto6.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
|
|
23713
25131
|
await db.execute({
|
|
23714
25132
|
sql: `INSERT INTO browser_agent_sessions (id, runtime_session_id, live_view_url, cdp_ws_url, status, label, user_id, concurrency_lock_id, last_action_at)
|
|
23715
25133
|
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, datetime('now'))`,
|
|
@@ -23770,7 +25188,7 @@ async function recordAction(input) {
|
|
|
23770
25188
|
sql: `INSERT INTO browser_agent_actions (id, session_id, type, params_json, ok, error)
|
|
23771
25189
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
23772
25190
|
args: [
|
|
23773
|
-
`baa_${(0,
|
|
25191
|
+
`baa_${(0, import_node_crypto6.randomUUID)().replace(/-/g, "").slice(0, 20)}`,
|
|
23774
25192
|
input.sessionId,
|
|
23775
25193
|
input.type,
|
|
23776
25194
|
input.params == null ? null : JSON.stringify(input.params),
|
|
@@ -23810,11 +25228,11 @@ async function listReplayRows(sessionId) {
|
|
|
23810
25228
|
});
|
|
23811
25229
|
return res.rows;
|
|
23812
25230
|
}
|
|
23813
|
-
var
|
|
25231
|
+
var import_node_crypto6, _ready;
|
|
23814
25232
|
var init_browser_agent_db = __esm({
|
|
23815
25233
|
"src/api/browser-agent-db.ts"() {
|
|
23816
25234
|
"use strict";
|
|
23817
|
-
|
|
25235
|
+
import_node_crypto6 = require("crypto");
|
|
23818
25236
|
init_db();
|
|
23819
25237
|
_ready = false;
|
|
23820
25238
|
}
|
|
@@ -24419,7 +25837,7 @@ function buildFanoutResult(ctx) {
|
|
|
24419
25837
|
interceptorReady: ready,
|
|
24420
25838
|
rawBytes,
|
|
24421
25839
|
unmappedKeys: parsed.unmappedKeys,
|
|
24422
|
-
note: ready ? "Capture hook is live but no fan-out was seen yet. Run a NEW prompt that triggers web search in this session, then call
|
|
25840
|
+
note: ready ? "Capture hook is live but no fan-out was seen yet. Run a NEW prompt that triggers web search in this session, then call query_fanout_workflow again. Fan-out is only captured as it streams; past answers from before the session opened cannot be recovered." : "Capture hook not detected on this page. Open a direct/no-proxy hosted browser session, navigate to chatgpt.com or claude.ai after opening, run a new prompt in that session, then call query_fanout_workflow again."
|
|
24423
25841
|
};
|
|
24424
25842
|
}
|
|
24425
25843
|
return result;
|
|
@@ -24496,20 +25914,20 @@ var init_classify = __esm({
|
|
|
24496
25914
|
|
|
24497
25915
|
// src/services/fanout/export.ts
|
|
24498
25916
|
function outputBaseDir3() {
|
|
24499
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
25917
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path12.join)((0, import_node_os7.homedir)(), "Downloads", "mcp-scraper");
|
|
24500
25918
|
}
|
|
24501
25919
|
function safe(value) {
|
|
24502
25920
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
|
|
24503
25921
|
}
|
|
24504
25922
|
function writeTable(path6, rows, delimiter) {
|
|
24505
|
-
(0,
|
|
25923
|
+
(0, import_node_fs9.writeFileSync)(path6, import_papaparse3.default.unparse(rows.length ? rows : [{}], { delimiter }));
|
|
24506
25924
|
}
|
|
24507
25925
|
function exportFanout(enriched) {
|
|
24508
25926
|
const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
|
|
24509
25927
|
const outputDir = outputBaseDir3();
|
|
24510
|
-
const relativeDir = (0,
|
|
24511
|
-
const dir = (0,
|
|
24512
|
-
(0,
|
|
25928
|
+
const relativeDir = (0, import_node_path12.join)("fanout", `${stamp}-${safe(enriched.platform)}`);
|
|
25929
|
+
const dir = (0, import_node_path12.join)(outputDir, relativeDir);
|
|
25930
|
+
(0, import_node_fs9.mkdirSync)(dir, { recursive: true });
|
|
24513
25931
|
const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
|
|
24514
25932
|
const citationRows = enriched.aggregates.citationOrder.map((c) => {
|
|
24515
25933
|
const src = enriched.citedUrls.find((s) => s.url === c.url);
|
|
@@ -24522,25 +25940,25 @@ function exportFanout(enriched) {
|
|
|
24522
25940
|
const relativePaths = {
|
|
24523
25941
|
relativeTo: "MCP_SCRAPER_OUTPUT_DIR or ~/Downloads/mcp-scraper",
|
|
24524
25942
|
dir: relativeDir,
|
|
24525
|
-
json: (0,
|
|
24526
|
-
queriesCsv: (0,
|
|
24527
|
-
queriesTsv: (0,
|
|
24528
|
-
citationsCsv: (0,
|
|
24529
|
-
sourcesCsv: (0,
|
|
24530
|
-
browsedOnlyCsv: (0,
|
|
24531
|
-
snippetsCsv: (0,
|
|
24532
|
-
domainsCsv: (0,
|
|
24533
|
-
report: (0,
|
|
25943
|
+
json: (0, import_node_path12.join)(relativeDir, "fanout.json"),
|
|
25944
|
+
queriesCsv: (0, import_node_path12.join)(relativeDir, "queries.csv"),
|
|
25945
|
+
queriesTsv: (0, import_node_path12.join)(relativeDir, "queries.tsv"),
|
|
25946
|
+
citationsCsv: (0, import_node_path12.join)(relativeDir, "citations.csv"),
|
|
25947
|
+
sourcesCsv: (0, import_node_path12.join)(relativeDir, "sources.csv"),
|
|
25948
|
+
browsedOnlyCsv: (0, import_node_path12.join)(relativeDir, "browsed-only.csv"),
|
|
25949
|
+
snippetsCsv: (0, import_node_path12.join)(relativeDir, "snippets.csv"),
|
|
25950
|
+
domainsCsv: (0, import_node_path12.join)(relativeDir, "domains.csv"),
|
|
25951
|
+
report: (0, import_node_path12.join)(relativeDir, "report.html")
|
|
24534
25952
|
};
|
|
24535
|
-
(0,
|
|
24536
|
-
writeTable((0,
|
|
24537
|
-
writeTable((0,
|
|
24538
|
-
writeTable((0,
|
|
24539
|
-
writeTable((0,
|
|
24540
|
-
writeTable((0,
|
|
24541
|
-
writeTable((0,
|
|
24542
|
-
writeTable((0,
|
|
24543
|
-
(0,
|
|
25953
|
+
(0, import_node_fs9.writeFileSync)((0, import_node_path12.join)(outputDir, relativePaths.json), JSON.stringify(enriched, null, 2));
|
|
25954
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.queriesCsv), queryRows, ",");
|
|
25955
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.queriesTsv), queryRows, " ");
|
|
25956
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.citationsCsv), citationRows, ",");
|
|
25957
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.sourcesCsv), sourceRows2, ",");
|
|
25958
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.browsedOnlyCsv), browsedOnlyRows, ",");
|
|
25959
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.snippetsCsv), snippetRows, ",");
|
|
25960
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.domainsCsv), domainRows, ",");
|
|
25961
|
+
(0, import_node_fs9.writeFileSync)((0, import_node_path12.join)(outputDir, relativePaths.report), renderReportHtml(enriched));
|
|
24544
25962
|
return relativePaths;
|
|
24545
25963
|
}
|
|
24546
25964
|
function esc(s) {
|
|
@@ -24618,13 +26036,13 @@ render();
|
|
|
24618
26036
|
</script>
|
|
24619
26037
|
</body></html>`;
|
|
24620
26038
|
}
|
|
24621
|
-
var
|
|
26039
|
+
var import_node_fs9, import_node_os7, import_node_path12, import_papaparse3;
|
|
24622
26040
|
var init_export = __esm({
|
|
24623
26041
|
"src/services/fanout/export.ts"() {
|
|
24624
26042
|
"use strict";
|
|
24625
|
-
|
|
24626
|
-
|
|
24627
|
-
|
|
26043
|
+
import_node_fs9 = require("fs");
|
|
26044
|
+
import_node_os7 = require("os");
|
|
26045
|
+
import_node_path12 = require("path");
|
|
24628
26046
|
import_papaparse3 = __toESM(require("papaparse"), 1);
|
|
24629
26047
|
}
|
|
24630
26048
|
});
|
|
@@ -24672,7 +26090,7 @@ async function runFanoutCapture(page, input) {
|
|
|
24672
26090
|
}
|
|
24673
26091
|
const adapter = adapterForHost(hostname);
|
|
24674
26092
|
if (!adapter) {
|
|
24675
|
-
throw new Error(`
|
|
26093
|
+
throw new Error(`query_fanout_workflow supports chatgpt.com and claude.ai only; the session is on ${hostname || "an unknown page"}. Navigate there first with browser_goto.`);
|
|
24676
26094
|
}
|
|
24677
26095
|
const text = cap.bestAnswerText();
|
|
24678
26096
|
const parsed = adapter.parse(text, input.prompt || "");
|
|
@@ -24710,7 +26128,7 @@ function keepHostedBrowserAlive(_browser) {
|
|
|
24710
26128
|
function client() {
|
|
24711
26129
|
const apiKey = browserServiceApiKey();
|
|
24712
26130
|
if (!apiKey) throw new Error("Browser backend API key is required");
|
|
24713
|
-
return new
|
|
26131
|
+
return new import_sdk7.default({ apiKey });
|
|
24714
26132
|
}
|
|
24715
26133
|
function isProfileConflict(err) {
|
|
24716
26134
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -24862,7 +26280,7 @@ async function pressKeys(runtimeSessionId, keys) {
|
|
|
24862
26280
|
await k.browsers.computer.pressKey(runtimeSessionId, { keys });
|
|
24863
26281
|
}
|
|
24864
26282
|
async function goto(cdpWsUrl, url) {
|
|
24865
|
-
const browser = await
|
|
26283
|
+
const browser = await import_playwright5.chromium.connectOverCDP(cdpWsUrl);
|
|
24866
26284
|
try {
|
|
24867
26285
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
24868
26286
|
const page = context.pages()[0] ?? await context.newPage();
|
|
@@ -24873,7 +26291,7 @@ async function goto(cdpWsUrl, url) {
|
|
|
24873
26291
|
}
|
|
24874
26292
|
}
|
|
24875
26293
|
async function captureFanout(cdpWsUrl, input) {
|
|
24876
|
-
const browser = await
|
|
26294
|
+
const browser = await import_playwright5.chromium.connectOverCDP(cdpWsUrl);
|
|
24877
26295
|
try {
|
|
24878
26296
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
24879
26297
|
const page = context.pages()[0] ?? await context.newPage();
|
|
@@ -24883,7 +26301,7 @@ async function captureFanout(cdpWsUrl, input) {
|
|
|
24883
26301
|
}
|
|
24884
26302
|
}
|
|
24885
26303
|
async function readPage(cdpWsUrl) {
|
|
24886
|
-
const browser = await
|
|
26304
|
+
const browser = await import_playwright5.chromium.connectOverCDP(cdpWsUrl);
|
|
24887
26305
|
try {
|
|
24888
26306
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
24889
26307
|
const page = context.pages()[0] ?? await context.newPage();
|
|
@@ -24938,7 +26356,7 @@ async function readPage(cdpWsUrl) {
|
|
|
24938
26356
|
}
|
|
24939
26357
|
}
|
|
24940
26358
|
async function locatePageTargets(cdpWsUrl, targets) {
|
|
24941
|
-
const browser = await
|
|
26359
|
+
const browser = await import_playwright5.chromium.connectOverCDP(cdpWsUrl);
|
|
24942
26360
|
try {
|
|
24943
26361
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
24944
26362
|
const page = context.pages()[0] ?? await context.newPage();
|
|
@@ -24950,16 +26368,16 @@ async function locatePageTargets(cdpWsUrl, targets) {
|
|
|
24950
26368
|
}).catch(() => {
|
|
24951
26369
|
});
|
|
24952
26370
|
const data = await page.evaluate((rawTargets) => {
|
|
24953
|
-
const
|
|
26371
|
+
const normalize3 = (value) => value.replace(/\s+/g, " ").trim();
|
|
24954
26372
|
const isVisibleRect = (r) => r.width >= 1 && r.height >= 1 && r.bottom >= 0 && r.right >= 0 && r.top <= window.innerHeight && r.left <= window.innerWidth;
|
|
24955
26373
|
const roleOf = (el2) => el2.getAttribute("role") || el2.tagName.toLowerCase();
|
|
24956
26374
|
const nameOf = (el2) => {
|
|
24957
26375
|
const e = el2;
|
|
24958
|
-
return
|
|
26376
|
+
return normalize3(
|
|
24959
26377
|
e.getAttribute("aria-label") || e.placeholder || e.innerText || e.getAttribute("value") || e.getAttribute("title") || ""
|
|
24960
26378
|
).slice(0, 160);
|
|
24961
26379
|
};
|
|
24962
|
-
const textOf = (el2) =>
|
|
26380
|
+
const textOf = (el2) => normalize3(el2.innerText || el2.textContent || "").slice(0, 500);
|
|
24963
26381
|
const unionRects = (rects) => {
|
|
24964
26382
|
const visible = rects.filter(isVisibleRect);
|
|
24965
26383
|
if (!visible.length) return null;
|
|
@@ -24998,7 +26416,7 @@ async function locatePageTargets(cdpWsUrl, targets) {
|
|
|
24998
26416
|
return nodes.map((el2) => ({ el: el2, rect: el2.getBoundingClientRect() })).filter((item) => isVisibleRect(item.rect));
|
|
24999
26417
|
};
|
|
25000
26418
|
const textMatches = (needle, match) => {
|
|
25001
|
-
const wanted =
|
|
26419
|
+
const wanted = normalize3(needle);
|
|
25002
26420
|
const wantedLower = wanted.toLowerCase();
|
|
25003
26421
|
if (!wantedLower) return [];
|
|
25004
26422
|
const matches = [];
|
|
@@ -25006,7 +26424,7 @@ async function locatePageTargets(cdpWsUrl, targets) {
|
|
|
25006
26424
|
let node = walker.nextNode();
|
|
25007
26425
|
while (node) {
|
|
25008
26426
|
const raw = node.textContent || "";
|
|
25009
|
-
const normalizedRaw =
|
|
26427
|
+
const normalizedRaw = normalize3(raw);
|
|
25010
26428
|
const rawLower = raw.toLowerCase();
|
|
25011
26429
|
const normalizedLower = normalizedRaw.toLowerCase();
|
|
25012
26430
|
const ok = match === "exact" ? normalizedLower === wantedLower : normalizedLower.includes(wantedLower);
|
|
@@ -25102,12 +26520,12 @@ async function replayList(runtimeSessionId) {
|
|
|
25102
26520
|
finishedAt: r.finished_at ?? null
|
|
25103
26521
|
}));
|
|
25104
26522
|
}
|
|
25105
|
-
var
|
|
26523
|
+
var import_sdk7, import_playwright5, DEFAULT_TIMEOUT_SECONDS;
|
|
25106
26524
|
var init_browser_agent_service = __esm({
|
|
25107
26525
|
"src/services/browser-agent/browser-agent-service.ts"() {
|
|
25108
26526
|
"use strict";
|
|
25109
|
-
|
|
25110
|
-
|
|
26527
|
+
import_sdk7 = __toESM(require("@onkernel/sdk"), 1);
|
|
26528
|
+
import_playwright5 = require("playwright");
|
|
25111
26529
|
init_browser_service_env();
|
|
25112
26530
|
init_run_capture();
|
|
25113
26531
|
DEFAULT_TIMEOUT_SECONDS = 600;
|
|
@@ -26108,14 +27526,14 @@ function getSessionSecret() {
|
|
|
26108
27526
|
function safeEqualHex(a, b) {
|
|
26109
27527
|
if (a.length !== b.length) return false;
|
|
26110
27528
|
try {
|
|
26111
|
-
return (0,
|
|
27529
|
+
return (0, import_node_crypto7.timingSafeEqual)(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
26112
27530
|
} catch {
|
|
26113
27531
|
return false;
|
|
26114
27532
|
}
|
|
26115
27533
|
}
|
|
26116
27534
|
function signSession(userId) {
|
|
26117
27535
|
const payload = String(userId);
|
|
26118
|
-
const sig = (0,
|
|
27536
|
+
const sig = (0, import_node_crypto7.createHmac)("sha256", secret()).update(payload).digest("hex");
|
|
26119
27537
|
return `${payload}.${sig}`;
|
|
26120
27538
|
}
|
|
26121
27539
|
function verifySession(token) {
|
|
@@ -26123,16 +27541,16 @@ function verifySession(token) {
|
|
|
26123
27541
|
if (dot === -1) return null;
|
|
26124
27542
|
const payload = token.slice(0, dot);
|
|
26125
27543
|
const sig = token.slice(dot + 1);
|
|
26126
|
-
const expected = (0,
|
|
27544
|
+
const expected = (0, import_node_crypto7.createHmac)("sha256", secret()).update(payload).digest("hex");
|
|
26127
27545
|
if (!safeEqualHex(sig, expected)) return null;
|
|
26128
27546
|
const id = parseInt(payload);
|
|
26129
27547
|
return isNaN(id) ? null : id;
|
|
26130
27548
|
}
|
|
26131
|
-
var
|
|
27549
|
+
var import_node_crypto7, isProduction, secret;
|
|
26132
27550
|
var init_session = __esm({
|
|
26133
27551
|
"src/api/session.ts"() {
|
|
26134
27552
|
"use strict";
|
|
26135
|
-
|
|
27553
|
+
import_node_crypto7 = require("crypto");
|
|
26136
27554
|
isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
|
|
26137
27555
|
secret = () => getSessionSecret();
|
|
26138
27556
|
}
|
|
@@ -26379,6 +27797,9 @@ var init_server = __esm({
|
|
|
26379
27797
|
import_hono14 = require("inngest/hono");
|
|
26380
27798
|
init_client();
|
|
26381
27799
|
init_site_audit();
|
|
27800
|
+
init_site_extract();
|
|
27801
|
+
init_site_extract_repository();
|
|
27802
|
+
init_catalog_seed();
|
|
26382
27803
|
init_site_audit_routes();
|
|
26383
27804
|
init_youtube_routes();
|
|
26384
27805
|
init_screenshot_routes();
|
|
@@ -26563,6 +27984,10 @@ var init_server = __esm({
|
|
|
26563
27984
|
const taken = await countActiveUsers();
|
|
26564
27985
|
return c.json({ taken, open: true });
|
|
26565
27986
|
});
|
|
27987
|
+
app.get("/catalog", (c) => {
|
|
27988
|
+
c.header("Cache-Control", "public, max-age=60");
|
|
27989
|
+
return c.json(CATALOG);
|
|
27990
|
+
});
|
|
26566
27991
|
app.get("/me", async (c) => {
|
|
26567
27992
|
const token = (0, import_cookie.getCookie)(c, "session");
|
|
26568
27993
|
if (!token) return c.json({ authenticated: false });
|
|
@@ -26797,7 +28222,7 @@ var init_server = __esm({
|
|
|
26797
28222
|
const raw = await c.req.json().catch(() => ({}));
|
|
26798
28223
|
const bodyResult = ExtractUrlBodySchema.safeParse(raw);
|
|
26799
28224
|
if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
|
|
26800
|
-
const { url, screenshot: screenshot2, screenshotDevice, extractBranding, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
|
|
28225
|
+
const { url, screenshot: screenshot2, screenshotDevice, extractBranding: extractBranding2, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
|
|
26801
28226
|
if (!allowLocal) {
|
|
26802
28227
|
const checked = await validatePublicHttpUrl(url, { field: "URL" });
|
|
26803
28228
|
if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
|
|
@@ -26830,7 +28255,7 @@ var init_server = __esm({
|
|
|
26830
28255
|
const device = screenshotDevice === "mobile" ? "mobile" : "desktop";
|
|
26831
28256
|
const [result, pageData] = await Promise.all([
|
|
26832
28257
|
extractKpo({ url: canonicalUrl, kernelApiKey }),
|
|
26833
|
-
screenshot2 ||
|
|
28258
|
+
screenshot2 || extractBranding2 ? capturePageData(canonicalUrl, { kernelApiKey, device, screenshot: !!screenshot2, branding: !!extractBranding2 }).catch((err) => {
|
|
26834
28259
|
console.error("[extract-url] capturePageData failed:", err instanceof Error ? err.message : err);
|
|
26835
28260
|
return null;
|
|
26836
28261
|
}) : null
|
|
@@ -26874,7 +28299,7 @@ var init_server = __esm({
|
|
|
26874
28299
|
debited = true;
|
|
26875
28300
|
const result = await spiderSite({
|
|
26876
28301
|
startUrl: parsed.href,
|
|
26877
|
-
maxUrls: Math.min(
|
|
28302
|
+
maxUrls: Math.min(1e4, Math.max(1, body.maxUrls ?? 500)),
|
|
26878
28303
|
concurrency: Math.min(20, Math.max(1, body.concurrency ?? 12)),
|
|
26879
28304
|
kernelApiKey: body.browserFallback ?? body.kernelFallback ? browserServiceApiKey() : void 0
|
|
26880
28305
|
});
|
|
@@ -26911,7 +28336,43 @@ var init_server = __esm({
|
|
|
26911
28336
|
if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
|
|
26912
28337
|
const parsed = checked.parsed;
|
|
26913
28338
|
const user = c.get("user");
|
|
26914
|
-
|
|
28339
|
+
if (body.background === true) {
|
|
28340
|
+
const affordablePages = Math.floor(user.balance_mc / MC_COSTS.page_scrape);
|
|
28341
|
+
if (affordablePages < 1) return c.json(insufficientBalanceResponse(user.balance_mc, MC_COSTS.page_scrape), 402);
|
|
28342
|
+
const maxPages = Math.min(Math.min(1e4, Math.max(1, body.maxPages ?? 1e4)), affordablePages);
|
|
28343
|
+
const heldMc = maxPages * MC_COSTS.page_scrape;
|
|
28344
|
+
const { ok: holdOk, balance_mc: holdBal } = await debitMc(user.id, heldMc, LedgerOperation.EXTRACT_SITE_HOLD, parsed.hostname);
|
|
28345
|
+
if (!holdOk) return c.json(insufficientBalanceResponse(holdBal, heldMc), 402);
|
|
28346
|
+
const jobId = `ext_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
28347
|
+
let jobCreated = false;
|
|
28348
|
+
try {
|
|
28349
|
+
await createExtractJob(jobId, user.id, parsed.href, {
|
|
28350
|
+
maxPages,
|
|
28351
|
+
concurrency: concurrencyLimitForUser(user),
|
|
28352
|
+
urlsPerBrowser: body.rotateProxyEvery ?? 10,
|
|
28353
|
+
formats: body.formats,
|
|
28354
|
+
heldMc
|
|
28355
|
+
});
|
|
28356
|
+
jobCreated = true;
|
|
28357
|
+
await inngest.send({ name: "mcp-scraper/extract.requested", data: { jobId } });
|
|
28358
|
+
} catch (err) {
|
|
28359
|
+
if (jobCreated) {
|
|
28360
|
+
await settleExtractJob(jobId, user.id, heldMc, 0, "enqueue failed").catch(() => {
|
|
28361
|
+
});
|
|
28362
|
+
} else {
|
|
28363
|
+
await creditMc(user.id, heldMc, LedgerOperation.EXTRACT_SITE_REFUND, "enqueue failed").catch(() => {
|
|
28364
|
+
});
|
|
28365
|
+
}
|
|
28366
|
+
await failExtractJob(jobId, err instanceof Error ? err.message : "enqueue failed").catch(() => {
|
|
28367
|
+
});
|
|
28368
|
+
return c.json({ error: "Failed to enqueue background crawl" }, 503);
|
|
28369
|
+
}
|
|
28370
|
+
return c.json({ jobId, status: "pending", statusUrl: `/extract-site/status/${jobId}` });
|
|
28371
|
+
}
|
|
28372
|
+
const affordablePagesSync = Math.floor(user.balance_mc / MC_COSTS.page_scrape);
|
|
28373
|
+
if (affordablePagesSync < 1) return c.json(insufficientBalanceResponse(user.balance_mc, MC_COSTS.page_scrape), 402);
|
|
28374
|
+
const siteMaxPages = Math.min(Math.min(1e4, Math.max(1, body.maxPages ?? 100)), affordablePagesSync);
|
|
28375
|
+
const siteHoldMc = siteMaxPages * MC_COSTS.page_scrape;
|
|
26915
28376
|
const gate = await acquireConcurrencyGate(user, "extract_site", {
|
|
26916
28377
|
reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
|
|
26917
28378
|
metadata: { url: parsed.href }
|
|
@@ -26922,10 +28383,17 @@ var init_server = __esm({
|
|
|
26922
28383
|
const { ok: siteOk, balance_mc: siteBal } = await debitMc(user.id, siteHoldMc, LedgerOperation.EXTRACT_SITE_HOLD, parsed.hostname);
|
|
26923
28384
|
if (!siteOk) return c.json(insufficientBalanceResponse(siteBal, siteHoldMc), 402);
|
|
26924
28385
|
debited = true;
|
|
28386
|
+
const rotateProxies = body.rotateProxies === true;
|
|
28387
|
+
const wantsBranding = body.formats?.includes("branding") === true;
|
|
26925
28388
|
const result = await extractSite({
|
|
26926
28389
|
startUrl: parsed.href,
|
|
26927
|
-
maxPages:
|
|
26928
|
-
kernelApiKey: body.browserFallback
|
|
28390
|
+
maxPages: siteMaxPages,
|
|
28391
|
+
kernelApiKey: rotateProxies || wantsBranding || body.browserFallback || body.kernelFallback ? browserServiceApiKey() : void 0,
|
|
28392
|
+
formats: body.formats,
|
|
28393
|
+
...rotateProxies ? {
|
|
28394
|
+
rotateProxyEvery: body.rotateProxyEvery ?? 30,
|
|
28395
|
+
parallelism: concurrencyLimitForUser(user)
|
|
28396
|
+
} : {}
|
|
26929
28397
|
});
|
|
26930
28398
|
const pageCount = result.pages?.length ?? 1;
|
|
26931
28399
|
const actualSiteMc = pageCount * MC_COSTS.page_scrape;
|
|
@@ -26956,6 +28424,21 @@ var init_server = __esm({
|
|
|
26956
28424
|
await releaseConcurrencyGate(gate.lockId);
|
|
26957
28425
|
}
|
|
26958
28426
|
});
|
|
28427
|
+
app.get("/extract-site/status/:id", auth2, async (c) => {
|
|
28428
|
+
const user = c.get("user");
|
|
28429
|
+
const job = await getExtractJob(c.req.param("id"));
|
|
28430
|
+
if (!job || job.userId !== user.id) return c.json({ error: "Job not found" }, 404);
|
|
28431
|
+
return c.json({
|
|
28432
|
+
jobId: job.id,
|
|
28433
|
+
status: job.status,
|
|
28434
|
+
startUrl: job.startUrl,
|
|
28435
|
+
totalUrls: job.totalUrls,
|
|
28436
|
+
doneUrls: job.doneUrls,
|
|
28437
|
+
artifacts: job.artifacts ?? [],
|
|
28438
|
+
error: job.error,
|
|
28439
|
+
updatedAt: job.updatedAt
|
|
28440
|
+
});
|
|
28441
|
+
});
|
|
26959
28442
|
app.post("/billing/checkout", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
26960
28443
|
try {
|
|
26961
28444
|
const user = c.get("sessionUser");
|
|
@@ -27144,7 +28627,7 @@ var init_server = __esm({
|
|
|
27144
28627
|
]);
|
|
27145
28628
|
return c.json({ drained: results.length, results, sweepResult, workflowDispatch: workflowDispatchResult });
|
|
27146
28629
|
});
|
|
27147
|
-
app.on(["GET", "POST", "PUT"], "/api/inngest", (0, import_hono14.serve)({ client: inngest, functions: [siteAuditFn] }));
|
|
28630
|
+
app.on(["GET", "POST", "PUT"], "/api/inngest", (0, import_hono14.serve)({ client: inngest, functions: [siteAuditFn, siteExtractFn] }));
|
|
27148
28631
|
app.route("/api/internal/site-architecture-auditor", siteAuditApp);
|
|
27149
28632
|
app.route("/youtube", youtubeApp);
|
|
27150
28633
|
app.route("/screenshot", screenshotApp);
|
|
@@ -27266,10 +28749,10 @@ ${ATTRIBUTION_PIXEL_SCRIPT}
|
|
|
27266
28749
|
});
|
|
27267
28750
|
|
|
27268
28751
|
// bin/api-server.ts
|
|
27269
|
-
var
|
|
28752
|
+
var import_node_fs10 = require("fs");
|
|
27270
28753
|
function loadDotEnv() {
|
|
27271
28754
|
try {
|
|
27272
|
-
for (const line of (0,
|
|
28755
|
+
for (const line of (0, import_node_fs10.readFileSync)(".env", "utf8").split("\n")) {
|
|
27273
28756
|
const eq = line.indexOf("=");
|
|
27274
28757
|
if (eq < 1 || line.trimStart().startsWith("#")) continue;
|
|
27275
28758
|
const k = line.slice(0, eq).trim();
|