mcp-scraper 0.6.0 → 0.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/dist/bin/api-server.cjs +3771 -3613
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +2 -2
  4. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +1 -1
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +3 -3
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +4 -4
  13. package/dist/bin/paa-harvest.cjs +166 -74
  14. package/dist/bin/paa-harvest.cjs.map +1 -1
  15. package/dist/bin/paa-harvest.js +1 -1
  16. package/dist/chunk-3MKSXDJ7.js +7 -0
  17. package/dist/chunk-3MKSXDJ7.js.map +1 -0
  18. package/dist/{chunk-APJO2XV5.js → chunk-HL33CGJF.js} +3 -3
  19. package/dist/{chunk-APJO2XV5.js.map → chunk-HL33CGJF.js.map} +1 -1
  20. package/dist/{chunk-3K5GT6VG.js → chunk-ITJC3NN4.js} +5 -4
  21. package/dist/{chunk-GWRIO6JT.js → chunk-R7EETU7Z.js} +134 -134
  22. package/dist/chunk-R7EETU7Z.js.map +1 -0
  23. package/dist/{chunk-AUCXKRRH.js → chunk-RMPPYKUV.js} +167 -75
  24. package/dist/chunk-RMPPYKUV.js.map +1 -0
  25. package/dist/{chunk-BL7BBSYF.js → chunk-RUGJE5EB.js} +2 -2
  26. package/dist/{extract-bundle-COS56ZDO.js → extract-bundle-U4D5LW5W.js} +4 -2
  27. package/dist/extract-bundle-U4D5LW5W.js.map +1 -0
  28. package/dist/index.cjs +166 -74
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +1 -1
  31. package/dist/{server-WHM446IZ.js → server-WBDVVHR6.js} +151 -96
  32. package/dist/server-WBDVVHR6.js.map +1 -0
  33. package/dist/{site-extract-repository-U476J44K.js → site-extract-repository-NVSZH35Y.js} +3 -3
  34. package/dist/{worker-KJZ3ZN2N.js → worker-JQTS437L.js} +4 -4
  35. package/dist/worker-JQTS437L.js.map +1 -0
  36. package/package.json +1 -1
  37. package/dist/chunk-AUCXKRRH.js.map +0 -1
  38. package/dist/chunk-F2BXETDX.js +0 -7
  39. package/dist/chunk-F2BXETDX.js.map +0 -1
  40. package/dist/chunk-GWRIO6JT.js.map +0 -1
  41. package/dist/extract-bundle-COS56ZDO.js.map +0 -1
  42. package/dist/server-WHM446IZ.js.map +0 -1
  43. package/dist/worker-KJZ3ZN2N.js.map +0 -1
  44. package/docs/spec-browser-routing.md +0 -108
  45. package/docs/spec-kernel-computer-controls.md +0 -166
  46. package/docs/spec-mcp-tools.md +0 -146
  47. package/docs/spec-server-instructions.md +0 -94
  48. package/docs/specs/agent-ready-seo-packet-spec.md +0 -237
  49. package/docs/specs/audit-visual-demo.md +0 -309
  50. package/docs/specs/cli-agent-wiring-spec.md +0 -206
  51. package/docs/specs/local-competitive-audit-spec.md +0 -311
  52. package/docs/specs/scheduled-workflows-api-spec.md +0 -304
  53. package/docs/specs/seo-cli-growth-roadmap-spec.md +0 -181
  54. package/docs/specs/seo-workflow-runner-and-reports-spec.md +0 -241
  55. /package/dist/{chunk-3K5GT6VG.js.map → chunk-ITJC3NN4.js.map} +0 -0
  56. /package/dist/{chunk-BL7BBSYF.js.map → chunk-RUGJE5EB.js.map} +0 -0
  57. /package/dist/{site-extract-repository-U476J44K.js.map → site-extract-repository-NVSZH35Y.js.map} +0 -0
package/dist/index.js CHANGED
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  harvest,
3
3
  recordVendorUsage
4
- } from "./chunk-AUCXKRRH.js";
4
+ } from "./chunk-RMPPYKUV.js";
5
5
  import "./chunk-HPV4VOQX.js";
6
6
  import "./chunk-K443GQY5.js";
7
7
  import "./chunk-XGIPATLV.js";
@@ -18,10 +18,11 @@ import {
18
18
  resolveKernelProxyId,
19
19
  runWithCostContext,
20
20
  vendorCostUsd
21
- } from "./chunk-AUCXKRRH.js";
21
+ } from "./chunk-RMPPYKUV.js";
22
22
  import {
23
23
  HttpMcpToolExecutor,
24
24
  MemoryMcpToolExecutor,
25
+ buildLinkGraph,
25
26
  buildPaaExtractorMcpServer,
26
27
  configureReportSaving,
27
28
  exportFanout,
@@ -33,11 +34,13 @@ import {
33
34
  registerSerpIntelligenceCaptureTools,
34
35
  sanitizeAttempts,
35
36
  sanitizeHarvestResult
36
- } from "./chunk-3K5GT6VG.js";
37
+ } from "./chunk-ITJC3NN4.js";
37
38
  import {
38
39
  auditImages,
40
+ buildLinkReport,
41
+ computeIssues,
39
42
  getBlobStore
40
- } from "./chunk-GWRIO6JT.js";
43
+ } from "./chunk-R7EETU7Z.js";
41
44
  import {
42
45
  browserServiceApiKey,
43
46
  browserServiceProfileName,
@@ -67,7 +70,7 @@ import {
67
70
  RawMapsOverviewSchema,
68
71
  RawMapsReviewStatsSchema
69
72
  } from "./chunk-XGIPATLV.js";
70
- import "./chunk-F2BXETDX.js";
73
+ import "./chunk-3MKSXDJ7.js";
71
74
  import {
72
75
  completeExtractJob,
73
76
  countSuccessfulPages,
@@ -78,7 +81,7 @@ import {
78
81
  saveExtractPages,
79
82
  setExtractJobTotal,
80
83
  settleExtractJob
81
- } from "./chunk-BL7BBSYF.js";
84
+ } from "./chunk-RUGJE5EB.js";
82
85
  import {
83
86
  BROWSER_OPEN_MIN_BALANCE_MC,
84
87
  CONCURRENCY_PRICE_ID,
@@ -101,7 +104,7 @@ import {
101
104
  browserActiveCostMc,
102
105
  concurrencySlotBillingInfo,
103
106
  insufficientBalanceResponse
104
- } from "./chunk-APJO2XV5.js";
107
+ } from "./chunk-HL33CGJF.js";
105
108
  import {
106
109
  CaptchaError,
107
110
  RECAPTCHA_INSTRUCTIONS,
@@ -4432,8 +4435,8 @@ async function downloadAsset(url, destDir, filename) {
4432
4435
  }
4433
4436
  const writer = createWriteStream(dest);
4434
4437
  await pipeline(Readable.fromWeb(res.body), writer);
4435
- const { statSync: statSync2 } = await import("fs");
4436
- const sizeBytes = statSync2(dest).size;
4438
+ const { statSync } = await import("fs");
4439
+ const sizeBytes = statSync(dest).size;
4437
4440
  return { savedPath: dest, sizeBytes, mimeType };
4438
4441
  }
4439
4442
  async function harvestPageMedia(html, pageUrl, options = {}) {
@@ -4780,7 +4783,8 @@ async function spiderSite(opts) {
4780
4783
  totalFound: results.length,
4781
4784
  durationMs: Date.now() - startMs,
4782
4785
  truncated: queue.length > 0 || results.length >= maxUrls,
4783
- browserRetries
4786
+ browserRetries,
4787
+ sitemapUrls: sitemapUrls.slice(0, maxUrls)
4784
4788
  };
4785
4789
  }
4786
4790
 
@@ -4904,6 +4908,23 @@ async function crawlWithRotation(urls, opts) {
4904
4908
  return urls.map((u) => byUrl.get(u) ?? { url: u, html: null, status: 0, via: "browser" });
4905
4909
  }
4906
4910
 
4911
+ // src/api/seo-audit.ts
4912
+ function buildSeoAudit(pages, startUrl) {
4913
+ const { edges, metrics } = buildLinkGraph(pages, startUrl);
4914
+ const issues = computeIssues(pages, metrics);
4915
+ const { summary } = buildLinkReport(edges, [...metrics.values()], startUrl);
4916
+ return {
4917
+ issues,
4918
+ linkSummary: summary,
4919
+ pageMetrics: [...metrics.values()].map((m) => ({
4920
+ url: m.url,
4921
+ inboundInternal: m.inlinks,
4922
+ outboundInternal: m.outlinksInternal,
4923
+ orphan: m.orphan
4924
+ }))
4925
+ };
4926
+ }
4927
+
4907
4928
  // src/api/site-extractor.ts
4908
4929
  var 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 };
4909
4930
  function estimatePixels(s) {
@@ -4947,6 +4968,7 @@ function emptyPageData(url, status, via) {
4947
4968
  imageLinks: [],
4948
4969
  indexable: status === 200,
4949
4970
  indexabilityReason: status === 200 ? null : "non-200",
4971
+ inSitemap: null,
4950
4972
  schemaTypes: [],
4951
4973
  canonicalUrl: null,
4952
4974
  internalLinks: 0,
@@ -4963,7 +4985,8 @@ function makeSeedSpider(startUrl, urls) {
4963
4985
  totalFound: urls.length,
4964
4986
  durationMs: 0,
4965
4987
  truncated: false,
4966
- browserRetries: 0
4988
+ browserRetries: 0,
4989
+ sitemapUrls: []
4967
4990
  };
4968
4991
  }
4969
4992
  var UA2 = "Mozilla/5.0 (compatible; ThorbitBot/1.0; +https://thorbit.ai)";
@@ -5134,6 +5157,7 @@ function parsePageData(url, html, status, via, resp) {
5134
5157
  imageLinks,
5135
5158
  indexable,
5136
5159
  indexabilityReason,
5160
+ inSitemap: null,
5137
5161
  schemaTypes,
5138
5162
  canonicalUrl,
5139
5163
  internalLinks,
@@ -5147,11 +5171,11 @@ async function extractPagesRotating(urls, opts) {
5147
5171
  const fetched = await crawlWithRotation(urls, { apiKey: opts.kernelApiKey, concurrency: opts.concurrency, urlsPerBrowser: opts.urlsPerBrowser });
5148
5172
  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"));
5149
5173
  }
5150
- async function discoverUrls(startUrl, maxPages, kernelApiKey) {
5174
+ async function discoverUrlsWithSitemap(startUrl, maxPages, kernelApiKey) {
5151
5175
  const sitemapUrls = await discoverSitemapUrls2(startUrl, maxPages);
5152
- if (sitemapUrls.length > 0) return sitemapUrls;
5176
+ if (sitemapUrls.length > 0) return { urls: sitemapUrls, sitemapUrls };
5153
5177
  const spider = await spiderSite({ startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey });
5154
- return spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
5178
+ return { urls: spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url), sitemapUrls: spider.sitemapUrls };
5155
5179
  }
5156
5180
  async function fetchAndParse(url, kernelApiKey) {
5157
5181
  const startedAt = Date.now();
@@ -5210,6 +5234,7 @@ async function extractSite(opts) {
5210
5234
  let browserRetries = 0;
5211
5235
  let truncated = false;
5212
5236
  let spider;
5237
+ let sitemapUrls = [];
5213
5238
  if (rotating) {
5214
5239
  const browserConcurrency = Math.max(1, Math.min(opts.parallelism ?? 1, 8));
5215
5240
  const urlsPerBrowser = opts.rotateProxyEvery ?? 10;
@@ -5227,7 +5252,8 @@ async function extractSite(opts) {
5227
5252
  for (const u of opts.seedUrls) enqueue(u);
5228
5253
  } else {
5229
5254
  enqueue(opts.startUrl);
5230
- for (const u of await discoverSitemapUrls2(opts.startUrl, maxPages)) enqueue(u);
5255
+ sitemapUrls = await discoverSitemapUrls2(opts.startUrl, maxPages);
5256
+ for (const u of sitemapUrls) enqueue(u);
5231
5257
  }
5232
5258
  const waveCap = Math.max(browserConcurrency * urlsPerBrowser, 30);
5233
5259
  let done = 0;
@@ -5256,6 +5282,7 @@ async function extractSite(opts) {
5256
5282
  spider = makeSeedSpider(opts.startUrl, [...seen]);
5257
5283
  } else {
5258
5284
  spider = seeded ? makeSeedSpider(opts.startUrl, opts.seedUrls) : await spiderSite({ startUrl: opts.startUrl, maxUrls: maxPages, concurrency: 12, kernelApiKey: opts.kernelApiKey });
5285
+ sitemapUrls = spider.sitemapUrls;
5259
5286
  const urlsToExtract = spider.urls.filter((u) => u.status === 200 || u.status === null).slice(0, maxPages).map((u) => u.url);
5260
5287
  browserRetries = spider.browserRetries;
5261
5288
  await runWithConcurrency(urlsToExtract, concurrency, async (url) => {
@@ -5265,10 +5292,17 @@ async function extractSite(opts) {
5265
5292
  });
5266
5293
  truncated = spider.truncated || pages.length >= maxPages;
5267
5294
  }
5295
+ const sitemapSet = sitemapUrls.length > 0 ? new Set(sitemapUrls.map((u) => normalizeUrl(u, opts.startUrl) ?? u)) : null;
5296
+ for (const p of pages) p.inSitemap = sitemapSet ? sitemapSet.has(normalizeUrl(p.url, opts.startUrl) ?? p.url) : null;
5268
5297
  let branding = null;
5269
5298
  if (opts.formats?.includes("branding") && opts.kernelApiKey) {
5270
5299
  branding = await extractBranding(opts.startUrl, opts.kernelApiKey).catch(() => null);
5271
5300
  }
5301
+ const seoAudit = opts.formats?.includes("issues") ? buildSeoAudit(pages, opts.startUrl) : null;
5302
+ let imageAudit = null;
5303
+ if (opts.formats?.includes("images")) {
5304
+ imageAudit = await auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
5305
+ }
5272
5306
  return {
5273
5307
  startUrl: opts.startUrl,
5274
5308
  pages,
@@ -5277,7 +5311,9 @@ async function extractSite(opts) {
5277
5311
  durationMs: Date.now() - startMs,
5278
5312
  truncated,
5279
5313
  browserRetries,
5280
- branding
5314
+ branding,
5315
+ seoAudit,
5316
+ imageAudit
5281
5317
  };
5282
5318
  }
5283
5319
 
@@ -7519,12 +7555,13 @@ var siteExtractFn = inngest.createFunction(
7519
7555
  const maxPages = Number(job.options.maxPages ?? 1e4);
7520
7556
  const concurrency = Number(job.options.concurrency ?? 1);
7521
7557
  const urlsPerBrowser = Number(job.options.urlsPerBrowser ?? job.options.rotateProxyEvery ?? 10);
7522
- const seed = await step.run("discover", async () => {
7523
- const discovered = await discoverUrls(job.startUrl, maxPages, key);
7524
- const out = discovered.length ? discovered : [job.startUrl];
7525
- await setExtractJobTotal(jobId, out.length);
7526
- return out;
7558
+ const seedRaw = await step.run("discover", async () => {
7559
+ const discovered = await discoverUrlsWithSitemap(job.startUrl, maxPages, key);
7560
+ const urls = discovered.urls.length ? discovered.urls : [job.startUrl];
7561
+ await setExtractJobTotal(jobId, urls.length);
7562
+ return { urls, sitemapUrls: discovered.sitemapUrls };
7527
7563
  });
7564
+ const seed = Array.isArray(seedRaw) ? { urls: seedRaw, sitemapUrls: [] } : seedRaw;
7528
7565
  const siteHost = (() => {
7529
7566
  try {
7530
7567
  return new URL(job.startUrl).hostname.replace(/^www\./, "");
@@ -7548,8 +7585,9 @@ var siteExtractFn = inngest.createFunction(
7548
7585
  return false;
7549
7586
  }
7550
7587
  };
7551
- const queue = [...seed];
7552
- const seen = new Set(seed.map(norm));
7588
+ const sitemapSet = seed.sitemapUrls.length > 0 ? new Set(seed.sitemapUrls.map(norm)) : null;
7589
+ const queue = [...seed.urls];
7590
+ const seen = new Set(seed.urls.map(norm));
7553
7591
  let crawled = 0;
7554
7592
  const MAX_BATCHES = Math.ceil(maxPages / BATCH) + 30;
7555
7593
  for (let i = 0; i < MAX_BATCHES && crawled < maxPages && crawled < queue.length; i++) {
@@ -7557,7 +7595,7 @@ var siteExtractFn = inngest.createFunction(
7557
7595
  if (!batch.length) break;
7558
7596
  const newLinks = await step.run(`crawl-batch-${i}`, async () => {
7559
7597
  const pages = await extractPagesRotating(batch, { kernelApiKey: key, concurrency, urlsPerBrowser });
7560
- await saveExtractPages(jobId, pages);
7598
+ await saveExtractPages(jobId, pages.map((p) => ({ ...p, inSitemap: sitemapSet ? sitemapSet.has(norm(p.url)) : null })));
7561
7599
  const found = [];
7562
7600
  for (const p of pages) for (const l of p.outlinks ?? []) {
7563
7601
  if (l.internal && l.href && sameSite(l.href)) found.push(l.href);
@@ -7581,9 +7619,13 @@ var siteExtractFn = inngest.createFunction(
7581
7619
  const pages = await getExtractedPages(jobId);
7582
7620
  return auditImages(pages, { concurrency: 12, timeoutMs: 12e3 });
7583
7621
  }) : null;
7622
+ const seoAudit = Array.isArray(job.options.formats) && job.options.formats.includes("issues") ? await step.run("seo-audit", async () => {
7623
+ const pages = await getExtractedPages(jobId);
7624
+ return buildSeoAudit(pages, job.startUrl);
7625
+ }) : null;
7584
7626
  const artifacts = await step.run("finalize", async () => {
7585
- const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
7586
- const stored = await assembleExtractArtifacts(job, { branding, imageAudit });
7627
+ const { assembleExtractArtifacts } = await import("./extract-bundle-U4D5LW5W.js");
7628
+ const stored = await assembleExtractArtifacts(job, { branding, imageAudit, seoAudit });
7587
7629
  await completeExtractJob(jobId, stored);
7588
7630
  return stored;
7589
7631
  });
@@ -9089,7 +9131,7 @@ var ExtractSiteBodySchema = z11.object({
9089
9131
  rotateProxies: z11.boolean().optional(),
9090
9132
  rotateProxyEvery: z11.number().int().min(1).max(100).optional(),
9091
9133
  background: z11.boolean().optional(),
9092
- formats: z11.array(z11.enum(["markdown", "links", "json", "images", "branding"])).optional()
9134
+ formats: z11.array(z11.enum(["markdown", "links", "json", "images", "branding", "issues"])).optional()
9093
9135
  });
9094
9136
  var YoutubeHarvestBodySchema = z11.object({
9095
9137
  mode: z11.enum(["search", "channel"]),
@@ -10734,6 +10776,12 @@ facebookAdApp.post("/search", createApiKeyAuth(), async (c) => {
10734
10776
  }
10735
10777
  }
10736
10778
  const results = [...advertiserMap.values()].sort((a, b) => b.adCount - a.adCount).slice(0, maxResults).map((a) => ({ name: a.pageName, pageName: a.pageName, libraryId: a.sampleLibraryId, sampleLibraryId: a.sampleLibraryId, adCount: a.adCount }));
10779
+ if (results.length === 0) {
10780
+ await creditMc(fbUser.id, MC_COSTS.fb_search, LedgerOperation.FB_SEARCH_REFUND, "empty result");
10781
+ searchRefunded = true;
10782
+ await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "failed", query: body.query.trim(), error: "empty result refunded" });
10783
+ return c.json({ error: "no advertisers found (refunded)" }, 503);
10784
+ }
10737
10785
  const searchResult = { query: body.query.trim(), searchUrl, results };
10738
10786
  await logRequestEvent({ userId: fbUser.id, source: "facebook_search", status: "done", query: body.query.trim(), resultCount: results.length, result: searchResult });
10739
10787
  return c.json(searchResult);
@@ -11124,11 +11172,9 @@ googleAdsApp.post("/transcribe", createApiKeyAuth(), async (c) => {
11124
11172
  // src/api/instagram-routes.ts
11125
11173
  import { Hono as Hono7 } from "hono";
11126
11174
  import { z as z15 } from "zod";
11127
- import { createWriteStream as createWriteStream2, mkdirSync as mkdirSync2, statSync, writeFileSync } from "fs";
11128
- import { homedir as homedir2 } from "os";
11175
+ import { mkdtempSync, readFileSync as readFileSync2, rmSync, writeFileSync } from "fs";
11176
+ import { tmpdir } from "os";
11129
11177
  import { join as join4 } from "path";
11130
- import { Readable as Readable2 } from "stream";
11131
- import { pipeline as pipeline2 } from "stream/promises";
11132
11178
  import { spawn } from "child_process";
11133
11179
 
11134
11180
  // src/extractor/InstagramContentExtractor.ts
@@ -11552,20 +11598,15 @@ async function resolveInstagramLaunch(body) {
11552
11598
  }
11553
11599
  };
11554
11600
  }
11555
- function outputBaseDir2() {
11556
- return process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join4(homedir2(), "Downloads", "mcp-scraper");
11557
- }
11558
11601
  function safeFilePart(input) {
11559
11602
  return input.replace(/^https?:\/\//, "").replace(/[^a-z0-9._-]+/gi, "-").replace(/^-+|-+$/g, "").slice(0, 80) || "instagram";
11560
11603
  }
11561
- function mediaOutputDir(shortcode, sourceUrl) {
11604
+ function blobKeyPrefix(shortcode, sourceUrl) {
11562
11605
  const stamp = (/* @__PURE__ */ new Date()).toISOString().replace(/[:.]/g, "-").slice(0, 19);
11563
11606
  const slug = shortcode ? `instagram-${shortcode}` : safeFilePart(sourceUrl);
11564
- const dir = join4(outputBaseDir2(), "instagram", `${stamp}-${slug}`);
11565
- mkdirSync2(dir, { recursive: true });
11566
- return dir;
11607
+ return `instagram/${stamp}-${slug}`;
11567
11608
  }
11568
- async function downloadToFile(url, destPath, referer) {
11609
+ async function fetchMediaBytes(url, referer) {
11569
11610
  const res = await fetch(url, {
11570
11611
  headers: {
11571
11612
  "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/124.0.0.0 Safari/537.36",
@@ -11575,12 +11616,8 @@ async function downloadToFile(url, destPath, referer) {
11575
11616
  });
11576
11617
  if (!res.ok) throw new Error(`HTTP ${res.status}`);
11577
11618
  if (!res.body) throw new Error("Empty response body");
11578
- await pipeline2(Readable2.fromWeb(res.body), createWriteStream2(destPath));
11579
- return {
11580
- savedPath: destPath,
11581
- sizeBytes: statSync(destPath).size,
11582
- mimeType: res.headers.get("content-type")?.split(";")[0]?.trim() ?? null
11583
- };
11619
+ const bytes = Buffer.from(await res.arrayBuffer());
11620
+ return { bytes, mimeType: res.headers.get("content-type")?.split(";")[0]?.trim() ?? null };
11584
11621
  }
11585
11622
  function extFromMime(mimeType, fallback) {
11586
11623
  const map = {
@@ -11699,55 +11736,65 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
11699
11736
  const mediaTypes = new Set(body.mediaTypes);
11700
11737
  const downloads = [];
11701
11738
  const warnings = [];
11702
- let outputDir = null;
11703
11739
  if (body.downloadMedia) {
11704
- outputDir = mediaOutputDir(extraction.shortcode, sourceUrl.href);
11705
- const textPath = join4(outputDir, `${extraction.shortcode ?? "instagram"}-text.txt`);
11706
- writeFileSync(textPath, [
11707
- `URL: ${extraction.pageUrl}`,
11708
- extraction.caption ? `Caption: ${extraction.caption}` : "",
11709
- "",
11710
- extraction.bodyText
11711
- ].filter(Boolean).join("\n"), "utf8");
11712
- downloads.push({ kind: "text", url: null, savedPath: textPath, sizeBytes: statSync(textPath).size, mimeType: "text/plain", error: null });
11713
- if (mediaTypes.has("image") && extraction.imageUrl) {
11714
- const imagePathBase = join4(outputDir, `${extraction.shortcode ?? "instagram"}-image`);
11715
- try {
11716
- const downloaded = await downloadToFile(extraction.imageUrl, imagePathBase, extraction.pageUrl);
11717
- const ext = extFromMime(downloaded.mimeType, "jpg");
11718
- const finalPath = downloaded.savedPath.endsWith(`.${ext}`) ? downloaded.savedPath : `${downloaded.savedPath}.${ext}`;
11719
- if (finalPath !== downloaded.savedPath) {
11720
- const { renameSync } = await import("fs");
11721
- renameSync(downloaded.savedPath, finalPath);
11740
+ let muxWorkDir = null;
11741
+ try {
11742
+ const store = getBlobStore();
11743
+ const prefix = blobKeyPrefix(extraction.shortcode, sourceUrl.href);
11744
+ const textContent = [
11745
+ `URL: ${extraction.pageUrl}`,
11746
+ extraction.caption ? `Caption: ${extraction.caption}` : "",
11747
+ "",
11748
+ extraction.bodyText
11749
+ ].filter(Boolean).join("\n");
11750
+ const storedText = await store.put(`${prefix}/text.txt`, textContent, "text/plain");
11751
+ downloads.push({ kind: "text", url: null, savedPath: storedText.url, sizeBytes: storedText.bytes, mimeType: "text/plain", error: null });
11752
+ if (mediaTypes.has("image") && extraction.imageUrl) {
11753
+ try {
11754
+ const { bytes, mimeType } = await fetchMediaBytes(extraction.imageUrl, extraction.pageUrl);
11755
+ const ext = extFromMime(mimeType, "jpg");
11756
+ const stored = await store.put(`${prefix}/image.${ext}`, bytes, mimeType ?? "application/octet-stream");
11757
+ downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: stored.url, sizeBytes: stored.bytes, mimeType, error: null });
11758
+ } catch (err) {
11759
+ downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
11722
11760
  }
11723
- downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: finalPath, sizeBytes: statSync(finalPath).size, mimeType: downloaded.mimeType, error: null });
11724
- } catch (err) {
11725
- downloads.push({ kind: "image", url: extraction.imageUrl, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
11726
11761
  }
11727
- }
11728
- const tracksToDownload = body.downloadAllTracks ? extraction.tracks : [extraction.selectedVideoTrack, extraction.selectedAudioTrack].filter((track) => Boolean(track));
11729
- const savedByUrl = /* @__PURE__ */ new Map();
11730
- for (const [index, track] of tracksToDownload.entries()) {
11731
- if (track.streamType === "video" && !mediaTypes.has("video")) continue;
11732
- if (track.streamType === "audio" && !mediaTypes.has("audio")) continue;
11733
- const kind = track.streamType === "audio" ? "audio" : "video";
11734
- const target = join4(outputDir, trackFilename(track, index, extraction.shortcode));
11735
- try {
11736
- const downloaded = await downloadToFile(track.url, target, extraction.pageUrl);
11737
- downloads.push({ kind, url: track.url, savedPath: downloaded.savedPath, sizeBytes: downloaded.sizeBytes, mimeType: downloaded.mimeType, error: null });
11738
- savedByUrl.set(track.url, downloaded.savedPath);
11739
- } catch (err) {
11740
- downloads.push({ kind, url: track.url, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
11762
+ const tracksToDownload = body.downloadAllTracks ? extraction.tracks : [extraction.selectedVideoTrack, extraction.selectedAudioTrack].filter((track) => Boolean(track));
11763
+ const muxEligible = Boolean(body.mux && mediaTypes.has("video") && mediaTypes.has("audio") && extraction.selectedVideoTrack && extraction.selectedAudioTrack);
11764
+ const muxInputs = /* @__PURE__ */ new Map();
11765
+ for (const [index, track] of tracksToDownload.entries()) {
11766
+ if (track.streamType === "video" && !mediaTypes.has("video")) continue;
11767
+ if (track.streamType === "audio" && !mediaTypes.has("audio")) continue;
11768
+ const kind = track.streamType === "audio" ? "audio" : "video";
11769
+ try {
11770
+ const { bytes, mimeType } = await fetchMediaBytes(track.url, extraction.pageUrl);
11771
+ const stored = await store.put(`${prefix}/${trackFilename(track, index, extraction.shortcode)}`, bytes, mimeType ?? "video/mp4");
11772
+ downloads.push({ kind, url: track.url, savedPath: stored.url, sizeBytes: stored.bytes, mimeType, error: null });
11773
+ const isMuxInput = muxEligible && (track.url === extraction.selectedVideoTrack.url || track.url === extraction.selectedAudioTrack.url);
11774
+ if (isMuxInput) {
11775
+ if (!muxWorkDir) muxWorkDir = mkdtempSync(join4(tmpdir(), "ig-mux-"));
11776
+ const localPath = join4(muxWorkDir, trackFilename(track, index, extraction.shortcode));
11777
+ writeFileSync(localPath, bytes);
11778
+ muxInputs.set(track.url, localPath);
11779
+ }
11780
+ } catch (err) {
11781
+ downloads.push({ kind, url: track.url, savedPath: null, sizeBytes: null, mimeType: null, error: err instanceof Error ? err.message : String(err) });
11782
+ }
11741
11783
  }
11742
- }
11743
- if (body.mux && mediaTypes.has("video") && mediaTypes.has("audio") && extraction.selectedVideoTrack && extraction.selectedAudioTrack && savedByUrl.has(extraction.selectedVideoTrack.url) && savedByUrl.has(extraction.selectedAudioTrack.url)) {
11744
- const outPath = join4(outputDir, `${extraction.shortcode ?? "instagram"}-muxed.mp4`);
11745
- const muxed = await runFfmpegMux(savedByUrl.get(extraction.selectedVideoTrack.url), savedByUrl.get(extraction.selectedAudioTrack.url), outPath);
11746
- if (muxed.ok) {
11747
- downloads.push({ kind: "muxed_video", url: null, savedPath: outPath, sizeBytes: statSync(outPath).size, mimeType: "video/mp4", error: null });
11748
- } else {
11749
- warnings.push(`Mux skipped/failed: ${muxed.error}`);
11784
+ if (muxEligible && muxWorkDir && muxInputs.has(extraction.selectedVideoTrack.url) && muxInputs.has(extraction.selectedAudioTrack.url)) {
11785
+ const outPath = join4(muxWorkDir, "muxed.mp4");
11786
+ const muxed = await runFfmpegMux(muxInputs.get(extraction.selectedVideoTrack.url), muxInputs.get(extraction.selectedAudioTrack.url), outPath);
11787
+ if (muxed.ok) {
11788
+ const stored = await store.put(`${prefix}/muxed.mp4`, readFileSync2(outPath), "video/mp4");
11789
+ downloads.push({ kind: "muxed_video", url: null, savedPath: stored.url, sizeBytes: stored.bytes, mimeType: "video/mp4", error: null });
11790
+ } else {
11791
+ warnings.push(`Mux skipped/failed: ${muxed.error}`);
11792
+ }
11750
11793
  }
11794
+ } catch (err) {
11795
+ warnings.push(`Media download unavailable: ${err instanceof Error ? err.message : String(err)}`);
11796
+ } finally {
11797
+ if (muxWorkDir) rmSync(muxWorkDir, { recursive: true, force: true });
11751
11798
  }
11752
11799
  }
11753
11800
  let transcript = null;
@@ -11770,7 +11817,7 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
11770
11817
  const result = {
11771
11818
  ...extraction,
11772
11819
  browser: launch.browser,
11773
- outputDir,
11820
+ outputDir: null,
11774
11821
  downloads,
11775
11822
  warnings,
11776
11823
  transcript
@@ -20079,7 +20126,7 @@ async function persistScrapeBody(user, opts) {
20079
20126
 
20080
20127
  // src/api/scrape-blob-cleanup.ts
20081
20128
  import { readdir, stat, unlink } from "fs/promises";
20082
- import { homedir as homedir3 } from "os";
20129
+ import { homedir as homedir2 } from "os";
20083
20130
  import { join as join6 } from "path";
20084
20131
  async function cleanupVercel(token, cutoff) {
20085
20132
  const { list, del } = await import("@vercel/blob");
@@ -20097,7 +20144,7 @@ async function cleanupVercel(token, cutoff) {
20097
20144
  return { deleted, store: "vercel-blob" };
20098
20145
  }
20099
20146
  async function cleanupLocal(cutoff) {
20100
- const baseDir = process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join6(homedir3(), "Downloads", "mcp-scraper");
20147
+ const baseDir = process.env.MCP_SCRAPER_OUTPUT_DIR?.trim() || join6(homedir2(), "Downloads", "mcp-scraper");
20101
20148
  const dir = join6(baseDir, "blobs", SCRAPE_FALLBACK_PREFIX.replace(/\/$/, ""));
20102
20149
  let deleted = 0;
20103
20150
  let entries;
@@ -20453,6 +20500,7 @@ app.use("*", async (c, next) => {
20453
20500
  });
20454
20501
  function opForCostPath(p) {
20455
20502
  if (p === "/harvest" || p === "/harvest/sync") return "harvest";
20503
+ if (p === "/diff-page") return "diff_page";
20456
20504
  if (p === "/extract-url") return "page_scrape";
20457
20505
  if (p === "/extract-site") return "extract_site";
20458
20506
  if (p === "/map-urls") return "url_map";
@@ -20625,6 +20673,13 @@ app.get("/catalog", (c) => {
20625
20673
  c.header("Cache-Control", "public, max-age=60");
20626
20674
  return c.json(CATALOG);
20627
20675
  });
20676
+ app.get("/rates", (c) => {
20677
+ const costs = CREDIT_COST_CATALOG.map(({ aliases, ...cost }) => ({
20678
+ ...cost,
20679
+ ...cost.notes ? { notes: cost.notes.replace(/ via fal\.ai/g, "") } : {}
20680
+ }));
20681
+ return c.json({ creditsPerDollarStarter: Math.round(1 / 15e-5), costs });
20682
+ });
20628
20683
  app.get("/me", async (c) => {
20629
20684
  const token = getCookie2(c, "session");
20630
20685
  if (!token) return c.json({ authenticated: false });
@@ -21881,7 +21936,7 @@ app.get("/cron/tick", async (c) => {
21881
21936
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
21882
21937
  return c.json({ error: "Unauthorized" }, 401);
21883
21938
  }
21884
- const { drainQueue } = await import("./worker-KJZ3ZN2N.js");
21939
+ const { drainQueue } = await import("./worker-JQTS437L.js");
21885
21940
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
21886
21941
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
21887
21942
  const [results, sweepResult, reapResult, expiredResult, blobCleanup] = await Promise.all([
@@ -21899,15 +21954,15 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
21899
21954
  return c.json({ error: "Unauthorized" }, 401);
21900
21955
  }
21901
21956
  const jobId = c.req.param("id");
21902
- const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-U476J44K.js");
21903
- const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
21957
+ const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-NVSZH35Y.js");
21958
+ const { assembleExtractArtifacts } = await import("./extract-bundle-U4D5LW5W.js");
21904
21959
  const job = await getExtractJob2(jobId);
21905
21960
  if (!job) return c.json({ error: "job not found" }, 404);
21906
21961
  const stored = await assembleExtractArtifacts(job, {});
21907
21962
  await completeExtractJob2(jobId, stored);
21908
21963
  let settlement = "already_settled_or_refunded";
21909
21964
  if (job.billedMc == null && job.userId != null) {
21910
- const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-U476J44K.js");
21965
+ const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-NVSZH35Y.js");
21911
21966
  const heldMc = Number(job.options.heldMc ?? 0);
21912
21967
  const successful = await countSuccessfulPages2(jobId);
21913
21968
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -22071,4 +22126,4 @@ app.get("/blog/:slug/", (c) => {
22071
22126
  export {
22072
22127
  app
22073
22128
  };
22074
- //# sourceMappingURL=server-WHM446IZ.js.map
22129
+ //# sourceMappingURL=server-WBDVVHR6.js.map