mcp-scraper 0.3.14 → 0.3.16
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 +2261 -511
- 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 +663 -48
- 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 +4 -4
- 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 +655 -40
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +2 -2
- package/dist/{chunk-DBQDG7EH.js → chunk-5H22TOXQ.js} +28 -1
- package/dist/chunk-5H22TOXQ.js.map +1 -0
- package/dist/{chunk-HM6FHV5U.js → chunk-7XARJHS6.js} +662 -41
- package/dist/chunk-7XARJHS6.js.map +1 -0
- package/dist/{chunk-KPXMPAJ3.js → chunk-ACIUCZ27.js} +4 -4
- package/dist/chunk-ACIUCZ27.js.map +1 -0
- package/dist/{chunk-AX7UBYLG.js → chunk-ISZWGIWL.js} +7 -7
- package/dist/chunk-ISZWGIWL.js.map +1 -0
- package/dist/chunk-SOMBZK3V.js +7 -0
- package/dist/chunk-SOMBZK3V.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-LPVBSE2E.js} +1133 -93
- package/dist/server-LPVBSE2E.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 +57 -22
- 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,394 @@ 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/image-audit.ts
|
|
9172
|
+
function formatBytes(n) {
|
|
9173
|
+
if (n == null) return null;
|
|
9174
|
+
if (n < 1024) return `${n} B`;
|
|
9175
|
+
if (n < 1024 * 1024) return `${(n / 1024).toFixed(1)} KB`;
|
|
9176
|
+
return `${(n / (1024 * 1024)).toFixed(2)} MB`;
|
|
9177
|
+
}
|
|
9178
|
+
function collectUrls(pages) {
|
|
9179
|
+
const byKey = /* @__PURE__ */ new Map();
|
|
9180
|
+
for (const p of pages) for (const raw of p.imageLinks || []) {
|
|
9181
|
+
const u = decodeEntities(raw);
|
|
9182
|
+
if (!/^https?:\/\//i.test(u)) continue;
|
|
9183
|
+
if (/[{}]|%7[bd]/i.test(u)) continue;
|
|
9184
|
+
const k = dedupKey(u);
|
|
9185
|
+
const prev = byKey.get(k);
|
|
9186
|
+
if (!prev || /^https:/i.test(u) && /^http:/i.test(prev)) byKey.set(k, u);
|
|
9187
|
+
}
|
|
9188
|
+
return [...byKey.values()];
|
|
9189
|
+
}
|
|
9190
|
+
async function sizeAndType(url, timeoutMs) {
|
|
9191
|
+
const ctrl = new AbortController();
|
|
9192
|
+
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
9193
|
+
const read = (res) => ({
|
|
9194
|
+
len: res.headers.get("content-length"),
|
|
9195
|
+
cr: res.headers.get("content-range"),
|
|
9196
|
+
ct: res.headers.get("content-type"),
|
|
9197
|
+
status: res.status
|
|
9198
|
+
});
|
|
9199
|
+
try {
|
|
9200
|
+
let bytes = null;
|
|
9201
|
+
let ct = null;
|
|
9202
|
+
let status = null;
|
|
9203
|
+
try {
|
|
9204
|
+
const h = read(await fetch(url, { method: "HEAD", redirect: "follow", signal: ctrl.signal }));
|
|
9205
|
+
status = h.status;
|
|
9206
|
+
ct = h.ct;
|
|
9207
|
+
if (h.len) bytes = Number(h.len);
|
|
9208
|
+
} catch {
|
|
9209
|
+
}
|
|
9210
|
+
if (bytes == null) {
|
|
9211
|
+
const g = await fetch(url, { method: "GET", headers: { Range: "bytes=0-0" }, redirect: "follow", signal: ctrl.signal });
|
|
9212
|
+
const r = read(g);
|
|
9213
|
+
status = status ?? r.status;
|
|
9214
|
+
ct = ct ?? r.ct;
|
|
9215
|
+
if (r.cr && r.cr.includes("/")) {
|
|
9216
|
+
const tail = r.cr.split("/")[1];
|
|
9217
|
+
if (tail && tail !== "*") bytes = Number(tail);
|
|
9218
|
+
} else if (r.len) bytes = Number(r.len);
|
|
9219
|
+
try {
|
|
9220
|
+
await g.body?.cancel();
|
|
9221
|
+
} catch {
|
|
8612
9222
|
}
|
|
8613
|
-
|
|
8614
|
-
|
|
8615
|
-
|
|
8616
|
-
|
|
8617
|
-
|
|
8618
|
-
|
|
8619
|
-
|
|
8620
|
-
|
|
8621
|
-
|
|
8622
|
-
|
|
9223
|
+
}
|
|
9224
|
+
return { url, status, bytes: Number.isFinite(bytes) ? bytes : null, contentType: ct };
|
|
9225
|
+
} catch (e) {
|
|
9226
|
+
return { url, status: null, bytes: null, contentType: null, error: String(e.message || e) };
|
|
9227
|
+
} finally {
|
|
9228
|
+
clearTimeout(t);
|
|
9229
|
+
}
|
|
9230
|
+
}
|
|
9231
|
+
async function pool(items, n, fn) {
|
|
9232
|
+
const out = new Array(items.length);
|
|
9233
|
+
let i = 0;
|
|
9234
|
+
await Promise.all(Array.from({ length: Math.min(n, items.length) }, async () => {
|
|
9235
|
+
while (i < items.length) {
|
|
9236
|
+
const idx = i++;
|
|
9237
|
+
out[idx] = await fn(items[idx]);
|
|
9238
|
+
}
|
|
9239
|
+
}));
|
|
9240
|
+
return out;
|
|
9241
|
+
}
|
|
9242
|
+
async function auditImages(pages, opts = {}) {
|
|
9243
|
+
const concurrency = opts.concurrency ?? 12;
|
|
9244
|
+
const timeoutMs = opts.timeoutMs ?? 12e3;
|
|
9245
|
+
const urls = collectUrls(pages).slice(0, opts.max ?? 5e3);
|
|
9246
|
+
const heads = await pool(urls, concurrency, (u) => sizeAndType(u, timeoutMs));
|
|
9247
|
+
const rows = heads.map((r) => {
|
|
9248
|
+
const format = formatOf(r.contentType, r.url);
|
|
9249
|
+
return {
|
|
9250
|
+
...r,
|
|
9251
|
+
size: formatBytes(r.bytes),
|
|
9252
|
+
format,
|
|
9253
|
+
over100kb: r.bytes != null && r.bytes > OVER_BYTES,
|
|
9254
|
+
legacyFormat: format !== "unknown" && !MODERN.has(format)
|
|
9255
|
+
};
|
|
9256
|
+
});
|
|
9257
|
+
const sized = rows.filter((r) => r.bytes != null);
|
|
9258
|
+
const totalBytes = sized.reduce((a, r) => a + r.bytes, 0);
|
|
9259
|
+
const formatCounts = {};
|
|
9260
|
+
for (const r of rows) formatCounts[r.format] = (formatCounts[r.format] || 0) + 1;
|
|
9261
|
+
return {
|
|
9262
|
+
rows: rows.sort((a, b) => (b.bytes ?? 0) - (a.bytes ?? 0)),
|
|
9263
|
+
summary: {
|
|
9264
|
+
unique: rows.length,
|
|
9265
|
+
sized: sized.length,
|
|
9266
|
+
totalBytes,
|
|
9267
|
+
totalSize: formatBytes(totalBytes) ?? "0 B",
|
|
9268
|
+
avgSize: formatBytes(Math.round(totalBytes / (sized.length || 1))) ?? "0 B",
|
|
9269
|
+
over100kb: rows.filter((r) => r.over100kb).length,
|
|
9270
|
+
legacyFormat: rows.filter((r) => r.legacyFormat).length,
|
|
9271
|
+
formatCounts
|
|
9272
|
+
}
|
|
9273
|
+
};
|
|
9274
|
+
}
|
|
9275
|
+
function renderImageSection(audit) {
|
|
9276
|
+
const s = audit.summary;
|
|
9277
|
+
const fmt = Object.entries(s.formatCounts).sort((a, b) => b[1] - a[1]).map(([k, v]) => `${k}:${v}`).join(" ");
|
|
9278
|
+
const heaviest = audit.rows.filter((r) => r.over100kb).slice(0, 15);
|
|
9279
|
+
const lines = [
|
|
9280
|
+
`## Images`,
|
|
9281
|
+
`**${s.unique} unique images** \xB7 ${s.totalSize} total \xB7 ${s.avgSize} avg \xB7 ${s.over100kb} over 100 KB \xB7 ${s.legacyFormat} legacy format`,
|
|
9282
|
+
`Formats: ${fmt}`
|
|
9283
|
+
];
|
|
9284
|
+
if (heaviest.length) {
|
|
9285
|
+
lines.push("", "| Size | Format | URL |", "|------|--------|-----|");
|
|
9286
|
+
for (const r of heaviest) lines.push(`| ${r.size} | ${r.format} | ${r.url} |`);
|
|
9287
|
+
}
|
|
9288
|
+
return lines.join("\n");
|
|
9289
|
+
}
|
|
9290
|
+
var OVER_BYTES, MODERN, decodeEntities, dedupKey, formatOf;
|
|
9291
|
+
var init_image_audit = __esm({
|
|
9292
|
+
"src/api/image-audit.ts"() {
|
|
9293
|
+
"use strict";
|
|
9294
|
+
OVER_BYTES = 100 * 1024;
|
|
9295
|
+
MODERN = /* @__PURE__ */ new Set(["webp", "avif", "svg", "svg+xml"]);
|
|
9296
|
+
decodeEntities = (u) => u.replace(/&/g, "&").replace(/�?38;/g, "&").replace(/&/gi, "&").trim();
|
|
9297
|
+
dedupKey = (u) => u.replace(/^https?:\/\//i, "//").replace(/\/$/, "");
|
|
9298
|
+
formatOf = (ct, url) => {
|
|
9299
|
+
if (ct) {
|
|
9300
|
+
const m = ct.split(";")[0].trim().toLowerCase();
|
|
9301
|
+
if (m.startsWith("image/")) return m.replace("image/", "");
|
|
9302
|
+
}
|
|
9303
|
+
return (url.split("?")[0].match(/\.([a-z0-9]+)$/i)?.[1] || "unknown").toLowerCase();
|
|
9304
|
+
};
|
|
9305
|
+
}
|
|
9306
|
+
});
|
|
9307
|
+
|
|
9308
|
+
// src/api/blob-store.ts
|
|
9309
|
+
function byteLength(data) {
|
|
9310
|
+
return Buffer.isBuffer(data) ? data.length : Buffer.byteLength(data);
|
|
9311
|
+
}
|
|
9312
|
+
function localBaseDir() {
|
|
9313
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path2.join)((0, import_node_os2.homedir)(), "Downloads", "mcp-scraper");
|
|
9314
|
+
}
|
|
9315
|
+
function getBlobStore() {
|
|
9316
|
+
if (cached) return cached;
|
|
9317
|
+
if (process.env.BLOB_READ_WRITE_TOKEN) {
|
|
9318
|
+
cached = new VercelBlobStore(process.env.BLOB_READ_WRITE_TOKEN);
|
|
9319
|
+
} else {
|
|
9320
|
+
cached = new LocalBlobStore(localBaseDir());
|
|
9321
|
+
}
|
|
9322
|
+
return cached;
|
|
9323
|
+
}
|
|
9324
|
+
var import_node_fs2, import_node_os2, import_node_path2, LocalBlobStore, cached, VercelBlobStore;
|
|
9325
|
+
var init_blob_store = __esm({
|
|
9326
|
+
"src/api/blob-store.ts"() {
|
|
9327
|
+
"use strict";
|
|
9328
|
+
import_node_fs2 = require("fs");
|
|
9329
|
+
import_node_os2 = require("os");
|
|
9330
|
+
import_node_path2 = require("path");
|
|
9331
|
+
LocalBlobStore = class {
|
|
9332
|
+
constructor(baseDir) {
|
|
9333
|
+
this.baseDir = baseDir;
|
|
8623
9334
|
}
|
|
8624
|
-
|
|
8625
|
-
|
|
8626
|
-
|
|
8627
|
-
|
|
8628
|
-
|
|
8629
|
-
|
|
8630
|
-
|
|
8631
|
-
body.visualSitemapsPath,
|
|
8632
|
-
body.gscPath,
|
|
8633
|
-
body.ga4Path,
|
|
8634
|
-
body.ahrefsPath
|
|
8635
|
-
];
|
|
8636
|
-
for (const filePath of pathFields) {
|
|
8637
|
-
if (filePath != null && !isPathInside(allowedRoot, import_node_path2.default.resolve(filePath))) {
|
|
8638
|
-
return c.json({ error: "Path traversal attempt" }, 400);
|
|
8639
|
-
}
|
|
9335
|
+
baseDir;
|
|
9336
|
+
kind = "local";
|
|
9337
|
+
async put(key, data, contentType = "application/octet-stream") {
|
|
9338
|
+
const path6 = (0, import_node_path2.join)(this.baseDir, "blobs", key);
|
|
9339
|
+
(0, import_node_fs2.mkdirSync)((0, import_node_path2.dirname)(path6), { recursive: true });
|
|
9340
|
+
(0, import_node_fs2.writeFileSync)(path6, data);
|
|
9341
|
+
return { key, url: `file://${path6}`, bytes: byteLength(data), contentType };
|
|
8640
9342
|
}
|
|
8641
|
-
|
|
8642
|
-
|
|
8643
|
-
|
|
8644
|
-
|
|
8645
|
-
|
|
8646
|
-
|
|
8647
|
-
|
|
8648
|
-
|
|
8649
|
-
|
|
8650
|
-
|
|
8651
|
-
|
|
8652
|
-
|
|
8653
|
-
|
|
8654
|
-
|
|
8655
|
-
|
|
8656
|
-
|
|
8657
|
-
|
|
8658
|
-
|
|
8659
|
-
|
|
8660
|
-
|
|
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
|
-
});
|
|
9343
|
+
};
|
|
9344
|
+
cached = null;
|
|
9345
|
+
VercelBlobStore = class {
|
|
9346
|
+
constructor(token) {
|
|
9347
|
+
this.token = token;
|
|
9348
|
+
}
|
|
9349
|
+
token;
|
|
9350
|
+
kind = "vercel-blob";
|
|
9351
|
+
async put(key, data, contentType = "application/octet-stream") {
|
|
9352
|
+
const { put } = await import("@vercel/blob");
|
|
9353
|
+
const body = Buffer.isBuffer(data) ? data : Buffer.from(data);
|
|
9354
|
+
const result = await put(key, body, {
|
|
9355
|
+
access: "public",
|
|
9356
|
+
token: this.token,
|
|
9357
|
+
contentType,
|
|
9358
|
+
addRandomSuffix: true
|
|
9359
|
+
});
|
|
9360
|
+
return { key, url: result.url, bytes: byteLength(data), contentType };
|
|
9361
|
+
}
|
|
9362
|
+
};
|
|
8777
9363
|
}
|
|
8778
9364
|
});
|
|
8779
9365
|
|
|
@@ -9023,6 +9609,792 @@ var init_rates = __esm({
|
|
|
9023
9609
|
}
|
|
9024
9610
|
});
|
|
9025
9611
|
|
|
9612
|
+
// src/api/site-extract-repository.ts
|
|
9613
|
+
function rowToJob(r) {
|
|
9614
|
+
return {
|
|
9615
|
+
id: String(r.id),
|
|
9616
|
+
userId: r.user_id != null ? Number(r.user_id) : null,
|
|
9617
|
+
status: r.status != null ? String(r.status) : "pending",
|
|
9618
|
+
startUrl: String(r.start_url ?? ""),
|
|
9619
|
+
options: r.options ? JSON.parse(String(r.options)) : {},
|
|
9620
|
+
totalUrls: Number(r.total_urls ?? 0),
|
|
9621
|
+
doneUrls: Number(r.done_urls ?? 0),
|
|
9622
|
+
artifacts: r.artifacts ? JSON.parse(String(r.artifacts)) : null,
|
|
9623
|
+
error: r.error != null ? String(r.error) : null,
|
|
9624
|
+
billedMc: r.billed_mc != null ? Number(r.billed_mc) : null,
|
|
9625
|
+
createdAt: String(r.created_at ?? ""),
|
|
9626
|
+
updatedAt: String(r.updated_at ?? "")
|
|
9627
|
+
};
|
|
9628
|
+
}
|
|
9629
|
+
async function createExtractJob(jobId, userId, startUrl, options) {
|
|
9630
|
+
const db = getDb();
|
|
9631
|
+
await db.execute({
|
|
9632
|
+
sql: `INSERT INTO site_extract_jobs (id, user_id, status, start_url, options, created_at, updated_at)
|
|
9633
|
+
VALUES (?, ?, 'pending', ?, ?, datetime('now'), datetime('now'))`,
|
|
9634
|
+
args: [jobId, userId, startUrl, JSON.stringify(options)]
|
|
9635
|
+
});
|
|
9636
|
+
}
|
|
9637
|
+
async function getExtractJob(jobId) {
|
|
9638
|
+
const db = getDb();
|
|
9639
|
+
const res = await db.execute({ sql: `SELECT * FROM site_extract_jobs WHERE id = ?`, args: [jobId] });
|
|
9640
|
+
return res.rows[0] ? rowToJob(res.rows[0]) : null;
|
|
9641
|
+
}
|
|
9642
|
+
async function setExtractJobTotal(jobId, totalUrls) {
|
|
9643
|
+
const db = getDb();
|
|
9644
|
+
await db.execute({
|
|
9645
|
+
sql: `UPDATE site_extract_jobs SET status = 'running', total_urls = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
9646
|
+
args: [totalUrls, jobId]
|
|
9647
|
+
});
|
|
9648
|
+
}
|
|
9649
|
+
async function saveExtractPages(jobId, pages) {
|
|
9650
|
+
if (pages.length === 0) return;
|
|
9651
|
+
const db = getDb();
|
|
9652
|
+
await db.batch(
|
|
9653
|
+
pages.map((p) => ({
|
|
9654
|
+
sql: `INSERT OR REPLACE INTO site_extract_pages (job_id, url, page) VALUES (?, ?, ?)`,
|
|
9655
|
+
args: [jobId, p.url, JSON.stringify({ ...p, bodyMarkdown: "", schema: [] })]
|
|
9656
|
+
}))
|
|
9657
|
+
);
|
|
9658
|
+
await db.execute({
|
|
9659
|
+
sql: `UPDATE site_extract_jobs
|
|
9660
|
+
SET done_urls = (SELECT COUNT(*) FROM site_extract_pages WHERE job_id = ?), updated_at = datetime('now')
|
|
9661
|
+
WHERE id = ?`,
|
|
9662
|
+
args: [jobId, jobId]
|
|
9663
|
+
});
|
|
9664
|
+
}
|
|
9665
|
+
async function getExtractedPages(jobId) {
|
|
9666
|
+
const db = getDb();
|
|
9667
|
+
const res = await db.execute({ sql: `SELECT page FROM site_extract_pages WHERE job_id = ?`, args: [jobId] });
|
|
9668
|
+
return res.rows.map((r) => JSON.parse(String(r.page)));
|
|
9669
|
+
}
|
|
9670
|
+
async function countSuccessfulPages(jobId) {
|
|
9671
|
+
const db = getDb();
|
|
9672
|
+
const res = await db.execute({
|
|
9673
|
+
sql: `SELECT COUNT(*) AS n FROM site_extract_pages
|
|
9674
|
+
WHERE job_id = ? AND json_extract(page, '$.status') = 200 AND json_extract(page, '$.wordCount') > 0`,
|
|
9675
|
+
args: [jobId]
|
|
9676
|
+
});
|
|
9677
|
+
return Number(res.rows[0]?.n ?? 0);
|
|
9678
|
+
}
|
|
9679
|
+
async function completeExtractJob(jobId, artifacts) {
|
|
9680
|
+
const db = getDb();
|
|
9681
|
+
await db.execute({
|
|
9682
|
+
sql: `UPDATE site_extract_jobs SET status = 'complete', artifacts = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
9683
|
+
args: [JSON.stringify(artifacts ?? []), jobId]
|
|
9684
|
+
});
|
|
9685
|
+
}
|
|
9686
|
+
async function failExtractJob(jobId, error) {
|
|
9687
|
+
const db = getDb();
|
|
9688
|
+
await db.execute({
|
|
9689
|
+
sql: `UPDATE site_extract_jobs SET status = 'failed', error = ?, updated_at = datetime('now') WHERE id = ?`,
|
|
9690
|
+
args: [sanitizeVendorName(error).slice(0, 2e3), jobId]
|
|
9691
|
+
});
|
|
9692
|
+
}
|
|
9693
|
+
async function settleExtractJob(jobId, userId, refundMc, netChargeMc, reference) {
|
|
9694
|
+
const db = getDb();
|
|
9695
|
+
const job = await getExtractJob(jobId);
|
|
9696
|
+
if (!job || job.billedMc != null) return;
|
|
9697
|
+
if (refundMc <= 0) {
|
|
9698
|
+
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] });
|
|
9699
|
+
return;
|
|
9700
|
+
}
|
|
9701
|
+
await db.batch([
|
|
9702
|
+
{ sql: "UPDATE users SET balance_mc = balance_mc + ? WHERE id = ?", args: [refundMc, userId] },
|
|
9703
|
+
{ sql: "INSERT INTO ledger (user_id, amount_mc, operation, description) VALUES (?, ?, ?, ?)", args: [userId, refundMc, LedgerOperation.EXTRACT_SITE_REFUND, reference] },
|
|
9704
|
+
{ 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] }
|
|
9705
|
+
]);
|
|
9706
|
+
}
|
|
9707
|
+
var init_site_extract_repository = __esm({
|
|
9708
|
+
"src/api/site-extract-repository.ts"() {
|
|
9709
|
+
"use strict";
|
|
9710
|
+
init_db();
|
|
9711
|
+
init_errors();
|
|
9712
|
+
init_rates();
|
|
9713
|
+
}
|
|
9714
|
+
});
|
|
9715
|
+
|
|
9716
|
+
// src/inngest/functions/site-extract.ts
|
|
9717
|
+
function chunk(items, size) {
|
|
9718
|
+
const out = [];
|
|
9719
|
+
for (let i = 0; i < items.length; i += size) out.push(items.slice(i, i + size));
|
|
9720
|
+
return out;
|
|
9721
|
+
}
|
|
9722
|
+
function buildArtifacts(job, pages, branding, imageAudit) {
|
|
9723
|
+
const { edges, metrics } = buildLinkGraph(pages, job.startUrl);
|
|
9724
|
+
const issues = computeIssues(pages, metrics);
|
|
9725
|
+
let reportMd = renderIssueReport(job.startUrl, pages, issues, metrics);
|
|
9726
|
+
if (imageAudit) reportMd += `
|
|
9727
|
+
|
|
9728
|
+
${renderImageSection(imageAudit)}`;
|
|
9729
|
+
const pageRows = pages.map((p) => {
|
|
9730
|
+
const { bodyMarkdown: _b, schema: _s, outlinks: _o, ...rest } = p;
|
|
9731
|
+
const m = metrics.get(p.url);
|
|
9732
|
+
return { ...rest, inlinks: m?.inlinks ?? 0, crawlDepth: m?.crawlDepth ?? null, orphan: m?.orphan ?? false };
|
|
9733
|
+
});
|
|
9734
|
+
const jsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n");
|
|
9735
|
+
const files = [
|
|
9736
|
+
{ name: "report.md", body: reportMd, type: "text/markdown" },
|
|
9737
|
+
{ name: "issues.json", body: JSON.stringify(issues, null, 2), type: "application/json" },
|
|
9738
|
+
{ name: "pages.jsonl", body: jsonl(pageRows), type: "application/x-ndjson" },
|
|
9739
|
+
{ name: "links.jsonl", body: jsonl(edges), type: "application/x-ndjson" },
|
|
9740
|
+
{ name: "link-metrics.jsonl", body: jsonl([...metrics.values()]), type: "application/x-ndjson" }
|
|
9741
|
+
];
|
|
9742
|
+
if (imageAudit) {
|
|
9743
|
+
files.push({ name: "images.jsonl", body: jsonl(imageAudit.rows), type: "application/x-ndjson" });
|
|
9744
|
+
files.push({ name: "images-summary.json", body: JSON.stringify(imageAudit.summary, null, 2), type: "application/json" });
|
|
9745
|
+
}
|
|
9746
|
+
if (branding) files.push({ name: "branding.json", body: JSON.stringify(branding, null, 2), type: "application/json" });
|
|
9747
|
+
return files;
|
|
9748
|
+
}
|
|
9749
|
+
var BATCH, siteExtractFn;
|
|
9750
|
+
var init_site_extract = __esm({
|
|
9751
|
+
"src/inngest/functions/site-extract.ts"() {
|
|
9752
|
+
"use strict";
|
|
9753
|
+
init_client();
|
|
9754
|
+
init_site_extractor();
|
|
9755
|
+
init_seo_link_graph();
|
|
9756
|
+
init_seo_issues();
|
|
9757
|
+
init_image_audit();
|
|
9758
|
+
init_blob_store();
|
|
9759
|
+
init_screenshot();
|
|
9760
|
+
init_browser_service_env();
|
|
9761
|
+
init_rates();
|
|
9762
|
+
init_site_extract_repository();
|
|
9763
|
+
BATCH = 30;
|
|
9764
|
+
siteExtractFn = inngest.createFunction(
|
|
9765
|
+
{
|
|
9766
|
+
id: "site-extract",
|
|
9767
|
+
retries: 2,
|
|
9768
|
+
triggers: [{ event: "mcp-scraper/extract.requested" }],
|
|
9769
|
+
onFailure: async ({ event }) => {
|
|
9770
|
+
const jobId = event?.data?.event?.data?.jobId;
|
|
9771
|
+
if (!jobId) return;
|
|
9772
|
+
const current = await getExtractJob(jobId);
|
|
9773
|
+
if (current && current.userId != null && current.billedMc == null) {
|
|
9774
|
+
const heldMc = Number(current.options.heldMc ?? 0);
|
|
9775
|
+
await settleExtractJob(jobId, current.userId, heldMc, 0, "crawl failed refund").catch(() => {
|
|
9776
|
+
});
|
|
9777
|
+
}
|
|
9778
|
+
await failExtractJob(jobId, String(event?.data?.error?.message ?? "crawl failed")).catch(() => {
|
|
9779
|
+
});
|
|
9780
|
+
}
|
|
9781
|
+
},
|
|
9782
|
+
async ({ event, step }) => {
|
|
9783
|
+
const jobId = event.data.jobId;
|
|
9784
|
+
const job = await step.run("load-job", () => getExtractJob(jobId));
|
|
9785
|
+
if (!job) return { jobId, status: "missing" };
|
|
9786
|
+
const key = browserServiceApiKey();
|
|
9787
|
+
if (!key) throw new Error("browser service key not configured");
|
|
9788
|
+
const maxPages = Number(job.options.maxPages ?? 1e4);
|
|
9789
|
+
const concurrency = Number(job.options.concurrency ?? 1);
|
|
9790
|
+
const urlsPerBrowser = Number(job.options.urlsPerBrowser ?? job.options.rotateProxyEvery ?? 10);
|
|
9791
|
+
const urls = await step.run("discover", async () => {
|
|
9792
|
+
const discovered = await discoverUrls(job.startUrl, maxPages, key);
|
|
9793
|
+
await setExtractJobTotal(jobId, discovered.length);
|
|
9794
|
+
return discovered;
|
|
9795
|
+
});
|
|
9796
|
+
const batches = chunk(urls, BATCH);
|
|
9797
|
+
for (let i = 0; i < batches.length; i++) {
|
|
9798
|
+
await step.run(`crawl-batch-${i}`, async () => {
|
|
9799
|
+
const pages = await extractPagesRotating(batches[i], { kernelApiKey: key, concurrency, urlsPerBrowser });
|
|
9800
|
+
await saveExtractPages(jobId, pages);
|
|
9801
|
+
return pages.length;
|
|
9802
|
+
});
|
|
9803
|
+
}
|
|
9804
|
+
const branding = Array.isArray(job.options.formats) && job.options.formats.includes("branding") ? await step.run("branding", () => extractBranding(job.startUrl, key).catch(() => null)) : null;
|
|
9805
|
+
const imageAudit = Array.isArray(job.options.formats) && job.options.formats.includes("images") ? await step.run("image-audit", async () => {
|
|
9806
|
+
const pages = await getExtractedPages(jobId);
|
|
9807
|
+
return auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
|
|
9808
|
+
}) : null;
|
|
9809
|
+
const artifacts = await step.run("finalize", async () => {
|
|
9810
|
+
const pages = await getExtractedPages(jobId);
|
|
9811
|
+
const store = getBlobStore();
|
|
9812
|
+
const files = buildArtifacts(job, pages, branding, imageAudit);
|
|
9813
|
+
const stored = [];
|
|
9814
|
+
for (const f of files) stored.push(await store.put(`extract/${jobId}/${f.name}`, f.body, f.type));
|
|
9815
|
+
await completeExtractJob(jobId, stored);
|
|
9816
|
+
return stored;
|
|
9817
|
+
});
|
|
9818
|
+
await step.run("settle", async () => {
|
|
9819
|
+
const current = await getExtractJob(jobId);
|
|
9820
|
+
if (!current || current.billedMc != null || current.userId == null) return;
|
|
9821
|
+
const heldMc = Number(current.options.heldMc ?? 0);
|
|
9822
|
+
const successful = await countSuccessfulPages(jobId);
|
|
9823
|
+
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
9824
|
+
await settleExtractJob(jobId, current.userId, heldMc - usedMc, usedMc, new URL(job.startUrl).hostname);
|
|
9825
|
+
});
|
|
9826
|
+
return { jobId, status: "complete", artifacts };
|
|
9827
|
+
}
|
|
9828
|
+
);
|
|
9829
|
+
}
|
|
9830
|
+
});
|
|
9831
|
+
|
|
9832
|
+
// src/api/catalog-seed.ts
|
|
9833
|
+
var URL_PARAM, FORMATS_PARAM, CATALOG;
|
|
9834
|
+
var init_catalog_seed = __esm({
|
|
9835
|
+
"src/api/catalog-seed.ts"() {
|
|
9836
|
+
"use strict";
|
|
9837
|
+
URL_PARAM = { key: "url", label: "URL", type: "url", required: true, placeholder: "https://example.com" };
|
|
9838
|
+
FORMATS_PARAM = {
|
|
9839
|
+
key: "formats",
|
|
9840
|
+
label: "Formats",
|
|
9841
|
+
type: "multi_enum",
|
|
9842
|
+
default: ["markdown", "links"],
|
|
9843
|
+
enumValues: [
|
|
9844
|
+
{ value: "markdown", label: "Markdown" },
|
|
9845
|
+
{ value: "links", label: "Links" },
|
|
9846
|
+
{ value: "json", label: "JSON / schema" },
|
|
9847
|
+
{ value: "images", label: "Image links" },
|
|
9848
|
+
{ value: "branding", label: "Branding" }
|
|
9849
|
+
]
|
|
9850
|
+
};
|
|
9851
|
+
CATALOG = {
|
|
9852
|
+
groups: [
|
|
9853
|
+
{
|
|
9854
|
+
key: "youtube",
|
|
9855
|
+
label: "YouTube",
|
|
9856
|
+
icon: "youtube",
|
|
9857
|
+
tools: [
|
|
9858
|
+
{
|
|
9859
|
+
key: "youtube_harvest",
|
|
9860
|
+
label: "Search",
|
|
9861
|
+
kind: "simple",
|
|
9862
|
+
endpoint: "/youtube/harvest",
|
|
9863
|
+
mcpName: "youtube_harvest",
|
|
9864
|
+
tagline: "Find or list videos by topic or channel",
|
|
9865
|
+
params: [
|
|
9866
|
+
{ key: "mode", label: "Mode", type: "enum", required: true, default: "search", enumValues: [{ value: "search", label: "Search" }, { value: "channel", label: "Channel" }] },
|
|
9867
|
+
{ key: "query", label: "Query", type: "string", placeholder: "best local SEO 2026" },
|
|
9868
|
+
{ key: "channelHandle", label: "Channel handle", type: "string", placeholder: "@mkbhd" },
|
|
9869
|
+
{ key: "maxVideos", label: "Max videos", type: "number", min: 1, max: 500, default: 50, advanced: true }
|
|
9870
|
+
],
|
|
9871
|
+
examples: ["Find YouTube videos about local SEO", "Harvest videos from @mkbhd"]
|
|
9872
|
+
},
|
|
9873
|
+
{
|
|
9874
|
+
key: "youtube_transcribe",
|
|
9875
|
+
label: "Transcribe",
|
|
9876
|
+
kind: "simple",
|
|
9877
|
+
endpoint: "/youtube/transcribe",
|
|
9878
|
+
mcpName: "youtube_transcribe",
|
|
9879
|
+
tagline: "Transcribe one video to text",
|
|
9880
|
+
params: [{ key: "url", label: "Video URL", type: "url", required: true, placeholder: "https://www.youtube.com/watch?v=\u2026" }],
|
|
9881
|
+
examples: ["Transcribe this YouTube URL"]
|
|
9882
|
+
}
|
|
9883
|
+
]
|
|
9884
|
+
},
|
|
9885
|
+
{
|
|
9886
|
+
key: "facebook",
|
|
9887
|
+
label: "Facebook",
|
|
9888
|
+
icon: "facebook",
|
|
9889
|
+
tools: [
|
|
9890
|
+
{
|
|
9891
|
+
key: "facebook_ad_search",
|
|
9892
|
+
label: "Ad Search",
|
|
9893
|
+
kind: "simple",
|
|
9894
|
+
endpoint: "/facebook/search",
|
|
9895
|
+
mcpName: "facebook_ad_search",
|
|
9896
|
+
tagline: "Find ads in the Ad Library",
|
|
9897
|
+
params: [
|
|
9898
|
+
{ key: "query", label: "Advertiser / query", type: "string", required: true },
|
|
9899
|
+
{ key: "country", label: "Country", type: "string", default: "US" },
|
|
9900
|
+
{ key: "maxResults", label: "Max results", type: "number", min: 1, max: 20, default: 10, advanced: true }
|
|
9901
|
+
],
|
|
9902
|
+
examples: ["Find Facebook ads for this brand"]
|
|
9903
|
+
},
|
|
9904
|
+
{
|
|
9905
|
+
key: "facebook_video_transcribe",
|
|
9906
|
+
label: "Video Transcribe",
|
|
9907
|
+
kind: "simple",
|
|
9908
|
+
endpoint: "/facebook/video-transcribe",
|
|
9909
|
+
mcpName: "facebook_video_transcribe",
|
|
9910
|
+
tagline: "Transcribe a Facebook video/reel",
|
|
9911
|
+
params: [{ ...URL_PARAM, label: "Video URL", placeholder: "https://www.facebook.com/share/v/\u2026" }]
|
|
9912
|
+
},
|
|
9913
|
+
{
|
|
9914
|
+
key: "facebook_ad_transcribe",
|
|
9915
|
+
label: "Ad Transcribe",
|
|
9916
|
+
kind: "simple",
|
|
9917
|
+
endpoint: "/facebook/transcribe",
|
|
9918
|
+
mcpName: "facebook_ad_transcribe",
|
|
9919
|
+
tagline: "Transcribe an ad creative",
|
|
9920
|
+
params: [{ ...URL_PARAM, label: "Ad video URL" }]
|
|
9921
|
+
},
|
|
9922
|
+
{
|
|
9923
|
+
key: "facebook_page_intel",
|
|
9924
|
+
label: "Page Intel",
|
|
9925
|
+
kind: "simple",
|
|
9926
|
+
endpoint: "/facebook/page-intel",
|
|
9927
|
+
mcpName: "facebook_page_intel",
|
|
9928
|
+
tagline: "Advertiser page profile + ads",
|
|
9929
|
+
params: [{ key: "libraryId", label: "Page / Library ID or URL", type: "string", required: true }]
|
|
9930
|
+
}
|
|
9931
|
+
]
|
|
9932
|
+
},
|
|
9933
|
+
{
|
|
9934
|
+
key: "instagram",
|
|
9935
|
+
label: "Instagram",
|
|
9936
|
+
icon: "instagram",
|
|
9937
|
+
tools: [
|
|
9938
|
+
{
|
|
9939
|
+
key: "instagram_profile_content",
|
|
9940
|
+
label: "Profile",
|
|
9941
|
+
kind: "simple",
|
|
9942
|
+
endpoint: "/instagram/profile-content",
|
|
9943
|
+
mcpName: "instagram_profile_content",
|
|
9944
|
+
tagline: "Inventory a profile grid",
|
|
9945
|
+
params: [
|
|
9946
|
+
{ key: "url", label: "Profile URL or handle", type: "string", required: true, placeholder: "https://instagram.com/nasa" },
|
|
9947
|
+
{ key: "maxItems", label: "Max items", type: "number", min: 1, max: 2e3, default: 50, advanced: true },
|
|
9948
|
+
{ key: "maxScrolls", label: "Max scrolls", type: "number", min: 0, max: 250, default: 10, advanced: true }
|
|
9949
|
+
]
|
|
9950
|
+
},
|
|
9951
|
+
{
|
|
9952
|
+
key: "instagram_media_download",
|
|
9953
|
+
label: "Post / Reel",
|
|
9954
|
+
kind: "simple",
|
|
9955
|
+
endpoint: "/instagram/media-download",
|
|
9956
|
+
mcpName: "instagram_media_download",
|
|
9957
|
+
tagline: "Download a post or reel (+ transcript)",
|
|
9958
|
+
params: [
|
|
9959
|
+
{ ...URL_PARAM, label: "Post / reel URL", placeholder: "https://www.instagram.com/reel/\u2026" },
|
|
9960
|
+
{ key: "includeTranscript", label: "Include transcript", type: "boolean", default: true }
|
|
9961
|
+
]
|
|
9962
|
+
}
|
|
9963
|
+
]
|
|
9964
|
+
},
|
|
9965
|
+
{
|
|
9966
|
+
key: "reddit",
|
|
9967
|
+
label: "Reddit",
|
|
9968
|
+
icon: "message-circle",
|
|
9969
|
+
tools: [
|
|
9970
|
+
{
|
|
9971
|
+
key: "reddit_browser",
|
|
9972
|
+
label: "Research (Browser)",
|
|
9973
|
+
kind: "simple",
|
|
9974
|
+
endpoint: "/agent",
|
|
9975
|
+
mcpName: "browser_open",
|
|
9976
|
+
tagline: "No dedicated tool yet \u2014 driven by the Browser agent",
|
|
9977
|
+
params: [{ key: "query", label: "What to research", type: "string", required: true, placeholder: "ICP pain points in [niche]" }]
|
|
9978
|
+
}
|
|
9979
|
+
]
|
|
9980
|
+
},
|
|
9981
|
+
{
|
|
9982
|
+
key: "google_search",
|
|
9983
|
+
label: "Google Search",
|
|
9984
|
+
icon: "search",
|
|
9985
|
+
tools: [
|
|
9986
|
+
{
|
|
9987
|
+
key: "search_serp",
|
|
9988
|
+
label: "SERP",
|
|
9989
|
+
kind: "simple",
|
|
9990
|
+
endpoint: "/harvest/sync",
|
|
9991
|
+
mcpName: "search_serp",
|
|
9992
|
+
tagline: "Organic results only",
|
|
9993
|
+
params: [
|
|
9994
|
+
{ key: "query", label: "Query", type: "string", required: true },
|
|
9995
|
+
{ key: "location", label: "Location", type: "string", placeholder: "Denver, CO" }
|
|
9996
|
+
],
|
|
9997
|
+
examples: ["Top restaurants in San Francisco"]
|
|
9998
|
+
},
|
|
9999
|
+
{
|
|
10000
|
+
key: "harvest_paa",
|
|
10001
|
+
label: "PAA + SERP Detail",
|
|
10002
|
+
kind: "simple",
|
|
10003
|
+
endpoint: "/harvest/sync",
|
|
10004
|
+
mcpName: "harvest_paa",
|
|
10005
|
+
tagline: "People-Also-Ask, AI Overview, full SERP features",
|
|
10006
|
+
params: [
|
|
10007
|
+
{ key: "query", label: "Query", type: "string", required: true },
|
|
10008
|
+
{ key: "maxQuestions", label: "Max questions", type: "number", min: 1, max: 200, default: 30, advanced: true },
|
|
10009
|
+
{ key: "location", label: "Location", type: "string" }
|
|
10010
|
+
]
|
|
10011
|
+
}
|
|
10012
|
+
]
|
|
10013
|
+
},
|
|
10014
|
+
{
|
|
10015
|
+
key: "google_maps",
|
|
10016
|
+
label: "Google Maps",
|
|
10017
|
+
icon: "map-pin",
|
|
10018
|
+
tools: [
|
|
10019
|
+
{
|
|
10020
|
+
key: "maps_search",
|
|
10021
|
+
label: "Search",
|
|
10022
|
+
kind: "simple",
|
|
10023
|
+
endpoint: "/maps/search",
|
|
10024
|
+
mcpName: "maps_search",
|
|
10025
|
+
tagline: "Find local businesses",
|
|
10026
|
+
params: [
|
|
10027
|
+
{ key: "query", label: "Query", type: "string", required: true, placeholder: "roofers" },
|
|
10028
|
+
{ key: "location", label: "Location", type: "string", required: true, placeholder: "Denver, CO" },
|
|
10029
|
+
{ key: "maxResults", label: "Max results", type: "number", min: 1, max: 20, default: 10, advanced: true }
|
|
10030
|
+
]
|
|
10031
|
+
},
|
|
10032
|
+
{
|
|
10033
|
+
key: "maps_place_intel",
|
|
10034
|
+
label: "Place Intel",
|
|
10035
|
+
kind: "simple",
|
|
10036
|
+
endpoint: "/maps/place",
|
|
10037
|
+
mcpName: "maps_place_intel",
|
|
10038
|
+
tagline: "One place: details + reviews",
|
|
10039
|
+
params: [
|
|
10040
|
+
{ key: "businessName", label: "Business name", type: "string", required: true },
|
|
10041
|
+
{ key: "location", label: "Location", type: "string", required: true },
|
|
10042
|
+
{ key: "includeReviews", label: "Include reviews", type: "boolean", default: false },
|
|
10043
|
+
{ key: "maxReviews", label: "Max reviews", type: "number", min: 1, max: 500, default: 50, advanced: true }
|
|
10044
|
+
]
|
|
10045
|
+
}
|
|
10046
|
+
]
|
|
10047
|
+
},
|
|
10048
|
+
{
|
|
10049
|
+
key: "websites",
|
|
10050
|
+
label: "Websites",
|
|
10051
|
+
icon: "globe",
|
|
10052
|
+
tools: [
|
|
10053
|
+
{
|
|
10054
|
+
key: "scrape",
|
|
10055
|
+
label: "Scrape",
|
|
10056
|
+
kind: "modal",
|
|
10057
|
+
endpoint: "/extract-url",
|
|
10058
|
+
mcpName: "extract_url",
|
|
10059
|
+
tagline: "Headless single or bulk page extraction",
|
|
10060
|
+
modes: [
|
|
10061
|
+
{ groupKey: "scope", groupLabel: "Scope", optionKey: "single", optionLabel: "Headless Single", isDefault: true, endpointOverride: "/extract-url", mcpNameOverride: "extract_url" },
|
|
10062
|
+
{ groupKey: "scope", groupLabel: "Scope", optionKey: "bulk", optionLabel: "Headless Bulk", endpointOverride: "/extract-site", mcpNameOverride: "extract_site", paramOverrides: { rotateProxies: true } }
|
|
10063
|
+
],
|
|
10064
|
+
params: [
|
|
10065
|
+
URL_PARAM,
|
|
10066
|
+
FORMATS_PARAM,
|
|
10067
|
+
{ key: "maxPages", label: "Max pages", type: "number", min: 1, max: 1e4, default: 100, advanced: true, sourceKey: "scope:bulk" },
|
|
10068
|
+
{ key: "screenshot", label: "Screenshot", type: "boolean", advanced: true }
|
|
10069
|
+
],
|
|
10070
|
+
examples: ["https://steenshoney.com/"]
|
|
10071
|
+
},
|
|
10072
|
+
{
|
|
10073
|
+
key: "map_site_urls",
|
|
10074
|
+
label: "Map",
|
|
10075
|
+
kind: "simple",
|
|
10076
|
+
endpoint: "/map-urls",
|
|
10077
|
+
mcpName: "map_site_urls",
|
|
10078
|
+
tagline: "URL inventory only",
|
|
10079
|
+
params: [URL_PARAM, { key: "maxUrls", label: "Max URLs", type: "number", min: 1, max: 1e4, default: 100, advanced: true }]
|
|
10080
|
+
},
|
|
10081
|
+
{
|
|
10082
|
+
key: "extract_site",
|
|
10083
|
+
label: "Crawl",
|
|
10084
|
+
kind: "simple",
|
|
10085
|
+
endpoint: "/extract-site",
|
|
10086
|
+
mcpName: "extract_site",
|
|
10087
|
+
tagline: "Map + bulk scrape (content)",
|
|
10088
|
+
params: [
|
|
10089
|
+
URL_PARAM,
|
|
10090
|
+
{ key: "maxPages", label: "Max pages", type: "number", min: 1, max: 1e4, default: 100 },
|
|
10091
|
+
FORMATS_PARAM,
|
|
10092
|
+
{ key: "rotateProxies", label: "Rotate proxies (blocked sites)", type: "boolean", default: true },
|
|
10093
|
+
{ key: "background", label: "Run in background", type: "boolean", default: true, advanced: true }
|
|
10094
|
+
],
|
|
10095
|
+
examples: ["https://steenshoney.com/ (full-site content crawl)"]
|
|
10096
|
+
},
|
|
10097
|
+
{
|
|
10098
|
+
key: "audit_site",
|
|
10099
|
+
label: "Audit",
|
|
10100
|
+
kind: "simple",
|
|
10101
|
+
endpoint: "/extract-site",
|
|
10102
|
+
mcpName: "audit_site",
|
|
10103
|
+
tagline: "Technical SEO audit (Screaming-Frog style)",
|
|
10104
|
+
params: [
|
|
10105
|
+
URL_PARAM,
|
|
10106
|
+
{ key: "maxPages", label: "Max pages", type: "number", min: 1, max: 1e4, default: 100 },
|
|
10107
|
+
{ key: "rotateProxies", label: "Rotate proxies (blocked sites)", type: "boolean", default: true }
|
|
10108
|
+
],
|
|
10109
|
+
examples: ["https://steenshoney.com/ (issues + link graph + image audit)"]
|
|
10110
|
+
}
|
|
10111
|
+
]
|
|
10112
|
+
},
|
|
10113
|
+
{
|
|
10114
|
+
key: "browser",
|
|
10115
|
+
label: "Browser",
|
|
10116
|
+
icon: "monitor",
|
|
10117
|
+
tools: [
|
|
10118
|
+
{
|
|
10119
|
+
key: "browser_agent",
|
|
10120
|
+
label: "Browser Agent",
|
|
10121
|
+
kind: "simple",
|
|
10122
|
+
endpoint: "/agent",
|
|
10123
|
+
mcpName: "browser_open",
|
|
10124
|
+
tagline: "Drive a live browser (open, navigate, read, click)",
|
|
10125
|
+
params: [
|
|
10126
|
+
{ key: "url", label: "Start URL", type: "url" },
|
|
10127
|
+
{ key: "profile", label: "Use Chrome profile", type: "string", placeholder: "saved profile name", advanced: true }
|
|
10128
|
+
]
|
|
10129
|
+
},
|
|
10130
|
+
{
|
|
10131
|
+
key: "query_fanout_workflow",
|
|
10132
|
+
label: "AI Visibility (Fan-out)",
|
|
10133
|
+
kind: "simple",
|
|
10134
|
+
mcpName: "query_fanout_workflow",
|
|
10135
|
+
tagline: "Capture what ChatGPT/Claude search & cite (AEO)",
|
|
10136
|
+
params: [
|
|
10137
|
+
{ key: "prompt", label: "Prompt", type: "string" },
|
|
10138
|
+
{ key: "first_party_domain", label: "Your domain", type: "string", advanced: true },
|
|
10139
|
+
{ key: "export", label: "Export artifacts", type: "boolean", advanced: true }
|
|
10140
|
+
]
|
|
10141
|
+
}
|
|
10142
|
+
]
|
|
10143
|
+
},
|
|
10144
|
+
{
|
|
10145
|
+
key: "workflows",
|
|
10146
|
+
label: "Workflows",
|
|
10147
|
+
icon: "workflow",
|
|
10148
|
+
tools: [
|
|
10149
|
+
{
|
|
10150
|
+
key: "directory_workflow",
|
|
10151
|
+
label: "Directory",
|
|
10152
|
+
kind: "simple",
|
|
10153
|
+
endpoint: "/directory/run",
|
|
10154
|
+
mcpName: "directory_workflow",
|
|
10155
|
+
isSpecial: true,
|
|
10156
|
+
tagline: "Build a local business directory",
|
|
10157
|
+
params: [
|
|
10158
|
+
{ key: "query", label: "Business type", type: "string", required: true, placeholder: "roofers" },
|
|
10159
|
+
{ key: "state", label: "State", type: "string", required: true, placeholder: "TN" },
|
|
10160
|
+
{ key: "minPopulation", label: "Min city population", type: "number", default: 1e5, advanced: true }
|
|
10161
|
+
]
|
|
10162
|
+
},
|
|
10163
|
+
{
|
|
10164
|
+
key: "rank_tracker_workflow",
|
|
10165
|
+
label: "Rank Tracker",
|
|
10166
|
+
kind: "simple",
|
|
10167
|
+
mcpName: "rank_tracker_workflow",
|
|
10168
|
+
isSpecial: true,
|
|
10169
|
+
tagline: "Generate a rank-tracker blueprint",
|
|
10170
|
+
params: [{ key: "domain", label: "Domain", type: "string", required: true }]
|
|
10171
|
+
},
|
|
10172
|
+
{
|
|
10173
|
+
key: "deep_research_workflow",
|
|
10174
|
+
label: "Deep Research",
|
|
10175
|
+
kind: "simple",
|
|
10176
|
+
endpoint: "/workflows/run",
|
|
10177
|
+
mcpName: "workflow_run",
|
|
10178
|
+
isSpecial: true,
|
|
10179
|
+
tagline: "Multi-source, fact-checked research report",
|
|
10180
|
+
params: [{ key: "question", label: "Question", type: "string", required: true }]
|
|
10181
|
+
}
|
|
10182
|
+
]
|
|
10183
|
+
}
|
|
10184
|
+
]
|
|
10185
|
+
};
|
|
10186
|
+
}
|
|
10187
|
+
});
|
|
10188
|
+
|
|
10189
|
+
// src/api/site-audit-middleware.ts
|
|
10190
|
+
var import_factory2, siteAuditAuth;
|
|
10191
|
+
var init_site_audit_middleware = __esm({
|
|
10192
|
+
"src/api/site-audit-middleware.ts"() {
|
|
10193
|
+
"use strict";
|
|
10194
|
+
import_factory2 = require("hono/factory");
|
|
10195
|
+
init_db();
|
|
10196
|
+
siteAuditAuth = (0, import_factory2.createMiddleware)(async (c, next) => {
|
|
10197
|
+
const apiKey = c.req.header("x-api-key");
|
|
10198
|
+
if (apiKey) {
|
|
10199
|
+
const user = await getUserByApiKey(apiKey);
|
|
10200
|
+
if (!user) return c.json({ error: "Unauthorized" }, 401);
|
|
10201
|
+
c.set("siteAuditUserId", user.id);
|
|
10202
|
+
return next();
|
|
10203
|
+
}
|
|
10204
|
+
return c.json({ error: "Unauthorized" }, 401);
|
|
10205
|
+
});
|
|
10206
|
+
}
|
|
10207
|
+
});
|
|
10208
|
+
|
|
10209
|
+
// src/api/site-audit-routes.ts
|
|
10210
|
+
function isPathInside(root, target) {
|
|
10211
|
+
const relative = import_node_path3.default.relative(root, target);
|
|
10212
|
+
return relative === "" || !relative.startsWith("..") && !import_node_path3.default.isAbsolute(relative);
|
|
10213
|
+
}
|
|
10214
|
+
var import_node_path3, import_node_os3, import_hono, import_zod11, siteAuditApp;
|
|
10215
|
+
var init_site_audit_routes = __esm({
|
|
10216
|
+
"src/api/site-audit-routes.ts"() {
|
|
10217
|
+
"use strict";
|
|
10218
|
+
import_node_path3 = __toESM(require("path"), 1);
|
|
10219
|
+
import_node_os3 = __toESM(require("os"), 1);
|
|
10220
|
+
import_hono = require("hono");
|
|
10221
|
+
import_zod11 = require("zod");
|
|
10222
|
+
init_site_audit_middleware();
|
|
10223
|
+
init_factory();
|
|
10224
|
+
init_phases();
|
|
10225
|
+
init_schemas();
|
|
10226
|
+
siteAuditApp = new import_hono.Hono();
|
|
10227
|
+
siteAuditApp.onError((err, c) => {
|
|
10228
|
+
if (err instanceof import_zod11.ZodError) {
|
|
10229
|
+
return c.json({ error: "Invalid request body", issues: err.issues }, 400);
|
|
10230
|
+
}
|
|
10231
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
10232
|
+
return c.json({ error: msg }, 500);
|
|
10233
|
+
});
|
|
10234
|
+
siteAuditApp.use("*", siteAuditAuth);
|
|
10235
|
+
siteAuditApp.post("/audit", async (c) => {
|
|
10236
|
+
const body = SiteAuditStartRequestSchema.parse(await c.req.json());
|
|
10237
|
+
const allowedRoot = import_node_path3.default.resolve(process.env["SESSION_ROOT"] ?? import_node_os3.default.tmpdir());
|
|
10238
|
+
const sessionPath = import_node_path3.default.resolve(body.sessionPath);
|
|
10239
|
+
if (!isPathInside(allowedRoot, sessionPath)) {
|
|
10240
|
+
return c.json({ error: "sessionPath must be inside the configured session root" }, 400);
|
|
10241
|
+
}
|
|
10242
|
+
const pathFields = [
|
|
10243
|
+
body.sessionPath,
|
|
10244
|
+
body.sfInternalPath,
|
|
10245
|
+
body.sfInlinksPath,
|
|
10246
|
+
body.sfOutlinksPath,
|
|
10247
|
+
body.sitemapPath,
|
|
10248
|
+
body.competitorDomainsCsvPath,
|
|
10249
|
+
body.visualSitemapsPath,
|
|
10250
|
+
body.gscPath,
|
|
10251
|
+
body.ga4Path,
|
|
10252
|
+
body.ahrefsPath
|
|
10253
|
+
];
|
|
10254
|
+
for (const filePath of pathFields) {
|
|
10255
|
+
if (filePath != null && !isPathInside(allowedRoot, import_node_path3.default.resolve(filePath))) {
|
|
10256
|
+
return c.json({ error: "Path traversal attempt" }, 400);
|
|
10257
|
+
}
|
|
10258
|
+
}
|
|
10259
|
+
const userId = c.get("siteAuditUserId");
|
|
10260
|
+
const service = makeSiteAuditService();
|
|
10261
|
+
const jobId = await service.startAudit(userId, body);
|
|
10262
|
+
void dispatchSiteAuditPhase(service, "phase1-ingest", { jobId, request: body }).catch(
|
|
10263
|
+
(err) => {
|
|
10264
|
+
console.error(
|
|
10265
|
+
"[site-audit-routes] phase1-ingest dispatch failed for job",
|
|
10266
|
+
jobId,
|
|
10267
|
+
err instanceof Error ? err.message : String(err)
|
|
10268
|
+
);
|
|
10269
|
+
}
|
|
10270
|
+
);
|
|
10271
|
+
return c.json({ jobId }, 202);
|
|
10272
|
+
});
|
|
10273
|
+
siteAuditApp.post("/ingest", async (c) => {
|
|
10274
|
+
const body = SiteAuditIngestRequestSchema.parse(await c.req.json());
|
|
10275
|
+
const service = makeSiteAuditService();
|
|
10276
|
+
const job = await service.getJob(body.jobId);
|
|
10277
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10278
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10279
|
+
void dispatchSiteAuditPhase(service, "phase1-ingest", {
|
|
10280
|
+
jobId: body.jobId,
|
|
10281
|
+
request: JSON.parse(job.request)
|
|
10282
|
+
}).catch((err) => {
|
|
10283
|
+
console.error(
|
|
10284
|
+
"[site-audit-routes] phase1-ingest re-dispatch failed for job",
|
|
10285
|
+
body.jobId,
|
|
10286
|
+
err instanceof Error ? err.message : String(err)
|
|
10287
|
+
);
|
|
10288
|
+
});
|
|
10289
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10290
|
+
});
|
|
10291
|
+
siteAuditApp.post("/build-graph", async (c) => {
|
|
10292
|
+
const body = SiteAuditBuildGraphRequestSchema.parse(await c.req.json());
|
|
10293
|
+
const service = makeSiteAuditService();
|
|
10294
|
+
const job = await service.getJob(body.jobId);
|
|
10295
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10296
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10297
|
+
void dispatchSiteAuditPhase(service, "phase2-build-graph", { jobId: body.jobId }).catch(
|
|
10298
|
+
(err) => {
|
|
10299
|
+
console.error(
|
|
10300
|
+
"[site-audit-routes] phase2-build-graph failed for job",
|
|
10301
|
+
body.jobId,
|
|
10302
|
+
err instanceof Error ? err.message : String(err)
|
|
10303
|
+
);
|
|
10304
|
+
}
|
|
10305
|
+
);
|
|
10306
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10307
|
+
});
|
|
10308
|
+
siteAuditApp.post("/classify", async (c) => {
|
|
10309
|
+
const body = SiteAuditClassifyRequestSchema.parse(await c.req.json());
|
|
10310
|
+
const service = makeSiteAuditService();
|
|
10311
|
+
const job = await service.getJob(body.jobId);
|
|
10312
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10313
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10314
|
+
void dispatchSiteAuditPhase(service, "phase3-classify", { jobId: body.jobId }).catch(
|
|
10315
|
+
(err) => {
|
|
10316
|
+
console.error(
|
|
10317
|
+
"[site-audit-routes] phase3-classify failed for job",
|
|
10318
|
+
body.jobId,
|
|
10319
|
+
err instanceof Error ? err.message : String(err)
|
|
10320
|
+
);
|
|
10321
|
+
}
|
|
10322
|
+
);
|
|
10323
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10324
|
+
});
|
|
10325
|
+
siteAuditApp.post("/compare", async (c) => {
|
|
10326
|
+
const body = SiteAuditCompareRequestSchema.parse(await c.req.json());
|
|
10327
|
+
const service = makeSiteAuditService();
|
|
10328
|
+
const job = await service.getJob(body.jobId);
|
|
10329
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10330
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10331
|
+
void dispatchSiteAuditPhase(service, "phase4-compare", { jobId: body.jobId }).catch(
|
|
10332
|
+
(err) => {
|
|
10333
|
+
console.error(
|
|
10334
|
+
"[site-audit-routes] phase4-compare failed for job",
|
|
10335
|
+
body.jobId,
|
|
10336
|
+
err instanceof Error ? err.message : String(err)
|
|
10337
|
+
);
|
|
10338
|
+
}
|
|
10339
|
+
);
|
|
10340
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10341
|
+
});
|
|
10342
|
+
siteAuditApp.post("/score", async (c) => {
|
|
10343
|
+
const body = SiteAuditScoreRequestSchema.parse(await c.req.json());
|
|
10344
|
+
const service = makeSiteAuditService();
|
|
10345
|
+
const job = await service.getJob(body.jobId);
|
|
10346
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10347
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10348
|
+
void dispatchSiteAuditPhase(service, "phase5-synthesize", {
|
|
10349
|
+
jobId: body.jobId,
|
|
10350
|
+
scoringOnly: true,
|
|
10351
|
+
reportOnly: false
|
|
10352
|
+
}).catch((err) => {
|
|
10353
|
+
console.error(
|
|
10354
|
+
"[site-audit-routes] phase5-synthesize(scoringOnly) failed for job",
|
|
10355
|
+
body.jobId,
|
|
10356
|
+
err instanceof Error ? err.message : String(err)
|
|
10357
|
+
);
|
|
10358
|
+
});
|
|
10359
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10360
|
+
});
|
|
10361
|
+
siteAuditApp.post("/report", async (c) => {
|
|
10362
|
+
const body = SiteAuditReportRequestSchema.parse(await c.req.json());
|
|
10363
|
+
const service = makeSiteAuditService();
|
|
10364
|
+
const job = await service.getJob(body.jobId);
|
|
10365
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10366
|
+
if (job.user_id !== c.get("siteAuditUserId")) return c.json({ error: "Not found" }, 404);
|
|
10367
|
+
void dispatchSiteAuditPhase(service, "phase5-synthesize", {
|
|
10368
|
+
jobId: body.jobId,
|
|
10369
|
+
scoringOnly: false,
|
|
10370
|
+
reportOnly: true
|
|
10371
|
+
}).catch((err) => {
|
|
10372
|
+
console.error(
|
|
10373
|
+
"[site-audit-routes] phase5-synthesize(reportOnly) failed for job",
|
|
10374
|
+
body.jobId,
|
|
10375
|
+
err instanceof Error ? err.message : String(err)
|
|
10376
|
+
);
|
|
10377
|
+
});
|
|
10378
|
+
return c.json({ jobId: body.jobId }, 202);
|
|
10379
|
+
});
|
|
10380
|
+
siteAuditApp.get("/jobs/:id", async (c) => {
|
|
10381
|
+
const jobId = c.req.param("id");
|
|
10382
|
+
const userId = c.get("siteAuditUserId");
|
|
10383
|
+
const service = makeSiteAuditService();
|
|
10384
|
+
const job = await service.getJob(jobId);
|
|
10385
|
+
if (!job) return c.json({ error: "Not found" }, 404);
|
|
10386
|
+
if (job.user_id !== userId) return c.json({ error: "Not found" }, 404);
|
|
10387
|
+
return c.json(job);
|
|
10388
|
+
});
|
|
10389
|
+
siteAuditApp.get("/jobs", async (c) => {
|
|
10390
|
+
const userId = c.get("siteAuditUserId");
|
|
10391
|
+
const service = makeSiteAuditService();
|
|
10392
|
+
const jobs = await service.getJobsForUser(userId);
|
|
10393
|
+
return c.json(jobs);
|
|
10394
|
+
});
|
|
10395
|
+
}
|
|
10396
|
+
});
|
|
10397
|
+
|
|
9026
10398
|
// src/youtube/schemas.ts
|
|
9027
10399
|
var import_zod12, YouTubeHarvestOptionsSchema;
|
|
9028
10400
|
var init_schemas2 = __esm({
|
|
@@ -9184,7 +10556,7 @@ function rankCheckContextOptions(config) {
|
|
|
9184
10556
|
...config.hasTouch !== void 0 ? { hasTouch: config.hasTouch } : {}
|
|
9185
10557
|
};
|
|
9186
10558
|
}
|
|
9187
|
-
async function
|
|
10559
|
+
async function withTimeout2(promise, timeoutMs, label) {
|
|
9188
10560
|
let timeout;
|
|
9189
10561
|
try {
|
|
9190
10562
|
return await Promise.race([
|
|
@@ -9224,14 +10596,14 @@ function buildYouTubeChannelVideosUrl(channelInput) {
|
|
|
9224
10596
|
}
|
|
9225
10597
|
return `https://www.youtube.com/${handle}/videos`;
|
|
9226
10598
|
}
|
|
9227
|
-
var import_playwright_extra, import_puppeteer_extra_plugin_stealth,
|
|
10599
|
+
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
10600
|
var init_BrowserDriver = __esm({
|
|
9229
10601
|
"src/driver/BrowserDriver.ts"() {
|
|
9230
10602
|
"use strict";
|
|
9231
10603
|
import_playwright_extra = require("playwright-extra");
|
|
9232
10604
|
import_puppeteer_extra_plugin_stealth = __toESM(require("puppeteer-extra-plugin-stealth"), 1);
|
|
9233
|
-
|
|
9234
|
-
|
|
10605
|
+
import_playwright4 = require("playwright");
|
|
10606
|
+
import_sdk5 = __toESM(require("@onkernel/sdk"), 1);
|
|
9235
10607
|
init_selectors();
|
|
9236
10608
|
init_errors();
|
|
9237
10609
|
import_playwright_extra.chromium.use((0, import_puppeteer_extra_plugin_stealth.default)());
|
|
@@ -9274,7 +10646,7 @@ var init_BrowserDriver = __esm({
|
|
|
9274
10646
|
serpNavigation: null
|
|
9275
10647
|
};
|
|
9276
10648
|
if (config.kernelApiKey) {
|
|
9277
|
-
this.kernelClient = new
|
|
10649
|
+
this.kernelClient = new import_sdk5.default({ apiKey: config.kernelApiKey });
|
|
9278
10650
|
const timeoutSeconds = positiveIntFromEnv("KERNEL_BROWSER_TIMEOUT_SECONDS", DEFAULT_KERNEL_BROWSER_TIMEOUT_SECONDS);
|
|
9279
10651
|
if (config.kernelProfileName && config.kernelProfileSaveChanges === true) {
|
|
9280
10652
|
await ensureKernelProfile(this.kernelClient, config.kernelProfileName);
|
|
@@ -9295,7 +10667,7 @@ var init_BrowserDriver = __esm({
|
|
|
9295
10667
|
let defaultProxyDisableError = null;
|
|
9296
10668
|
if (proxyMode === "none") {
|
|
9297
10669
|
try {
|
|
9298
|
-
await
|
|
10670
|
+
await withTimeout2(
|
|
9299
10671
|
this.kernelClient.browsers.update(this.kernelSessionId, { disable_default_proxy: true }),
|
|
9300
10672
|
5e3,
|
|
9301
10673
|
`Kernel session ${this.kernelSessionId} disable default proxy`
|
|
@@ -9338,7 +10710,7 @@ var init_BrowserDriver = __esm({
|
|
|
9338
10710
|
if (this.debugEnabled) {
|
|
9339
10711
|
await this.populateKernelRetrieveDebug(kernelDebug, config.kernelProxyId);
|
|
9340
10712
|
}
|
|
9341
|
-
this.browser = await
|
|
10713
|
+
this.browser = await import_playwright4.chromium.connectOverCDP(kernelBrowser.cdp_ws_url);
|
|
9342
10714
|
this.context = await this.browser.newContext(rankCheckContextOptions(config));
|
|
9343
10715
|
await this.installEsbuildHelperShims(this.context);
|
|
9344
10716
|
this.page = await this.context.newPage();
|
|
@@ -9375,7 +10747,7 @@ var init_BrowserDriver = __esm({
|
|
|
9375
10747
|
async populateKernelRetrieveDebug(kernelDebug, requestedProxyId) {
|
|
9376
10748
|
if (!this.kernelClient || !this.kernelSessionId) return;
|
|
9377
10749
|
try {
|
|
9378
|
-
const retrieved = await
|
|
10750
|
+
const retrieved = await withTimeout2(
|
|
9379
10751
|
this.kernelClient.browsers.retrieve(this.kernelSessionId),
|
|
9380
10752
|
5e3,
|
|
9381
10753
|
`Kernel session ${this.kernelSessionId} retrieve`
|
|
@@ -9433,7 +10805,7 @@ var init_BrowserDriver = __esm({
|
|
|
9433
10805
|
error: null
|
|
9434
10806
|
};
|
|
9435
10807
|
}
|
|
9436
|
-
await
|
|
10808
|
+
await withTimeout2(
|
|
9437
10809
|
debugPage.goto("https://ipapi.co/json/", { waitUntil: "domcontentloaded", timeout: 7e3 }),
|
|
9438
10810
|
8e3,
|
|
9439
10811
|
"browser network location navigation"
|
|
@@ -9459,7 +10831,7 @@ var init_BrowserDriver = __esm({
|
|
|
9459
10831
|
}
|
|
9460
10832
|
async loadJsonInDebugPage(debugPage, url) {
|
|
9461
10833
|
try {
|
|
9462
|
-
await
|
|
10834
|
+
await withTimeout2(
|
|
9463
10835
|
debugPage.goto(url, { waitUntil: "domcontentloaded", timeout: 7e3 }),
|
|
9464
10836
|
8e3,
|
|
9465
10837
|
`browser network location navigation ${url}`
|
|
@@ -9654,12 +11026,12 @@ var init_BrowserDriver = __esm({
|
|
|
9654
11026
|
event: "kernel_browser_delete_started",
|
|
9655
11027
|
kernel_session_id: sessionId
|
|
9656
11028
|
}));
|
|
9657
|
-
const deleteSession =
|
|
11029
|
+
const deleteSession = withTimeout2(
|
|
9658
11030
|
client2.browsers.deleteByID(sessionId),
|
|
9659
11031
|
KERNEL_SESSION_DELETE_TIMEOUT_MS,
|
|
9660
11032
|
`Kernel session ${sessionId} delete`
|
|
9661
11033
|
);
|
|
9662
|
-
const closeBrowser =
|
|
11034
|
+
const closeBrowser = withTimeout2(
|
|
9663
11035
|
b.close(),
|
|
9664
11036
|
KERNEL_BROWSER_CLOSE_TIMEOUT_MS,
|
|
9665
11037
|
`Kernel browser ${sessionId} close`
|
|
@@ -10060,14 +11432,14 @@ var init_YouTubeExtractor = __esm({
|
|
|
10060
11432
|
});
|
|
10061
11433
|
|
|
10062
11434
|
// src/youtube/MP3Downloader.ts
|
|
10063
|
-
var import_node_child_process, import_node_util, import_promises3,
|
|
11435
|
+
var import_node_child_process, import_node_util, import_promises3, import_node_path4, execFileAsync, MP3Downloader;
|
|
10064
11436
|
var init_MP3Downloader = __esm({
|
|
10065
11437
|
"src/youtube/MP3Downloader.ts"() {
|
|
10066
11438
|
"use strict";
|
|
10067
11439
|
import_node_child_process = require("child_process");
|
|
10068
11440
|
import_node_util = require("util");
|
|
10069
11441
|
import_promises3 = require("fs/promises");
|
|
10070
|
-
|
|
11442
|
+
import_node_path4 = __toESM(require("path"), 1);
|
|
10071
11443
|
execFileAsync = (0, import_node_util.promisify)(import_node_child_process.execFile);
|
|
10072
11444
|
MP3Downloader = class {
|
|
10073
11445
|
constructor(outputDir, reporter, concurrency = 3) {
|
|
@@ -10089,7 +11461,7 @@ var init_MP3Downloader = __esm({
|
|
|
10089
11461
|
}
|
|
10090
11462
|
async downloadOne(video) {
|
|
10091
11463
|
const url = video.url;
|
|
10092
|
-
const outputTemplate =
|
|
11464
|
+
const outputTemplate = import_node_path4.default.join(this.outputDir, "%(title)s.%(ext)s");
|
|
10093
11465
|
try {
|
|
10094
11466
|
const { stdout } = await execFileAsync("yt-dlp", [
|
|
10095
11467
|
"--extract-audio",
|
|
@@ -10167,24 +11539,24 @@ async function extractOnce(options) {
|
|
|
10167
11539
|
return extractor.extract(options);
|
|
10168
11540
|
}
|
|
10169
11541
|
async function writeOutputs(result, outputDir) {
|
|
10170
|
-
await
|
|
11542
|
+
await import_node_fs3.promises.mkdir(outputDir, { recursive: true });
|
|
10171
11543
|
const slug = result.target.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
10172
11544
|
const ts = Date.now();
|
|
10173
|
-
await
|
|
10174
|
-
|
|
11545
|
+
await import_node_fs3.promises.writeFile(
|
|
11546
|
+
import_node_path5.default.join(outputDir, `${slug}-${ts}.json`),
|
|
10175
11547
|
JSON.stringify(result, null, 2),
|
|
10176
11548
|
"utf8"
|
|
10177
11549
|
);
|
|
10178
11550
|
if (result.videos.length > 0) {
|
|
10179
|
-
await
|
|
10180
|
-
|
|
11551
|
+
await import_node_fs3.promises.writeFile(
|
|
11552
|
+
import_node_path5.default.join(outputDir, `${slug}-videos-${ts}.csv`),
|
|
10181
11553
|
import_papaparse.default.unparse(result.videos, { header: true }),
|
|
10182
11554
|
"utf8"
|
|
10183
11555
|
);
|
|
10184
11556
|
}
|
|
10185
11557
|
if (result.downloads.length > 0) {
|
|
10186
|
-
await
|
|
10187
|
-
|
|
11558
|
+
await import_node_fs3.promises.writeFile(
|
|
11559
|
+
import_node_path5.default.join(outputDir, `${slug}-downloads-${ts}.csv`),
|
|
10188
11560
|
import_papaparse.default.unparse(result.downloads, { header: true }),
|
|
10189
11561
|
"utf8"
|
|
10190
11562
|
);
|
|
@@ -10224,13 +11596,13 @@ async function ytHarvest(rawOptions) {
|
|
|
10224
11596
|
`YouTube blocked all ${MAX_ATTEMPTS} attempts. Try again in a few minutes.`
|
|
10225
11597
|
);
|
|
10226
11598
|
}
|
|
10227
|
-
var
|
|
11599
|
+
var import_node_fs3, import_node_path5, import_papaparse, MAX_ATTEMPTS, RETRY_DELAY_MS;
|
|
10228
11600
|
var init_youtube_harvest = __esm({
|
|
10229
11601
|
"src/youtube/youtube-harvest.ts"() {
|
|
10230
11602
|
"use strict";
|
|
10231
|
-
|
|
11603
|
+
import_node_fs3 = require("fs");
|
|
10232
11604
|
init_browser_service_env();
|
|
10233
|
-
|
|
11605
|
+
import_node_path5 = __toESM(require("path"), 1);
|
|
10234
11606
|
import_papaparse = __toESM(require("papaparse"), 1);
|
|
10235
11607
|
init_schemas2();
|
|
10236
11608
|
init_BrowserDriver();
|
|
@@ -10428,11 +11800,11 @@ async function attemptKernelWhisper(videoId, kernelApiKey, kernelProxyId, falKey
|
|
|
10428
11800
|
const audioInfo = await captureAudioViaMediaRecorder(page);
|
|
10429
11801
|
await driver.close();
|
|
10430
11802
|
if (!audioInfo || audioInfo.bytes.length < 1e4) return null;
|
|
10431
|
-
|
|
11803
|
+
import_client3.fal.config({ credentials: falKey });
|
|
10432
11804
|
const ext = audioInfo.mimeType.includes("ogg") ? "ogg" : "webm";
|
|
10433
11805
|
const audioFile = new File([new Uint8Array(audioInfo.bytes)], `audio.${ext}`, { type: audioInfo.mimeType });
|
|
10434
|
-
const uploadedUrl = await
|
|
10435
|
-
const result = await
|
|
11806
|
+
const uploadedUrl = await import_client3.fal.storage.upload(audioFile);
|
|
11807
|
+
const result = await import_client3.fal.subscribe("fal-ai/wizper", {
|
|
10436
11808
|
input: { audio_url: uploadedUrl, task: "transcribe", language: "en" },
|
|
10437
11809
|
logs: false,
|
|
10438
11810
|
pollInterval: 3e3
|
|
@@ -10487,13 +11859,13 @@ async function fetchCaptions(videoId) {
|
|
|
10487
11859
|
if (whisper) return finalizeCaptions(whisper);
|
|
10488
11860
|
throw new Error(`No captions available for ${videoId} \u2014 all three tiers failed`);
|
|
10489
11861
|
}
|
|
10490
|
-
var
|
|
11862
|
+
var import_client3, WHISPER_RECORD_SECONDS;
|
|
10491
11863
|
var init_CaptionFetcher = __esm({
|
|
10492
11864
|
"src/youtube/CaptionFetcher.ts"() {
|
|
10493
11865
|
"use strict";
|
|
10494
11866
|
init_BrowserDriver();
|
|
10495
11867
|
init_browser_service_env();
|
|
10496
|
-
|
|
11868
|
+
import_client3 = require("@fal-ai/client");
|
|
10497
11869
|
WHISPER_RECORD_SECONDS = 90;
|
|
10498
11870
|
}
|
|
10499
11871
|
});
|
|
@@ -10557,9 +11929,13 @@ var init_server_schemas = __esm({
|
|
|
10557
11929
|
});
|
|
10558
11930
|
ExtractSiteBodySchema = import_zod13.z.object({
|
|
10559
11931
|
url: import_zod13.z.string().min(1, "url is required"),
|
|
10560
|
-
maxPages: import_zod13.z.number().int().min(1).max(
|
|
11932
|
+
maxPages: import_zod13.z.number().int().min(1).max(1e4).optional(),
|
|
10561
11933
|
browserFallback: import_zod13.z.boolean().optional(),
|
|
10562
|
-
kernelFallback: import_zod13.z.boolean().optional()
|
|
11934
|
+
kernelFallback: import_zod13.z.boolean().optional(),
|
|
11935
|
+
rotateProxies: import_zod13.z.boolean().optional(),
|
|
11936
|
+
rotateProxyEvery: import_zod13.z.number().int().min(1).max(100).optional(),
|
|
11937
|
+
background: import_zod13.z.boolean().optional(),
|
|
11938
|
+
formats: import_zod13.z.array(import_zod13.z.enum(["markdown", "links", "json", "images", "branding"])).optional()
|
|
10563
11939
|
});
|
|
10564
11940
|
YoutubeHarvestBodySchema = import_zod13.z.object({
|
|
10565
11941
|
mode: import_zod13.z.enum(["search", "channel"]),
|
|
@@ -10686,7 +12062,7 @@ async function acquireConcurrencyGate(user, operation, options = {}) {
|
|
|
10686
12062
|
return { ok: true, lockId: null, active: await countActiveUsageForUser(user.id), limit, operation, reused: true };
|
|
10687
12063
|
}
|
|
10688
12064
|
await expireConcurrencyLocksForUser(user.id);
|
|
10689
|
-
const lockId = `cl_${(0,
|
|
12065
|
+
const lockId = `cl_${(0, import_node_crypto3.randomUUID)().replace(/-/g, "").slice(0, 24)}`;
|
|
10690
12066
|
const res = await getDb().execute({
|
|
10691
12067
|
sql: `INSERT INTO concurrency_locks (id, user_id, operation, status, expires_at, metadata)
|
|
10692
12068
|
SELECT ?, ?, ?, 'active', datetime('now', ?), ?
|
|
@@ -10740,11 +12116,11 @@ async function extendConcurrencyGate(lockId, ttlSeconds = DEFAULT_LOCK_TTL_SECON
|
|
|
10740
12116
|
args: [lockTtlModifier(ttlSeconds), lockId]
|
|
10741
12117
|
});
|
|
10742
12118
|
}
|
|
10743
|
-
var
|
|
12119
|
+
var import_node_crypto3, DEFAULT_LOCK_TTL_SECONDS, DEFAULT_RETRY_AFTER_SECONDS, MAX_LOCK_TTL_SECONDS, schemaReady;
|
|
10744
12120
|
var init_concurrency_gates = __esm({
|
|
10745
12121
|
"src/api/concurrency-gates.ts"() {
|
|
10746
12122
|
"use strict";
|
|
10747
|
-
|
|
12123
|
+
import_node_crypto3 = require("crypto");
|
|
10748
12124
|
init_db();
|
|
10749
12125
|
init_rates();
|
|
10750
12126
|
DEFAULT_LOCK_TTL_SECONDS = 15 * 60;
|
|
@@ -10768,10 +12144,10 @@ function buildTranscriptMarkdown(result) {
|
|
|
10768
12144
|
if (result.chunks?.length) {
|
|
10769
12145
|
lines.push("## Timestamped Segments");
|
|
10770
12146
|
lines.push("");
|
|
10771
|
-
for (const
|
|
10772
|
-
const start = fmtTs(
|
|
10773
|
-
const end = fmtTs(
|
|
10774
|
-
lines.push(`**[${start} \u2192 ${end}]** ${
|
|
12147
|
+
for (const chunk2 of result.chunks) {
|
|
12148
|
+
const start = fmtTs(chunk2.timestamp[0]);
|
|
12149
|
+
const end = fmtTs(chunk2.timestamp[1]);
|
|
12150
|
+
lines.push(`**[${start} \u2192 ${end}]** ${chunk2.text.trim()}`);
|
|
10775
12151
|
lines.push("");
|
|
10776
12152
|
}
|
|
10777
12153
|
}
|
|
@@ -11409,20 +12785,20 @@ var init_FacebookAdExtractor = __esm({
|
|
|
11409
12785
|
if (splitRe.lastIndex === m.index) splitRe.lastIndex++;
|
|
11410
12786
|
}
|
|
11411
12787
|
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 =
|
|
12788
|
+
const ads = adChunks.filter((c) => /Library ID[:\s]+\d{10,20}/i.test(c)).slice(0, maxAds).map((chunk2) => {
|
|
12789
|
+
const lidM = chunk2.match(/Library ID[:\s]+([0-9]{10,20})/i);
|
|
11414
12790
|
const libraryId = lidM ? lidM[1] : null;
|
|
11415
|
-
const statusM =
|
|
12791
|
+
const statusM = chunk2.match(/^(Active|Inactive)/i);
|
|
11416
12792
|
const status = statusM ? statusM[1] : null;
|
|
11417
|
-
const startM =
|
|
12793
|
+
const startM = chunk2.match(/Started running on\s+([A-Za-z]+\.?\s+\d{1,2},?\s*\d{4})/i);
|
|
11418
12794
|
const started = startM ? startM[1].replace(/\s+/g, " ").trim() : null;
|
|
11419
12795
|
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(
|
|
12796
|
+
const hasVideoText = /0:00\s*\/\s*0:00|\d+:\d+\s*\/\s*\d+:\d+/.test(chunk2);
|
|
11421
12797
|
const creativeType = visual.videoSrc || hasVideoText ? "video" : visual.imageSrc ? "image" : "unknown";
|
|
11422
12798
|
let domain = null;
|
|
11423
12799
|
const domainRe = /\b([A-Z0-9]{2,}(?:\.[A-Z]{2,})+)\b/g;
|
|
11424
12800
|
let dm;
|
|
11425
|
-
while ((dm = domainRe.exec(
|
|
12801
|
+
while ((dm = domainRe.exec(chunk2)) !== null) {
|
|
11426
12802
|
const c = dm[1];
|
|
11427
12803
|
if (c.length >= 6 && c !== "LIBRARY") {
|
|
11428
12804
|
domain = c;
|
|
@@ -11431,9 +12807,9 @@ var init_FacebookAdExtractor = __esm({
|
|
|
11431
12807
|
}
|
|
11432
12808
|
let primaryText = null;
|
|
11433
12809
|
let headline = null;
|
|
11434
|
-
const sponsoredIdx =
|
|
12810
|
+
const sponsoredIdx = chunk2.search(/\bSponsored\b/i);
|
|
11435
12811
|
if (sponsoredIdx >= 0) {
|
|
11436
|
-
let afterSponsored =
|
|
12812
|
+
let afterSponsored = chunk2.slice(sponsoredIdx + "Sponsored".length).trim();
|
|
11437
12813
|
afterSponsored = afterSponsored.replace(/\s*\d*:?\d+\s*\/\s*\d*:?\d+.*$/, "");
|
|
11438
12814
|
if (domain) {
|
|
11439
12815
|
const di = afterSponsored.indexOf(domain);
|
|
@@ -11444,18 +12820,18 @@ var init_FacebookAdExtractor = __esm({
|
|
|
11444
12820
|
}
|
|
11445
12821
|
const ctaRe2 = new RegExp("\\b(" + CTA_LABELS.map((l) => l.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")).join("|") + ")\\b", "i");
|
|
11446
12822
|
if (domain) {
|
|
11447
|
-
const di =
|
|
12823
|
+
const di = chunk2.indexOf(domain);
|
|
11448
12824
|
if (di >= 0) {
|
|
11449
|
-
let afterDomain =
|
|
12825
|
+
let afterDomain = chunk2.slice(di + domain.length).trim();
|
|
11450
12826
|
const ctaMatch = afterDomain.search(ctaRe2);
|
|
11451
12827
|
if (ctaMatch >= 0) afterDomain = afterDomain.slice(0, ctaMatch).trim();
|
|
11452
12828
|
if (afterDomain.length > 3 && afterDomain.length < 120) headline = afterDomain;
|
|
11453
12829
|
}
|
|
11454
12830
|
}
|
|
11455
12831
|
if (!headline) {
|
|
11456
|
-
const ctaIdx =
|
|
12832
|
+
const ctaIdx = chunk2.search(ctaRe2);
|
|
11457
12833
|
if (ctaIdx > 0) {
|
|
11458
|
-
const beforeCta =
|
|
12834
|
+
const beforeCta = chunk2.slice(0, ctaIdx).trimEnd();
|
|
11459
12835
|
const sentenceEnd = Math.max(beforeCta.lastIndexOf(". "), beforeCta.lastIndexOf("! "), beforeCta.lastIndexOf("? "));
|
|
11460
12836
|
if (sentenceEnd >= 0) {
|
|
11461
12837
|
const candidate = beforeCta.slice(sentenceEnd + 2).trim();
|
|
@@ -11467,12 +12843,12 @@ var init_FacebookAdExtractor = __esm({
|
|
|
11467
12843
|
}
|
|
11468
12844
|
let cta = null;
|
|
11469
12845
|
for (const lbl of CTA_LABELS) {
|
|
11470
|
-
if (
|
|
12846
|
+
if (chunk2.toLowerCase().includes(lbl.toLowerCase())) {
|
|
11471
12847
|
cta = lbl;
|
|
11472
12848
|
break;
|
|
11473
12849
|
}
|
|
11474
12850
|
}
|
|
11475
|
-
const multiM =
|
|
12851
|
+
const multiM = chunk2.match(/(\d+)\s+ad[s]?\s+use[s]?\s+this/i);
|
|
11476
12852
|
const clusterCount = multiM ? parseInt(multiM[1]) : null;
|
|
11477
12853
|
return {
|
|
11478
12854
|
libraryId,
|
|
@@ -11495,8 +12871,8 @@ var init_FacebookAdExtractor = __esm({
|
|
|
11495
12871
|
const unknownCreativeCount = ads.filter((a) => a.creativeType === "unknown").length;
|
|
11496
12872
|
const advertiserPageId = new URL(page.url()).searchParams.get("view_all_page_id") ?? null;
|
|
11497
12873
|
const nameFreq = /* @__PURE__ */ new Map();
|
|
11498
|
-
for (const
|
|
11499
|
-
const detailsParts =
|
|
12874
|
+
for (const chunk2 of adChunks) {
|
|
12875
|
+
const detailsParts = chunk2.split("See ad details ");
|
|
11500
12876
|
if (detailsParts.length < 2) continue;
|
|
11501
12877
|
const sponsoredIdx = detailsParts[1].indexOf(" Sponsored");
|
|
11502
12878
|
if (sponsoredIdx < 0) continue;
|
|
@@ -12221,7 +13597,7 @@ async function createFreshLocationProxy(kernel, options, target) {
|
|
|
12221
13597
|
}
|
|
12222
13598
|
async function deleteKernelProxyId(kernelApiKey, proxyId) {
|
|
12223
13599
|
if (!kernelApiKey || !proxyId) return;
|
|
12224
|
-
const kernel = new
|
|
13600
|
+
const kernel = new import_sdk6.default({ apiKey: kernelApiKey });
|
|
12225
13601
|
await kernel.proxies.delete(proxyId);
|
|
12226
13602
|
}
|
|
12227
13603
|
async function resolveKernelProxyId(options) {
|
|
@@ -12235,7 +13611,7 @@ async function resolveKernelProxyId(options) {
|
|
|
12235
13611
|
if (!target || !options.kernelApiKey) {
|
|
12236
13612
|
return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, target ? null : "location could not be normalized to a US city/state proxy target");
|
|
12237
13613
|
}
|
|
12238
|
-
const kernel = new
|
|
13614
|
+
const kernel = new import_sdk6.default({ apiKey: options.kernelApiKey });
|
|
12239
13615
|
try {
|
|
12240
13616
|
const attemptIndex = options.attemptIndex ?? 0;
|
|
12241
13617
|
if (options.fresh) {
|
|
@@ -12341,11 +13717,11 @@ async function resolveKernelProxyId(options) {
|
|
|
12341
13717
|
return resolution("configured_fallback", options.proxyMode, options.configuredKernelProxyId, target, errorText2(err));
|
|
12342
13718
|
}
|
|
12343
13719
|
}
|
|
12344
|
-
var
|
|
13720
|
+
var import_sdk6, US_STATE_CODES, US_CITY_CENTER_ZIPS;
|
|
12345
13721
|
var init_kernel_proxy_resolver = __esm({
|
|
12346
13722
|
"src/kernel-proxy-resolver.ts"() {
|
|
12347
13723
|
"use strict";
|
|
12348
|
-
|
|
13724
|
+
import_sdk6 = __toESM(require("@onkernel/sdk"), 1);
|
|
12349
13725
|
init_uule();
|
|
12350
13726
|
US_STATE_CODES = {
|
|
12351
13727
|
alabama: "AL",
|
|
@@ -12561,7 +13937,7 @@ function transcriptMarkdown(title, text, chunks, durationMs) {
|
|
|
12561
13937
|
}
|
|
12562
13938
|
async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript") {
|
|
12563
13939
|
const startMs = Date.now();
|
|
12564
|
-
const result = await
|
|
13940
|
+
const result = await import_client4.fal.subscribe("fal-ai/wizper", {
|
|
12565
13941
|
input: { audio_url: mediaUrl, task: "transcribe", language: "en" },
|
|
12566
13942
|
logs: false,
|
|
12567
13943
|
pollInterval: 3e3
|
|
@@ -12577,11 +13953,11 @@ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript"
|
|
|
12577
13953
|
markdown: transcriptMarkdown(markdownTitle, text, chunks, durationMs)
|
|
12578
13954
|
};
|
|
12579
13955
|
}
|
|
12580
|
-
var
|
|
13956
|
+
var import_client4;
|
|
12581
13957
|
var init_media_transcription = __esm({
|
|
12582
13958
|
"src/services/media-transcription.ts"() {
|
|
12583
13959
|
"use strict";
|
|
12584
|
-
|
|
13960
|
+
import_client4 = require("@fal-ai/client");
|
|
12585
13961
|
}
|
|
12586
13962
|
});
|
|
12587
13963
|
|
|
@@ -12624,7 +14000,7 @@ async function kernelLaunchOptsResidential() {
|
|
|
12624
14000
|
}
|
|
12625
14001
|
return { headless: true, kernelApiKey: browserServiceApiKey(), kernelProxyId: proxyId, viewport: { width: 1280, height: 900 }, locale: "en-US" };
|
|
12626
14002
|
}
|
|
12627
|
-
var import_hono4, import_zod16,
|
|
14003
|
+
var import_hono4, import_zod16, import_client5, FacebookAdBodySchema, FacebookPageIntelBodySchema, FacebookTranscribeBodySchema, FacebookVideoTranscribeBodySchema, FacebookSearchBodySchema, FacebookMediaBodySchema, facebookAdApp, ALLOWED_MEDIA_HOSTS;
|
|
12628
14004
|
var init_facebook_ad_routes = __esm({
|
|
12629
14005
|
"src/api/facebook-ad-routes.ts"() {
|
|
12630
14006
|
"use strict";
|
|
@@ -12639,7 +14015,7 @@ var init_facebook_ad_routes = __esm({
|
|
|
12639
14015
|
init_FacebookOrganicVideoExtractor();
|
|
12640
14016
|
init_kernel_proxy_resolver();
|
|
12641
14017
|
init_schemas3();
|
|
12642
|
-
|
|
14018
|
+
import_client5 = require("@fal-ai/client");
|
|
12643
14019
|
init_api_auth();
|
|
12644
14020
|
init_url_utils();
|
|
12645
14021
|
init_concurrency_gates();
|
|
@@ -12782,7 +14158,7 @@ var init_facebook_ad_routes = __esm({
|
|
|
12782
14158
|
metadata: { videoUrl }
|
|
12783
14159
|
});
|
|
12784
14160
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
12785
|
-
|
|
14161
|
+
import_client5.fal.config({ credentials: process.env.FAL_KEY });
|
|
12786
14162
|
let debited = false;
|
|
12787
14163
|
try {
|
|
12788
14164
|
const { ok, balance_mc } = await debitMc(fbUser.id, MC_COSTS.fb_transcribe, LedgerOperation.FB_TRANSCRIBE, videoUrl);
|
|
@@ -12821,7 +14197,7 @@ var init_facebook_ad_routes = __esm({
|
|
|
12821
14197
|
metadata: { url: sourceUrl.href }
|
|
12822
14198
|
});
|
|
12823
14199
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
12824
|
-
|
|
14200
|
+
import_client5.fal.config({ credentials: process.env.FAL_KEY });
|
|
12825
14201
|
const driver = new BrowserDriver();
|
|
12826
14202
|
let debited = false;
|
|
12827
14203
|
try {
|
|
@@ -12915,11 +14291,11 @@ var init_facebook_ad_routes = __esm({
|
|
|
12915
14291
|
}
|
|
12916
14292
|
if (last < bodyText.length) adChunks.push(bodyText.slice(last));
|
|
12917
14293
|
const advertiserMap = /* @__PURE__ */ new Map();
|
|
12918
|
-
for (const
|
|
12919
|
-
const lidM =
|
|
14294
|
+
for (const chunk2 of adChunks) {
|
|
14295
|
+
const lidM = chunk2.match(/Library ID[:\s]+([0-9]{10,20})/i);
|
|
12920
14296
|
if (!lidM) continue;
|
|
12921
14297
|
const libraryId = lidM[1];
|
|
12922
|
-
const detailsParts =
|
|
14298
|
+
const detailsParts = chunk2.split("See ad details ");
|
|
12923
14299
|
if (detailsParts.length < 2) continue;
|
|
12924
14300
|
const afterDetails = detailsParts[1];
|
|
12925
14301
|
const sponsoredIdx = afterDetails.indexOf(" Sponsored");
|
|
@@ -13396,7 +14772,7 @@ async function resolveInstagramLaunch(body) {
|
|
|
13396
14772
|
};
|
|
13397
14773
|
}
|
|
13398
14774
|
function outputBaseDir() {
|
|
13399
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
14775
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path6.join)((0, import_node_os4.homedir)(), "Downloads", "mcp-scraper");
|
|
13400
14776
|
}
|
|
13401
14777
|
function safeFilePart(input) {
|
|
13402
14778
|
return input.replace(/^https?:\/\//, "").replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "instagram";
|
|
@@ -13404,8 +14780,8 @@ function safeFilePart(input) {
|
|
|
13404
14780
|
function mediaOutputDir(shortcode, sourceUrl) {
|
|
13405
14781
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
|
|
13406
14782
|
const slug = shortcode ? `instagram-${shortcode}` : safeFilePart(sourceUrl);
|
|
13407
|
-
const dir = (0,
|
|
13408
|
-
(0,
|
|
14783
|
+
const dir = (0, import_node_path6.join)(outputBaseDir(), "instagram", `${stamp}-${slug}`);
|
|
14784
|
+
(0, import_node_fs4.mkdirSync)(dir, { recursive: true });
|
|
13409
14785
|
return dir;
|
|
13410
14786
|
}
|
|
13411
14787
|
async function downloadToFile(url, destPath, referer) {
|
|
@@ -13418,10 +14794,10 @@ async function downloadToFile(url, destPath, referer) {
|
|
|
13418
14794
|
});
|
|
13419
14795
|
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
13420
14796
|
if (!res.body) throw new Error("Empty response body");
|
|
13421
|
-
await (0, import_promises4.pipeline)(import_node_stream2.Readable.fromWeb(res.body), (0,
|
|
14797
|
+
await (0, import_promises4.pipeline)(import_node_stream2.Readable.fromWeb(res.body), (0, import_node_fs4.createWriteStream)(destPath));
|
|
13422
14798
|
return {
|
|
13423
14799
|
savedPath: destPath,
|
|
13424
|
-
sizeBytes: (0,
|
|
14800
|
+
sizeBytes: (0, import_node_fs4.statSync)(destPath).size,
|
|
13425
14801
|
mimeType: res.headers.get("content-type")?.split(";")[0]?.trim() ?? null
|
|
13426
14802
|
};
|
|
13427
14803
|
}
|
|
@@ -13443,8 +14819,8 @@ function runFfmpegMux(videoPath, audioPath, outPath) {
|
|
|
13443
14819
|
stdio: ["ignore", "ignore", "pipe"]
|
|
13444
14820
|
});
|
|
13445
14821
|
let stderr = "";
|
|
13446
|
-
child.stderr.on("data", (
|
|
13447
|
-
stderr += String(
|
|
14822
|
+
child.stderr.on("data", (chunk2) => {
|
|
14823
|
+
stderr += String(chunk2);
|
|
13448
14824
|
});
|
|
13449
14825
|
child.on("error", (err) => resolve({ ok: false, error: err.message }));
|
|
13450
14826
|
child.on("close", (code) => resolve({ ok: code === 0, error: code === 0 ? null : stderr.trim() || `ffmpeg exited ${code}` }));
|
|
@@ -13456,15 +14832,15 @@ function trackFilename(track, index, shortcode) {
|
|
|
13456
14832
|
const bitrate = track.bitrate ? `-${track.bitrate}` : "";
|
|
13457
14833
|
return `${label}-${type}-${index}${bitrate}.mp4`;
|
|
13458
14834
|
}
|
|
13459
|
-
var import_hono5, import_zod17,
|
|
14835
|
+
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
14836
|
var init_instagram_routes = __esm({
|
|
13461
14837
|
"src/api/instagram-routes.ts"() {
|
|
13462
14838
|
"use strict";
|
|
13463
14839
|
import_hono5 = require("hono");
|
|
13464
14840
|
import_zod17 = require("zod");
|
|
13465
|
-
|
|
13466
|
-
|
|
13467
|
-
|
|
14841
|
+
import_node_fs4 = require("fs");
|
|
14842
|
+
import_node_os4 = require("os");
|
|
14843
|
+
import_node_path6 = require("path");
|
|
13468
14844
|
import_node_stream2 = require("stream");
|
|
13469
14845
|
import_promises4 = require("stream/promises");
|
|
13470
14846
|
import_node_child_process2 = require("child_process");
|
|
@@ -13592,16 +14968,16 @@ var init_instagram_routes = __esm({
|
|
|
13592
14968
|
let outputDir = null;
|
|
13593
14969
|
if (body.downloadMedia) {
|
|
13594
14970
|
outputDir = mediaOutputDir(extraction.shortcode, sourceUrl.href);
|
|
13595
|
-
const textPath = (0,
|
|
13596
|
-
(0,
|
|
14971
|
+
const textPath = (0, import_node_path6.join)(outputDir, `${extraction.shortcode ?? "instagram"}-text.txt`);
|
|
14972
|
+
(0, import_node_fs4.writeFileSync)(textPath, [
|
|
13597
14973
|
`URL: ${extraction.pageUrl}`,
|
|
13598
14974
|
extraction.caption ? `Caption: ${extraction.caption}` : "",
|
|
13599
14975
|
"",
|
|
13600
14976
|
extraction.bodyText
|
|
13601
14977
|
].filter(Boolean).join("\n"), "utf8");
|
|
13602
|
-
downloads.push({ kind: "text", url: null, savedPath: textPath, sizeBytes: (0,
|
|
14978
|
+
downloads.push({ kind: "text", url: null, savedPath: textPath, sizeBytes: (0, import_node_fs4.statSync)(textPath).size, mimeType: "text/plain", error: null });
|
|
13603
14979
|
if (mediaTypes.has("image") && extraction.imageUrl) {
|
|
13604
|
-
const imagePathBase = (0,
|
|
14980
|
+
const imagePathBase = (0, import_node_path6.join)(outputDir, `${extraction.shortcode ?? "instagram"}-image`);
|
|
13605
14981
|
try {
|
|
13606
14982
|
const downloaded = await downloadToFile(extraction.imageUrl, imagePathBase, extraction.pageUrl);
|
|
13607
14983
|
const ext = extFromMime(downloaded.mimeType, "jpg");
|
|
@@ -13610,7 +14986,7 @@ var init_instagram_routes = __esm({
|
|
|
13610
14986
|
const { renameSync } = await import("fs");
|
|
13611
14987
|
renameSync(downloaded.savedPath, finalPath);
|
|
13612
14988
|
}
|
|
13613
|
-
downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: finalPath, sizeBytes: (0,
|
|
14989
|
+
downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: finalPath, sizeBytes: (0, import_node_fs4.statSync)(finalPath).size, mimeType: downloaded.mimeType, error: null });
|
|
13614
14990
|
} catch (err) {
|
|
13615
14991
|
downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
|
|
13616
14992
|
}
|
|
@@ -13621,7 +14997,7 @@ var init_instagram_routes = __esm({
|
|
|
13621
14997
|
if (track.streamType === "video" && !mediaTypes.has("video")) continue;
|
|
13622
14998
|
if (track.streamType === "audio" && !mediaTypes.has("audio")) continue;
|
|
13623
14999
|
const kind = track.streamType === "audio" ? "audio" : "video";
|
|
13624
|
-
const target = (0,
|
|
15000
|
+
const target = (0, import_node_path6.join)(outputDir, trackFilename(track, index, extraction.shortcode));
|
|
13625
15001
|
try {
|
|
13626
15002
|
const downloaded = await downloadToFile(track.url, target, extraction.pageUrl);
|
|
13627
15003
|
downloads.push({ kind, url: track.url, savedPath: downloaded.savedPath, sizeBytes: downloaded.sizeBytes, mimeType: downloaded.mimeType, error: null });
|
|
@@ -13631,10 +15007,10 @@ var init_instagram_routes = __esm({
|
|
|
13631
15007
|
}
|
|
13632
15008
|
}
|
|
13633
15009
|
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,
|
|
15010
|
+
const outPath = (0, import_node_path6.join)(outputDir, `${extraction.shortcode ?? "instagram"}-muxed.mp4`);
|
|
13635
15011
|
const muxed = await runFfmpegMux(savedByUrl.get(extraction.selectedVideoTrack.url), savedByUrl.get(extraction.selectedAudioTrack.url), outPath);
|
|
13636
15012
|
if (muxed.ok) {
|
|
13637
|
-
downloads.push({ kind: "muxed_video", url: null, savedPath: outPath, sizeBytes: (0,
|
|
15013
|
+
downloads.push({ kind: "muxed_video", url: null, savedPath: outPath, sizeBytes: (0, import_node_fs4.statSync)(outPath).size, mimeType: "video/mp4", error: null });
|
|
13638
15014
|
} else {
|
|
13639
15015
|
warnings.push(`Mux skipped/failed: ${muxed.error}`);
|
|
13640
15016
|
}
|
|
@@ -14669,13 +16045,13 @@ var init_maps_routes = __esm({
|
|
|
14669
16045
|
});
|
|
14670
16046
|
|
|
14671
16047
|
// src/mcp/workflow-catalog.ts
|
|
14672
|
-
function
|
|
16048
|
+
function normalize2(value) {
|
|
14673
16049
|
return value.toLowerCase().replace(/[^a-z0-9]+/g, " ").trim();
|
|
14674
16050
|
}
|
|
14675
16051
|
function suggestWorkflowRecipes(goal, limit = 3) {
|
|
14676
|
-
const normalized =
|
|
16052
|
+
const normalized = normalize2(goal);
|
|
14677
16053
|
const scored = WORKFLOW_RECIPES.map((recipe) => {
|
|
14678
|
-
const haystack =
|
|
16054
|
+
const haystack = normalize2([
|
|
14679
16055
|
recipe.id,
|
|
14680
16056
|
recipe.title,
|
|
14681
16057
|
recipe.description,
|
|
@@ -14806,16 +16182,119 @@ function reportTitle(full) {
|
|
|
14806
16182
|
return title?.replace(/^#\s+/, "").trim() || "MCP Scraper Report";
|
|
14807
16183
|
}
|
|
14808
16184
|
function outputBaseDir2() {
|
|
14809
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
16185
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path7.join)((0, import_node_os5.homedir)(), "Downloads", "mcp-scraper");
|
|
14810
16186
|
}
|
|
14811
16187
|
function saveFullReport(full) {
|
|
14812
16188
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
14813
16189
|
const outDir = outputBaseDir2();
|
|
14814
16190
|
try {
|
|
14815
|
-
(0,
|
|
16191
|
+
(0, import_node_fs5.mkdirSync)(outDir, { recursive: true });
|
|
16192
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
16193
|
+
const file = (0, import_node_path7.join)(outDir, `${stamp}-${slugifyReportName(reportTitle(full))}.md`);
|
|
16194
|
+
(0, import_node_fs5.writeFileSync)(file, full, "utf8");
|
|
16195
|
+
return file;
|
|
16196
|
+
} catch {
|
|
16197
|
+
return null;
|
|
16198
|
+
}
|
|
16199
|
+
}
|
|
16200
|
+
function reportSavingActive() {
|
|
16201
|
+
return reportSavingEnabled && process.env.MCP_SCRAPER_SAVE_REPORTS !== "false";
|
|
16202
|
+
}
|
|
16203
|
+
function saveBulkSite(siteUrl, pages, seo, imageAudit) {
|
|
16204
|
+
if (!reportSavingActive()) return null;
|
|
16205
|
+
try {
|
|
16206
|
+
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
16207
|
+
const dir = (0, import_node_path7.join)(outputBaseDir2(), `extract-${slugifyReportName(siteUrl)}-${stamp}`);
|
|
16208
|
+
const pagesDir = (0, import_node_path7.join)(dir, "pages");
|
|
16209
|
+
(0, import_node_fs5.mkdirSync)(pagesDir, { recursive: true });
|
|
16210
|
+
const indexRows = pages.map((p, i) => {
|
|
16211
|
+
const num = String(i + 1).padStart(4, "0");
|
|
16212
|
+
const slug = slugifyReportName(p.url.replace(/^https?:\/\//, "")).slice(0, 60) || "page";
|
|
16213
|
+
const fname = `${num}-${slug}.md`;
|
|
16214
|
+
const body = (p.bodyMarkdown ?? "").trim();
|
|
16215
|
+
const content = [
|
|
16216
|
+
`# ${p.title ?? "Untitled"}`,
|
|
16217
|
+
`- **URL:** ${p.url}`,
|
|
16218
|
+
p.metaDescription ? `- **Description:** ${p.metaDescription}` : "",
|
|
16219
|
+
p.schemaTypes?.length ? `- **Schema:** ${p.schemaTypes.join(", ")}` : "",
|
|
16220
|
+
"",
|
|
16221
|
+
body || "_(no content extracted)_"
|
|
16222
|
+
].filter(Boolean).join("\n");
|
|
16223
|
+
(0, import_node_fs5.writeFileSync)((0, import_node_path7.join)(pagesDir, fname), content, "utf8");
|
|
16224
|
+
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | pages/${fname} |`;
|
|
16225
|
+
});
|
|
16226
|
+
const dataFilesSection = seo ? [
|
|
16227
|
+
`
|
|
16228
|
+
## Data files (in this folder)`,
|
|
16229
|
+
`- \`report.md\` \u2014 SEO issues summary (counts + example URLs)`,
|
|
16230
|
+
`- \`issues.json\` \u2014 every finding with the full list of offender URLs`,
|
|
16231
|
+
`- \`pages.jsonl\` \u2014 per-page SEO fields, one JSON object per page`,
|
|
16232
|
+
`- \`links.jsonl\` \u2014 internal/external link edges (anchor, rel, target status)`,
|
|
16233
|
+
`- \`link-metrics.jsonl\` \u2014 inlinks, crawl depth, orphan flags per page`,
|
|
16234
|
+
...imageAudit ? [
|
|
16235
|
+
`- \`images.jsonl\` \u2014 every image with byte size, format, over-100KB and legacy-format flags`,
|
|
16236
|
+
`- \`images-summary.json\` \u2014 image totals (count, total weight, over-100KB, legacy)`
|
|
16237
|
+
] : [],
|
|
16238
|
+
...seo.branding ? [`- \`branding.json\` \u2014 site logo, colors, fonts`] : []
|
|
16239
|
+
].join("\n") : "";
|
|
16240
|
+
const index = [
|
|
16241
|
+
`# Site Extract: ${siteUrl}`,
|
|
16242
|
+
`**${pages.length} pages** crawled. Everything is in this folder.`,
|
|
16243
|
+
`- Page content: one Markdown file per page under \`pages/\` (listed in the table below).`,
|
|
16244
|
+
seo ? `- SEO crawl data: the data files listed below.` : "",
|
|
16245
|
+
dataFilesSection,
|
|
16246
|
+
`
|
|
16247
|
+
## Pages
|
|
16248
|
+
| # | Title | URL | File |
|
|
16249
|
+
|---|-------|-----|------|
|
|
16250
|
+
${indexRows.join("\n")}`
|
|
16251
|
+
].filter(Boolean).join("\n");
|
|
16252
|
+
const indexFile = (0, import_node_path7.join)(dir, "index.md");
|
|
16253
|
+
(0, import_node_fs5.writeFileSync)(indexFile, index, "utf8");
|
|
16254
|
+
let seoFiles;
|
|
16255
|
+
if (seo) {
|
|
16256
|
+
const toJsonl = (rows) => rows.map((r) => JSON.stringify(r)).join("\n");
|
|
16257
|
+
const pagesJsonl = (0, import_node_path7.join)(dir, "pages.jsonl");
|
|
16258
|
+
const linksJsonl = (0, import_node_path7.join)(dir, "links.jsonl");
|
|
16259
|
+
const metricsJsonl = (0, import_node_path7.join)(dir, "link-metrics.jsonl");
|
|
16260
|
+
const issuesFile = (0, import_node_path7.join)(dir, "issues.json");
|
|
16261
|
+
const reportFile = (0, import_node_path7.join)(dir, "report.md");
|
|
16262
|
+
(0, import_node_fs5.writeFileSync)(pagesJsonl, toJsonl(seo.pageRows), "utf8");
|
|
16263
|
+
(0, import_node_fs5.writeFileSync)(linksJsonl, toJsonl(seo.edges), "utf8");
|
|
16264
|
+
(0, import_node_fs5.writeFileSync)(metricsJsonl, toJsonl(seo.metrics), "utf8");
|
|
16265
|
+
(0, import_node_fs5.writeFileSync)(issuesFile, JSON.stringify(seo.issues, null, 2), "utf8");
|
|
16266
|
+
(0, import_node_fs5.writeFileSync)(reportFile, seo.reportMd + (imageAudit ? `
|
|
16267
|
+
|
|
16268
|
+
${renderImageSection(imageAudit)}` : ""), "utf8");
|
|
16269
|
+
seoFiles = [pagesJsonl, linksJsonl, metricsJsonl, issuesFile, reportFile];
|
|
16270
|
+
if (imageAudit) {
|
|
16271
|
+
const imagesJsonl = (0, import_node_path7.join)(dir, "images.jsonl");
|
|
16272
|
+
const imagesSummary = (0, import_node_path7.join)(dir, "images-summary.json");
|
|
16273
|
+
(0, import_node_fs5.writeFileSync)(imagesJsonl, imageAudit.rows.map((r) => JSON.stringify(r)).join("\n"), "utf8");
|
|
16274
|
+
(0, import_node_fs5.writeFileSync)(imagesSummary, JSON.stringify(imageAudit.summary, null, 2), "utf8");
|
|
16275
|
+
seoFiles.push(imagesJsonl, imagesSummary);
|
|
16276
|
+
}
|
|
16277
|
+
if (seo.branding) {
|
|
16278
|
+
const brandingFile = (0, import_node_path7.join)(dir, "branding.json");
|
|
16279
|
+
(0, import_node_fs5.writeFileSync)(brandingFile, JSON.stringify(seo.branding, null, 2), "utf8");
|
|
16280
|
+
seoFiles.push(brandingFile);
|
|
16281
|
+
}
|
|
16282
|
+
}
|
|
16283
|
+
return { dir, indexFile, fileCount: pages.length, seoFiles };
|
|
16284
|
+
} catch {
|
|
16285
|
+
return null;
|
|
16286
|
+
}
|
|
16287
|
+
}
|
|
16288
|
+
function saveUrlInventory(siteUrl, urls) {
|
|
16289
|
+
if (!reportSavingActive()) return null;
|
|
16290
|
+
try {
|
|
16291
|
+
const outDir = outputBaseDir2();
|
|
16292
|
+
(0, import_node_fs5.mkdirSync)(outDir, { recursive: true });
|
|
14816
16293
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
14817
|
-
const file = (0,
|
|
14818
|
-
(
|
|
16294
|
+
const file = (0, import_node_path7.join)(outDir, `${stamp}-urlmap-${slugifyReportName(siteUrl.replace(/^https?:\/\//, ""))}.csv`);
|
|
16295
|
+
const csv = (v) => /[",\n]/.test(v) ? `"${v.replace(/"/g, '""')}"` : v;
|
|
16296
|
+
const rows = ["url,status", ...urls.map((u) => `${csv(u.url)},${u.status ?? ""}`)];
|
|
16297
|
+
(0, import_node_fs5.writeFileSync)(file, rows.join("\n"), "utf8");
|
|
14819
16298
|
return file;
|
|
14820
16299
|
} catch {
|
|
14821
16300
|
return null;
|
|
@@ -14824,19 +16303,19 @@ function saveFullReport(full) {
|
|
|
14824
16303
|
function persistScreenshotLocally(base64, url) {
|
|
14825
16304
|
if (!reportSavingEnabled || process.env.MCP_SCRAPER_SAVE_REPORTS === "false") return null;
|
|
14826
16305
|
try {
|
|
14827
|
-
const dir = (0,
|
|
14828
|
-
(0,
|
|
16306
|
+
const dir = (0, import_node_path7.join)(outputBaseDir2(), "screenshots");
|
|
16307
|
+
(0, import_node_fs5.mkdirSync)(dir, { recursive: true });
|
|
14829
16308
|
const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
14830
16309
|
const slug = url.replace(/^https?:\/\//, "").replace(/[^a-z0-9]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 60);
|
|
14831
|
-
const filePath = (0,
|
|
14832
|
-
(0,
|
|
16310
|
+
const filePath = (0, import_node_path7.join)(dir, `${stamp}-${slug}.png`);
|
|
16311
|
+
(0, import_node_fs5.writeFileSync)(filePath, Buffer.from(base64, "base64"));
|
|
14833
16312
|
return filePath;
|
|
14834
16313
|
} catch {
|
|
14835
16314
|
return null;
|
|
14836
16315
|
}
|
|
14837
16316
|
}
|
|
14838
|
-
function oneBlock(content) {
|
|
14839
|
-
const filePath = saveFullReport(content);
|
|
16317
|
+
function oneBlock(content, diskContent) {
|
|
16318
|
+
const filePath = saveFullReport(diskContent ?? content);
|
|
14840
16319
|
const text = filePath ? `${content}
|
|
14841
16320
|
|
|
14842
16321
|
\u{1F4C4} Saved: \`${filePath}\`` : content;
|
|
@@ -15105,7 +16584,12 @@ ${[h1Lines, h2Lines].filter(Boolean).join("\n")}` : "";
|
|
|
15105
16584
|
].filter(Boolean).join("\n") : "";
|
|
15106
16585
|
const bodySection = bodyMd ? `
|
|
15107
16586
|
## Page Content
|
|
15108
|
-
${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ?
|
|
16587
|
+
${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? `
|
|
16588
|
+
|
|
16589
|
+
*(truncated to 3,000 of ${bodyMd.length.toLocaleString()} chars \u2014 full content in the saved report)*` : ""}` : "";
|
|
16590
|
+
const bodySectionFull = bodyMd ? `
|
|
16591
|
+
## Page Content
|
|
16592
|
+
${bodyMd}` : "";
|
|
15109
16593
|
const screenshotSection = screenshotMeta ? `
|
|
15110
16594
|
## Screenshot
|
|
15111
16595
|
- **File:** ${screenshotPath ?? "(returned inline only \u2014 disk write unavailable in this environment)"}
|
|
@@ -15136,7 +16620,10 @@ ${bodyMd.slice(0, 3e3)}${bodyMd.length > 3e3 ? "\n\n*(truncated)*" : ""}` : "";
|
|
|
15136
16620
|
const full = `# URL Extract: ${url}
|
|
15137
16621
|
**${title}**
|
|
15138
16622
|
${headingSection}${kpoSection}${brandingSection}${bodySection}${screenshotSection}${mediaSection}${tips}`;
|
|
15139
|
-
const
|
|
16623
|
+
const diskReport = `# URL Extract: ${url}
|
|
16624
|
+
**${title}**
|
|
16625
|
+
${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${screenshotSection}${mediaSection}${tips}`;
|
|
16626
|
+
const textResult = oneBlock(full, diskReport);
|
|
15140
16627
|
const structuredContent = {
|
|
15141
16628
|
url,
|
|
15142
16629
|
title: d.title ?? null,
|
|
@@ -15169,7 +16656,16 @@ function formatMapSiteUrls(raw, input) {
|
|
|
15169
16656
|
const ok = urls.filter((u) => (u.status ?? 0) >= 200 && (u.status ?? 0) < 300);
|
|
15170
16657
|
const broken = urls.filter((u) => u.status !== null && u.status >= 400);
|
|
15171
16658
|
const redirects = urls.filter((u) => u.status !== null && u.status >= 300 && u.status < 400);
|
|
15172
|
-
const
|
|
16659
|
+
const isBulk = urls.length > BULK_URL_THRESHOLD;
|
|
16660
|
+
const inventoryFile = isBulk ? saveUrlInventory(input.url, urls.map((u) => ({ url: u.url, status: u.status ?? null }))) : null;
|
|
16661
|
+
const inlineCount = isBulk ? BULK_URL_THRESHOLD : 200;
|
|
16662
|
+
const urlRows = urls.slice(0, inlineCount).map((u, i) => `| ${i + 1} | ${u.url} | ${u.status ?? "\u2014"} |`).join("\n");
|
|
16663
|
+
const inventoryNote = isBulk ? inventoryFile ? `
|
|
16664
|
+
## \u{1F4C1} Full inventory saved
|
|
16665
|
+
- **File:** \`${inventoryFile}\` (CSV: url, status \u2014 all ${urls.length} URLs)
|
|
16666
|
+
Showing the first ${inlineCount} below; read the file for the complete list.` : `
|
|
16667
|
+
## \u26A0\uFE0F Full inventory not saved
|
|
16668
|
+
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
16669
|
const full = [
|
|
15174
16670
|
`# URL Map: ${input.url}`,
|
|
15175
16671
|
`**${d.totalFound} URLs** \xB7 ${(d.durationMs / 1e3).toFixed(1)}s${d.truncated ? " \xB7 *truncated*" : ""}`,
|
|
@@ -15178,12 +16674,13 @@ function formatMapSiteUrls(raw, input) {
|
|
|
15178
16674
|
- \u2705 2xx: ${ok.length}
|
|
15179
16675
|
- \u{1F500} 3xx: ${redirects.length}
|
|
15180
16676
|
- \u274C 4xx+: ${broken.length}`,
|
|
16677
|
+
inventoryNote,
|
|
15181
16678
|
`
|
|
15182
|
-
## URL Inventory
|
|
16679
|
+
## URL Inventory${isBulk ? ` (first ${inlineCount} of ${urls.length})` : ""}
|
|
15183
16680
|
| # | URL | Status |
|
|
15184
16681
|
|---|-----|--------|
|
|
15185
16682
|
${urlRows}`,
|
|
15186
|
-
broken.length ? `
|
|
16683
|
+
!isBulk && broken.length ? `
|
|
15187
16684
|
## Broken URLs
|
|
15188
16685
|
${broken.map((u) => `- ${u.url} (${u.status})`).join("\n")}` : "",
|
|
15189
16686
|
`
|
|
@@ -15201,47 +16698,164 @@ ${broken.map((u) => `- ${u.url} (${u.status})`).join("\n")}` : "",
|
|
|
15201
16698
|
okCount: ok.length,
|
|
15202
16699
|
redirectCount: redirects.length,
|
|
15203
16700
|
brokenCount: broken.length,
|
|
16701
|
+
inventoryFile: inventoryFile ?? null,
|
|
15204
16702
|
urls: urls.map((u) => ({ url: u.url, status: u.status ?? null })),
|
|
15205
16703
|
durationMs: d.durationMs ?? 0
|
|
15206
16704
|
}
|
|
15207
16705
|
};
|
|
15208
16706
|
}
|
|
16707
|
+
function buildSeoExport(siteUrl, pages, branding) {
|
|
16708
|
+
const { edges, metrics } = buildLinkGraph(pages, siteUrl);
|
|
16709
|
+
const pageRows = pages.map((p) => {
|
|
16710
|
+
const { bodyMarkdown: _b, schema: _s, outlinks: _o, ...rest } = p;
|
|
16711
|
+
const m = metrics.get(p.url);
|
|
16712
|
+
return { ...rest, inlinks: m?.inlinks ?? 0, crawlDepth: m?.crawlDepth ?? null, orphan: m?.orphan ?? false };
|
|
16713
|
+
});
|
|
16714
|
+
const issues = computeIssues(pages, metrics);
|
|
16715
|
+
return {
|
|
16716
|
+
pageRows,
|
|
16717
|
+
edges,
|
|
16718
|
+
metrics: [...metrics.values()],
|
|
16719
|
+
issues,
|
|
16720
|
+
reportMd: renderIssueReport(siteUrl, pages, issues, metrics),
|
|
16721
|
+
branding: branding ?? void 0
|
|
16722
|
+
};
|
|
16723
|
+
}
|
|
15209
16724
|
function formatExtractSite(raw, input) {
|
|
15210
16725
|
const parsed = parseData(raw);
|
|
15211
16726
|
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
15212
16727
|
const d = parsed.data;
|
|
15213
16728
|
const pages = d.pages ?? [];
|
|
15214
|
-
const
|
|
15215
|
-
|
|
16729
|
+
const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
|
|
16730
|
+
const pageRow = (p, i) => {
|
|
16731
|
+
const schemaInfo = schemaTypesOf(p).join(", ") || "\u2014";
|
|
15216
16732
|
return `| ${i + 1} | ${cell(p.title ?? "Untitled")} | ${p.url} | ${schemaInfo} |`;
|
|
15217
|
-
}
|
|
15218
|
-
const
|
|
16733
|
+
};
|
|
16734
|
+
const tips = `
|
|
16735
|
+
---
|
|
16736
|
+
\u{1F4A1} **Tips**
|
|
16737
|
+
- Map URLs first: use \`map_site_urls\`
|
|
16738
|
+
- Inspect a single page: use \`extract_url\``;
|
|
16739
|
+
const durationLine = `**${pages.length} pages** \xB7 ${((d.durationMs ?? 0) / 1e3).toFixed(1)}s`;
|
|
16740
|
+
const structuredContent = {
|
|
16741
|
+
url: input.url,
|
|
16742
|
+
pageCount: pages.length,
|
|
16743
|
+
pages: pages.map((p) => ({
|
|
16744
|
+
url: String(p.url ?? ""),
|
|
16745
|
+
title: p.title ?? null,
|
|
16746
|
+
schemaTypes: schemaTypesOf(p)
|
|
16747
|
+
})),
|
|
16748
|
+
durationMs: d.durationMs ?? 0
|
|
16749
|
+
};
|
|
16750
|
+
if (pages.length > BULK_PAGE_THRESHOLD) {
|
|
16751
|
+
const bulk = saveBulkSite(input.url, pages.map((p) => ({
|
|
16752
|
+
url: String(p.url ?? ""),
|
|
16753
|
+
title: p.title ?? null,
|
|
16754
|
+
bodyMarkdown: p.bodyMarkdown ?? null,
|
|
16755
|
+
metaDescription: p.metaDescription ?? null,
|
|
16756
|
+
schemaTypes: schemaTypesOf(p)
|
|
16757
|
+
})));
|
|
16758
|
+
const preview = pages.slice(0, BULK_PAGE_THRESHOLD).map(pageRow).join("\n");
|
|
16759
|
+
const location2 = bulk ? `
|
|
16760
|
+
## \u{1F4C1} Bulk scrape saved
|
|
16761
|
+
- **Folder:** \`${bulk.dir}\` \u2190 all scraped page content is here
|
|
16762
|
+
- **Start here:** \`${bulk.indexFile}\` \u2014 lists every page and its file
|
|
16763
|
+
- **Page content:** ${bulk.fileCount} Markdown files under \`pages/\` (one per page)
|
|
16764
|
+
|
|
16765
|
+
The page content is NOT inlined here to keep the context window clear \u2014 it is saved to disk. Open \`index.md\`, then open the page file you need from \`pages/\`.
|
|
16766
|
+
|
|
16767
|
+
\u{1F4A1} Want the full technical SEO audit (headings, link graph, indexability, image sizes/formats, issues)? Run \`audit_site\` on this URL instead.` : `
|
|
16768
|
+
## \u26A0\uFE0F Bulk scrape not saved
|
|
16769
|
+
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.`;
|
|
16770
|
+
const full2 = [
|
|
16771
|
+
`# Site Extract: ${input.url}`,
|
|
16772
|
+
durationLine,
|
|
16773
|
+
location2,
|
|
16774
|
+
`
|
|
16775
|
+
## Pages (first ${Math.min(BULK_PAGE_THRESHOLD, pages.length)} of ${pages.length})
|
|
16776
|
+
| # | Title | URL | Schema |
|
|
16777
|
+
|---|-------|-----|--------|
|
|
16778
|
+
${preview}`,
|
|
16779
|
+
tips
|
|
16780
|
+
].join("\n");
|
|
16781
|
+
return { content: [{ type: "text", text: full2 }], structuredContent: { ...structuredContent, bulkFolder: bulk?.dir ?? null } };
|
|
16782
|
+
}
|
|
16783
|
+
const header = [
|
|
15219
16784
|
`# Site Extract: ${input.url}`,
|
|
15220
|
-
|
|
16785
|
+
durationLine,
|
|
15221
16786
|
`
|
|
15222
16787
|
## Pages
|
|
15223
16788
|
| # | Title | URL | Schema |
|
|
15224
16789
|
|---|-------|-----|--------|
|
|
15225
|
-
${
|
|
15226
|
-
`
|
|
15227
|
-
---
|
|
15228
|
-
\u{1F4A1} **Tips**
|
|
15229
|
-
- Map URLs first: use \`map_site_urls\`
|
|
15230
|
-
- Inspect a single page: use \`extract_url\``
|
|
16790
|
+
${pages.map(pageRow).join("\n")}`
|
|
15231
16791
|
].join("\n");
|
|
15232
|
-
|
|
15233
|
-
|
|
15234
|
-
|
|
15235
|
-
|
|
15236
|
-
|
|
15237
|
-
|
|
15238
|
-
|
|
15239
|
-
|
|
15240
|
-
|
|
15241
|
-
|
|
15242
|
-
|
|
15243
|
-
|
|
16792
|
+
const full = `${header}${tips}`;
|
|
16793
|
+
const pageDetails = pages.map((p, i) => {
|
|
16794
|
+
const body = (p.bodyMarkdown ?? "").trim();
|
|
16795
|
+
return [
|
|
16796
|
+
`
|
|
16797
|
+
## ${i + 1}. ${p.title ?? "Untitled"}`,
|
|
16798
|
+
`- **URL:** ${p.url}`,
|
|
16799
|
+
p.metaDescription ? `- **Description:** ${p.metaDescription}` : "",
|
|
16800
|
+
body ? `
|
|
16801
|
+
${body}` : "_(no content extracted)_"
|
|
16802
|
+
].filter(Boolean).join("\n");
|
|
16803
|
+
}).join("\n");
|
|
16804
|
+
const diskReport = `${header}
|
|
16805
|
+
|
|
16806
|
+
---
|
|
16807
|
+
# Full Page Content
|
|
16808
|
+
${pageDetails}${tips}`;
|
|
16809
|
+
return { ...oneBlock(full, diskReport), structuredContent };
|
|
16810
|
+
}
|
|
16811
|
+
async function formatAuditSite(raw, input) {
|
|
16812
|
+
const parsed = parseData(raw);
|
|
16813
|
+
if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
|
|
16814
|
+
const d = parsed.data;
|
|
16815
|
+
const pages = d.pages ?? [];
|
|
16816
|
+
const schemaTypesOf = (p) => p.kpo?.type ?? (Array.isArray(p.schema) && p.schema.length ? [`${p.schema.length} block(s)`] : []);
|
|
16817
|
+
const seo = buildSeoExport(input.url, pages, d.branding);
|
|
16818
|
+
const imageAudit = await auditImages(pages, { concurrency: 12 });
|
|
16819
|
+
const bulk = saveBulkSite(input.url, pages.map((p) => ({
|
|
16820
|
+
url: String(p.url ?? ""),
|
|
16821
|
+
title: p.title ?? null,
|
|
16822
|
+
bodyMarkdown: p.bodyMarkdown ?? null,
|
|
16823
|
+
metaDescription: p.metaDescription ?? null,
|
|
16824
|
+
schemaTypes: schemaTypesOf(p)
|
|
16825
|
+
})), seo, imageAudit);
|
|
16826
|
+
const topIssues = Object.entries(seo.issues).sort((a, b) => b[1].count - a[1].count).slice(0, 8).map(([k, v]) => `- \`${k}\` \u2014 ${v.count}`).join("\n");
|
|
16827
|
+
const img = imageAudit.summary;
|
|
16828
|
+
const imgLine = `${img.unique} images \xB7 ${img.totalSize} total \xB7 ${img.over100kb} over 100 KB \xB7 ${img.legacyFormat} legacy format`;
|
|
16829
|
+
const durationLine = `**${pages.length} pages** \xB7 ${((d.durationMs ?? 0) / 1e3).toFixed(1)}s`;
|
|
16830
|
+
const structuredContent = {
|
|
16831
|
+
url: input.url,
|
|
16832
|
+
pageCount: pages.length,
|
|
16833
|
+
durationMs: d.durationMs ?? 0,
|
|
16834
|
+
bulkFolder: bulk?.dir ?? null,
|
|
16835
|
+
issues: Object.fromEntries(Object.entries(seo.issues).map(([k, v]) => [k, v.count])),
|
|
16836
|
+
images: { unique: img.unique, totalBytes: img.totalBytes, over100kb: img.over100kb, legacyFormat: img.legacyFormat }
|
|
15244
16837
|
};
|
|
16838
|
+
const location2 = bulk ? `
|
|
16839
|
+
## \u{1F4C1} Technical audit saved
|
|
16840
|
+
- **Folder:** \`${bulk.dir}\` \u2190 full audit + page content here
|
|
16841
|
+
- **Start here:** \`${bulk.indexFile}\` \u2014 maps every file in the folder
|
|
16842
|
+
- **Data:** \`report.md\` (issues + image audit), \`issues.json\`, \`pages.jsonl\` (per-page fields), \`links.jsonl\`, \`link-metrics.jsonl\`, \`images.jsonl\`
|
|
16843
|
+
|
|
16844
|
+
Detail is saved to disk, not inlined. Open \`report.md\` for the audit, \`pages.jsonl\` for per-page fields (headings, canonical, indexability), \`images.jsonl\` for image sizes/formats.` : `
|
|
16845
|
+
## \u26A0\uFE0F Audit not saved
|
|
16846
|
+
Report saving is disabled in this environment. Enable \`MCP_SCRAPER_SAVE_REPORTS\` to capture the technical audit.`;
|
|
16847
|
+
const full = [
|
|
16848
|
+
`# Technical SEO Audit: ${input.url}`,
|
|
16849
|
+
durationLine,
|
|
16850
|
+
location2,
|
|
16851
|
+
`
|
|
16852
|
+
## Top issues
|
|
16853
|
+
${topIssues || "_none found_"}`,
|
|
16854
|
+
`
|
|
16855
|
+
## Images
|
|
16856
|
+
${imgLine}`
|
|
16857
|
+
].join("\n");
|
|
16858
|
+
return { content: [{ type: "text", text: full }], structuredContent };
|
|
15245
16859
|
}
|
|
15246
16860
|
function formatYoutubeHarvest(raw, input) {
|
|
15247
16861
|
const parsed = parseData(raw);
|
|
@@ -16417,16 +18031,21 @@ ${rows}` : ""
|
|
|
16417
18031
|
}
|
|
16418
18032
|
};
|
|
16419
18033
|
}
|
|
16420
|
-
var
|
|
18034
|
+
var import_node_fs5, import_node_os5, import_node_path7, reportSavingEnabled, BULK_PAGE_THRESHOLD, BULK_URL_THRESHOLD;
|
|
16421
18035
|
var init_mcp_response_formatter = __esm({
|
|
16422
18036
|
"src/mcp/mcp-response-formatter.ts"() {
|
|
16423
18037
|
"use strict";
|
|
16424
|
-
|
|
16425
|
-
|
|
16426
|
-
|
|
18038
|
+
import_node_fs5 = require("fs");
|
|
18039
|
+
import_node_os5 = require("os");
|
|
18040
|
+
import_node_path7 = require("path");
|
|
16427
18041
|
init_errors();
|
|
16428
18042
|
init_workflow_catalog();
|
|
18043
|
+
init_seo_link_graph();
|
|
18044
|
+
init_seo_issues();
|
|
18045
|
+
init_image_audit();
|
|
16429
18046
|
reportSavingEnabled = true;
|
|
18047
|
+
BULK_PAGE_THRESHOLD = 25;
|
|
18048
|
+
BULK_URL_THRESHOLD = 500;
|
|
16430
18049
|
}
|
|
16431
18050
|
});
|
|
16432
18051
|
|
|
@@ -16819,11 +18438,11 @@ function csvRowsFor(result) {
|
|
|
16819
18438
|
return rows;
|
|
16820
18439
|
}
|
|
16821
18440
|
async function saveDirectoryCsv(result) {
|
|
16822
|
-
const outDir = (0,
|
|
18441
|
+
const outDir = (0, import_node_path8.join)(outputBaseDir2(), "directory-workflows");
|
|
16823
18442
|
await (0, import_promises6.mkdir)(outDir, { recursive: true });
|
|
16824
18443
|
const stamp = result.extractedAt.replace(/[:.]/g, "-");
|
|
16825
18444
|
const slug = `${result.state}-${result.query}`.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80);
|
|
16826
|
-
const path6 = (0,
|
|
18445
|
+
const path6 = (0, import_node_path8.join)(outDir, `${stamp}-${slug}-directory-workflow.csv`);
|
|
16827
18446
|
const headers = [
|
|
16828
18447
|
"source_query",
|
|
16829
18448
|
"source_location",
|
|
@@ -16881,12 +18500,12 @@ async function runDirectoryWorkflowFromPlan(options, plan) {
|
|
|
16881
18500
|
const csvPath = options.saveCsv ? await saveDirectoryCsv(base) : null;
|
|
16882
18501
|
return { ...base, csvPath };
|
|
16883
18502
|
}
|
|
16884
|
-
var import_promises6,
|
|
18503
|
+
var import_promises6, import_node_path8, import_zod19, DirectoryWorkflowOptionsSchema;
|
|
16885
18504
|
var init_directory_workflow = __esm({
|
|
16886
18505
|
"src/directory/directory-workflow.ts"() {
|
|
16887
18506
|
"use strict";
|
|
16888
18507
|
import_promises6 = require("fs/promises");
|
|
16889
|
-
|
|
18508
|
+
import_node_path8 = require("path");
|
|
16890
18509
|
import_zod19 = require("zod");
|
|
16891
18510
|
init_mcp_response_formatter();
|
|
16892
18511
|
init_browser_service_env();
|
|
@@ -17013,7 +18632,7 @@ var init_slugify = __esm({
|
|
|
17013
18632
|
|
|
17014
18633
|
// src/workflows/artifact-writer.ts
|
|
17015
18634
|
function workflowOutputBaseDir(outputDir) {
|
|
17016
|
-
return outputDir?.trim() || process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
18635
|
+
return outputDir?.trim() || process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path9.join)((0, import_node_os6.homedir)(), "Downloads", "mcp-scraper");
|
|
17017
18636
|
}
|
|
17018
18637
|
function timestamp() {
|
|
17019
18638
|
return (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-");
|
|
@@ -17022,11 +18641,11 @@ function safeSlug(value) {
|
|
|
17022
18641
|
return slugify(value).replace(/^-+|-+$/g, "").slice(0, 80) || "run";
|
|
17023
18642
|
}
|
|
17024
18643
|
function indexPath(baseDir = workflowOutputBaseDir()) {
|
|
17025
|
-
return (0,
|
|
18644
|
+
return (0, import_node_path9.join)(baseDir, "workflows", "index.json");
|
|
17026
18645
|
}
|
|
17027
18646
|
async function readIndex(baseDir) {
|
|
17028
18647
|
const path6 = indexPath(baseDir);
|
|
17029
|
-
if (!(0,
|
|
18648
|
+
if (!(0, import_node_fs6.existsSync)(path6)) return { runs: [] };
|
|
17030
18649
|
try {
|
|
17031
18650
|
return JSON.parse(await (0, import_promises7.readFile)(path6, "utf8"));
|
|
17032
18651
|
} catch {
|
|
@@ -17048,17 +18667,17 @@ async function updateWorkflowIndex(manifest, manifestPath, baseDir) {
|
|
|
17048
18667
|
summary: `${manifest.title} (${manifest.status})`
|
|
17049
18668
|
};
|
|
17050
18669
|
index.runs = [entry, ...index.runs.filter((r) => r.runId !== manifest.runId)].slice(0, 200);
|
|
17051
|
-
await (0, import_promises7.mkdir)((0,
|
|
18670
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path6), { recursive: true });
|
|
17052
18671
|
await (0, import_promises7.writeFile)(path6, JSON.stringify(index, null, 2), "utf8");
|
|
17053
18672
|
}
|
|
17054
|
-
var import_promises7,
|
|
18673
|
+
var import_promises7, import_node_fs6, import_node_os6, import_node_path9, import_node_child_process3, ArtifactWriter;
|
|
17055
18674
|
var init_artifact_writer = __esm({
|
|
17056
18675
|
"src/workflows/artifact-writer.ts"() {
|
|
17057
18676
|
"use strict";
|
|
17058
18677
|
import_promises7 = require("fs/promises");
|
|
17059
|
-
|
|
17060
|
-
|
|
17061
|
-
|
|
18678
|
+
import_node_fs6 = require("fs");
|
|
18679
|
+
import_node_os6 = require("os");
|
|
18680
|
+
import_node_path9 = require("path");
|
|
17062
18681
|
import_node_child_process3 = require("child_process");
|
|
17063
18682
|
init_csv();
|
|
17064
18683
|
init_slugify();
|
|
@@ -17085,7 +18704,7 @@ var init_artifact_writer = __esm({
|
|
|
17085
18704
|
const runId = forcedRunId?.trim() || `${timestamp()}-${safeSlug(workflowId)}-${Math.random().toString(36).slice(2, 8)}`;
|
|
17086
18705
|
const nameSource = String(input.keyword ?? input.query ?? input.domain ?? input.state ?? workflowId);
|
|
17087
18706
|
const baseDir = workflowOutputBaseDir(outputDir);
|
|
17088
|
-
const runDir = (0,
|
|
18707
|
+
const runDir = (0, import_node_path9.join)(baseDir, "workflows", workflowId, `${timestamp()}-${safeSlug(nameSource)}-${safeSlug(runId).slice(0, 20)}`);
|
|
17089
18708
|
await (0, import_promises7.mkdir)(runDir, { recursive: true });
|
|
17090
18709
|
return new _ArtifactWriter(workflowId, title, runId, baseDir, runDir, startedAt, input);
|
|
17091
18710
|
}
|
|
@@ -17095,20 +18714,20 @@ var init_artifact_writer = __esm({
|
|
|
17095
18714
|
return path6;
|
|
17096
18715
|
}
|
|
17097
18716
|
async writeJson(label, relativePath, data) {
|
|
17098
|
-
const path6 = (0,
|
|
17099
|
-
await (0, import_promises7.mkdir)((0,
|
|
18717
|
+
const path6 = (0, import_node_path9.join)(this.runDir, relativePath);
|
|
18718
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path6), { recursive: true });
|
|
17100
18719
|
await (0, import_promises7.writeFile)(path6, JSON.stringify(data, null, 2), "utf8");
|
|
17101
18720
|
return this.remember("json", label, path6);
|
|
17102
18721
|
}
|
|
17103
18722
|
async writeText(label, relativePath, text, kind = "markdown") {
|
|
17104
|
-
const path6 = (0,
|
|
17105
|
-
await (0, import_promises7.mkdir)((0,
|
|
18723
|
+
const path6 = (0, import_node_path9.join)(this.runDir, relativePath);
|
|
18724
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path6), { recursive: true });
|
|
17106
18725
|
await (0, import_promises7.writeFile)(path6, text, "utf8");
|
|
17107
18726
|
return this.remember(kind, label, path6);
|
|
17108
18727
|
}
|
|
17109
18728
|
async writeCsv(label, relativePath, headers, rows) {
|
|
17110
|
-
const path6 = (0,
|
|
17111
|
-
await (0, import_promises7.mkdir)((0,
|
|
18729
|
+
const path6 = (0, import_node_path9.join)(this.runDir, relativePath);
|
|
18730
|
+
await (0, import_promises7.mkdir)((0, import_node_path9.dirname)(path6), { recursive: true });
|
|
17112
18731
|
await (0, import_promises7.writeFile)(path6, rowsToCsv(headers, rows), "utf8");
|
|
17113
18732
|
return this.remember("csv", label, path6, rows.length);
|
|
17114
18733
|
}
|
|
@@ -17129,7 +18748,7 @@ var init_artifact_writer = __esm({
|
|
|
17129
18748
|
errors,
|
|
17130
18749
|
counts
|
|
17131
18750
|
};
|
|
17132
|
-
const path6 = (0,
|
|
18751
|
+
const path6 = (0, import_node_path9.join)(this.runDir, "manifest.json");
|
|
17133
18752
|
await (0, import_promises7.writeFile)(path6, JSON.stringify(manifest, null, 2), "utf8");
|
|
17134
18753
|
await updateWorkflowIndex(manifest, path6, this.baseDir);
|
|
17135
18754
|
return path6;
|
|
@@ -18709,7 +20328,7 @@ async function readManifestFromSummary(summary) {
|
|
|
18709
20328
|
function webhookSignature(body, timestamp2) {
|
|
18710
20329
|
const secret2 = process.env.MCP_SCRAPER_WEBHOOK_SECRET?.trim();
|
|
18711
20330
|
if (!secret2) return null;
|
|
18712
|
-
return (0,
|
|
20331
|
+
return (0, import_node_crypto4.createHmac)("sha256", secret2).update(`${timestamp2}.${body}`).digest("hex");
|
|
18713
20332
|
}
|
|
18714
20333
|
async function deliverWorkflowWebhook(input) {
|
|
18715
20334
|
if (!input.webhookUrl) return;
|
|
@@ -18916,11 +20535,11 @@ async function dispatchDueWorkflowSchedules(apiUrl, limit = 3) {
|
|
|
18916
20535
|
}
|
|
18917
20536
|
return { dispatched: results.length, results };
|
|
18918
20537
|
}
|
|
18919
|
-
var
|
|
20538
|
+
var import_node_crypto4, import_promises9, import_hono8, import_zod25, workflowApp, WorkflowInputSchema, WorkflowIdSchema, CadenceSchema, ScheduleStatusSchema, RunBodySchema, ScheduleCreateSchema, SchedulePatchSchema, TERMINAL_RUN_STATUSES;
|
|
18920
20539
|
var init_workflow_routes = __esm({
|
|
18921
20540
|
"src/api/workflow-routes.ts"() {
|
|
18922
20541
|
"use strict";
|
|
18923
|
-
|
|
20542
|
+
import_node_crypto4 = require("crypto");
|
|
18924
20543
|
import_promises9 = require("fs/promises");
|
|
18925
20544
|
import_hono8 = require("hono");
|
|
18926
20545
|
import_zod25 = require("zod");
|
|
@@ -19201,7 +20820,7 @@ var init_workflow_routes = __esm({
|
|
|
19201
20820
|
// src/serp-intelligence/page-snapshot-extractor.ts
|
|
19202
20821
|
function sha256(value) {
|
|
19203
20822
|
if (!value) return null;
|
|
19204
|
-
return (0,
|
|
20823
|
+
return (0, import_node_crypto5.createHash)("sha256").update(value).digest("hex");
|
|
19205
20824
|
}
|
|
19206
20825
|
function countWords(markdown) {
|
|
19207
20826
|
const matches = markdown.trim().match(/\b[\p{L}\p{N}][\p{L}\p{N}'-]*\b/gu);
|
|
@@ -19250,7 +20869,7 @@ function countLinks(html, baseUrl) {
|
|
|
19250
20869
|
totalLinkCount: internalLinkCount + externalLinkCount
|
|
19251
20870
|
};
|
|
19252
20871
|
}
|
|
19253
|
-
function
|
|
20872
|
+
function withTimeout3(operation, timeoutMs) {
|
|
19254
20873
|
return new Promise((resolve, reject) => {
|
|
19255
20874
|
const timer = setTimeout(() => reject(new Error(`Page snapshot timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
19256
20875
|
operation.then(
|
|
@@ -19367,7 +20986,7 @@ async function capturePageSnapshot(target, options = {}) {
|
|
|
19367
20986
|
});
|
|
19368
20987
|
}
|
|
19369
20988
|
try {
|
|
19370
|
-
const extraction = await
|
|
20989
|
+
const extraction = await withTimeout3(
|
|
19371
20990
|
extractKpo({ url: checked.parsed.href, kernelApiKey }),
|
|
19372
20991
|
timeoutMs
|
|
19373
20992
|
);
|
|
@@ -19501,11 +21120,11 @@ async function capturePageSnapshots(targets, options = {}) {
|
|
|
19501
21120
|
}
|
|
19502
21121
|
};
|
|
19503
21122
|
}
|
|
19504
|
-
var
|
|
21123
|
+
var import_node_crypto5, import_p_limit3, DEFAULT_TIMEOUT_MS, DEFAULT_MAX_CONCURRENCY, DEFAULT_MAX_CONTENT_CHARS;
|
|
19505
21124
|
var init_page_snapshot_extractor = __esm({
|
|
19506
21125
|
"src/serp-intelligence/page-snapshot-extractor.ts"() {
|
|
19507
21126
|
"use strict";
|
|
19508
|
-
|
|
21127
|
+
import_node_crypto5 = require("crypto");
|
|
19509
21128
|
import_p_limit3 = __toESM(require("p-limit"), 1);
|
|
19510
21129
|
init_kpo_extractor();
|
|
19511
21130
|
init_url_utils();
|
|
@@ -20750,52 +22369,52 @@ var init_PAAExtractor = __esm({
|
|
|
20750
22369
|
});
|
|
20751
22370
|
|
|
20752
22371
|
// src/output/OutputSerializer.ts
|
|
20753
|
-
var
|
|
22372
|
+
var import_node_fs7, import_node_path10, import_papaparse2, OutputSerializer;
|
|
20754
22373
|
var init_OutputSerializer = __esm({
|
|
20755
22374
|
"src/output/OutputSerializer.ts"() {
|
|
20756
22375
|
"use strict";
|
|
20757
|
-
|
|
20758
|
-
|
|
22376
|
+
import_node_fs7 = require("fs");
|
|
22377
|
+
import_node_path10 = __toESM(require("path"), 1);
|
|
20759
22378
|
import_papaparse2 = __toESM(require("papaparse"), 1);
|
|
20760
22379
|
OutputSerializer = class {
|
|
20761
22380
|
async writeJSON(result, outputDir) {
|
|
20762
|
-
await
|
|
22381
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20763
22382
|
const slug = result.seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20764
22383
|
const filename = `${slug}-${Date.now()}.json`;
|
|
20765
|
-
const fullPath =
|
|
20766
|
-
await
|
|
22384
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22385
|
+
await import_node_fs7.promises.writeFile(fullPath, JSON.stringify(result, null, 2), "utf8");
|
|
20767
22386
|
return fullPath;
|
|
20768
22387
|
}
|
|
20769
22388
|
async writeCSV(rows, outputDir) {
|
|
20770
|
-
await
|
|
22389
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20771
22390
|
const seedRaw = rows[0]?.seed_query ?? "paa";
|
|
20772
22391
|
const slug = seedRaw.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20773
22392
|
const csv = import_papaparse2.default.unparse(rows, { header: true });
|
|
20774
22393
|
const filename = `${slug}-${Date.now()}.csv`;
|
|
20775
|
-
const fullPath =
|
|
20776
|
-
await
|
|
22394
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22395
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20777
22396
|
return fullPath;
|
|
20778
22397
|
}
|
|
20779
22398
|
async writeVideoCSV(videos, seed, outputDir) {
|
|
20780
|
-
await
|
|
22399
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20781
22400
|
const slug = seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20782
22401
|
const csv = import_papaparse2.default.unparse(videos, { header: true });
|
|
20783
22402
|
const filename = `${slug}-videos-${Date.now()}.csv`;
|
|
20784
|
-
const fullPath =
|
|
20785
|
-
await
|
|
22403
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22404
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20786
22405
|
return fullPath;
|
|
20787
22406
|
}
|
|
20788
22407
|
async writeForumCSV(forums, seed, outputDir) {
|
|
20789
|
-
await
|
|
22408
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20790
22409
|
const slug = seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20791
22410
|
const csv = import_papaparse2.default.unparse(forums, { header: true });
|
|
20792
22411
|
const filename = `${slug}-forums-${Date.now()}.csv`;
|
|
20793
|
-
const fullPath =
|
|
20794
|
-
await
|
|
22412
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22413
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20795
22414
|
return fullPath;
|
|
20796
22415
|
}
|
|
20797
22416
|
async writeAIOverviewCSV(citations, text, seed, outputDir) {
|
|
20798
|
-
await
|
|
22417
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20799
22418
|
const slug = seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20800
22419
|
const rows = citations.map((c, i) => ({
|
|
20801
22420
|
seed_query: seed,
|
|
@@ -20805,12 +22424,12 @@ var init_OutputSerializer = __esm({
|
|
|
20805
22424
|
}));
|
|
20806
22425
|
const csv = import_papaparse2.default.unparse(rows, { header: true });
|
|
20807
22426
|
const filename = `${slug}-ai-overview-${Date.now()}.csv`;
|
|
20808
|
-
const fullPath =
|
|
20809
|
-
await
|
|
22427
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22428
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20810
22429
|
return fullPath;
|
|
20811
22430
|
}
|
|
20812
22431
|
async writeAIModeCSV(citations, text, seed, outputDir) {
|
|
20813
|
-
await
|
|
22432
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20814
22433
|
const slug = seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20815
22434
|
const rows = citations.map((c, i) => ({
|
|
20816
22435
|
seed_query: seed,
|
|
@@ -20820,18 +22439,18 @@ var init_OutputSerializer = __esm({
|
|
|
20820
22439
|
}));
|
|
20821
22440
|
const csv = import_papaparse2.default.unparse(rows, { header: true });
|
|
20822
22441
|
const filename = `${slug}-ai-mode-${Date.now()}.csv`;
|
|
20823
|
-
const fullPath =
|
|
20824
|
-
await
|
|
22442
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22443
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20825
22444
|
return fullPath;
|
|
20826
22445
|
}
|
|
20827
22446
|
async writeWhatPeopleSayingCSV(cards, seed, outputDir) {
|
|
20828
|
-
await
|
|
22447
|
+
await import_node_fs7.promises.mkdir(outputDir, { recursive: true });
|
|
20829
22448
|
const slug = seed.toLowerCase().replace(/\W+/g, "-").slice(0, 40);
|
|
20830
22449
|
const rows = cards.map((c) => ({ seed_query: seed, ...c }));
|
|
20831
22450
|
const csv = import_papaparse2.default.unparse(rows, { header: true });
|
|
20832
22451
|
const filename = `${slug}-what-people-saying-${Date.now()}.csv`;
|
|
20833
|
-
const fullPath =
|
|
20834
|
-
await
|
|
22452
|
+
const fullPath = import_node_path10.default.join(outputDir, filename);
|
|
22453
|
+
await import_node_fs7.promises.writeFile(fullPath, csv, "utf8");
|
|
20835
22454
|
return fullPath;
|
|
20836
22455
|
}
|
|
20837
22456
|
};
|
|
@@ -21958,12 +23577,45 @@ var PACKAGE_VERSION;
|
|
|
21958
23577
|
var init_version = __esm({
|
|
21959
23578
|
"src/version.ts"() {
|
|
21960
23579
|
"use strict";
|
|
21961
|
-
PACKAGE_VERSION = "0.3.
|
|
23580
|
+
PACKAGE_VERSION = "0.3.16";
|
|
23581
|
+
}
|
|
23582
|
+
});
|
|
23583
|
+
|
|
23584
|
+
// src/mcp/server-instructions.ts
|
|
23585
|
+
var SERVER_INSTRUCTIONS;
|
|
23586
|
+
var init_server_instructions = __esm({
|
|
23587
|
+
"src/mcp/server-instructions.ts"() {
|
|
23588
|
+
"use strict";
|
|
23589
|
+
SERVER_INSTRUCTIONS = `
|
|
23590
|
+
This server scrapes and analyzes web, social, search, maps, and site data. Pick a tool by NAME using
|
|
23591
|
+
this routing map, then load its schema before calling.
|
|
23592
|
+
|
|
23593
|
+
ROUTING
|
|
23594
|
+
- One web page -> extract_url. Whole site (crawl + SEO report) -> extract_site. Just the URL list -> map_site_urls.
|
|
23595
|
+
- Web search (organic SERP) -> search_serp. Full SERP + People-Also-Ask / AI-Overview detail -> harvest_paa.
|
|
23596
|
+
- Google Maps: find places -> maps_search; one place deep-dive + reviews -> maps_place_intel.
|
|
23597
|
+
- YouTube: find or list videos -> youtube_harvest; transcribe one video -> youtube_transcribe.
|
|
23598
|
+
- Facebook: find ads -> facebook_ad_search; transcribe an ad -> facebook_ad_transcribe; transcribe a
|
|
23599
|
+
video -> facebook_video_transcribe; page profile/intel -> facebook_page_intel.
|
|
23600
|
+
- Instagram: profile inventory -> instagram_profile_content; one post or reel -> instagram_media_download.
|
|
23601
|
+
- Workflows (multi-step): local directory build -> directory_workflow; rank blueprint -> rank_tracker_workflow;
|
|
23602
|
+
AI-answer citation fan-out (AEO) -> query_fanout_workflow; deep research -> deep_research_workflow.
|
|
23603
|
+
- Anything without a dedicated tool (e.g. Reddit, arbitrary logged-in sites) -> the browser_* agent
|
|
23604
|
+
(browser_open, then navigate/read); save/reuse logins via browser_profile_*.
|
|
23605
|
+
|
|
23606
|
+
NOTES
|
|
23607
|
+
- Bulk / full-site crawls: call extract_site with rotateProxies:true for blocked or rate-limited sites.
|
|
23608
|
+
It discovers URLs and returns a saved folder/artifact plus a summary, not the full content inline.
|
|
23609
|
+
- Tools that open a browser session or transcribe media cost credits and take longer. Prefer the
|
|
23610
|
+
cheapest tool that answers the question (plain search/extract before browser agents).
|
|
23611
|
+
- Large results are saved to disk or an artifact and returned as a summary plus a path; read the path
|
|
23612
|
+
for full detail rather than expecting the whole payload inline.
|
|
23613
|
+
`.trim();
|
|
21962
23614
|
}
|
|
21963
23615
|
});
|
|
21964
23616
|
|
|
21965
23617
|
// src/mcp/mcp-tool-schemas.ts
|
|
21966
|
-
var import_zod27, HarvestPaaInputSchema, ExtractUrlInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, ExtractSiteOutputSchema, MapsPlaceIntelOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, FacebookPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema;
|
|
23618
|
+
var import_zod27, HarvestPaaInputSchema, ExtractUrlInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, MapsPlaceIntelOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, FacebookPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema;
|
|
21967
23619
|
var init_mcp_tool_schemas = __esm({
|
|
21968
23620
|
"src/mcp/mcp-tool-schemas.ts"() {
|
|
21969
23621
|
"use strict";
|
|
@@ -21991,11 +23643,20 @@ var init_mcp_tool_schemas = __esm({
|
|
|
21991
23643
|
};
|
|
21992
23644
|
MapSiteUrlsInputSchema = {
|
|
21993
23645
|
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(
|
|
23646
|
+
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
23647
|
};
|
|
21996
23648
|
ExtractSiteInputSchema = {
|
|
21997
|
-
url: import_zod27.z.string().url().describe("Public website URL or domain to
|
|
21998
|
-
maxPages: import_zod27.z.number().int().min(1).max(
|
|
23649
|
+
url: import_zod27.z.string().url().describe("Public website URL or domain to crawl for page CONTENT across multiple pages (map + scrape). Use when the user wants the content/text/markdown of a site's pages. For a technical SEO audit (issues, link graph, indexability, headings, image weights) use audit_site instead \u2014 extract_site returns content only, not analysis."),
|
|
23650
|
+
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."),
|
|
23651
|
+
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."),
|
|
23652
|
+
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."),
|
|
23653
|
+
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.")
|
|
23654
|
+
};
|
|
23655
|
+
AuditSiteInputSchema = {
|
|
23656
|
+
url: import_zod27.z.string().url().describe("Public website URL or domain to run a full technical SEO audit on. Use when the user asks for a technical audit, SEO audit, site health check, or a Screaming-Frog-style crawl \u2014 i.e. they want ANALYSIS (issues, internal link graph, indexability, heading breakdown, image sizes/formats), not just page content. For plain content scraping use extract_site instead."),
|
|
23657
|
+
maxPages: import_zod27.z.number().int().min(1).max(1e4).optional().describe("Maximum pages to crawl and audit. Use 50 for a normal audit, up to 10000 for a full-site audit. The audit always writes a folder of analysis files (report.md, issues.json, pages.jsonl, links.jsonl, link-metrics.jsonl, images.jsonl) plus per-page content, and returns a headline summary plus the folder path."),
|
|
23658
|
+
rotateProxies: import_zod27.z.boolean().optional().describe("Route page fetches through rotating residential proxies to defeat rate-limiting and bot blocks (403/429). Slower and pricier than the default fetch path, so use only when a site blocks normal crawling."),
|
|
23659
|
+
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.")
|
|
21999
23660
|
};
|
|
22000
23661
|
YoutubeHarvestInputSchema = {
|
|
22001
23662
|
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."),
|
|
@@ -22297,6 +23958,19 @@ var init_mcp_tool_schemas = __esm({
|
|
|
22297
23958
|
})),
|
|
22298
23959
|
durationMs: import_zod27.z.number().min(0)
|
|
22299
23960
|
};
|
|
23961
|
+
AuditSiteOutputSchema = {
|
|
23962
|
+
url: import_zod27.z.string(),
|
|
23963
|
+
pageCount: import_zod27.z.number().int().min(0),
|
|
23964
|
+
durationMs: import_zod27.z.number().min(0),
|
|
23965
|
+
bulkFolder: import_zod27.z.string().nullable(),
|
|
23966
|
+
issues: import_zod27.z.record(import_zod27.z.string(), import_zod27.z.number()),
|
|
23967
|
+
images: import_zod27.z.object({
|
|
23968
|
+
unique: import_zod27.z.number().int().min(0),
|
|
23969
|
+
totalBytes: import_zod27.z.number().min(0),
|
|
23970
|
+
over100kb: import_zod27.z.number().int().min(0),
|
|
23971
|
+
legacyFormat: import_zod27.z.number().int().min(0)
|
|
23972
|
+
})
|
|
23973
|
+
};
|
|
22300
23974
|
MapsPlaceIntelOutputSchema = {
|
|
22301
23975
|
name: import_zod27.z.string(),
|
|
22302
23976
|
rating: NullableString,
|
|
@@ -23076,7 +24750,7 @@ function localPlanningToolAnnotations(title) {
|
|
|
23076
24750
|
function listSavedReports() {
|
|
23077
24751
|
try {
|
|
23078
24752
|
const dir = outputBaseDir2();
|
|
23079
|
-
return (0,
|
|
24753
|
+
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
24754
|
} catch {
|
|
23081
24755
|
return [];
|
|
23082
24756
|
}
|
|
@@ -23100,15 +24774,15 @@ function registerSavedReportResources(server) {
|
|
|
23100
24774
|
},
|
|
23101
24775
|
async (uri, variables) => {
|
|
23102
24776
|
const requested = Array.isArray(variables.filename) ? variables.filename[0] : variables.filename;
|
|
23103
|
-
const filename = (0,
|
|
24777
|
+
const filename = (0, import_node_path11.basename)(decodeURIComponent(String(requested ?? "")));
|
|
23104
24778
|
if (!filename.endsWith(".md")) throw new Error("Only saved .md reports can be read");
|
|
23105
|
-
const text = (0,
|
|
24779
|
+
const text = (0, import_node_fs8.readFileSync)((0, import_node_path11.join)(outputBaseDir2(), filename), "utf8");
|
|
23106
24780
|
return { contents: [{ uri: uri.href, mimeType: "text/markdown", text }] };
|
|
23107
24781
|
}
|
|
23108
24782
|
);
|
|
23109
24783
|
}
|
|
23110
24784
|
function buildPaaExtractorMcpServer(executor, options = {}) {
|
|
23111
|
-
const server = new import_mcp.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION });
|
|
24785
|
+
const server = new import_mcp.McpServer({ name: "mcp-scraper", version: PACKAGE_VERSION }, { instructions: SERVER_INSTRUCTIONS });
|
|
23112
24786
|
registerPaaExtractorMcpTools(server, executor, options);
|
|
23113
24787
|
return server;
|
|
23114
24788
|
}
|
|
@@ -23140,18 +24814,25 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
23140
24814
|
}, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
|
|
23141
24815
|
server.registerTool("map_site_urls", {
|
|
23142
24816
|
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."),
|
|
24817
|
+
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
24818
|
inputSchema: MapSiteUrlsInputSchema,
|
|
23145
24819
|
outputSchema: MapSiteUrlsOutputSchema,
|
|
23146
24820
|
annotations: liveWebToolAnnotations("Site URL Map")
|
|
23147
24821
|
}, async (input) => formatMapSiteUrls(await executor.mapSiteUrls(input), input));
|
|
23148
24822
|
server.registerTool("extract_site", {
|
|
23149
|
-
title: "Multi-Page Site
|
|
23150
|
-
description: withReportNote("
|
|
24823
|
+
title: "Multi-Page Site Content Crawl",
|
|
24824
|
+
description: withReportNote("Crawl a public website and return the page CONTENT across multiple pages (map + scrape). Returns per-page titles and Markdown 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 content. This tool returns content only \u2014 for a technical SEO audit (issues, internal link graph, indexability, heading breakdown, image sizes/formats) use audit_site. Use map_site_urls first when URL selection matters; use extract_url for one page."),
|
|
23151
24825
|
inputSchema: ExtractSiteInputSchema,
|
|
23152
24826
|
outputSchema: ExtractSiteOutputSchema,
|
|
23153
|
-
annotations: liveWebToolAnnotations("Multi-Page Site
|
|
24827
|
+
annotations: liveWebToolAnnotations("Multi-Page Site Content Crawl")
|
|
23154
24828
|
}, async (input) => formatExtractSite(await executor.extractSite(input), input));
|
|
24829
|
+
server.registerTool("audit_site", {
|
|
24830
|
+
title: "Technical SEO Audit",
|
|
24831
|
+
description: withReportNote("Run a full technical SEO audit (Screaming-Frog-style) on a public website. Crawls up to maxPages and produces a complete on-page + crawl analysis: per-page title/meta lengths, heading breakdown, indexability and canonical status, schema, an internal link graph with inlinks/orphans/crawl-depth, an issues report, and an image audit (byte size, format, over-100KB and legacy-format flags). Writes everything to a local folder (report.md, issues.json, pages.jsonl, links.jsonl, link-metrics.jsonl, images.jsonl) plus per-page content, and returns a headline summary plus the folder path. Use this when the user wants an audit or analysis; use extract_site when they just want page content."),
|
|
24832
|
+
inputSchema: AuditSiteInputSchema,
|
|
24833
|
+
outputSchema: AuditSiteOutputSchema,
|
|
24834
|
+
annotations: liveWebToolAnnotations("Technical SEO Audit")
|
|
24835
|
+
}, async (input) => formatAuditSite(await executor.auditSite(input), input));
|
|
23155
24836
|
server.registerTool("youtube_harvest", {
|
|
23156
24837
|
title: "YouTube Video Harvest",
|
|
23157
24838
|
description: withReportNote('Harvest YouTube video metadata when the user wants to find videos by topic, inspect a channel library, compare video angles, or get videoIds for later transcription. Use mode "search" for keyword/topic requests and mode "channel" for @handles, channel IDs, or channel URLs. Returns titles, views, durations, URLs, and videoIds for follow-up transcription. Use youtube_transcribe after selecting one video.'),
|
|
@@ -23271,7 +24952,7 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
23271
24952
|
outputSchema: WorkflowArtifactReadOutputSchema,
|
|
23272
24953
|
annotations: liveWebToolAnnotations("Read Workflow Artifact")
|
|
23273
24954
|
}, async (input) => formatWorkflowArtifactRead(await executor.workflowArtifactRead(input), input));
|
|
23274
|
-
server.registerTool("
|
|
24955
|
+
server.registerTool("rank_tracker_workflow", {
|
|
23275
24956
|
title: "Rank Tracker Blueprint Builder",
|
|
23276
24957
|
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
24958
|
inputSchema: RankTrackerBlueprintInputSchema,
|
|
@@ -23292,15 +24973,16 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
|
|
|
23292
24973
|
}
|
|
23293
24974
|
}, async (input) => formatCreditsInfo(await executor.creditsInfo(input), input));
|
|
23294
24975
|
}
|
|
23295
|
-
var import_mcp,
|
|
24976
|
+
var import_mcp, import_node_fs8, import_node_path11;
|
|
23296
24977
|
var init_paa_mcp_server = __esm({
|
|
23297
24978
|
"src/mcp/paa-mcp-server.ts"() {
|
|
23298
24979
|
"use strict";
|
|
23299
24980
|
import_mcp = require("@modelcontextprotocol/sdk/server/mcp.js");
|
|
23300
|
-
|
|
23301
|
-
|
|
24981
|
+
import_node_fs8 = require("fs");
|
|
24982
|
+
import_node_path11 = require("path");
|
|
23302
24983
|
init_version();
|
|
23303
24984
|
init_mcp_response_formatter();
|
|
24985
|
+
init_server_instructions();
|
|
23304
24986
|
init_mcp_tool_schemas();
|
|
23305
24987
|
init_rank_tracker_blueprint();
|
|
23306
24988
|
init_mcp_response_formatter();
|
|
@@ -23488,6 +25170,9 @@ var init_http_mcp_tool_executor = __esm({
|
|
|
23488
25170
|
extractSite(input) {
|
|
23489
25171
|
return this.call("/extract-site", input);
|
|
23490
25172
|
}
|
|
25173
|
+
auditSite(input) {
|
|
25174
|
+
return this.call("/extract-site", input);
|
|
25175
|
+
}
|
|
23491
25176
|
youtubeHarvest(input) {
|
|
23492
25177
|
return this.call("/youtube/harvest", input);
|
|
23493
25178
|
}
|
|
@@ -23709,7 +25394,7 @@ async function migrateBrowserAgent() {
|
|
|
23709
25394
|
}
|
|
23710
25395
|
async function createSessionRow(input) {
|
|
23711
25396
|
const db = getDb();
|
|
23712
|
-
const id = `bas_${(0,
|
|
25397
|
+
const id = `bas_${(0, import_node_crypto6.randomUUID)().replace(/-/g, "").slice(0, 20)}`;
|
|
23713
25398
|
await db.execute({
|
|
23714
25399
|
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
25400
|
VALUES (?, ?, ?, ?, 'open', ?, ?, ?, datetime('now'))`,
|
|
@@ -23770,7 +25455,7 @@ async function recordAction(input) {
|
|
|
23770
25455
|
sql: `INSERT INTO browser_agent_actions (id, session_id, type, params_json, ok, error)
|
|
23771
25456
|
VALUES (?, ?, ?, ?, ?, ?)`,
|
|
23772
25457
|
args: [
|
|
23773
|
-
`baa_${(0,
|
|
25458
|
+
`baa_${(0, import_node_crypto6.randomUUID)().replace(/-/g, "").slice(0, 20)}`,
|
|
23774
25459
|
input.sessionId,
|
|
23775
25460
|
input.type,
|
|
23776
25461
|
input.params == null ? null : JSON.stringify(input.params),
|
|
@@ -23810,11 +25495,11 @@ async function listReplayRows(sessionId) {
|
|
|
23810
25495
|
});
|
|
23811
25496
|
return res.rows;
|
|
23812
25497
|
}
|
|
23813
|
-
var
|
|
25498
|
+
var import_node_crypto6, _ready;
|
|
23814
25499
|
var init_browser_agent_db = __esm({
|
|
23815
25500
|
"src/api/browser-agent-db.ts"() {
|
|
23816
25501
|
"use strict";
|
|
23817
|
-
|
|
25502
|
+
import_node_crypto6 = require("crypto");
|
|
23818
25503
|
init_db();
|
|
23819
25504
|
_ready = false;
|
|
23820
25505
|
}
|
|
@@ -24419,7 +26104,7 @@ function buildFanoutResult(ctx) {
|
|
|
24419
26104
|
interceptorReady: ready,
|
|
24420
26105
|
rawBytes,
|
|
24421
26106
|
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
|
|
26107
|
+
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
26108
|
};
|
|
24424
26109
|
}
|
|
24425
26110
|
return result;
|
|
@@ -24496,20 +26181,20 @@ var init_classify = __esm({
|
|
|
24496
26181
|
|
|
24497
26182
|
// src/services/fanout/export.ts
|
|
24498
26183
|
function outputBaseDir3() {
|
|
24499
|
-
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0,
|
|
26184
|
+
return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || (0, import_node_path12.join)((0, import_node_os7.homedir)(), "Downloads", "mcp-scraper");
|
|
24500
26185
|
}
|
|
24501
26186
|
function safe(value) {
|
|
24502
26187
|
return value.replace(/[^a-zA-Z0-9._-]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "capture";
|
|
24503
26188
|
}
|
|
24504
26189
|
function writeTable(path6, rows, delimiter) {
|
|
24505
|
-
(0,
|
|
26190
|
+
(0, import_node_fs9.writeFileSync)(path6, import_papaparse3.default.unparse(rows.length ? rows : [{}], { delimiter }));
|
|
24506
26191
|
}
|
|
24507
26192
|
function exportFanout(enriched) {
|
|
24508
26193
|
const stamp = safe(enriched.capturedAt.replace(/[:.]/g, "-"));
|
|
24509
26194
|
const outputDir = outputBaseDir3();
|
|
24510
|
-
const relativeDir = (0,
|
|
24511
|
-
const dir = (0,
|
|
24512
|
-
(0,
|
|
26195
|
+
const relativeDir = (0, import_node_path12.join)("fanout", `${stamp}-${safe(enriched.platform)}`);
|
|
26196
|
+
const dir = (0, import_node_path12.join)(outputDir, relativeDir);
|
|
26197
|
+
(0, import_node_fs9.mkdirSync)(dir, { recursive: true });
|
|
24513
26198
|
const queryRows = enriched.queries.map((q, i) => ({ index: i + 1, query: q }));
|
|
24514
26199
|
const citationRows = enriched.aggregates.citationOrder.map((c) => {
|
|
24515
26200
|
const src = enriched.citedUrls.find((s) => s.url === c.url);
|
|
@@ -24522,25 +26207,25 @@ function exportFanout(enriched) {
|
|
|
24522
26207
|
const relativePaths = {
|
|
24523
26208
|
relativeTo: "MCP_SCRAPER_OUTPUT_DIR or ~/Downloads/mcp-scraper",
|
|
24524
26209
|
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,
|
|
26210
|
+
json: (0, import_node_path12.join)(relativeDir, "fanout.json"),
|
|
26211
|
+
queriesCsv: (0, import_node_path12.join)(relativeDir, "queries.csv"),
|
|
26212
|
+
queriesTsv: (0, import_node_path12.join)(relativeDir, "queries.tsv"),
|
|
26213
|
+
citationsCsv: (0, import_node_path12.join)(relativeDir, "citations.csv"),
|
|
26214
|
+
sourcesCsv: (0, import_node_path12.join)(relativeDir, "sources.csv"),
|
|
26215
|
+
browsedOnlyCsv: (0, import_node_path12.join)(relativeDir, "browsed-only.csv"),
|
|
26216
|
+
snippetsCsv: (0, import_node_path12.join)(relativeDir, "snippets.csv"),
|
|
26217
|
+
domainsCsv: (0, import_node_path12.join)(relativeDir, "domains.csv"),
|
|
26218
|
+
report: (0, import_node_path12.join)(relativeDir, "report.html")
|
|
24534
26219
|
};
|
|
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,
|
|
26220
|
+
(0, import_node_fs9.writeFileSync)((0, import_node_path12.join)(outputDir, relativePaths.json), JSON.stringify(enriched, null, 2));
|
|
26221
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.queriesCsv), queryRows, ",");
|
|
26222
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.queriesTsv), queryRows, " ");
|
|
26223
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.citationsCsv), citationRows, ",");
|
|
26224
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.sourcesCsv), sourceRows2, ",");
|
|
26225
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.browsedOnlyCsv), browsedOnlyRows, ",");
|
|
26226
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.snippetsCsv), snippetRows, ",");
|
|
26227
|
+
writeTable((0, import_node_path12.join)(outputDir, relativePaths.domainsCsv), domainRows, ",");
|
|
26228
|
+
(0, import_node_fs9.writeFileSync)((0, import_node_path12.join)(outputDir, relativePaths.report), renderReportHtml(enriched));
|
|
24544
26229
|
return relativePaths;
|
|
24545
26230
|
}
|
|
24546
26231
|
function esc(s) {
|
|
@@ -24618,13 +26303,13 @@ render();
|
|
|
24618
26303
|
</script>
|
|
24619
26304
|
</body></html>`;
|
|
24620
26305
|
}
|
|
24621
|
-
var
|
|
26306
|
+
var import_node_fs9, import_node_os7, import_node_path12, import_papaparse3;
|
|
24622
26307
|
var init_export = __esm({
|
|
24623
26308
|
"src/services/fanout/export.ts"() {
|
|
24624
26309
|
"use strict";
|
|
24625
|
-
|
|
24626
|
-
|
|
24627
|
-
|
|
26310
|
+
import_node_fs9 = require("fs");
|
|
26311
|
+
import_node_os7 = require("os");
|
|
26312
|
+
import_node_path12 = require("path");
|
|
24628
26313
|
import_papaparse3 = __toESM(require("papaparse"), 1);
|
|
24629
26314
|
}
|
|
24630
26315
|
});
|
|
@@ -24672,7 +26357,7 @@ async function runFanoutCapture(page, input) {
|
|
|
24672
26357
|
}
|
|
24673
26358
|
const adapter = adapterForHost(hostname);
|
|
24674
26359
|
if (!adapter) {
|
|
24675
|
-
throw new Error(`
|
|
26360
|
+
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
26361
|
}
|
|
24677
26362
|
const text = cap.bestAnswerText();
|
|
24678
26363
|
const parsed = adapter.parse(text, input.prompt || "");
|
|
@@ -24710,7 +26395,7 @@ function keepHostedBrowserAlive(_browser) {
|
|
|
24710
26395
|
function client() {
|
|
24711
26396
|
const apiKey = browserServiceApiKey();
|
|
24712
26397
|
if (!apiKey) throw new Error("Browser backend API key is required");
|
|
24713
|
-
return new
|
|
26398
|
+
return new import_sdk7.default({ apiKey });
|
|
24714
26399
|
}
|
|
24715
26400
|
function isProfileConflict(err) {
|
|
24716
26401
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -24862,7 +26547,7 @@ async function pressKeys(runtimeSessionId, keys) {
|
|
|
24862
26547
|
await k.browsers.computer.pressKey(runtimeSessionId, { keys });
|
|
24863
26548
|
}
|
|
24864
26549
|
async function goto(cdpWsUrl, url) {
|
|
24865
|
-
const browser = await
|
|
26550
|
+
const browser = await import_playwright5.chromium.connectOverCDP(cdpWsUrl);
|
|
24866
26551
|
try {
|
|
24867
26552
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
24868
26553
|
const page = context.pages()[0] ?? await context.newPage();
|
|
@@ -24873,7 +26558,7 @@ async function goto(cdpWsUrl, url) {
|
|
|
24873
26558
|
}
|
|
24874
26559
|
}
|
|
24875
26560
|
async function captureFanout(cdpWsUrl, input) {
|
|
24876
|
-
const browser = await
|
|
26561
|
+
const browser = await import_playwright5.chromium.connectOverCDP(cdpWsUrl);
|
|
24877
26562
|
try {
|
|
24878
26563
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
24879
26564
|
const page = context.pages()[0] ?? await context.newPage();
|
|
@@ -24883,7 +26568,7 @@ async function captureFanout(cdpWsUrl, input) {
|
|
|
24883
26568
|
}
|
|
24884
26569
|
}
|
|
24885
26570
|
async function readPage(cdpWsUrl) {
|
|
24886
|
-
const browser = await
|
|
26571
|
+
const browser = await import_playwright5.chromium.connectOverCDP(cdpWsUrl);
|
|
24887
26572
|
try {
|
|
24888
26573
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
24889
26574
|
const page = context.pages()[0] ?? await context.newPage();
|
|
@@ -24938,7 +26623,7 @@ async function readPage(cdpWsUrl) {
|
|
|
24938
26623
|
}
|
|
24939
26624
|
}
|
|
24940
26625
|
async function locatePageTargets(cdpWsUrl, targets) {
|
|
24941
|
-
const browser = await
|
|
26626
|
+
const browser = await import_playwright5.chromium.connectOverCDP(cdpWsUrl);
|
|
24942
26627
|
try {
|
|
24943
26628
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
24944
26629
|
const page = context.pages()[0] ?? await context.newPage();
|
|
@@ -24950,16 +26635,16 @@ async function locatePageTargets(cdpWsUrl, targets) {
|
|
|
24950
26635
|
}).catch(() => {
|
|
24951
26636
|
});
|
|
24952
26637
|
const data = await page.evaluate((rawTargets) => {
|
|
24953
|
-
const
|
|
26638
|
+
const normalize3 = (value) => value.replace(/\s+/g, " ").trim();
|
|
24954
26639
|
const isVisibleRect = (r) => r.width >= 1 && r.height >= 1 && r.bottom >= 0 && r.right >= 0 && r.top <= window.innerHeight && r.left <= window.innerWidth;
|
|
24955
26640
|
const roleOf = (el2) => el2.getAttribute("role") || el2.tagName.toLowerCase();
|
|
24956
26641
|
const nameOf = (el2) => {
|
|
24957
26642
|
const e = el2;
|
|
24958
|
-
return
|
|
26643
|
+
return normalize3(
|
|
24959
26644
|
e.getAttribute("aria-label") || e.placeholder || e.innerText || e.getAttribute("value") || e.getAttribute("title") || ""
|
|
24960
26645
|
).slice(0, 160);
|
|
24961
26646
|
};
|
|
24962
|
-
const textOf = (el2) =>
|
|
26647
|
+
const textOf = (el2) => normalize3(el2.innerText || el2.textContent || "").slice(0, 500);
|
|
24963
26648
|
const unionRects = (rects) => {
|
|
24964
26649
|
const visible = rects.filter(isVisibleRect);
|
|
24965
26650
|
if (!visible.length) return null;
|
|
@@ -24998,7 +26683,7 @@ async function locatePageTargets(cdpWsUrl, targets) {
|
|
|
24998
26683
|
return nodes.map((el2) => ({ el: el2, rect: el2.getBoundingClientRect() })).filter((item) => isVisibleRect(item.rect));
|
|
24999
26684
|
};
|
|
25000
26685
|
const textMatches = (needle, match) => {
|
|
25001
|
-
const wanted =
|
|
26686
|
+
const wanted = normalize3(needle);
|
|
25002
26687
|
const wantedLower = wanted.toLowerCase();
|
|
25003
26688
|
if (!wantedLower) return [];
|
|
25004
26689
|
const matches = [];
|
|
@@ -25006,7 +26691,7 @@ async function locatePageTargets(cdpWsUrl, targets) {
|
|
|
25006
26691
|
let node = walker.nextNode();
|
|
25007
26692
|
while (node) {
|
|
25008
26693
|
const raw = node.textContent || "";
|
|
25009
|
-
const normalizedRaw =
|
|
26694
|
+
const normalizedRaw = normalize3(raw);
|
|
25010
26695
|
const rawLower = raw.toLowerCase();
|
|
25011
26696
|
const normalizedLower = normalizedRaw.toLowerCase();
|
|
25012
26697
|
const ok = match === "exact" ? normalizedLower === wantedLower : normalizedLower.includes(wantedLower);
|
|
@@ -25102,12 +26787,12 @@ async function replayList(runtimeSessionId) {
|
|
|
25102
26787
|
finishedAt: r.finished_at ?? null
|
|
25103
26788
|
}));
|
|
25104
26789
|
}
|
|
25105
|
-
var
|
|
26790
|
+
var import_sdk7, import_playwright5, DEFAULT_TIMEOUT_SECONDS;
|
|
25106
26791
|
var init_browser_agent_service = __esm({
|
|
25107
26792
|
"src/services/browser-agent/browser-agent-service.ts"() {
|
|
25108
26793
|
"use strict";
|
|
25109
|
-
|
|
25110
|
-
|
|
26794
|
+
import_sdk7 = __toESM(require("@onkernel/sdk"), 1);
|
|
26795
|
+
import_playwright5 = require("playwright");
|
|
25111
26796
|
init_browser_service_env();
|
|
25112
26797
|
init_run_capture();
|
|
25113
26798
|
DEFAULT_TIMEOUT_SECONDS = 600;
|
|
@@ -26108,14 +27793,14 @@ function getSessionSecret() {
|
|
|
26108
27793
|
function safeEqualHex(a, b) {
|
|
26109
27794
|
if (a.length !== b.length) return false;
|
|
26110
27795
|
try {
|
|
26111
|
-
return (0,
|
|
27796
|
+
return (0, import_node_crypto7.timingSafeEqual)(Buffer.from(a, "hex"), Buffer.from(b, "hex"));
|
|
26112
27797
|
} catch {
|
|
26113
27798
|
return false;
|
|
26114
27799
|
}
|
|
26115
27800
|
}
|
|
26116
27801
|
function signSession(userId) {
|
|
26117
27802
|
const payload = String(userId);
|
|
26118
|
-
const sig = (0,
|
|
27803
|
+
const sig = (0, import_node_crypto7.createHmac)("sha256", secret()).update(payload).digest("hex");
|
|
26119
27804
|
return `${payload}.${sig}`;
|
|
26120
27805
|
}
|
|
26121
27806
|
function verifySession(token) {
|
|
@@ -26123,16 +27808,16 @@ function verifySession(token) {
|
|
|
26123
27808
|
if (dot === -1) return null;
|
|
26124
27809
|
const payload = token.slice(0, dot);
|
|
26125
27810
|
const sig = token.slice(dot + 1);
|
|
26126
|
-
const expected = (0,
|
|
27811
|
+
const expected = (0, import_node_crypto7.createHmac)("sha256", secret()).update(payload).digest("hex");
|
|
26127
27812
|
if (!safeEqualHex(sig, expected)) return null;
|
|
26128
27813
|
const id = parseInt(payload);
|
|
26129
27814
|
return isNaN(id) ? null : id;
|
|
26130
27815
|
}
|
|
26131
|
-
var
|
|
27816
|
+
var import_node_crypto7, isProduction, secret;
|
|
26132
27817
|
var init_session = __esm({
|
|
26133
27818
|
"src/api/session.ts"() {
|
|
26134
27819
|
"use strict";
|
|
26135
|
-
|
|
27820
|
+
import_node_crypto7 = require("crypto");
|
|
26136
27821
|
isProduction = () => process.env.NODE_ENV === "production" || process.env.VERCEL === "1";
|
|
26137
27822
|
secret = () => getSessionSecret();
|
|
26138
27823
|
}
|
|
@@ -26379,6 +28064,9 @@ var init_server = __esm({
|
|
|
26379
28064
|
import_hono14 = require("inngest/hono");
|
|
26380
28065
|
init_client();
|
|
26381
28066
|
init_site_audit();
|
|
28067
|
+
init_site_extract();
|
|
28068
|
+
init_site_extract_repository();
|
|
28069
|
+
init_catalog_seed();
|
|
26382
28070
|
init_site_audit_routes();
|
|
26383
28071
|
init_youtube_routes();
|
|
26384
28072
|
init_screenshot_routes();
|
|
@@ -26563,6 +28251,10 @@ var init_server = __esm({
|
|
|
26563
28251
|
const taken = await countActiveUsers();
|
|
26564
28252
|
return c.json({ taken, open: true });
|
|
26565
28253
|
});
|
|
28254
|
+
app.get("/catalog", (c) => {
|
|
28255
|
+
c.header("Cache-Control", "public, max-age=60");
|
|
28256
|
+
return c.json(CATALOG);
|
|
28257
|
+
});
|
|
26566
28258
|
app.get("/me", async (c) => {
|
|
26567
28259
|
const token = (0, import_cookie.getCookie)(c, "session");
|
|
26568
28260
|
if (!token) return c.json({ authenticated: false });
|
|
@@ -26797,7 +28489,7 @@ var init_server = __esm({
|
|
|
26797
28489
|
const raw = await c.req.json().catch(() => ({}));
|
|
26798
28490
|
const bodyResult = ExtractUrlBodySchema.safeParse(raw);
|
|
26799
28491
|
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;
|
|
28492
|
+
const { url, screenshot: screenshot2, screenshotDevice, extractBranding: extractBranding2, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
|
|
26801
28493
|
if (!allowLocal) {
|
|
26802
28494
|
const checked = await validatePublicHttpUrl(url, { field: "URL" });
|
|
26803
28495
|
if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
|
|
@@ -26830,7 +28522,7 @@ var init_server = __esm({
|
|
|
26830
28522
|
const device = screenshotDevice === "mobile" ? "mobile" : "desktop";
|
|
26831
28523
|
const [result, pageData] = await Promise.all([
|
|
26832
28524
|
extractKpo({ url: canonicalUrl, kernelApiKey }),
|
|
26833
|
-
screenshot2 ||
|
|
28525
|
+
screenshot2 || extractBranding2 ? capturePageData(canonicalUrl, { kernelApiKey, device, screenshot: !!screenshot2, branding: !!extractBranding2 }).catch((err) => {
|
|
26834
28526
|
console.error("[extract-url] capturePageData failed:", err instanceof Error ? err.message : err);
|
|
26835
28527
|
return null;
|
|
26836
28528
|
}) : null
|
|
@@ -26874,7 +28566,7 @@ var init_server = __esm({
|
|
|
26874
28566
|
debited = true;
|
|
26875
28567
|
const result = await spiderSite({
|
|
26876
28568
|
startUrl: parsed.href,
|
|
26877
|
-
maxUrls: Math.min(
|
|
28569
|
+
maxUrls: Math.min(1e4, Math.max(1, body.maxUrls ?? 500)),
|
|
26878
28570
|
concurrency: Math.min(20, Math.max(1, body.concurrency ?? 12)),
|
|
26879
28571
|
kernelApiKey: body.browserFallback ?? body.kernelFallback ? browserServiceApiKey() : void 0
|
|
26880
28572
|
});
|
|
@@ -26911,7 +28603,43 @@ var init_server = __esm({
|
|
|
26911
28603
|
if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
|
|
26912
28604
|
const parsed = checked.parsed;
|
|
26913
28605
|
const user = c.get("user");
|
|
26914
|
-
|
|
28606
|
+
if (body.background === true) {
|
|
28607
|
+
const affordablePages = Math.floor(user.balance_mc / MC_COSTS.page_scrape);
|
|
28608
|
+
if (affordablePages < 1) return c.json(insufficientBalanceResponse(user.balance_mc, MC_COSTS.page_scrape), 402);
|
|
28609
|
+
const maxPages = Math.min(Math.min(1e4, Math.max(1, body.maxPages ?? 1e4)), affordablePages);
|
|
28610
|
+
const heldMc = maxPages * MC_COSTS.page_scrape;
|
|
28611
|
+
const { ok: holdOk, balance_mc: holdBal } = await debitMc(user.id, heldMc, LedgerOperation.EXTRACT_SITE_HOLD, parsed.hostname);
|
|
28612
|
+
if (!holdOk) return c.json(insufficientBalanceResponse(holdBal, heldMc), 402);
|
|
28613
|
+
const jobId = `ext_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
|
|
28614
|
+
let jobCreated = false;
|
|
28615
|
+
try {
|
|
28616
|
+
await createExtractJob(jobId, user.id, parsed.href, {
|
|
28617
|
+
maxPages,
|
|
28618
|
+
concurrency: concurrencyLimitForUser(user),
|
|
28619
|
+
urlsPerBrowser: body.rotateProxyEvery ?? 10,
|
|
28620
|
+
formats: body.formats,
|
|
28621
|
+
heldMc
|
|
28622
|
+
});
|
|
28623
|
+
jobCreated = true;
|
|
28624
|
+
await inngest.send({ name: "mcp-scraper/extract.requested", data: { jobId } });
|
|
28625
|
+
} catch (err) {
|
|
28626
|
+
if (jobCreated) {
|
|
28627
|
+
await settleExtractJob(jobId, user.id, heldMc, 0, "enqueue failed").catch(() => {
|
|
28628
|
+
});
|
|
28629
|
+
} else {
|
|
28630
|
+
await creditMc(user.id, heldMc, LedgerOperation.EXTRACT_SITE_REFUND, "enqueue failed").catch(() => {
|
|
28631
|
+
});
|
|
28632
|
+
}
|
|
28633
|
+
await failExtractJob(jobId, err instanceof Error ? err.message : "enqueue failed").catch(() => {
|
|
28634
|
+
});
|
|
28635
|
+
return c.json({ error: "Failed to enqueue background crawl" }, 503);
|
|
28636
|
+
}
|
|
28637
|
+
return c.json({ jobId, status: "pending", statusUrl: `/extract-site/status/${jobId}` });
|
|
28638
|
+
}
|
|
28639
|
+
const affordablePagesSync = Math.floor(user.balance_mc / MC_COSTS.page_scrape);
|
|
28640
|
+
if (affordablePagesSync < 1) return c.json(insufficientBalanceResponse(user.balance_mc, MC_COSTS.page_scrape), 402);
|
|
28641
|
+
const siteMaxPages = Math.min(Math.min(1e4, Math.max(1, body.maxPages ?? 100)), affordablePagesSync);
|
|
28642
|
+
const siteHoldMc = siteMaxPages * MC_COSTS.page_scrape;
|
|
26915
28643
|
const gate = await acquireConcurrencyGate(user, "extract_site", {
|
|
26916
28644
|
reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
|
|
26917
28645
|
metadata: { url: parsed.href }
|
|
@@ -26922,10 +28650,17 @@ var init_server = __esm({
|
|
|
26922
28650
|
const { ok: siteOk, balance_mc: siteBal } = await debitMc(user.id, siteHoldMc, LedgerOperation.EXTRACT_SITE_HOLD, parsed.hostname);
|
|
26923
28651
|
if (!siteOk) return c.json(insufficientBalanceResponse(siteBal, siteHoldMc), 402);
|
|
26924
28652
|
debited = true;
|
|
28653
|
+
const rotateProxies = body.rotateProxies === true;
|
|
28654
|
+
const wantsBranding = body.formats?.includes("branding") === true;
|
|
26925
28655
|
const result = await extractSite({
|
|
26926
28656
|
startUrl: parsed.href,
|
|
26927
|
-
maxPages:
|
|
26928
|
-
kernelApiKey: body.browserFallback
|
|
28657
|
+
maxPages: siteMaxPages,
|
|
28658
|
+
kernelApiKey: rotateProxies || wantsBranding || body.browserFallback || body.kernelFallback ? browserServiceApiKey() : void 0,
|
|
28659
|
+
formats: body.formats,
|
|
28660
|
+
...rotateProxies ? {
|
|
28661
|
+
rotateProxyEvery: body.rotateProxyEvery ?? 30,
|
|
28662
|
+
parallelism: concurrencyLimitForUser(user)
|
|
28663
|
+
} : {}
|
|
26929
28664
|
});
|
|
26930
28665
|
const pageCount = result.pages?.length ?? 1;
|
|
26931
28666
|
const actualSiteMc = pageCount * MC_COSTS.page_scrape;
|
|
@@ -26956,6 +28691,21 @@ var init_server = __esm({
|
|
|
26956
28691
|
await releaseConcurrencyGate(gate.lockId);
|
|
26957
28692
|
}
|
|
26958
28693
|
});
|
|
28694
|
+
app.get("/extract-site/status/:id", auth2, async (c) => {
|
|
28695
|
+
const user = c.get("user");
|
|
28696
|
+
const job = await getExtractJob(c.req.param("id"));
|
|
28697
|
+
if (!job || job.userId !== user.id) return c.json({ error: "Job not found" }, 404);
|
|
28698
|
+
return c.json({
|
|
28699
|
+
jobId: job.id,
|
|
28700
|
+
status: job.status,
|
|
28701
|
+
startUrl: job.startUrl,
|
|
28702
|
+
totalUrls: job.totalUrls,
|
|
28703
|
+
doneUrls: job.doneUrls,
|
|
28704
|
+
artifacts: job.artifacts ?? [],
|
|
28705
|
+
error: job.error,
|
|
28706
|
+
updatedAt: job.updatedAt
|
|
28707
|
+
});
|
|
28708
|
+
});
|
|
26959
28709
|
app.post("/billing/checkout", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
26960
28710
|
try {
|
|
26961
28711
|
const user = c.get("sessionUser");
|
|
@@ -27144,7 +28894,7 @@ var init_server = __esm({
|
|
|
27144
28894
|
]);
|
|
27145
28895
|
return c.json({ drained: results.length, results, sweepResult, workflowDispatch: workflowDispatchResult });
|
|
27146
28896
|
});
|
|
27147
|
-
app.on(["GET", "POST", "PUT"], "/api/inngest", (0, import_hono14.serve)({ client: inngest, functions: [siteAuditFn] }));
|
|
28897
|
+
app.on(["GET", "POST", "PUT"], "/api/inngest", (0, import_hono14.serve)({ client: inngest, functions: [siteAuditFn, siteExtractFn] }));
|
|
27148
28898
|
app.route("/api/internal/site-architecture-auditor", siteAuditApp);
|
|
27149
28899
|
app.route("/youtube", youtubeApp);
|
|
27150
28900
|
app.route("/screenshot", screenshotApp);
|
|
@@ -27266,10 +29016,10 @@ ${ATTRIBUTION_PIXEL_SCRIPT}
|
|
|
27266
29016
|
});
|
|
27267
29017
|
|
|
27268
29018
|
// bin/api-server.ts
|
|
27269
|
-
var
|
|
29019
|
+
var import_node_fs10 = require("fs");
|
|
27270
29020
|
function loadDotEnv() {
|
|
27271
29021
|
try {
|
|
27272
|
-
for (const line of (0,
|
|
29022
|
+
for (const line of (0, import_node_fs10.readFileSync)(".env", "utf8").split("\n")) {
|
|
27273
29023
|
const eq = line.indexOf("=");
|
|
27274
29024
|
if (eq < 1 || line.trimStart().startsWith("#")) continue;
|
|
27275
29025
|
const k = line.slice(0, eq).trim();
|