mcp-scraper 0.34.0 → 0.35.1

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 (53) hide show
  1. package/README.md +6 -5
  2. package/dist/bin/api-server.cjs +3788 -2742
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +2 -2
  5. package/dist/bin/mcp-scraper-cli.cjs +1 -1
  6. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  7. package/dist/bin/mcp-scraper-cli.js +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs +2 -2
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +2 -2
  11. package/dist/bin/mcp-stdio-server.cjs +2129 -1932
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +5 -5
  14. package/dist/bin/paa-harvest.js +2 -2
  15. package/dist/{chunk-V36LS5YV.js → chunk-4ZB3X6BQ.js} +4 -3
  16. package/dist/chunk-4ZB3X6BQ.js.map +1 -0
  17. package/dist/{chunk-JBSGBSGT.js → chunk-BWXLTWF7.js} +21 -16
  18. package/dist/chunk-BWXLTWF7.js.map +1 -0
  19. package/dist/{chunk-J32XSMJI.js → chunk-NPMW5HUS.js} +2350 -2151
  20. package/dist/chunk-NPMW5HUS.js.map +1 -0
  21. package/dist/{chunk-SUOHUXQS.js → chunk-U44TPRST.js} +3 -3
  22. package/dist/chunk-U44TPRST.js.map +1 -0
  23. package/dist/{chunk-4KQVIYIH.js → chunk-XVVNKASZ.js} +2 -2
  24. package/dist/chunk-YR4LJ6AQ.js +7 -0
  25. package/dist/chunk-YR4LJ6AQ.js.map +1 -0
  26. package/dist/{chunk-2EDFOQD7.js → chunk-YRGSEY5L.js} +2 -2
  27. package/dist/{chunk-2EDFOQD7.js.map → chunk-YRGSEY5L.js.map} +1 -1
  28. package/dist/chunk-YV2FUEBX.js +851 -0
  29. package/dist/chunk-YV2FUEBX.js.map +1 -0
  30. package/dist/{extract-bundle-GPZRLBVY.js → extract-bundle-ONWZVV55.js} +59 -8
  31. package/dist/extract-bundle-ONWZVV55.js.map +1 -0
  32. package/dist/index.js +2 -2
  33. package/dist/{server-LKVPEQFE.js → server-GKUTC73B.js} +278 -52
  34. package/dist/server-GKUTC73B.js.map +1 -0
  35. package/dist/{site-extract-repository-JHMVHENZ.js → site-extract-repository-L6BHWVDU.js} +3 -3
  36. package/dist/{worker-ZZHSYI3Y.js → worker-645BZPEK.js} +4 -4
  37. package/docs/mcp-tool-craft-lint.generated.md +171 -49
  38. package/docs/mcp-tool-manifest.generated.json +508 -10
  39. package/docs/specs/query-fanout-transport-contract-fix.md +45 -0
  40. package/package.json +1 -1
  41. package/dist/chunk-J32XSMJI.js.map +0 -1
  42. package/dist/chunk-JBSGBSGT.js.map +0 -1
  43. package/dist/chunk-JWIE5NCR.js +0 -284
  44. package/dist/chunk-JWIE5NCR.js.map +0 -1
  45. package/dist/chunk-NFMIHEYE.js +0 -7
  46. package/dist/chunk-NFMIHEYE.js.map +0 -1
  47. package/dist/chunk-SUOHUXQS.js.map +0 -1
  48. package/dist/chunk-V36LS5YV.js.map +0 -1
  49. package/dist/extract-bundle-GPZRLBVY.js.map +0 -1
  50. package/dist/server-LKVPEQFE.js.map +0 -1
  51. /package/dist/{chunk-4KQVIYIH.js.map → chunk-XVVNKASZ.js.map} +0 -0
  52. /package/dist/{site-extract-repository-JHMVHENZ.js.map → site-extract-repository-L6BHWVDU.js.map} +0 -0
  53. /package/dist/{worker-ZZHSYI3Y.js.map → worker-645BZPEK.js.map} +0 -0
@@ -1,6 +1,12 @@
1
1
  import {
2
- harvestPageMedia
3
- } from "./chunk-JWIE5NCR.js";
2
+ expandWaybackMonths,
3
+ harvestPageMedia,
4
+ inventoryWaybackSnapshots,
5
+ parseWaybackReplayUrl,
6
+ resolveFeaturedImage,
7
+ resolveWaybackSiteCaptures,
8
+ resolveWaybackTimelineCaptures
9
+ } from "./chunk-YV2FUEBX.js";
4
10
  import {
5
11
  classifyHarvestProblem,
6
12
  createHarvestAttemptRecorder,
@@ -20,9 +26,11 @@ import {
20
26
  resolveBackupKernelProxyId,
21
27
  resolveKernelProxyId,
22
28
  serpArrivalUrl
23
- } from "./chunk-4KQVIYIH.js";
29
+ } from "./chunk-XVVNKASZ.js";
24
30
  import {
25
31
  HttpMcpToolExecutor,
32
+ WaybackInventoryOptionsSchema,
33
+ WaybackTimelineSchema,
26
34
  assessTranscriptSignal,
27
35
  buildLinkGraph,
28
36
  buildPaaExtractorMcpServer,
@@ -40,7 +48,7 @@ import {
40
48
  sanitizeAttempts,
41
49
  sanitizeHarvestResult,
42
50
  transcribeMediaUrl
43
- } from "./chunk-J32XSMJI.js";
51
+ } from "./chunk-NPMW5HUS.js";
44
52
  import {
45
53
  auditImages,
46
54
  buildLinkReport,
@@ -53,11 +61,10 @@ import {
53
61
  browserServiceProfileSaveChanges,
54
62
  browserServiceProxyId,
55
63
  currentCostContext,
56
- kernelCostUsd,
57
64
  recordKernelSession,
58
65
  recordVendorUsage,
59
66
  runWithCostContext
60
- } from "./chunk-V36LS5YV.js";
67
+ } from "./chunk-4ZB3X6BQ.js";
61
68
  import {
62
69
  csvRecords,
63
70
  listWorkflowDefinitions,
@@ -81,7 +88,7 @@ import {
81
88
  RawMapsOverviewSchema,
82
89
  RawMapsReviewStatsSchema
83
90
  } from "./chunk-CB5C3BPB.js";
84
- import "./chunk-NFMIHEYE.js";
91
+ import "./chunk-YR4LJ6AQ.js";
85
92
  import {
86
93
  completeExtractJob,
87
94
  countSuccessfulPages,
@@ -92,7 +99,7 @@ import {
92
99
  saveExtractPages,
93
100
  setExtractJobTotal,
94
101
  settleExtractJob
95
- } from "./chunk-SUOHUXQS.js";
102
+ } from "./chunk-U44TPRST.js";
96
103
  import {
97
104
  BROWSER_OPEN_MIN_BALANCE_MC,
98
105
  CONCURRENCY_PRICE_ID,
@@ -110,7 +117,6 @@ import {
110
117
  MC_PER_CREDIT,
111
118
  MC_PER_USD,
112
119
  MEDIA_TRANSCRIBE_HOLD_MC,
113
- MEDIA_TRANSCRIBE_MARGIN_MULTIPLE,
114
120
  MEMORY_AI_MARGIN_MULTIPLE,
115
121
  MEMORY_PLANS,
116
122
  MEMORY_PLAN_QUOTA,
@@ -131,13 +137,14 @@ import {
131
137
  getPublicRatesPayload,
132
138
  insufficientBalanceResponse,
133
139
  mcToCredits,
140
+ mediaTranscribeChargeMc,
134
141
  offerClaimPathForTier,
135
142
  serpActualCostMc,
136
143
  tierAmountChargedUsd,
137
144
  tierBillingInterval,
138
145
  tierCreditsPerInterval,
139
146
  vendorCostUsdToMc
140
- } from "./chunk-JBSGBSGT.js";
147
+ } from "./chunk-BWXLTWF7.js";
141
148
  import {
142
149
  CaptchaError,
143
150
  ExtractionError,
@@ -7319,13 +7326,41 @@ var siteExtractFn = inngest.createFunction(
7319
7326
  const maxPages = Number(job.options.maxPages ?? 1e4);
7320
7327
  const concurrency = Number(job.options.concurrency ?? 1);
7321
7328
  const urlsPerBrowser = Number(job.options.urlsPerBrowser ?? job.options.rotateProxyEvery ?? 10);
7329
+ const waybackReplay = parseWaybackReplayUrl(job.startUrl);
7330
+ const waybackTimeline = job.options.waybackTimeline;
7331
+ const disableLinkDiscovery = Boolean(waybackReplay) || Boolean(waybackTimeline);
7322
7332
  const seedRaw = await step.run("discover", async () => {
7333
+ if (waybackTimeline) {
7334
+ const plan = await resolveWaybackTimelineCaptures({
7335
+ rootUrl: waybackTimeline.rootUrl,
7336
+ timeline: waybackTimeline.timeline,
7337
+ maxPagesPerSnapshot: waybackTimeline.maxPagesPerSnapshot,
7338
+ maxCaptures: maxPages
7339
+ });
7340
+ const urls2 = plan.captures.map((capture) => capture.rawReplayUrl);
7341
+ const requestedCells = plan.requestedUrls ? plan.months.length * plan.requestedUrls.length : urls2.length + plan.missing.length;
7342
+ await setExtractJobTotal(jobId, Math.max(urls2.length, requestedCells));
7343
+ return {
7344
+ urls: urls2,
7345
+ sitemapUrls: [],
7346
+ waybackMonthsByUrl: Object.fromEntries(
7347
+ plan.captures.map((capture) => [capture.rawReplayUrl, capture.requestedMonth ?? ""])
7348
+ )
7349
+ };
7350
+ }
7351
+ if (waybackReplay) {
7352
+ const captures = await resolveWaybackSiteCaptures(waybackReplay, maxPages);
7353
+ const urls2 = captures.map((capture) => capture.rawReplayUrl);
7354
+ await setExtractJobTotal(jobId, urls2.length);
7355
+ return { urls: urls2, sitemapUrls: [] };
7356
+ }
7323
7357
  const discovered = await discoverUrlsWithSitemap(job.startUrl, maxPages, key);
7324
7358
  const urls = discovered.urls.length ? discovered.urls : [job.startUrl];
7325
7359
  await setExtractJobTotal(jobId, urls.length);
7326
7360
  return { urls, sitemapUrls: discovered.sitemapUrls };
7327
7361
  });
7328
- const seed = Array.isArray(seedRaw) ? { urls: seedRaw, sitemapUrls: [] } : seedRaw;
7362
+ const seed = Array.isArray(seedRaw) ? { urls: seedRaw, sitemapUrls: [], waybackMonthsByUrl: void 0 } : seedRaw;
7363
+ const waybackMonthsByUrl = seed.waybackMonthsByUrl ?? {};
7329
7364
  const siteHost = (() => {
7330
7365
  try {
7331
7366
  return new URL(job.startUrl).hostname.replace(/^www\./, "");
@@ -7359,7 +7394,20 @@ var siteExtractFn = inngest.createFunction(
7359
7394
  if (!batch.length) break;
7360
7395
  const newLinks = await step.run(`crawl-batch-${i}`, async () => {
7361
7396
  const pages = await extractPagesRotating(batch, { kernelApiKey: key, concurrency, urlsPerBrowser });
7362
- await saveExtractPages(jobId, pages.map((p) => ({ ...p, inSitemap: sitemapSet ? sitemapSet.has(norm(p.url)) : null })));
7397
+ await saveExtractPages(jobId, pages.map((p) => {
7398
+ const replay = parseWaybackReplayUrl(p.url);
7399
+ return {
7400
+ ...p,
7401
+ inSitemap: sitemapSet ? sitemapSet.has(norm(p.url)) : null,
7402
+ ...replay ? {
7403
+ archivedUrl: p.url,
7404
+ archiveTimestamp: replay.timestamp,
7405
+ originalUrl: replay.originalUrl,
7406
+ requestedMonth: waybackMonthsByUrl[p.url] || void 0
7407
+ } : {}
7408
+ };
7409
+ }));
7410
+ if (disableLinkDiscovery) return [];
7363
7411
  const found = [];
7364
7412
  for (const p of pages) for (const l of p.outlinks ?? []) {
7365
7413
  if (l.internal && l.href && sameSite(l.href)) found.push(l.href);
@@ -7388,7 +7436,7 @@ var siteExtractFn = inngest.createFunction(
7388
7436
  return buildSeoAudit(pages, job.startUrl);
7389
7437
  }) : null;
7390
7438
  const artifacts = await step.run("finalize", async () => {
7391
- const { assembleExtractArtifacts } = await import("./extract-bundle-GPZRLBVY.js");
7439
+ const { assembleExtractArtifacts } = await import("./extract-bundle-ONWZVV55.js");
7392
7440
  const stored = await assembleExtractArtifacts(job, { branding, imageAudit, seoAudit });
7393
7441
  await completeExtractJob(jobId, stored);
7394
7442
  return stored;
@@ -10375,6 +10423,7 @@ var ExtractUrlBodySchema = z14.object({
10375
10423
  screenshot: z14.boolean().optional(),
10376
10424
  screenshotDevice: z14.enum(["desktop", "mobile"]).optional(),
10377
10425
  extractBranding: z14.boolean().optional(),
10426
+ includeFeaturedImage: z14.boolean().optional(),
10378
10427
  downloadMedia: z14.boolean().optional(),
10379
10428
  mediaTypes: z14.array(z14.enum(["image", "video", "audio"])).optional(),
10380
10429
  allowLocal: z14.boolean().optional(),
@@ -10393,9 +10442,14 @@ var MapUrlsBodySchema = z14.object({
10393
10442
  browserFallback: z14.boolean().optional(),
10394
10443
  kernelFallback: z14.boolean().optional()
10395
10444
  });
10445
+ var WaybackInventoryBodySchema = z14.object({
10446
+ url: z14.string().min(1, "url is required"),
10447
+ ...WaybackInventoryOptionsSchema
10448
+ });
10396
10449
  var ExtractSiteBodySchema = z14.object({
10397
10450
  url: z14.string().min(1, "url is required"),
10398
10451
  maxPages: z14.number().int().min(1).max(1e4).optional(),
10452
+ wayback: WaybackTimelineSchema.optional(),
10399
10453
  browserFallback: z14.boolean().optional(),
10400
10454
  kernelFallback: z14.boolean().optional(),
10401
10455
  rotateProxies: z14.boolean().optional(),
@@ -10654,8 +10708,7 @@ youtubeApp.post("/transcribe", createApiKeyAuth(), async (c) => {
10654
10708
  const { videoId } = parsed.data;
10655
10709
  const id = videoId.trim();
10656
10710
  const user = c.get("user");
10657
- const holdMins = 5;
10658
- const holdMc = MC_COSTS.yt_transcription * holdMins;
10711
+ const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
10659
10712
  const gate = await acquireConcurrencyGate(user, "youtube_transcribe", {
10660
10713
  reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
10661
10714
  metadata: { videoId: id }
@@ -10671,8 +10724,7 @@ youtubeApp.post("/transcribe", createApiKeyAuth(), async (c) => {
10671
10724
  const chunkSecs = lastChunk && Number.isFinite(lastChunk.timestamp[1]) ? lastChunk.timestamp[1] : 0;
10672
10725
  const durationSecs = Number.isFinite(result.durationMs) ? result.durationMs / 1e3 : 0;
10673
10726
  const videoSecs = Math.max(chunkSecs, durationSecs) || 60;
10674
- const actualMins = Math.max(1, Math.ceil(videoSecs / 60));
10675
- const actualMc = MC_COSTS.yt_transcription * actualMins;
10727
+ const actualMc = mediaTranscribeChargeMc(videoSecs);
10676
10728
  const diff = holdMc - actualMc;
10677
10729
  if (diff > 0) await creditMc(user.id, diff, LedgerOperation.TRANSCRIPTION_REFUND, "overestimate refund");
10678
10730
  else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.TRANSCRIPTION, id);
@@ -11941,7 +11993,7 @@ facebookAdApp.post("/transcribe", createApiKeyAuth(), async (c) => {
11941
11993
  if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
11942
11994
  debited = true;
11943
11995
  const transcript = await transcribeFacebookVideoUrl(videoUrl, "# Facebook Ad Transcript");
11944
- const settleMc = Math.round(transcript.costUsd * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
11996
+ const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
11945
11997
  const diff = holdMc - settleMc;
11946
11998
  if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "real-cost reconcile");
11947
11999
  else if (diff < 0) await debitMc(fbUser.id, -diff, LedgerOperation.FB_TRANSCRIBE, videoUrl);
@@ -11992,7 +12044,7 @@ facebookAdApp.post("/video-transcribe", createApiKeyAuth(), async (c) => {
11992
12044
  const video = await extractFacebookOrganicVideoFromPage(page, sourceUrl.href, body.quality);
11993
12045
  const kernelMs = Date.now() - kernelStartMs;
11994
12046
  const transcript = await transcribeFacebookVideoUrl(video.videoUrl, "# Facebook Organic Video Transcript");
11995
- const settleMc = Math.round((transcript.costUsd + kernelCostUsd(kernelMs, true)) * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
12047
+ const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
11996
12048
  const diff = holdMc - settleMc;
11997
12049
  if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "real-cost reconcile");
11998
12050
  else if (diff < 0) await debitMc(fbUser.id, -diff, LedgerOperation.FB_TRANSCRIBE, sourceUrl.href);
@@ -12475,7 +12527,7 @@ googleAdsApp.post("/transcribe", createApiKeyAuth(), async (c) => {
12475
12527
  if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
12476
12528
  debited = true;
12477
12529
  const transcript = await runWithCostContext({ ...currentCostContext(), op: "google_ads_transcribe", userId: user.id }, () => transcribeMediaUrl(videoUrl, "# Google Ad Transcript"));
12478
- const settleMc = Math.round(transcript.costUsd * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
12530
+ const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
12479
12531
  const diff = holdMc - settleMc;
12480
12532
  if (diff > 0) await creditMc(user.id, diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE_REFUND, "real-cost reconcile");
12481
12533
  else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE, videoUrl);
@@ -13130,7 +13182,7 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
13130
13182
  if (!tx.ok) return c.json(insufficientBalanceResponse(tx.balance_mc, holdMc), 402);
13131
13183
  transcriptDebited = true;
13132
13184
  transcript = await transcribeMediaUrl(audioUrl, "# Instagram Media Transcript");
13133
- const settleMc = Math.round(transcript.costUsd * MEDIA_TRANSCRIBE_MARGIN_MULTIPLE * MC_PER_USD);
13185
+ const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
13134
13186
  const diff = holdMc - settleMc;
13135
13187
  if (diff > 0) await creditMc(user.id, diff, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "real-cost reconcile");
13136
13188
  else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
@@ -20544,6 +20596,19 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
20544
20596
  import Stripe from "stripe";
20545
20597
  import { Hono as Hono20 } from "hono";
20546
20598
  var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
20599
+ async function cancelStandaloneMemorySub(memSubId, baseSubId) {
20600
+ if (!memSubId || memSubId === baseSubId) return;
20601
+ try {
20602
+ const memSub = await stripe.subscriptions.retrieve(memSubId);
20603
+ const priceId = memSub.items.data[0]?.price?.id;
20604
+ const stillOpen = memSub.status !== "canceled" && !memSub.cancel_at_period_end;
20605
+ if (priceId && priceId in MEMORY_PLANS && stillOpen) {
20606
+ await stripe.subscriptions.update(memSubId, { cancel_at_period_end: true });
20607
+ }
20608
+ } catch (err) {
20609
+ console.error("[stripe] could not wind down standalone memory subscription", memSubId, err instanceof Error ? err.message : err);
20610
+ }
20611
+ }
20547
20612
  var stripeApp = new Hono20();
20548
20613
  stripeApp.post("/webhooks", async (c) => {
20549
20614
  const sig = c.req.header("stripe-signature");
@@ -20575,14 +20640,7 @@ stripeApp.post("/webhooks", async (c) => {
20575
20640
  await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id, { neverExpires: tier.credits_never_expire });
20576
20641
  await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
20577
20642
  if (tier.includes_memory) {
20578
- const standaloneMemorySub = user.memory_subscription_id;
20579
- if (standaloneMemorySub) {
20580
- try {
20581
- await stripe.subscriptions.update(standaloneMemorySub, { cancel_at_period_end: true });
20582
- } catch (err) {
20583
- console.error("[stripe] could not wind down standalone memory subscription", standaloneMemorySub, err instanceof Error ? err.message : err);
20584
- }
20585
- }
20643
+ await cancelStandaloneMemorySub(user.memory_subscription_id, subId ?? user.subscription_id);
20586
20644
  await setMemoryPlan(user.id, "pro", null);
20587
20645
  await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "pro", subscription_tier: tier.tier }));
20588
20646
  }
@@ -20634,6 +20692,7 @@ stripeApp.post("/webhooks", async (c) => {
20634
20692
  await setSubscriptionTier(user.id, newTier, live ? tier.concurrency : 0, live ? sub.id : null);
20635
20693
  if (tier.includes_memory) {
20636
20694
  const newMemoryPlan = live ? "pro" : "free";
20695
+ if (live) await cancelStandaloneMemorySub(user.memory_subscription_id, sub.id);
20637
20696
  await setMemoryPlan(user.id, newMemoryPlan, live ? sub.id : null);
20638
20697
  await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: newMemoryPlan }));
20639
20698
  } else if (user.memory_subscription_id === sub.id) {
@@ -26595,6 +26654,7 @@ function opForCostPath(p) {
26595
26654
  if (p === "/extract-url") return "page_scrape";
26596
26655
  if (p === "/extract-site") return "extract_site";
26597
26656
  if (p === "/map-urls") return "url_map";
26657
+ if (p === "/wayback/snapshots") return "url_map";
26598
26658
  if (p === "/youtube/harvest") return "yt_channel";
26599
26659
  if (p === "/youtube/transcribe") return "yt_transcription";
26600
26660
  if (p === "/maps/search") return "maps_search";
@@ -28309,7 +28369,7 @@ app.post("/extract-url", auth2, async (c) => {
28309
28369
  const raw = await c.req.json().catch(() => ({}));
28310
28370
  const bodyResult = ExtractUrlBodySchema.safeParse(raw);
28311
28371
  if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
28312
- const { url, screenshot: screenshot2, screenshotDevice, extractBranding: extractBranding2, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
28372
+ const { url, screenshot: screenshot2, screenshotDevice, extractBranding: extractBranding2, includeFeaturedImage, downloadMedia, mediaTypes, allowLocal } = bodyResult.data;
28313
28373
  if (!allowLocal) {
28314
28374
  const checked = await validatePublicHttpUrl(url, { field: "URL" });
28315
28375
  if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
@@ -28327,6 +28387,8 @@ app.post("/extract-url", auth2, async (c) => {
28327
28387
  return url;
28328
28388
  }
28329
28389
  })();
28390
+ const waybackReplay = parseWaybackReplayUrl(canonicalUrl);
28391
+ const extractionUrl = waybackReplay?.rawReplayUrl ?? canonicalUrl;
28330
28392
  const user = c.get("user");
28331
28393
  const gate = await acquireConcurrencyGate(user, "extract_url", {
28332
28394
  reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
@@ -28341,7 +28403,7 @@ app.post("/extract-url", auth2, async (c) => {
28341
28403
  const kernelApiKey = browserServiceApiKey();
28342
28404
  const device = screenshotDevice === "mobile" ? "mobile" : "desktop";
28343
28405
  const [result, pageData] = await Promise.all([
28344
- extractKpo({ url: canonicalUrl, kernelApiKey }),
28406
+ extractKpo({ url: extractionUrl, kernelApiKey }),
28345
28407
  screenshot2 || extractBranding2 ? capturePageData(canonicalUrl, { kernelApiKey, device, screenshot: !!screenshot2, branding: !!extractBranding2 }).catch((err) => {
28346
28408
  console.error("[extract-url] capturePageData failed:", err instanceof Error ? err.message : err);
28347
28409
  return null;
@@ -28353,15 +28415,36 @@ app.post("/extract-url", auth2, async (c) => {
28353
28415
  if (screenshotBuf) {
28354
28416
  screenshotMeta = { base64: screenshotBuf.toString("base64"), sizeBytes: screenshotBuf.length, device };
28355
28417
  }
28356
- const mediaMeta = downloadMedia ? await harvestPageMedia(result.bodyHtml, canonicalUrl, { types: mediaTypes ?? ["image", "video", "audio"], outputDir: null }) : null;
28418
+ const mediaMeta = downloadMedia ? await harvestPageMedia(result.bodyHtml, waybackReplay?.originalUrl ?? canonicalUrl, { types: mediaTypes ?? ["image", "video", "audio"], outputDir: null }) : null;
28419
+ const featuredImage = includeFeaturedImage ? resolveFeaturedImage({
28420
+ html: result.bodyHtml,
28421
+ meta: result.meta,
28422
+ schema: result.schema,
28423
+ pageUrl: waybackReplay?.rawReplayUrl ?? canonicalUrl
28424
+ }) : null;
28357
28425
  const memory = bodyResult.data.depositToVault ? await persistScrapeBody(user, {
28358
28426
  title: result.title ?? canonicalUrl,
28359
28427
  content: result.bodyMarkdown ?? "",
28360
28428
  source: canonicalUrl,
28361
28429
  vault: bodyResult.data.vaultName
28362
28430
  }) : void 0;
28431
+ const archive = waybackReplay ? {
28432
+ timestamp: waybackReplay.timestamp,
28433
+ originalUrl: waybackReplay.originalUrl,
28434
+ replayUrl: waybackReplay.replayUrl,
28435
+ rawReplayUrl: waybackReplay.rawReplayUrl
28436
+ } : null;
28363
28437
  await logRequestEvent({ userId: user.id, source: "extract_url", status: "done", query: canonicalUrl, resultCount: result.headings.length, result });
28364
- return c.json({ ...result, screenshot: screenshotMeta, branding: brandingData, media: mediaMeta, memory });
28438
+ return c.json({
28439
+ ...result,
28440
+ url: canonicalUrl,
28441
+ archive,
28442
+ featuredImage,
28443
+ screenshot: screenshotMeta,
28444
+ branding: brandingData,
28445
+ media: mediaMeta,
28446
+ memory
28447
+ });
28365
28448
  } catch (err) {
28366
28449
  const msg = err instanceof Error ? err.message : String(err);
28367
28450
  if (debited) await creditMc(user.id, MC_COSTS.page_scrape, LedgerOperation.EXTRACT_URL, "failed call");
@@ -28505,6 +28588,76 @@ app.post("/map-urls", auth2, async (c) => {
28505
28588
  await releaseConcurrencyGate(gate.lockId);
28506
28589
  }
28507
28590
  });
28591
+ app.post("/wayback/snapshots", auth2, async (c) => {
28592
+ const raw = await c.req.json().catch(() => ({}));
28593
+ const bodyResult = WaybackInventoryBodySchema.safeParse(raw);
28594
+ if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
28595
+ const body = bodyResult.data;
28596
+ const checked = await validatePublicHttpUrl(body.url, { field: "URL" });
28597
+ if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
28598
+ for (const selectedUrl of body.urls ?? []) {
28599
+ const selected = await validatePublicHttpUrl(selectedUrl, { field: "Wayback selected URL" });
28600
+ if (selected.error || !selected.parsed) return c.json({ error: selected.error ?? "Invalid Wayback selected URL" }, 400);
28601
+ }
28602
+ const user = c.get("user");
28603
+ const gate = await acquireConcurrencyGate(user, "wayback_inventory", {
28604
+ reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
28605
+ metadata: { url: checked.parsed.href }
28606
+ });
28607
+ if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
28608
+ let debited = false;
28609
+ try {
28610
+ const { ok, balance_mc: balanceMc } = await debitMc(
28611
+ user.id,
28612
+ MC_COSTS.url_map,
28613
+ LedgerOperation.URL_MAP,
28614
+ checked.parsed.hostname
28615
+ );
28616
+ if (!ok) return c.json(insufficientBalanceResponse(balanceMc, MC_COSTS.url_map), 402);
28617
+ debited = true;
28618
+ const result = await inventoryWaybackSnapshots({
28619
+ url: checked.parsed.href,
28620
+ scope: body.scope,
28621
+ urls: body.urls,
28622
+ from: body.from,
28623
+ to: body.to,
28624
+ successfulHtmlOnly: body.successfulHtmlOnly,
28625
+ maxCaptures: body.maxCaptures,
28626
+ includeCaptures: body.includeCaptures,
28627
+ maxCaptureRows: body.maxCaptureRows
28628
+ });
28629
+ await logRequestEvent({
28630
+ userId: user.id,
28631
+ source: "wayback_inventory",
28632
+ status: "done",
28633
+ query: result.url,
28634
+ resultCount: result.totalCaptures,
28635
+ result: {
28636
+ scope: result.scope,
28637
+ from: result.from,
28638
+ to: result.to,
28639
+ totalCaptures: result.totalCaptures,
28640
+ countType: result.countType,
28641
+ uniqueUrls: result.uniqueUrls,
28642
+ uniqueDigests: result.uniqueDigests
28643
+ }
28644
+ });
28645
+ return c.json(result);
28646
+ } catch (error) {
28647
+ const message = error instanceof Error ? error.message : String(error);
28648
+ if (debited) await creditMc(user.id, MC_COSTS.url_map, LedgerOperation.URL_MAP_REFUND, "failed call");
28649
+ await logRequestEvent({
28650
+ userId: user.id,
28651
+ source: "wayback_inventory",
28652
+ status: "failed",
28653
+ query: checked.parsed.href,
28654
+ error: message
28655
+ });
28656
+ return c.json({ error: message }, 500);
28657
+ } finally {
28658
+ await releaseConcurrencyGate(gate.lockId);
28659
+ }
28660
+ });
28508
28661
  app.post("/extract-site", auth2, async (c) => {
28509
28662
  const raw = await c.req.json().catch(() => ({}));
28510
28663
  const bodyResult = ExtractSiteBodySchema.safeParse(raw);
@@ -28514,24 +28667,58 @@ app.post("/extract-site", auth2, async (c) => {
28514
28667
  if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
28515
28668
  const parsed = checked.parsed;
28516
28669
  const user = c.get("user");
28670
+ const waybackReplay = parseWaybackReplayUrl(parsed.href);
28671
+ const waybackMonths = body.wayback ? expandWaybackMonths(body.wayback) : [];
28672
+ const waybackRootUrl = waybackReplay?.originalUrl ?? parsed.href;
28673
+ if (body.wayback?.urls) {
28674
+ const rootHost = new URL(waybackRootUrl).hostname.replace(/^www\./, "").toLowerCase();
28675
+ for (const selectedUrl of body.wayback.urls) {
28676
+ const selected = await validatePublicHttpUrl(selectedUrl, { field: "Wayback selected URL" });
28677
+ if (selected.error || !selected.parsed) return c.json({ error: selected.error ?? "Invalid Wayback selected URL" }, 400);
28678
+ if (selected.parsed.hostname.replace(/^www\./, "").toLowerCase() !== rootHost) {
28679
+ return c.json({ error: "Every selected Wayback URL must belong to the same site as url." }, 400);
28680
+ }
28681
+ }
28682
+ }
28683
+ const maxPagesPerSnapshot = Math.min(500, Math.max(1, body.maxPages ?? 100));
28684
+ const requestedMaxPages = body.wayback ? Math.min(
28685
+ 1e4,
28686
+ waybackMonths.length * Math.min(maxPagesPerSnapshot, body.wayback.urls?.length ?? maxPagesPerSnapshot)
28687
+ ) : Math.min(1e4, Math.max(1, body.maxPages ?? 1e4));
28688
+ const crawlStartUrl = body.wayback ? waybackRootUrl : waybackReplay?.rawReplayUrl ?? parsed.href;
28517
28689
  const downloadImages = body.downloadImages === true;
28518
- if (body.background === true || downloadImages) {
28690
+ if (body.background === true || downloadImages || Boolean(body.wayback)) {
28519
28691
  const affordablePages = Math.floor(user.balance_mc / MC_COSTS.page_scrape);
28520
28692
  if (affordablePages < 1) return c.json(insufficientBalanceResponse(user.balance_mc, MC_COSTS.page_scrape), 402);
28521
- const maxPages = Math.min(Math.min(1e4, Math.max(1, body.maxPages ?? 1e4)), affordablePages);
28693
+ const maxPages = Math.min(requestedMaxPages, affordablePages);
28522
28694
  const heldMc = maxPages * MC_COSTS.page_scrape;
28523
28695
  const { ok: holdOk, balance_mc: holdBal } = await debitMc(user.id, heldMc, LedgerOperation.EXTRACT_SITE_HOLD, parsed.hostname);
28524
28696
  if (!holdOk) return c.json(insufficientBalanceResponse(holdBal, heldMc), 402);
28525
28697
  const jobId = `ext_${Date.now().toString(36)}${Math.random().toString(36).slice(2, 8)}`;
28526
28698
  let jobCreated = false;
28527
28699
  try {
28528
- await createExtractJob(jobId, user.id, parsed.href, {
28700
+ await createExtractJob(jobId, user.id, crawlStartUrl, {
28529
28701
  maxPages,
28530
28702
  concurrency: concurrencyLimitForUser(user),
28531
28703
  urlsPerBrowser: body.rotateProxyEvery ?? 10,
28532
28704
  formats: body.formats,
28533
28705
  downloadImages,
28534
- heldMc
28706
+ heldMc,
28707
+ ...waybackReplay && !body.wayback ? {
28708
+ waybackReplay: {
28709
+ timestamp: waybackReplay.timestamp,
28710
+ originalUrl: waybackReplay.originalUrl,
28711
+ replayUrl: waybackReplay.replayUrl,
28712
+ rawReplayUrl: waybackReplay.rawReplayUrl
28713
+ }
28714
+ } : {},
28715
+ ...body.wayback ? {
28716
+ waybackTimeline: {
28717
+ rootUrl: waybackRootUrl,
28718
+ timeline: body.wayback,
28719
+ maxPagesPerSnapshot
28720
+ }
28721
+ } : {}
28535
28722
  });
28536
28723
  jobCreated = true;
28537
28724
  await inngest.send({ name: "mcp-scraper/extract.requested", data: { jobId } });
@@ -28565,9 +28752,11 @@ app.post("/extract-site", auth2, async (c) => {
28565
28752
  debited = true;
28566
28753
  const rotateProxies = body.rotateProxies === true;
28567
28754
  const wantsBranding = body.formats?.includes("branding") === true;
28755
+ const waybackCaptures = waybackReplay ? await resolveWaybackSiteCaptures(waybackReplay, siteMaxPages) : null;
28568
28756
  const result = await extractSite({
28569
- startUrl: parsed.href,
28757
+ startUrl: crawlStartUrl,
28570
28758
  maxPages: siteMaxPages,
28759
+ seedUrls: waybackCaptures?.map((capture) => capture.rawReplayUrl),
28571
28760
  kernelApiKey: rotateProxies || wantsBranding || body.browserFallback || body.kernelFallback ? browserServiceApiKey() : void 0,
28572
28761
  formats: body.formats,
28573
28762
  ...rotateProxies ? {
@@ -28575,8 +28764,25 @@ app.post("/extract-site", auth2, async (c) => {
28575
28764
  parallelism: concurrencyLimitForUser(user)
28576
28765
  } : {}
28577
28766
  });
28767
+ if (waybackReplay) {
28768
+ result.startUrl = waybackReplay.originalUrl;
28769
+ result.pages = result.pages.map((page) => {
28770
+ const replay = parseWaybackReplayUrl(page.url);
28771
+ return replay ? {
28772
+ ...page,
28773
+ archivedUrl: page.url,
28774
+ archiveTimestamp: replay.timestamp,
28775
+ originalUrl: replay.originalUrl,
28776
+ url: replay.originalUrl,
28777
+ finalUrl: replay.originalUrl
28778
+ } : page;
28779
+ });
28780
+ }
28578
28781
  const pageCount = result.pages?.length ?? 1;
28579
- const actualSiteMc = pageCount * MC_COSTS.page_scrape;
28782
+ const successfulPageCount = result.pages.filter(
28783
+ (page) => page.status != null && page.status >= 200 && page.status < 300 && page.wordCount > 0
28784
+ ).length;
28785
+ const actualSiteMc = successfulPageCount * MC_COSTS.page_scrape;
28580
28786
  const siteDiff = siteHoldMc - actualSiteMc;
28581
28787
  if (siteDiff > 0) await creditMc(user.id, siteDiff, LedgerOperation.EXTRACT_SITE_REFUND, "overestimate refund");
28582
28788
  else if (siteDiff < 0) await debitMc(user.id, -siteDiff, LedgerOperation.EXTRACT_SITE, parsed.hostname);
@@ -28689,15 +28895,25 @@ app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
28689
28895
  customerId = customer.id;
28690
28896
  await setStripeCustomerId(user.id, customerId);
28691
28897
  }
28692
- if (user.subscription_id) {
28693
- const sub = await stripeClient.subscriptions.retrieve(user.subscription_id);
28898
+ let existingSubId = user.subscription_id;
28899
+ if (!existingSubId) {
28900
+ try {
28901
+ const active = await stripeClient.subscriptions.list({ customer: customerId, status: "active", limit: 20 });
28902
+ const baseSub = active.data.find((s) => findBasePlanItem(s));
28903
+ if (baseSub) existingSubId = baseSub.id;
28904
+ } catch (err) {
28905
+ console.warn("[billing/subscribe] could not check for existing subscription", err instanceof Error ? err.message : err);
28906
+ }
28907
+ }
28908
+ if (existingSubId) {
28909
+ const sub = await stripeClient.subscriptions.retrieve(existingSubId);
28694
28910
  const itemId = findBasePlanItem(sub)?.id;
28695
28911
  if (itemId) {
28696
- await stripeClient.subscriptions.update(user.subscription_id, {
28912
+ await stripeClient.subscriptions.update(existingSubId, {
28697
28913
  items: [{ id: itemId, price: tier.price_id }],
28698
28914
  proration_behavior: "create_prorations"
28699
28915
  });
28700
- const billed = await chargeTierChangeNow(stripeClient, user.subscription_id, customerId);
28916
+ const billed = await chargeTierChangeNow(stripeClient, existingSubId, customerId);
28701
28917
  if (!billed.ok) {
28702
28918
  return c.json({
28703
28919
  updated: true,
@@ -28815,15 +29031,25 @@ app.post("/billing/subscribe/terminal-checkout", auth2, async (c) => {
28815
29031
  customerId = customer.id;
28816
29032
  await setStripeCustomerId(user.id, customerId);
28817
29033
  }
28818
- if (user.subscription_id) {
28819
- const sub = await stripeClient.subscriptions.retrieve(user.subscription_id);
29034
+ let existingSubId = user.subscription_id;
29035
+ if (!existingSubId) {
29036
+ try {
29037
+ const active = await stripeClient.subscriptions.list({ customer: customerId, status: "active", limit: 20 });
29038
+ const baseSub = active.data.find((s) => findBasePlanItem(s));
29039
+ if (baseSub) existingSubId = baseSub.id;
29040
+ } catch (err) {
29041
+ console.warn("[billing/subscribe] could not check for existing subscription", err instanceof Error ? err.message : err);
29042
+ }
29043
+ }
29044
+ if (existingSubId) {
29045
+ const sub = await stripeClient.subscriptions.retrieve(existingSubId);
28820
29046
  const itemId = findBasePlanItem(sub)?.id;
28821
29047
  if (itemId) {
28822
- await stripeClient.subscriptions.update(user.subscription_id, {
29048
+ await stripeClient.subscriptions.update(existingSubId, {
28823
29049
  items: [{ id: itemId, price: tier.price_id }],
28824
29050
  proration_behavior: "create_prorations"
28825
29051
  });
28826
- const billed = await chargeTierChangeNow(stripeClient, user.subscription_id, customerId);
29052
+ const billed = await chargeTierChangeNow(stripeClient, existingSubId, customerId);
28827
29053
  if (!billed.ok) {
28828
29054
  return c.json({
28829
29055
  updated: true,
@@ -28937,7 +29163,7 @@ app.get("/cron/tick", async (c) => {
28937
29163
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
28938
29164
  return c.json({ error: "Unauthorized" }, 401);
28939
29165
  }
28940
- const { drainQueue } = await import("./worker-ZZHSYI3Y.js");
29166
+ const { drainQueue } = await import("./worker-645BZPEK.js");
28941
29167
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
28942
29168
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
28943
29169
  const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
@@ -28965,15 +29191,15 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
28965
29191
  return c.json({ error: "Unauthorized" }, 401);
28966
29192
  }
28967
29193
  const jobId = c.req.param("id");
28968
- const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-JHMVHENZ.js");
28969
- const { assembleExtractArtifacts } = await import("./extract-bundle-GPZRLBVY.js");
29194
+ const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-L6BHWVDU.js");
29195
+ const { assembleExtractArtifacts } = await import("./extract-bundle-ONWZVV55.js");
28970
29196
  const job = await getExtractJob2(jobId);
28971
29197
  if (!job) return c.json({ error: "job not found" }, 404);
28972
29198
  const stored = await assembleExtractArtifacts(job, {});
28973
29199
  await completeExtractJob2(jobId, stored);
28974
29200
  let settlement = "already_settled_or_refunded";
28975
29201
  if (job.billedMc == null && job.userId != null) {
28976
- const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-JHMVHENZ.js");
29202
+ const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-L6BHWVDU.js");
28977
29203
  const heldMc = Number(job.options.heldMc ?? 0);
28978
29204
  const successful = await countSuccessfulPages2(jobId);
28979
29205
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -29142,4 +29368,4 @@ app.get("/blog/:slug/", (c) => {
29142
29368
  export {
29143
29369
  app
29144
29370
  };
29145
- //# sourceMappingURL=server-LKVPEQFE.js.map
29371
+ //# sourceMappingURL=server-GKUTC73B.js.map