mcp-scraper 0.4.5 → 0.4.6

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 (47) hide show
  1. package/dist/bin/api-server.cjs +315 -6
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +3 -3
  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 +89 -1
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +2 -2
  13. package/dist/bin/paa-harvest.cjs.map +1 -1
  14. package/dist/bin/paa-harvest.js +2 -2
  15. package/dist/chunk-5BB6Y3BB.js +7 -0
  16. package/dist/chunk-5BB6Y3BB.js.map +1 -0
  17. package/dist/{chunk-NJK5BTUK.js → chunk-5GAD2AN2.js} +2 -2
  18. package/dist/{chunk-FUVQR6GK.js → chunk-GL4BW4CP.js} +49 -1
  19. package/dist/chunk-GL4BW4CP.js.map +1 -0
  20. package/dist/{chunk-3UH3BEIZ.js → chunk-HE2LQPJ2.js} +2 -2
  21. package/dist/{chunk-WI4A3KFQ.js → chunk-IFXACD4K.js} +90 -2
  22. package/dist/chunk-IFXACD4K.js.map +1 -0
  23. package/dist/{chunk-5LO6EHEH.js → chunk-IVQYNY45.js} +14 -3
  24. package/dist/chunk-IVQYNY45.js.map +1 -0
  25. package/dist/{chunk-4ZZWFI6L.js → chunk-NYAWN7CZ.js} +3 -3
  26. package/dist/{db-H3S3M6KK.js → db-LIOTIWVN.js} +6 -2
  27. package/dist/{extract-bundle-UKE273TV.js → extract-bundle-COS56ZDO.js} +2 -2
  28. package/dist/index.cjs.map +1 -1
  29. package/dist/index.js +2 -2
  30. package/dist/{server-XCDFHHLE.js → server-R3KVQNOT.js} +170 -13
  31. package/dist/server-R3KVQNOT.js.map +1 -0
  32. package/dist/{site-extract-repository-THBVEXMP.js → site-extract-repository-LI2S3S6E.js} +4 -4
  33. package/dist/{worker-YHKUJR5P.js → worker-VASAJ7FZ.js} +5 -5
  34. package/package.json +2 -1
  35. package/dist/chunk-5LO6EHEH.js.map +0 -1
  36. package/dist/chunk-FUVQR6GK.js.map +0 -1
  37. package/dist/chunk-UGQC2FOX.js +0 -7
  38. package/dist/chunk-UGQC2FOX.js.map +0 -1
  39. package/dist/chunk-WI4A3KFQ.js.map +0 -1
  40. package/dist/server-XCDFHHLE.js.map +0 -1
  41. /package/dist/{chunk-NJK5BTUK.js.map → chunk-5GAD2AN2.js.map} +0 -0
  42. /package/dist/{chunk-3UH3BEIZ.js.map → chunk-HE2LQPJ2.js.map} +0 -0
  43. /package/dist/{chunk-4ZZWFI6L.js.map → chunk-NYAWN7CZ.js.map} +0 -0
  44. /package/dist/{db-H3S3M6KK.js.map → db-LIOTIWVN.js.map} +0 -0
  45. /package/dist/{extract-bundle-UKE273TV.js.map → extract-bundle-COS56ZDO.js.map} +0 -0
  46. /package/dist/{site-extract-repository-THBVEXMP.js.map → site-extract-repository-LI2S3S6E.js.map} +0 -0
  47. /package/dist/{worker-YHKUJR5P.js.map → worker-VASAJ7FZ.js.map} +0 -0
@@ -3617,6 +3617,7 @@ __export(db_exports, {
3617
3617
  getLedger: () => getLedger,
3618
3618
  getMemoryPlan: () => getMemoryPlan,
3619
3619
  getMemoryProvisioned: () => getMemoryProvisioned,
3620
+ getPageSnapshot: () => getPageSnapshot,
3620
3621
  getRefresh: () => getRefresh,
3621
3622
  getUserByApiKey: () => getUserByApiKey,
3622
3623
  getUserByEmail: () => getUserByEmail,
@@ -3672,6 +3673,7 @@ __export(db_exports, {
3672
3673
  startHarvestAttempt: () => startHarvestAttempt,
3673
3674
  stripeEventAlreadyProcessed: () => stripeEventAlreadyProcessed,
3674
3675
  updateKpoJobState: () => updateKpoJobState,
3676
+ upsertPageSnapshot: () => upsertPageSnapshot,
3675
3677
  verifyPassword: () => verifyPassword
3676
3678
  });
3677
3679
  function getDb() {
@@ -4109,6 +4111,20 @@ async function migrate() {
4109
4111
  } catch {
4110
4112
  }
4111
4113
  }
4114
+ await db.execute(`
4115
+ CREATE TABLE IF NOT EXISTS page_snapshots (
4116
+ user_id INTEGER NOT NULL REFERENCES users(id),
4117
+ url TEXT NOT NULL,
4118
+ content_hash TEXT NOT NULL,
4119
+ content TEXT NOT NULL,
4120
+ title TEXT,
4121
+ content_bytes INTEGER NOT NULL,
4122
+ truncated INTEGER NOT NULL DEFAULT 0,
4123
+ checked_at TEXT NOT NULL DEFAULT (datetime('now')),
4124
+ PRIMARY KEY (user_id, url)
4125
+ )
4126
+ `);
4127
+ await db.execute(`CREATE INDEX IF NOT EXISTS page_snapshots_user_checked_at ON page_snapshots(user_id, checked_at DESC)`);
4112
4128
  }
4113
4129
  async function registerClient(clientId, redirectUris, name) {
4114
4130
  await getDb().execute({
@@ -4539,6 +4555,38 @@ async function listJobs(userId) {
4539
4555
  const res = await getDb().execute({ sql: "SELECT * FROM jobs WHERE user_id = ? ORDER BY created_at DESC LIMIT 50", args: [userId] });
4540
4556
  return res.rows.map((r) => deserialize(rowToRawJob(r)));
4541
4557
  }
4558
+ async function getPageSnapshot(userId, url) {
4559
+ const res = await getDb().execute({
4560
+ sql: "SELECT content_hash, content, title, content_bytes, truncated, checked_at FROM page_snapshots WHERE user_id = ? AND url = ?",
4561
+ args: [userId, url]
4562
+ });
4563
+ const row = res.rows[0];
4564
+ if (!row) return null;
4565
+ return {
4566
+ contentHash: String(row.content_hash),
4567
+ content: String(row.content),
4568
+ title: row.title == null ? null : String(row.title),
4569
+ contentBytes: Number(row.content_bytes),
4570
+ truncated: Number(row.truncated) === 1,
4571
+ checkedAt: String(row.checked_at)
4572
+ };
4573
+ }
4574
+ async function upsertPageSnapshot(userId, url, snapshot) {
4575
+ await getDb().execute({
4576
+ sql: `
4577
+ INSERT INTO page_snapshots (user_id, url, content_hash, content, title, content_bytes, truncated, checked_at)
4578
+ VALUES (?, ?, ?, ?, ?, ?, ?, datetime('now'))
4579
+ ON CONFLICT(user_id, url) DO UPDATE SET
4580
+ content_hash = excluded.content_hash,
4581
+ content = excluded.content,
4582
+ title = excluded.title,
4583
+ content_bytes = excluded.content_bytes,
4584
+ truncated = excluded.truncated,
4585
+ checked_at = excluded.checked_at
4586
+ `,
4587
+ args: [userId, url, snapshot.contentHash, snapshot.content, snapshot.title, snapshot.contentBytes, snapshot.truncated ? 1 : 0]
4588
+ });
4589
+ }
4542
4590
  function rowToRequestEvent(row) {
4543
4591
  const rawResult = row.result != null ? String(row.result) : null;
4544
4592
  return {
@@ -10046,7 +10094,8 @@ var init_rates = __esm({
10046
10094
  trustpilot_reviews: 500,
10047
10095
  trustpilot_review: 100,
10048
10096
  g2_reviews: 500,
10049
- g2_review: 150
10097
+ g2_review: 150,
10098
+ diff_page: 100
10050
10099
  };
10051
10100
  MC_PER_BROWSER_MS = MC_COSTS.browser_minute / 6e4;
10052
10101
  BROWSER_OPEN_MIN_BALANCE_MC = 1e3;
@@ -10249,6 +10298,14 @@ var init_rates = __esm({
10249
10298
  credits: mcToCredits(MC_COSTS.g2_review),
10250
10299
  unit: "per review card",
10251
10300
  notes: "Charged per review actually extracted (each card carries up to 3 Q&A sections), billed down automatically if the balance runs out mid-page."
10301
+ },
10302
+ {
10303
+ key: "diff_page",
10304
+ label: "Page change check",
10305
+ aliases: ["diff_page", "page diff", "change detection"],
10306
+ credits: mcToCredits(MC_COSTS.diff_page),
10307
+ unit: "per check",
10308
+ notes: "Same cost as a single page extract \u2014 one scrape is performed per check, then compared against your last stored snapshot for that URL."
10252
10309
  }
10253
10310
  ];
10254
10311
  CONCURRENCY_PRICE_ID = "price_1Ta1NRS8aAcsk3TGwsRnYbix";
@@ -10334,7 +10391,9 @@ var init_rates = __esm({
10334
10391
  G2_REVIEWS: "g2_reviews",
10335
10392
  G2_REVIEWS_REFUND: "g2_reviews_refund",
10336
10393
  G2_REVIEW: "g2_review",
10337
- G2_REVIEW_REFUND: "g2_review_refund"
10394
+ G2_REVIEW_REFUND: "g2_review_refund",
10395
+ DIFF_PAGE: "diff_page",
10396
+ DIFF_PAGE_REFUND: "diff_page_refund"
10338
10397
  };
10339
10398
  MEMORY_AI_MARGIN_MULTIPLE = 3;
10340
10399
  MC_PER_USD = SUBSCRIPTION_TIERS["price_1TmiHRS8aAcsk3TGwmSNfNIa"].credits_mc / SUBSCRIPTION_TIERS["price_1TmiHRS8aAcsk3TGwmSNfNIa"].monthly_usd;
@@ -13113,7 +13172,7 @@ var init_api_auth = __esm({
13113
13172
  });
13114
13173
 
13115
13174
  // src/api/server-schemas.ts
13116
- var import_zod13, HarvestBodySchema, ExtractUrlBodySchema, MapUrlsBodySchema, ExtractSiteBodySchema, YoutubeHarvestBodySchema, YoutubeTranscribeBodySchema;
13175
+ var import_zod13, HarvestBodySchema, ExtractUrlBodySchema, DiffPageBodySchema, MapUrlsBodySchema, ExtractSiteBodySchema, YoutubeHarvestBodySchema, YoutubeTranscribeBodySchema;
13117
13176
  var init_server_schemas = __esm({
13118
13177
  "src/api/server-schemas.ts"() {
13119
13178
  "use strict";
@@ -13144,6 +13203,11 @@ var init_server_schemas = __esm({
13144
13203
  depositToVault: import_zod13.z.boolean().optional(),
13145
13204
  vaultName: import_zod13.z.string().trim().min(1).max(120).optional()
13146
13205
  });
13206
+ DiffPageBodySchema = import_zod13.z.object({
13207
+ url: import_zod13.z.string().min(1, "url is required"),
13208
+ allowLocal: import_zod13.z.boolean().optional(),
13209
+ resetBaseline: import_zod13.z.boolean().optional()
13210
+ });
13147
13211
  MapUrlsBodySchema = import_zod13.z.object({
13148
13212
  url: import_zod13.z.string().min(1, "url is required"),
13149
13213
  maxUrls: import_zod13.z.number().int().min(1).max(2e3).optional(),
@@ -19330,6 +19394,55 @@ ${headingSection}${kpoSection}${brandingSection}${bodySectionFull}${memSection}$
19330
19394
  }
19331
19395
  return { ...textResult, structuredContent };
19332
19396
  }
19397
+ function formatDiffPage(raw, input) {
19398
+ const parsed = parseData(raw);
19399
+ if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
19400
+ const d = parsed.data;
19401
+ const url = d.url ?? input.url;
19402
+ const title = d.title ?? "Untitled";
19403
+ const statusLine = d.status === "baseline" ? d.isReset ? "\u{1F195} **Baseline reset** \u2014 prior history discarded, this check is now the new baseline." : "\u{1F195} **Baseline captured** \u2014 first check for this URL, nothing to compare yet." : d.status === "unchanged" ? `\u2705 **No changes** since ${d.previousCheckedAt ?? "the last check"}.` : `\u{1F504} **Changed** since ${d.previousCheckedAt ?? "the last check"} \u2014 +${d.summary.linesAdded}/-${d.summary.linesRemoved} lines (${d.summary.percentChanged ?? 0}% of content).`;
19404
+ const previewHunks = d.hunks.slice(0, DIFF_PAGE_PREVIEW_HUNKS);
19405
+ const diffBlock = previewHunks.length ? [
19406
+ "\n## Diff",
19407
+ "```diff",
19408
+ ...previewHunks.flatMap((h) => h.lines.map((l) => `${h.type === "added" ? "+" : "-"} ${l}`)),
19409
+ "```",
19410
+ previewHunks.length < d.hunks.length ? `*(showing ${previewHunks.length} of ${d.hunks.length} returned hunks)*` : ""
19411
+ ].filter(Boolean).join("\n") : "";
19412
+ const truncationNotes = [
19413
+ d.contentTruncated ? "\n\u26A0\uFE0F Page content exceeded the storable cap and was truncated before hashing/diffing \u2014 changes past that point are not visible to this comparison." : "",
19414
+ d.hunksTruncatedReason ? `
19415
+ \u26A0\uFE0F ${d.hunksTruncatedReason}` : ""
19416
+ ].filter(Boolean).join("\n");
19417
+ const tips = `
19418
+ ---
19419
+ \u{1F4A1} **Tips**
19420
+ - Call \`diff_page\` again anytime to re-check \u2014 this tool is on-demand, not automatic.
19421
+ - Want the page's current content with nothing to compare? use \`extract_url\`.`;
19422
+ const full = `# Page Diff: ${url}
19423
+ **${title}**
19424
+
19425
+ ${statusLine}${diffBlock}${truncationNotes}${tips}`;
19426
+ return {
19427
+ ...oneBlock(full),
19428
+ structuredContent: {
19429
+ url,
19430
+ title: d.title,
19431
+ status: d.status,
19432
+ isReset: d.isReset,
19433
+ previousCheckedAt: d.previousCheckedAt,
19434
+ currentCheckedAt: d.currentCheckedAt,
19435
+ contentHash: d.contentHash,
19436
+ previousContentHash: d.previousContentHash,
19437
+ summary: d.summary,
19438
+ hunks: d.hunks,
19439
+ contentTruncated: d.contentTruncated,
19440
+ hunksTruncated: d.hunksTruncated,
19441
+ hunksTruncatedReason: d.hunksTruncatedReason,
19442
+ totalChangedLineCount: d.totalChangedLineCount
19443
+ }
19444
+ };
19445
+ }
19333
19446
  async function formatMapSiteUrls(raw, input, ctx) {
19334
19447
  const parsed = parseData(raw);
19335
19448
  if ("error" in parsed) return { content: [{ type: "text", text: parsed.error }], isError: true };
@@ -21054,7 +21167,7 @@ ${rows}` : ""
21054
21167
  }
21055
21168
  };
21056
21169
  }
21057
- var import_node_fs6, import_node_os6, import_node_path8, INLINE_BUDGET_BYTES, STRUCTURED_ARRAY_CAP, reportSavingEnabled, BULK_PAGE_THRESHOLD, BULK_URL_THRESHOLD;
21170
+ var import_node_fs6, import_node_os6, import_node_path8, INLINE_BUDGET_BYTES, STRUCTURED_ARRAY_CAP, reportSavingEnabled, BULK_PAGE_THRESHOLD, BULK_URL_THRESHOLD, DIFF_PAGE_PREVIEW_HUNKS;
21058
21171
  var init_mcp_response_formatter = __esm({
21059
21172
  "src/mcp/mcp-response-formatter.ts"() {
21060
21173
  "use strict";
@@ -21073,6 +21186,7 @@ var init_mcp_response_formatter = __esm({
21073
21186
  reportSavingEnabled = true;
21074
21187
  BULK_PAGE_THRESHOLD = 25;
21075
21188
  BULK_URL_THRESHOLD = 500;
21189
+ DIFF_PAGE_PREVIEW_HUNKS = 20;
21076
21190
  }
21077
21191
  });
21078
21192
 
@@ -27003,7 +27117,7 @@ var PACKAGE_VERSION;
27003
27117
  var init_version = __esm({
27004
27118
  "src/version.ts"() {
27005
27119
  "use strict";
27006
- PACKAGE_VERSION = "0.4.5";
27120
+ PACKAGE_VERSION = "0.4.6";
27007
27121
  }
27008
27122
  });
27009
27123
 
@@ -27142,7 +27256,7 @@ var init_output_schema_registry = __esm({
27142
27256
  });
27143
27257
 
27144
27258
  // src/mcp/mcp-tool-schemas.ts
27145
- var import_zod33, HarvestPaaInputSchema, ExtractUrlInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, RedditThreadInputSchema, VideoFrameAnalysisInputSchema, VideoFrameAnalysisStatusInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, GoogleAdsSearchInputSchema, GoogleAdsPageIntelInputSchema, GoogleAdsTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, TrustpilotReviewsInputSchema, G2ReviewsInputSchema, ReviewCardSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, ArtifactPointerOutputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, MapsPlaceIntelOutputSchema, TrustpilotReviewsOutputSchema, G2ReviewsOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, VideoFrameAnalysisOutputSchema, VideoFrameAnalysisStatusOutputSchema, RedditThreadOutputSchema, FacebookPageIntelOutputSchema, GoogleAdsSearchOutputSchema, GoogleAdsPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, GoogleAdsTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema, ReportArtifactReadInputSchema, ReportArtifactReadOutputSchema;
27259
+ var import_zod33, HarvestPaaInputSchema, ExtractUrlInputSchema, DiffPageInputSchema, MapSiteUrlsInputSchema, ExtractSiteInputSchema, AuditSiteInputSchema, YoutubeHarvestInputSchema, YoutubeTranscribeInputSchema, FacebookPageIntelInputSchema, FacebookAdSearchInputSchema, RedditThreadInputSchema, VideoFrameAnalysisInputSchema, VideoFrameAnalysisStatusInputSchema, FacebookAdTranscribeInputSchema, FacebookVideoTranscribeInputSchema, GoogleAdsSearchInputSchema, GoogleAdsPageIntelInputSchema, GoogleAdsTranscribeInputSchema, InstagramProfileContentInputSchema, InstagramMediaDownloadInputSchema, MapsPlaceIntelInputSchema, TrustpilotReviewsInputSchema, G2ReviewsInputSchema, ReviewCardSchema, MapsSearchInputSchema, DirectoryWorkflowInputSchema, ArtifactPointerOutputSchema, RankTrackerModeSchema, RankTrackerBlueprintInputSchema, NullableString, MapsSearchAttemptOutput, MapsSearchOutputSchema, DirectoryMapsBusinessOutput, DirectoryWorkflowOutputSchema, RankTrackerToolPlanOutput, RankTrackerTableOutput, RankTrackerCronJobOutput, RankTrackerBlueprintOutputSchema, OrganicResultOutput, AiOverviewOutput, EntityIdsOutput, HarvestPaaOutputSchema, SearchSerpOutputSchema, ExtractUrlOutputSchema, DiffPageOutputSchema, ExtractSiteOutputSchema, AuditSiteOutputSchema, MapsPlaceIntelOutputSchema, TrustpilotReviewsOutputSchema, G2ReviewsOutputSchema, CreditsInfoOutputSchema, MapSiteUrlsOutputSchema, YoutubeHarvestOutputSchema, FacebookAdSearchOutputSchema, VideoFrameAnalysisOutputSchema, VideoFrameAnalysisStatusOutputSchema, RedditThreadOutputSchema, FacebookPageIntelOutputSchema, GoogleAdsSearchOutputSchema, GoogleAdsPageIntelOutputSchema, FacebookVideoTranscribeOutputSchema, TranscriptChunkOutput, InstagramBrowserOutput, InstagramPaginationOutput, InstagramProfileContentOutputSchema, InstagramMediaTrackOutput, InstagramDownloadOutput, InstagramMediaDownloadOutputSchema, YoutubeTranscribeOutputSchema, FacebookAdTranscribeOutputSchema, GoogleAdsTranscribeOutputSchema, CaptureSerpSnapshotOutputSchema, CaptureSerpPageSnapshotsOutputSchema, CreditsInfoInputSchema, WorkflowIdSchema2, WorkflowListInputSchema, WorkflowSuggestInputSchema, WorkflowRunInputSchema, WorkflowStepInputSchema, WorkflowStatusInputSchema, WorkflowArtifactReadInputSchema, WorkflowRecipeOutput, WorkflowDefinitionOutput, WorkflowArtifactOutput, WorkflowListOutputSchema, WorkflowSuggestOutputSchema, WorkflowRunOutputSchema, WorkflowStepOutputSchema, WorkflowStatusOutputSchema, WorkflowArtifactReadOutputSchema, SearchSerpInputSchema, CaptureSerpSnapshotInputSchema, ScreenshotInputSchema, CaptureSerpPageSnapshotsInputSchema, ReportArtifactReadInputSchema, ReportArtifactReadOutputSchema;
27146
27260
  var init_mcp_tool_schemas = __esm({
27147
27261
  "src/mcp/mcp-tool-schemas.ts"() {
27148
27262
  "use strict";
@@ -27170,6 +27284,11 @@ var init_mcp_tool_schemas = __esm({
27170
27284
  depositToVault: import_zod33.z.boolean().default(false).describe("Save the full page content into the user's MCP Memory vault server-side, embedded for semantic recall \u2014 the full body is NOT returned to chat."),
27171
27285
  vaultName: import_zod33.z.string().trim().min(1).max(120).optional().describe("Optional vault to deposit into. Defaults to the user's personal vault.")
27172
27286
  };
27287
+ DiffPageInputSchema = {
27288
+ url: import_zod33.z.string().url().describe("Public http/https URL to check for changes since the last diff_page call."),
27289
+ allowLocal: import_zod33.z.boolean().default(false).describe("Allow localhost and private-network URLs. Local development only."),
27290
+ resetBaseline: import_zod33.z.boolean().default(false).describe("Discard any previously stored snapshot for this URL and capture the current content as a fresh baseline instead of diffing against history. Use when you deliberately want to restart change tracking.")
27291
+ };
27173
27292
  MapSiteUrlsInputSchema = {
27174
27293
  url: import_zod33.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."),
27175
27294
  maxUrls: import_zod33.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.")
@@ -27563,6 +27682,29 @@ var init_mcp_tool_schemas = __esm({
27563
27682
  error: import_zod33.z.string().optional()
27564
27683
  }).optional()
27565
27684
  };
27685
+ DiffPageOutputSchema = {
27686
+ url: import_zod33.z.string(),
27687
+ title: NullableString,
27688
+ status: import_zod33.z.enum(["baseline", "unchanged", "changed"]).describe('"baseline" = first-ever check for this URL, or resetBaseline was used \u2014 nothing to compare against. "unchanged" = content hash matched the stored snapshot. "changed" = a diff was computed.'),
27689
+ isReset: import_zod33.z.boolean().describe("True only when resetBaseline discarded a real prior snapshot \u2014 distinguishes an explicit reset from a URL's true first-ever check."),
27690
+ previousCheckedAt: NullableString.describe("ISO timestamp of the snapshot this was compared against, or null if there was none."),
27691
+ currentCheckedAt: import_zod33.z.string().describe("ISO timestamp of this check, now stored as the new snapshot."),
27692
+ contentHash: import_zod33.z.string().describe("sha256 of the full (untruncated) page content just captured."),
27693
+ previousContentHash: NullableString,
27694
+ summary: import_zod33.z.object({
27695
+ linesAdded: import_zod33.z.number().int().min(0),
27696
+ linesRemoved: import_zod33.z.number().int().min(0),
27697
+ percentChanged: import_zod33.z.number().min(0).max(100).nullable().describe('Proportion of changed lines relative to the larger of the two versions. Null when status is "baseline".')
27698
+ }),
27699
+ hunks: import_zod33.z.array(import_zod33.z.object({
27700
+ type: import_zod33.z.enum(["added", "removed"]).describe("Unchanged context lines are omitted \u2014 only added/removed lines are returned."),
27701
+ lines: import_zod33.z.array(import_zod33.z.string())
27702
+ })).describe("Ordered added/removed line hunks, capped for response size \u2014 see hunksTruncated."),
27703
+ contentTruncated: import_zod33.z.boolean().describe("True if the scraped page exceeded the 250,000-character storable cap and was truncated before hashing/diffing/storing \u2014 changes past that point are invisible to this comparison."),
27704
+ hunksTruncated: import_zod33.z.boolean().describe("True if the hunks list above was capped for response size \u2014 see hunksTruncatedReason."),
27705
+ hunksTruncatedReason: NullableString,
27706
+ totalChangedLineCount: import_zod33.z.number().int().min(0).describe("Total changed lines found before any hunksTruncated capping was applied.")
27707
+ };
27566
27708
  ExtractSiteOutputSchema = {
27567
27709
  url: import_zod33.z.string(),
27568
27710
  pageCount: import_zod33.z.number().int().min(0),
@@ -28575,6 +28717,13 @@ function registerPaaExtractorMcpTools(server, executor, options = {}) {
28575
28717
  outputSchema: recordOutputSchema("extract_url", ExtractUrlOutputSchema),
28576
28718
  annotations: liveWebToolAnnotations("Single URL Extract")
28577
28719
  }, async (input) => formatExtractUrl(await executor.extractUrl(input), input));
28720
+ server.registerTool("diff_page", {
28721
+ title: "Page Change Check",
28722
+ description: "Check whether a public URL has changed since you last checked it with this tool: scrapes the current page, diffs it against your last stored snapshot for that URL, and returns what was added or removed (or confirms no change). Stores the new snapshot as the baseline for next time \u2014 on-demand only, no automatic recurring checks. Use extract_url instead when you just want the page's current content with nothing to compare against.",
28723
+ inputSchema: DiffPageInputSchema,
28724
+ outputSchema: recordOutputSchema("diff_page", DiffPageOutputSchema),
28725
+ annotations: liveWebToolAnnotations("Page Change Check")
28726
+ }, async (input) => formatDiffPage(await executor.diffPage(input), input));
28578
28727
  server.registerTool("map_site_urls", {
28579
28728
  title: "Site URL Map",
28580
28729
  description: `Map/crawl a public website for a sitemap, URL inventory, or broken-link scan. Returns internal URLs with HTTP status; ${fileBehavior("maps over 500 URLs are written to a local CSV file instead of inlined.", "large results are stored as a retrievable artifact \u2014 you get an inline summary plus an artifactId for report_artifact_read.")}`,
@@ -29006,6 +29155,9 @@ var init_http_mcp_tool_executor = __esm({
29006
29155
  extractUrl(input) {
29007
29156
  return this.call("/extract-url", input);
29008
29157
  }
29158
+ diffPage(input) {
29159
+ return this.call("/diff-page", input);
29160
+ }
29009
29161
  mapSiteUrls(input) {
29010
29162
  return this.call("/map-urls", input);
29011
29163
  }
@@ -37392,6 +37544,77 @@ var init_site_audit_worker = __esm({
37392
37544
  }
37393
37545
  });
37394
37546
 
37547
+ // src/api/page-diff.ts
37548
+ function sha256Hex(value) {
37549
+ return (0, import_node_crypto14.createHash)("sha256").update(value).digest("hex");
37550
+ }
37551
+ function truncateForStorage(value, maxChars = MAX_SNAPSHOT_CONTENT_CHARS) {
37552
+ if (value.length <= maxChars) return { value, truncated: false };
37553
+ return { value: value.slice(0, maxChars), truncated: true };
37554
+ }
37555
+ function changeLines(change) {
37556
+ const lines = change.value.split("\n");
37557
+ if (lines.length > 0 && lines[lines.length - 1] === "") lines.pop();
37558
+ return lines;
37559
+ }
37560
+ function diffPageContent(oldContent, newContent) {
37561
+ const changes = (0, import_diff.diffLines)(oldContent, newContent);
37562
+ const rawHunks = [];
37563
+ let linesAdded = 0;
37564
+ let linesRemoved = 0;
37565
+ let totalChangedLineCount = 0;
37566
+ for (const change of changes) {
37567
+ if (!change.added && !change.removed) continue;
37568
+ const lines = changeLines(change);
37569
+ totalChangedLineCount += lines.length;
37570
+ if (change.added) linesAdded += lines.length;
37571
+ if (change.removed) linesRemoved += lines.length;
37572
+ rawHunks.push({ type: change.added ? "added" : "removed", lines });
37573
+ }
37574
+ let hunksTruncated = false;
37575
+ const hunksWithinCount = rawHunks.length > MAX_DIFF_HUNKS ? rawHunks.slice(0, MAX_DIFF_HUNKS) : rawHunks;
37576
+ if (rawHunks.length > MAX_DIFF_HUNKS) hunksTruncated = true;
37577
+ const hunks = [];
37578
+ let emittedLines = 0;
37579
+ for (const hunk of hunksWithinCount) {
37580
+ if (emittedLines >= MAX_DIFF_LINES_PER_RESPONSE) {
37581
+ hunksTruncated = true;
37582
+ break;
37583
+ }
37584
+ const remaining = MAX_DIFF_LINES_PER_RESPONSE - emittedLines;
37585
+ if (hunk.lines.length > remaining) {
37586
+ hunks.push({ type: hunk.type, lines: hunk.lines.slice(0, remaining) });
37587
+ emittedLines += remaining;
37588
+ hunksTruncated = true;
37589
+ break;
37590
+ }
37591
+ hunks.push(hunk);
37592
+ emittedLines += hunk.lines.length;
37593
+ }
37594
+ const denominator = Math.max(oldContent.split("\n").length, newContent.split("\n").length, 1);
37595
+ const percentChanged = totalChangedLineCount > 0 ? Math.min(100, Math.round(totalChangedLineCount / denominator * 1e3) / 10) : 0;
37596
+ return {
37597
+ hunks,
37598
+ linesAdded,
37599
+ linesRemoved,
37600
+ percentChanged,
37601
+ hunksTruncated,
37602
+ hunksTruncatedReason: hunksTruncated ? `Showing ${emittedLines} of ${totalChangedLineCount} changed lines (capped at ${MAX_DIFF_HUNKS} hunks / ${MAX_DIFF_LINES_PER_RESPONSE} lines). Use contentHash/previousContentHash to confirm the full scope of change.` : null,
37603
+ totalChangedLineCount
37604
+ };
37605
+ }
37606
+ var import_node_crypto14, import_diff, MAX_SNAPSHOT_CONTENT_CHARS, MAX_DIFF_HUNKS, MAX_DIFF_LINES_PER_RESPONSE;
37607
+ var init_page_diff = __esm({
37608
+ "src/api/page-diff.ts"() {
37609
+ "use strict";
37610
+ import_node_crypto14 = require("crypto");
37611
+ import_diff = require("diff");
37612
+ MAX_SNAPSHOT_CONTENT_CHARS = 25e4;
37613
+ MAX_DIFF_HUNKS = 200;
37614
+ MAX_DIFF_LINES_PER_RESPONSE = 2e3;
37615
+ }
37616
+ });
37617
+
37395
37618
  // src/api/scrape-vault-sink.ts
37396
37619
  async function depositScrapeToVault(user, opts) {
37397
37620
  try {
@@ -38089,6 +38312,7 @@ var init_server = __esm({
38089
38312
  import_stripe2 = __toESM(require("stripe"), 1);
38090
38313
  init_rates();
38091
38314
  init_server_schemas();
38315
+ init_page_diff();
38092
38316
  init_scrape_vault_sink();
38093
38317
  init_scrape_blob_cleanup();
38094
38318
  init_schemas3();
@@ -38988,6 +39212,91 @@ var init_server = __esm({
38988
39212
  await releaseConcurrencyGate(gate.lockId);
38989
39213
  }
38990
39214
  });
39215
+ app.post("/diff-page", auth2, async (c) => {
39216
+ const raw = await c.req.json().catch(() => ({}));
39217
+ const bodyResult = DiffPageBodySchema.safeParse(raw);
39218
+ if (!bodyResult.success) return c.json({ error: bodyResult.error.issues[0]?.message ?? "Invalid request" }, 400);
39219
+ const { url, allowLocal, resetBaseline } = bodyResult.data;
39220
+ if (!allowLocal) {
39221
+ const checked = await validatePublicHttpUrl(url, { field: "URL" });
39222
+ if (checked.error || !checked.parsed) return c.json({ error: checked.error ?? "Invalid URL" }, 400);
39223
+ } else {
39224
+ try {
39225
+ new URL(url);
39226
+ } catch {
39227
+ return c.json({ error: "Invalid URL" }, 400);
39228
+ }
39229
+ }
39230
+ const canonicalUrl = (() => {
39231
+ try {
39232
+ return new URL(url).href;
39233
+ } catch {
39234
+ return url;
39235
+ }
39236
+ })();
39237
+ const user = c.get("user");
39238
+ const gate = await acquireConcurrencyGate(user, "diff_page", {
39239
+ reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
39240
+ metadata: { url: canonicalUrl }
39241
+ });
39242
+ if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
39243
+ let debited = false;
39244
+ try {
39245
+ const { ok: dpOk, balance_mc: dpBal } = await debitMc(user.id, MC_COSTS.diff_page, LedgerOperation.DIFF_PAGE, new URL(canonicalUrl).hostname);
39246
+ if (!dpOk) return c.json(insufficientBalanceResponse(dpBal, MC_COSTS.diff_page), 402);
39247
+ debited = true;
39248
+ const existingSnapshot = await getPageSnapshot(user.id, canonicalUrl);
39249
+ const previous = resetBaseline ? null : existingSnapshot;
39250
+ const kernelApiKey = browserServiceApiKey();
39251
+ const result = await extractKpo({ url: canonicalUrl, kernelApiKey });
39252
+ const currentContent = result.bodyMarkdown ?? "";
39253
+ const currentHash = sha256Hex(currentContent);
39254
+ const { value: storedContent, truncated: contentTruncated } = truncateForStorage(currentContent);
39255
+ const currentCheckedAt = (/* @__PURE__ */ new Date()).toISOString();
39256
+ await upsertPageSnapshot(user.id, canonicalUrl, {
39257
+ contentHash: currentHash,
39258
+ content: storedContent,
39259
+ title: result.title ?? null,
39260
+ contentBytes: Buffer.byteLength(currentContent, "utf8"),
39261
+ truncated: contentTruncated
39262
+ });
39263
+ let status;
39264
+ let diff = { hunks: [], linesAdded: 0, linesRemoved: 0, percentChanged: 0, hunksTruncated: false, hunksTruncatedReason: null, totalChangedLineCount: 0 };
39265
+ if (!previous) {
39266
+ status = "baseline";
39267
+ diff.percentChanged = null;
39268
+ } else if (previous.contentHash === currentHash) {
39269
+ status = "unchanged";
39270
+ } else {
39271
+ status = "changed";
39272
+ diff = diffPageContent(previous.content, storedContent);
39273
+ }
39274
+ await logRequestEvent({ userId: user.id, source: "diff_page", status: "done", query: canonicalUrl, result: { status } });
39275
+ return c.json({
39276
+ url: canonicalUrl,
39277
+ title: result.title ?? null,
39278
+ status,
39279
+ isReset: !!resetBaseline && !!existingSnapshot,
39280
+ previousCheckedAt: previous?.checkedAt ?? null,
39281
+ currentCheckedAt,
39282
+ contentHash: currentHash,
39283
+ previousContentHash: previous?.contentHash ?? null,
39284
+ summary: { linesAdded: diff.linesAdded, linesRemoved: diff.linesRemoved, percentChanged: diff.percentChanged },
39285
+ hunks: diff.hunks,
39286
+ contentTruncated,
39287
+ hunksTruncated: diff.hunksTruncated,
39288
+ hunksTruncatedReason: diff.hunksTruncatedReason,
39289
+ totalChangedLineCount: diff.totalChangedLineCount
39290
+ });
39291
+ } catch (err) {
39292
+ const msg = err instanceof Error ? err.message : String(err);
39293
+ if (debited) await creditMc(user.id, MC_COSTS.diff_page, LedgerOperation.DIFF_PAGE, "failed call");
39294
+ await logRequestEvent({ userId: user.id, source: "diff_page", status: "failed", query: canonicalUrl, error: msg });
39295
+ return c.json({ error: msg }, 500);
39296
+ } finally {
39297
+ await releaseConcurrencyGate(gate.lockId);
39298
+ }
39299
+ });
38991
39300
  app.post("/map-urls", auth2, async (c) => {
38992
39301
  const raw = await c.req.json().catch(() => ({}));
38993
39302
  const bodyResult = MapUrlsBodySchema.safeParse(raw);