mcp-scraper 0.39.0 → 0.40.2

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 (80) hide show
  1. package/README.md +1 -1
  2. package/dist/bin/api-server.cjs +382 -44
  3. package/dist/bin/api-server.cjs.map +1 -1
  4. package/dist/bin/api-server.js +3 -3
  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 +1 -1
  9. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  10. package/dist/bin/mcp-scraper-install.js +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs +6 -6
  12. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  13. package/dist/bin/mcp-stdio-server.js +4 -4
  14. package/dist/bin/paa-harvest.cjs.map +1 -1
  15. package/dist/bin/paa-harvest.js +3 -3
  16. package/dist/{chunk-DO6WGZRS.js → chunk-3PIWJS6Y.js} +2 -2
  17. package/dist/{chunk-YUE46JGV.js → chunk-7XBBFBYY.js} +2 -2
  18. package/dist/{chunk-AFY6DNL7.js → chunk-GUVKHCKE.js} +13 -3
  19. package/dist/chunk-GUVKHCKE.js.map +1 -0
  20. package/dist/{chunk-TJRCAP7C.js → chunk-LP6E462I.js} +8 -8
  21. package/dist/chunk-LP6E462I.js.map +1 -0
  22. package/dist/{chunk-4WAE4AV5.js → chunk-NVXNEOUQ.js} +2 -2
  23. package/dist/{chunk-3HOANNNR.js → chunk-X2LKCX6H.js} +2 -2
  24. package/dist/{chunk-ATWQRW3K.js → chunk-XORPNO3Z.js} +2 -2
  25. package/dist/chunk-ZUJLSICT.js +7 -0
  26. package/dist/chunk-ZUJLSICT.js.map +1 -0
  27. package/dist/{db-6AJW47NF.js → db-W3CP562I.js} +2 -2
  28. package/dist/{extract-bundle-CWG35SKV.js → extract-bundle-K4PG3RZJ.js} +3 -3
  29. package/dist/index.cjs.map +1 -1
  30. package/dist/index.js +3 -3
  31. package/dist/{location-data-repository-BGKZF7DV.js → location-data-repository-RLQX6SNM.js} +3 -3
  32. package/dist/{server-BXNVNAMT.js → server-KM3CFWCF.js} +373 -52
  33. package/dist/server-KM3CFWCF.js.map +1 -0
  34. package/dist/{site-extract-repository-QZKZSQLQ.js → site-extract-repository-2SMMFKKL.js} +3 -3
  35. package/dist/{worker-YTIMPE57.js → worker-FXAGFYOE.js} +5 -5
  36. package/package.json +7 -7
  37. package/dist/chunk-6NWL3MVU.js +0 -7
  38. package/dist/chunk-6NWL3MVU.js.map +0 -1
  39. package/dist/chunk-AFY6DNL7.js.map +0 -1
  40. package/dist/chunk-TJRCAP7C.js.map +0 -1
  41. package/dist/server-BXNVNAMT.js.map +0 -1
  42. package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +0 -58
  43. package/docs/adr/0002-hybrid-smart-rag-vault-retrieval.md +0 -62
  44. package/docs/adr/0003-waive-unrecoverable-scheduled-model-cost.md +0 -22
  45. package/docs/adr/2026-07-28-owned-aug-webinar-registration.md +0 -61
  46. package/docs/adr/README.md +0 -13
  47. package/docs/final-tooling-spec.md +0 -206
  48. package/docs/hosted-location-data.md +0 -108
  49. package/docs/kernel-proxy-future-enhancements.md +0 -80
  50. package/docs/mcp-tool-craft-lint.generated.md +0 -186
  51. package/docs/mcp-tool-design-guide.md +0 -225
  52. package/docs/mcp-tool-manifest.generated.json +0 -23305
  53. package/docs/mcp-tool-quality-spec.md +0 -240
  54. package/docs/oauth-legal-review.md +0 -38
  55. package/docs/seo-crawl-report-spec.md +0 -287
  56. package/docs/specs/api-forge-spec.md +0 -234
  57. package/docs/specs/connected-services-control-plane-decoupling-spec.md +0 -1044
  58. package/docs/specs/deferred-work-spec.md +0 -86
  59. package/docs/specs/google-drive-bulk-access-and-mcp-schema-passthrough-spec.md +0 -1689
  60. package/docs/specs/kernel-stealth-captcha-test-matrix.md +0 -278
  61. package/docs/specs/main-mcp-integration-ownership-spec.md +0 -1164
  62. package/docs/specs/mcp-tool-definition-quality-audit-spec.md +0 -1602
  63. package/docs/specs/meta-ad-creative-media-resolution-spec.md +0 -31
  64. package/docs/specs/multimodal-image-memory-architecture-spec.md +0 -1022
  65. package/docs/specs/oauth-mcp-spec.md +0 -213
  66. package/docs/specs/plan-visibility-and-management-roadmap.md +0 -293
  67. package/docs/specs/query-fanout-transport-contract-fix.md +0 -45
  68. package/docs/specs/relationship-workspace-ai-behavior-plan.md +0 -26
  69. package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +0 -995
  70. package/docs/tool-catalog-spec.md +0 -388
  71. /package/dist/{chunk-DO6WGZRS.js.map → chunk-3PIWJS6Y.js.map} +0 -0
  72. /package/dist/{chunk-YUE46JGV.js.map → chunk-7XBBFBYY.js.map} +0 -0
  73. /package/dist/{chunk-4WAE4AV5.js.map → chunk-NVXNEOUQ.js.map} +0 -0
  74. /package/dist/{chunk-3HOANNNR.js.map → chunk-X2LKCX6H.js.map} +0 -0
  75. /package/dist/{chunk-ATWQRW3K.js.map → chunk-XORPNO3Z.js.map} +0 -0
  76. /package/dist/{db-6AJW47NF.js.map → db-W3CP562I.js.map} +0 -0
  77. /package/dist/{extract-bundle-CWG35SKV.js.map → extract-bundle-K4PG3RZJ.js.map} +0 -0
  78. /package/dist/{location-data-repository-BGKZF7DV.js.map → location-data-repository-RLQX6SNM.js.map} +0 -0
  79. /package/dist/{site-extract-repository-QZKZSQLQ.js.map → site-extract-repository-2SMMFKKL.js.map} +0 -0
  80. /package/dist/{worker-YTIMPE57.js.map → worker-FXAGFYOE.js.map} +0 -0
@@ -9,13 +9,13 @@ import {
9
9
  importHostedCensusPlacesCsv,
10
10
  importHostedZipGroupsCsv,
11
11
  listActiveHostedLocationDatasets
12
- } from "./chunk-ATWQRW3K.js";
12
+ } from "./chunk-XORPNO3Z.js";
13
13
  import {
14
14
  classifyHarvestProblem,
15
15
  createHarvestAttemptRecorder,
16
16
  harvestProblemResponse,
17
17
  serializeHarvestProblem
18
- } from "./chunk-4WAE4AV5.js";
18
+ } from "./chunk-NVXNEOUQ.js";
19
19
  import {
20
20
  BACKUP_LAST_RESORT_ATTEMPTS,
21
21
  BrowserDriver,
@@ -29,7 +29,7 @@ import {
29
29
  resolveBackupKernelProxyId,
30
30
  resolveKernelProxyId,
31
31
  serpArrivalUrl
32
- } from "./chunk-YUE46JGV.js";
32
+ } from "./chunk-7XBBFBYY.js";
33
33
  import {
34
34
  CreateEditorialReadingRoomInputSchema,
35
35
  HttpMcpToolExecutor,
@@ -58,7 +58,7 @@ import {
58
58
  sanitizeAttempts,
59
59
  sanitizeHarvestResult,
60
60
  transcribeMediaUrl
61
- } from "./chunk-TJRCAP7C.js";
61
+ } from "./chunk-LP6E462I.js";
62
62
  import {
63
63
  auditImageUrls,
64
64
  auditImages,
@@ -78,7 +78,7 @@ import {
78
78
  recordKernelSession,
79
79
  recordVendorUsage,
80
80
  runWithCostContext
81
- } from "./chunk-DO6WGZRS.js";
81
+ } from "./chunk-3PIWJS6Y.js";
82
82
  import {
83
83
  listWorkflowDefinitions,
84
84
  runWorkflow,
@@ -105,7 +105,7 @@ import {
105
105
  RawMapsOverviewSchema,
106
106
  RawMapsReviewStatsSchema
107
107
  } from "./chunk-CB5C3BPB.js";
108
- import "./chunk-6NWL3MVU.js";
108
+ import "./chunk-ZUJLSICT.js";
109
109
  import {
110
110
  abandonExtractSettlement,
111
111
  countSuccessfulPages,
@@ -128,7 +128,7 @@ import {
128
128
  setExtractJobTotal,
129
129
  settleExtractJob,
130
130
  terminalExtractJobStatus
131
- } from "./chunk-3HOANNNR.js";
131
+ } from "./chunk-X2LKCX6H.js";
132
132
  import {
133
133
  BROWSER_OPEN_MIN_BALANCE_MC,
134
134
  CONCURRENCY_PRICE_ID,
@@ -273,7 +273,7 @@ import {
273
273
  softDeleteInboxMessage,
274
274
  upsertPageSnapshot,
275
275
  verifyPassword
276
- } from "./chunk-AFY6DNL7.js";
276
+ } from "./chunk-GUVKHCKE.js";
277
277
 
278
278
  // src/blog/registry.ts
279
279
  var posts = [
@@ -8618,7 +8618,7 @@ var siteExtractFn = inngest.createFunction(
8618
8618
  });
8619
8619
  }) : null;
8620
8620
  const finalized = await step.run("finalize", async () => {
8621
- const { assembleExtractArtifacts } = await import("./extract-bundle-CWG35SKV.js");
8621
+ const { assembleExtractArtifacts } = await import("./extract-bundle-K4PG3RZJ.js");
8622
8622
  const current = await getExtractJob(jobId2);
8623
8623
  if (!current) throw new Error("extract job disappeared before finalization");
8624
8624
  const stored = await assembleExtractArtifacts(current, { branding, imageAudit });
@@ -9560,7 +9560,7 @@ async function resolveUsZipsPath(requestedPath) {
9560
9560
  async function loadZipGroups(stateAbbr, requestedPath, warnings, sourceMode) {
9561
9561
  const mode = resolvedLocationDataMode({ zipSource: sourceMode });
9562
9562
  if (mode === "hosted") {
9563
- const { getHostedZipGroups } = await import("./location-data-repository-BGKZF7DV.js");
9563
+ const { getHostedZipGroups } = await import("./location-data-repository-RLQX6SNM.js");
9564
9564
  const hosted = await getHostedZipGroups(stateAbbr);
9565
9565
  if (!hosted.dataset) {
9566
9566
  throw new DirectoryLocationError(
@@ -9623,7 +9623,7 @@ async function resolveDirectoryMarkets(options, config = {}) {
9623
9623
  const sourceUrl = censusStateUrl(state.fips);
9624
9624
  const warnings = [];
9625
9625
  if (resolvedLocationDataMode(config) === "hosted") {
9626
- const { queryHostedLocationMarkets } = await import("./location-data-repository-BGKZF7DV.js");
9626
+ const { queryHostedLocationMarkets } = await import("./location-data-repository-RLQX6SNM.js");
9627
9627
  const hosted = await queryHostedLocationMarkets({
9628
9628
  state: state.abbr,
9629
9629
  minPopulation: options.minPopulation,
@@ -26849,6 +26849,253 @@ import { Hono as Hono28 } from "hono";
26849
26849
  import { Resend as Resend2 } from "resend";
26850
26850
  import { z as z28 } from "zod";
26851
26851
 
26852
+ // src/email/workshop-field-notes.ts
26853
+ var WORKSHOP_FIELD_NOTES_URL = "https://mcp-field-notes.vercel.app/";
26854
+ var workshopFieldNotes = [
26855
+ {
26856
+ slug: "operating-model",
26857
+ category: "Start Here",
26858
+ title: "The Operating Model",
26859
+ snippet: [
26860
+ "MCP Scraper is an operating system for evidence work, not simply a page fetcher.",
26861
+ "It connects a question to the right sources, method, and durable output.",
26862
+ "This note gives you the mental model for everything else."
26863
+ ]
26864
+ },
26865
+ {
26866
+ slug: "context-aware-vault-capture",
26867
+ category: "Start Here",
26868
+ title: "Context-Aware Vault Capture",
26869
+ snippet: [
26870
+ "This demonstrates what a governed memory write actually looks like.",
26871
+ "The agent inspects the vault, chooses the destination, writes with supported links and tags, then reads it back.",
26872
+ "The final retrieval check proves the material can be found later."
26873
+ ]
26874
+ },
26875
+ {
26876
+ slug: "complete-competitive-surface-scan",
26877
+ category: "Start Here",
26878
+ title: "The Complete Competitive Surface Scan",
26879
+ snippet: [
26880
+ "A competitive scan is complete when it covers the evidence needed for a decision.",
26881
+ "That might combine search, sites, reviews, community language, ads, or video without scraping everything.",
26882
+ "This note shows how to define coverage and report gaps honestly."
26883
+ ]
26884
+ },
26885
+ {
26886
+ slug: "search-serp-and-paa",
26887
+ category: "Research Surfaces & Systems",
26888
+ title: "Search, SERPs, and People Also Ask",
26889
+ snippet: [
26890
+ "Rankings, questions, local results, sources, and AI answers are different kinds of search evidence.",
26891
+ "A fast SERP and a deeper People Also Ask harvest answer different questions.",
26892
+ "This note shows how to choose the least expensive search depth that changes the decision."
26893
+ ]
26894
+ },
26895
+ {
26896
+ slug: "reddit-and-community-research",
26897
+ category: "Research Surfaces & Systems",
26898
+ title: "Reddit and Community Research",
26899
+ snippet: [
26900
+ "Community research exposes language, objections, workarounds, and disagreement that polished pages often hide.",
26901
+ "It should preserve thread context and dissent instead of turning comments into a market-wide vote.",
26902
+ "This note explains discovery, deep reading, and careful synthesis."
26903
+ ]
26904
+ },
26905
+ {
26906
+ slug: "browser-profiles-and-ai-fanout",
26907
+ category: "Research Surfaces & Systems",
26908
+ title: "Browser Profiles and AI Citation Fan-Out",
26909
+ snippet: [
26910
+ "A browser profile preserves delegated access to sites that require login.",
26911
+ "Citation fan-out studies the subqueries, browsed sources, and citations inside an AI answer.",
26912
+ "This note keeps the authenticated surface and the research method conceptually separate."
26913
+ ]
26914
+ },
26915
+ {
26916
+ slug: "youtube-and-video-research",
26917
+ category: "Research Surfaces & Systems",
26918
+ title: "YouTube and Video Research",
26919
+ snippet: [
26920
+ "Video research can stop at discovery, continue to transcription, or go deeper into frame analysis.",
26921
+ "Each level costs more and answers a different question.",
26922
+ "This note shows how to choose the cheapest sufficient depth."
26923
+ ]
26924
+ },
26925
+ {
26926
+ slug: "advertising-intelligence",
26927
+ category: "Research Surfaces & Systems",
26928
+ title: "Advertising Intelligence",
26929
+ snippet: [
26930
+ "Advertising intelligence studies the messages, offers, proof, formats, and creative patterns a company distributes.",
26931
+ "It does not prove spend, profitability, or performance.",
26932
+ "This note maps the right evidence path for Meta, Google, public libraries, and authorized account creative."
26933
+ ]
26934
+ },
26935
+ {
26936
+ slug: "search-console-and-analytics-skills",
26937
+ category: "Connected Data & Automation",
26938
+ title: "Search Console and Analytics Skills",
26939
+ snippet: [
26940
+ "Search Console and Analytics supply first-party evidence, not strategy by themselves.",
26941
+ "A user-owned skill adds business doctrine, comparison rules, and priorities.",
26942
+ "This note shows how connected data, public research, Memory, and scheduling become a repeatable decision system."
26943
+ ]
26944
+ },
26945
+ {
26946
+ slug: "scheduled-gmail-zoom-and-research-digestion",
26947
+ category: "Connected Data & Automation",
26948
+ title: "Scheduled Gmail, Zoom, and Research Digestion",
26949
+ snippet: [
26950
+ "Scheduled digestion turns accumulating email, meetings, and research into bounded durable updates.",
26951
+ "It should not silently bulk-import an account or invent people, tasks, deals, or projects.",
26952
+ "This note defines the source, time window, filters, classifications, and destination that make a digest safe."
26953
+ ]
26954
+ },
26955
+ {
26956
+ slug: "rank-tracking-and-change-monitoring",
26957
+ category: "Connected Data & Automation",
26958
+ title: "Rank Tracking and Change Monitoring",
26959
+ snippet: [
26960
+ "A tracker compares observations, and a monitor remembers a baseline before looking again.",
26961
+ "Repeating the same prompt does not create either system.",
26962
+ "This note explains stable subjects, comparable inputs, materiality thresholds, stored history, and change receipts."
26963
+ ]
26964
+ },
26965
+ {
26966
+ slug: "research-artifacts-and-reading-rooms",
26967
+ category: "Artifacts, Memory & Skills",
26968
+ title: "Research Artifacts and Reading Rooms",
26969
+ snippet: [
26970
+ "Research should end in something people can inspect, reopen, and use.",
26971
+ "MCP Scraper can choose among inline reports, private artifacts, tables, governed Memory, and editorial reading rooms.",
26972
+ "This note explains which output belongs where."
26973
+ ]
26974
+ },
26975
+ {
26976
+ slug: "skills-as-user-owned-judgment",
26977
+ category: "Artifacts, Memory & Skills",
26978
+ title: "Skills as User-Owned Judgment",
26979
+ snippet: [
26980
+ "MCP Scraper supplies evidence surfaces and operating primitives.",
26981
+ "A skill supplies the user\u2019s doctrine, sequence, standards, rejection rules, storage choices, and verification.",
26982
+ "This note shows how repeated corrections become reusable judgment."
26983
+ ]
26984
+ },
26985
+ {
26986
+ slug: "seo-and-content",
26987
+ category: "Applications & Contexts",
26988
+ title: "SEO & Content",
26989
+ snippet: [
26990
+ "Start with a real search decision, then inspect the live SERP, questions, and competing pages.",
26991
+ "Preserve the evidence before turning it into a source-backed brief.",
26992
+ "Memory and monitoring make the method reusable instead of disposable."
26993
+ ]
26994
+ },
26995
+ {
26996
+ slug: "local-markets",
26997
+ category: "Applications & Contexts",
26998
+ title: "Local Markets",
26999
+ snippet: [
27000
+ "Local research combines businesses, services, reviews, positioning, search visibility, and geography.",
27001
+ "The goal is to find testable gaps without flattening every source into one score.",
27002
+ "This note shows how to preserve a market baseline and monitor meaningful change."
27003
+ ]
27004
+ },
27005
+ {
27006
+ slug: "sales-and-client-work",
27007
+ category: "Applications & Contexts",
27008
+ title: "Sales & Client Work",
27009
+ snippet: [
27010
+ "This is a relationship graph, not a contact database.",
27011
+ "People, organizations, opportunities, projects, conversations, meetings, decisions, and follow-ups remain separate records connected by verified links.",
27012
+ "This note shows why that distinction prevents invented CRM context."
27013
+ ]
27014
+ },
27015
+ {
27016
+ slug: "competitor-monitoring",
27017
+ category: "Applications & Contexts",
27018
+ title: "Competitor Monitoring",
27019
+ snippet: [
27020
+ "Monitoring begins with a dated baseline and a defined set of things worth watching.",
27021
+ "Each run compares trustworthy evidence and classifies only material change.",
27022
+ "The output reports the difference, including source failure and meaningful no-change."
27023
+ ]
27024
+ },
27025
+ {
27026
+ slug: "learning-and-research",
27027
+ category: "Applications & Contexts",
27028
+ title: "Learning & Research",
27029
+ snippet: [
27030
+ "Raw articles, transcripts, pages, and datasets belong in Library with provenance intact.",
27031
+ "Reusable lessons belong separately in Knowledge and link back to their sources.",
27032
+ "This note explains recall by meaning, time, or graph without confusing relevance with truth."
27033
+ ]
27034
+ },
27035
+ {
27036
+ slug: "build-your-own-tools",
27037
+ category: "Applications & Contexts",
27038
+ title: "Build Your Own Tools",
27039
+ snippet: [
27040
+ "The same acquisition, browser, connection, Memory, artifact, workflow, and scheduling primitives can sit behind something you build.",
27041
+ "That could be a skill, internal process, client report, monitoring service, or narrow SaaS.",
27042
+ "This note starts from one recurring decision and defines the product contract around it."
27043
+ ]
27044
+ },
27045
+ {
27046
+ slug: "improve-originating-skill",
27047
+ category: "How MCP Scraper Learned",
27048
+ title: "Improve the Originating Skill",
27049
+ snippet: [
27050
+ "A corrected output often reveals a problem in the workflow that produced it.",
27051
+ "This reflection method traces the failure to the highest owning layer and improves the originating skill or validator.",
27052
+ "Future work then inherits the lesson instead of repeating the same repair."
27053
+ ]
27054
+ },
27055
+ {
27056
+ slug: "landing-page-problem-solving",
27057
+ category: "How MCP Scraper Learned",
27058
+ title: "Problem-Solving Lessons for Use-Case Pages",
27059
+ snippet: [
27060
+ "This is the concrete receipt from applying that reflection method to MCP Scraper landing pages.",
27061
+ "It captures the fixes for generic copy, weak proof, wrong archetypes, visible seams, inconsistent calls to action, and missed mobile defects.",
27062
+ "The result is a stronger generator, not merely one repaired page."
27063
+ ]
27064
+ },
27065
+ {
27066
+ slug: "workshop-playbook",
27067
+ category: "Workshop",
27068
+ title: "MCP Scraper Workshop Prompt Playbook",
27069
+ snippet: [
27070
+ "This is the practical prompt set for the live session.",
27071
+ "It covers research surfaces, connected data, authenticated browser work, scheduling, skills, completeness receipts, and governed Memory.",
27072
+ "You can replace the examples with your own business, competitors, accounts, and questions."
27073
+ ]
27074
+ }
27075
+ ];
27076
+ function escapeHtml3(value) {
27077
+ return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
27078
+ }
27079
+ function workshopFieldNoteUrl(slug) {
27080
+ return `${WORKSHOP_FIELD_NOTES_URL}?article=${encodeURIComponent(slug)}`;
27081
+ }
27082
+ function renderWorkshopFieldNotesBody() {
27083
+ let activeCategory = "";
27084
+ return [
27085
+ '<p style="margin:0">The written companion to our live workshop is already available in MCP Scraper Field Notes. You do not need to read all of it beforehand\u2014browse the subjects that match the work you already do.</p>',
27086
+ ...workshopFieldNotes.flatMap((note, index) => {
27087
+ const category = note.category === activeCategory ? [] : [`<h2 style="margin:${index === 0 ? 32 : 40}px 0 12px;color:#2563eb;font-size:13px;line-height:1.3;letter-spacing:.08em;text-transform:uppercase">${escapeHtml3(note.category)}</h2>`];
27088
+ activeCategory = note.category;
27089
+ const url = escapeHtml3(workshopFieldNoteUrl(note.slug));
27090
+ const sentences = note.snippet.map(escapeHtml3).join(" ");
27091
+ return [
27092
+ ...category,
27093
+ `<div style="padding:20px 0;border-top:1px solid #e5e7eb"><div style="color:#6b7280;font-size:12px;font-weight:700;letter-spacing:.06em">${String(index + 1).padStart(2, "0")}</div><h3 style="margin:6px 0 8px;font-size:19px;line-height:1.3"><a href="${url}" style="color:#101114;text-decoration:none">${escapeHtml3(note.title)}</a></h3><p style="margin:0;color:#454952;font-size:15px;line-height:1.65">${sentences}</p><p style="margin:10px 0 0"><a href="${url}" style="color:#2563eb;font-size:14px;font-weight:700;text-decoration:none">Read this note \u2192</a></p></div>`
27094
+ ];
27095
+ })
27096
+ ].join("");
27097
+ }
27098
+
26852
27099
  // src/api/workshop-registration-repository.ts
26853
27100
  import { randomUUID as randomUUID12 } from "crypto";
26854
27101
  function rowToRegistration(row) {
@@ -26863,6 +27110,8 @@ function rowToRegistration(row) {
26863
27110
  zoomJoinUrl: row.zoom_join_url == null ? null : String(row.zoom_join_url),
26864
27111
  resendContactId: row.resend_contact_id == null ? null : String(row.resend_contact_id),
26865
27112
  confirmationEmailId: row.confirmation_email_id == null ? null : String(row.confirmation_email_id),
27113
+ fieldNotesEmailId: row.field_notes_email_id == null ? null : String(row.field_notes_email_id),
27114
+ reminder48hEmailId: row.reminder_48h_email_id == null ? null : String(row.reminder_48h_email_id),
26866
27115
  reminder24hEmailId: row.reminder_24h_email_id == null ? null : String(row.reminder_24h_email_id),
26867
27116
  reminder1hEmailId: row.reminder_1h_email_id == null ? null : String(row.reminder_1h_email_id),
26868
27117
  lastError: row.last_error == null ? null : String(row.last_error)
@@ -26962,6 +27211,8 @@ async function updateWorkshopRegistration(id, patch) {
26962
27211
  zoom_join_url: patch.zoomJoinUrl,
26963
27212
  resend_contact_id: patch.resendContactId,
26964
27213
  confirmation_email_id: patch.confirmationEmailId,
27214
+ field_notes_email_id: patch.fieldNotesEmailId,
27215
+ reminder_48h_email_id: patch.reminder48hEmailId,
26965
27216
  reminder_24h_email_id: patch.reminder24hEmailId,
26966
27217
  reminder_1h_email_id: patch.reminder1hEmailId,
26967
27218
  last_error: patch.lastError
@@ -27021,10 +27272,19 @@ function workshopReminderSchedule(startAt) {
27021
27272
  const start = Date.parse(startAt);
27022
27273
  if (!Number.isFinite(start)) throw new Error("Workshop start time is invalid.");
27023
27274
  return {
27275
+ reminder48h: new Date(start - 48 * 60 * 60 * 1e3).toISOString(),
27024
27276
  reminder24h: new Date(start - 24 * 60 * 60 * 1e3).toISOString(),
27025
27277
  reminder1h: new Date(start - 60 * 60 * 1e3).toISOString()
27026
27278
  };
27027
27279
  }
27280
+ function workshopRemindersComplete(registration, schedule, now = Date.now()) {
27281
+ const reminders = [
27282
+ [registration.reminder48hEmailId, schedule.reminder48h],
27283
+ [registration.reminder24hEmailId, schedule.reminder24h],
27284
+ [registration.reminder1hEmailId, schedule.reminder1h]
27285
+ ];
27286
+ return reminders.every(([emailId, scheduledAt]) => Boolean(emailId) || Date.parse(scheduledAt) <= now);
27287
+ }
27028
27288
  function allowedOrigins() {
27029
27289
  const origins = /* @__PURE__ */ new Set(["https://mcpscraper.dev", "https://www.mcpscraper.dev"]);
27030
27290
  for (const value of (process.env.ALLOWED_ORIGINS ?? process.env.APP_ORIGIN ?? "").split(",")) {
@@ -27039,12 +27299,12 @@ function requestOriginAllowed(origin) {
27039
27299
  function clientIp(headers) {
27040
27300
  return headers.get("cf-connecting-ip") || headers.get("x-real-ip") || headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
27041
27301
  }
27042
- function escapeHtml3(value) {
27302
+ function escapeHtml4(value) {
27043
27303
  return value.replaceAll("&", "&amp;").replaceAll("<", "&lt;").replaceAll(">", "&gt;").replaceAll('"', "&quot;").replaceAll("'", "&#039;");
27044
27304
  }
27045
- function emailFrame(input) {
27046
- const button = input.button ? `<p style="margin:28px 0"><a href="${escapeHtml3(input.button.url)}" style="display:inline-block;padding:14px 22px;border-radius:999px;background:#0a0a0a;color:#fff;text-decoration:none;font-weight:700">${escapeHtml3(input.button.label)}</a></p>` : "";
27047
- return `<!doctype html><html><body style="margin:0;background:#f5f7fb;color:#101114;font-family:Arial,sans-serif"><div style="display:none;max-height:0;overflow:hidden">${escapeHtml3(input.eyebrow)}</div><table role="presentation" width="100%" cellspacing="0" cellpadding="0"><tr><td align="center" style="padding:32px 16px"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" style="max-width:620px;background:#fff;border-radius:24px;overflow:hidden"><tr><td style="padding:42px;background:#2563eb;color:#fff"><div style="font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase">${escapeHtml3(input.eyebrow)}</div><h1 style="margin:12px 0 0;font-size:38px;line-height:1.04;letter-spacing:-.04em">${escapeHtml3(input.title)}</h1></td></tr><tr><td style="padding:36px;font-size:16px;line-height:1.65">${input.body}${button}<p style="margin:30px 0 0;color:#686b73;font-size:13px">You received this because you registered for this workshop. It does not subscribe you to general marketing emails.</p></td></tr></table></td></tr></table></body></html>`;
27305
+ function renderWorkshopEmailHtml(input) {
27306
+ const button = input.button ? `<table role="presentation" cellspacing="0" cellpadding="0" style="margin:30px 0 0"><tr><td bgcolor="#2563eb" style="border-radius:999px"><a href="${escapeHtml4(input.button.url)}" style="display:inline-block;padding:15px 24px;border-radius:999px;background:#2563eb;color:#ffffff;text-decoration:none;font-size:16px;font-weight:700;line-height:1">${escapeHtml4(input.button.label)}</a></td></tr></table>` : "";
27307
+ return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><style>@media only screen and (max-width:620px){.email-shell{padding:16px 10px!important}.email-header{padding:30px 24px!important}.email-content{padding:30px 24px!important}.email-title{font-size:34px!important}}</style></head><body style="margin:0;background:#f5f7fb;color:#101114;font-family:Arial,sans-serif"><div style="display:none;max-height:0;overflow:hidden">${escapeHtml4(input.eyebrow)}</div><table role="presentation" width="100%" cellspacing="0" cellpadding="0" bgcolor="#f5f7fb"><tr><td class="email-shell" align="center" style="padding:32px 16px"><table role="presentation" width="100%" cellspacing="0" cellpadding="0" bgcolor="#ffffff" style="max-width:620px;background:#ffffff;border-radius:24px;overflow:hidden"><tr><td class="email-header" bgcolor="#2563eb" style="padding:42px;background:#2563eb;color:#ffffff"><table role="presentation" cellspacing="0" cellpadding="0"><tr><td><img src="https://mcpscraper.dev/email/mcp-brain-white.png" width="48" height="48" alt="MCP Scraper" style="display:block;width:48px;height:48px;border:0"></td><td style="padding-left:12px;color:#ffffff;font-size:17px;font-weight:700;letter-spacing:-.01em">MCP Scraper</td></tr></table><div style="margin-top:32px;font-size:12px;font-weight:700;letter-spacing:.08em;text-transform:uppercase;color:#dbeafe">${escapeHtml4(input.eyebrow)}</div><h1 class="email-title" style="margin:12px 0 0;color:#ffffff;font-size:42px;line-height:1.04;letter-spacing:-.04em">${escapeHtml4(input.title)}</h1></td></tr><tr><td class="email-content" style="padding:36px;color:#101114;font-size:16px;line-height:1.65">${input.body}${button}<p style="margin:32px 0 0;padding-top:24px;border-top:1px solid #e5e7eb;color:#686b73;font-size:13px;line-height:1.5">You received this because you registered for this workshop. It does not subscribe you to general marketing emails.</p></td></tr></table></td></tr></table></body></html>`;
27048
27308
  }
27049
27309
  function icsTimestamp(value) {
27050
27310
  return new Date(value).toISOString().replaceAll("-", "").replaceAll(":", "").replace(/\.\d{3}Z$/, "Z");
@@ -27094,21 +27354,26 @@ async function addEventContact(resend, registration, config) {
27094
27354
  email: registration.email,
27095
27355
  segmentId: config.resendSegmentId
27096
27356
  });
27097
- if (segment.error) throw new Error(`Resend could not add the event contact: ${segment.error.message}`);
27357
+ if (segment.error) {
27358
+ console.warn("[workshop-registration] event contact sync skipped:", segment.error.message);
27359
+ return null;
27360
+ }
27098
27361
  if (config.resendTopicId) {
27099
27362
  const topic = await resend.contacts.topics.update({
27100
27363
  email: registration.email,
27101
27364
  topics: [{ id: config.resendTopicId, subscription: "opt_in" }]
27102
27365
  });
27103
- if (topic.error) throw new Error(`Resend could not update the event topic: ${topic.error.message}`);
27366
+ if (topic.error) {
27367
+ console.warn("[workshop-registration] event topic sync skipped:", topic.error.message);
27368
+ }
27104
27369
  }
27105
27370
  return null;
27106
27371
  }
27107
27372
  async function sendConfirmation(resend, registration, config) {
27108
27373
  if (!registration.zoomJoinUrl) throw new Error("Workshop access details are not ready.");
27109
- const date = escapeHtml3(config.dateLabel);
27110
- const timezone = escapeHtml3(config.timezoneLabel);
27111
- const duration = escapeHtml3(config.durationLabel);
27374
+ const date = escapeHtml4(config.dateLabel);
27375
+ const timezone = escapeHtml4(config.timezoneLabel);
27376
+ const duration = escapeHtml4(config.durationLabel);
27112
27377
  const sent = await resend.emails.send({
27113
27378
  from: "Andrew at MCP Scraper <updates@updates.mcpscraper.dev>",
27114
27379
  replyTo: "support@updates.mcpscraper.dev",
@@ -27119,7 +27384,7 @@ async function sendConfirmation(resend, registration, config) {
27119
27384
  content: workshopInvitation(registration, config),
27120
27385
  contentType: "text/calendar; method=REQUEST; charset=UTF-8"
27121
27386
  }],
27122
- html: emailFrame({
27387
+ html: renderWorkshopEmailHtml({
27123
27388
  eyebrow: "Your live workshop seat is saved",
27124
27389
  title: `You\u2019re in, ${registration.firstName}.`,
27125
27390
  body: `<p style="margin:0">I\u2019ll show you how the pieces of MCP Scraper fit together around real work: research, Memory, connected accounts, Scheduled Actions, and reusable workflows.</p><p><strong>${date}</strong><br>${timezone}<br>${duration}</p><p>Your calendar invitation is attached, and your Zoom access link is below.</p>`,
@@ -27129,21 +27394,64 @@ async function sendConfirmation(resend, registration, config) {
27129
27394
  if (sent.error || !sent.data?.id) throw new Error(`Resend could not send confirmation: ${sent.error?.message ?? "unknown response"}`);
27130
27395
  return sent.data.id;
27131
27396
  }
27397
+ async function sendFieldNotesGuide(resend, registration) {
27398
+ const sent = await resend.emails.send({
27399
+ from: "Andrew at MCP Scraper <updates@updates.mcpscraper.dev>",
27400
+ replyTo: "support@updates.mcpscraper.dev",
27401
+ to: registration.email,
27402
+ subject: "Your MCP Scraper workshop reading guide",
27403
+ html: renderWorkshopEmailHtml({
27404
+ eyebrow: "Before the live workshop",
27405
+ title: "The material is already here.",
27406
+ body: renderWorkshopFieldNotesBody(),
27407
+ button: { label: "Open the Field Notes", url: WORKSHOP_FIELD_NOTES_URL }
27408
+ })
27409
+ });
27410
+ if (sent.error || !sent.data?.id) {
27411
+ throw new Error(`Resend could not send the Field Notes guide: ${sent.error?.message ?? "unknown response"}`);
27412
+ }
27413
+ return sent.data.id;
27414
+ }
27415
+ function workshopReminderMessage(input) {
27416
+ const isTwoDays = input.kind === "48h";
27417
+ const isTomorrow = input.kind === "24h";
27418
+ const subject = isTwoDays ? "Two days out: bring the work you want MCP Scraper to handle" : isTomorrow ? "Tomorrow: see what MCP Scraper can actually do" : "One hour: your MCP Scraper workshop link";
27419
+ const eyebrow = isTwoDays ? "48 hours to go" : isTomorrow ? "Your workshop is tomorrow" : "We start in one hour";
27420
+ const title = isTwoDays ? `Bring one real problem, ${input.firstName}.` : isTomorrow ? "Tomorrow, we build around your work." : "Grab your coffee. Bring a real use case.";
27421
+ const opening = isTwoDays ? "This will not be a march through every feature. I\u2019ll show you how to choose the right combination of research, Memory, connected accounts, Scheduled Actions, and skills for work you already need done." : isTomorrow ? "The point is not to use every tool. It is to recognize which pieces belong together for research, monitoring, client work, learning, or something you are building." : "We go live in one hour. Keep one real project, question, or recurring task in mind\u2014I\u2019ll show you how to map the MCP Scraper pieces around it without turning the session into a dry feature tour.";
27422
+ const prompt = isTwoDays ? "A stubborn workflow, research question, client account, or product idea you want to move forward." : isTomorrow ? "Choose the one job you most want your AI to remember, research, monitor, or help you build." : "Keep one project, question, or recurring task in mind. We will map the right MCP Scraper pieces around it.";
27423
+ const promptCard = `<div style="margin:24px 0 0;padding:18px 20px;border:1px solid #bfdbfe;border-radius:16px;background:#eff6ff"><div style="color:#1d4ed8;font-size:11px;font-weight:700;letter-spacing:.08em;text-transform:uppercase">Bring this with you</div><p style="margin:7px 0 0;color:#1f2937;font-size:15px;line-height:1.55">${escapeHtml4(prompt)}</p></div>`;
27424
+ const details = `<p style="margin:24px 0 0"><strong>${escapeHtml4(input.dateLabel)}</strong><br>${escapeHtml4(input.timezoneLabel)}<br>${escapeHtml4(input.durationLabel)}</p>`;
27425
+ return {
27426
+ subject,
27427
+ html: renderWorkshopEmailHtml({
27428
+ eyebrow,
27429
+ title,
27430
+ body: `<p style="margin:0">${escapeHtml4(opening)}</p>${promptCard}${details}`,
27431
+ button: {
27432
+ label: input.kind === "1h" ? "Join the live workshop" : "Open your workshop link",
27433
+ url: input.zoomJoinUrl
27434
+ }
27435
+ })
27436
+ };
27437
+ }
27132
27438
  async function scheduleReminder(resend, registration, config, kind, scheduledAt) {
27133
27439
  if (!registration.zoomJoinUrl) throw new Error("Workshop access details are not ready.");
27134
- const isTomorrow = kind === "24h";
27440
+ const message = workshopReminderMessage({
27441
+ kind,
27442
+ firstName: registration.firstName,
27443
+ dateLabel: config.dateLabel,
27444
+ timezoneLabel: config.timezoneLabel,
27445
+ durationLabel: config.durationLabel,
27446
+ zoomJoinUrl: registration.zoomJoinUrl
27447
+ });
27135
27448
  const sent = await resend.emails.send({
27136
27449
  from: "Andrew at MCP Scraper <updates@updates.mcpscraper.dev>",
27137
27450
  replyTo: "support@updates.mcpscraper.dev",
27138
27451
  to: registration.email,
27139
- subject: isTomorrow ? "Tomorrow: What You Can Actually Do With MCP Scraper" : "Starts in one hour: MCP Scraper live workshop",
27452
+ subject: message.subject,
27140
27453
  scheduledAt,
27141
- html: emailFrame({
27142
- eyebrow: isTomorrow ? "Your workshop is tomorrow" : "We start in one hour",
27143
- title: isTomorrow ? `See you tomorrow, ${registration.firstName}.` : `We\u2019re starting soon, ${registration.firstName}.`,
27144
- body: `<p style="margin:0">${isTomorrow ? "Tomorrow I\u2019ll show you how MCP Scraper\u2019s research, Memory, connected accounts, Scheduled Actions, and reusable skills fit together around real work." : "The live MCP Scraper workshop starts in one hour. Your Zoom access link is below."}</p><p><strong>${escapeHtml3(config.dateLabel)}</strong><br>${escapeHtml3(config.timezoneLabel)}<br>${escapeHtml3(config.durationLabel)}</p>`,
27145
- button: { label: "Join the live workshop", url: registration.zoomJoinUrl }
27146
- })
27454
+ html: message.html
27147
27455
  });
27148
27456
  if (sent.error || !sent.data?.id) {
27149
27457
  throw new Error(`Resend could not schedule the ${kind} reminder: ${sent.error?.message ?? "unknown response"}`);
@@ -27156,10 +27464,10 @@ async function sendWaitlistEmail(resend, registration, config) {
27156
27464
  replyTo: "support@updates.mcpscraper.dev",
27157
27465
  to: registration.email,
27158
27466
  subject: "You\u2019re on the MCP Scraper workshop waitlist",
27159
- html: emailFrame({
27467
+ html: renderWorkshopEmailHtml({
27160
27468
  eyebrow: "MCP Scraper live workshop",
27161
27469
  title: `You\u2019re on the list, ${registration.firstName}.`,
27162
- body: `<p style="margin:0">The live room is currently full. I have saved your place on the waitlist and will email you if a seat opens.</p><p><strong>${escapeHtml3(config.dateLabel)}</strong><br>${escapeHtml3(config.timezoneLabel)}<br>${escapeHtml3(config.durationLabel)}</p>`
27470
+ body: `<p style="margin:0">The live room is currently full. I have saved your place on the waitlist and will email you if a seat opens.</p><p><strong>${escapeHtml4(config.dateLabel)}</strong><br>${escapeHtml4(config.timezoneLabel)}<br>${escapeHtml4(config.durationLabel)}</p>`
27163
27471
  })
27164
27472
  });
27165
27473
  if (sent.error || !sent.data?.id) throw new Error(`Resend could not send waitlist confirmation: ${sent.error?.message ?? "unknown response"}`);
@@ -27171,7 +27479,7 @@ async function sendInterestEmail(resend, registration) {
27171
27479
  replyTo: "support@updates.mcpscraper.dev",
27172
27480
  to: registration.email,
27173
27481
  subject: "You\u2019re on the early list for the MCP Scraper workshop",
27174
- html: emailFrame({
27482
+ html: renderWorkshopEmailHtml({
27175
27483
  eyebrow: "MCP Scraper live workshop",
27176
27484
  title: `You\u2019re on the early list, ${registration.firstName}.`,
27177
27485
  body: '<p style="margin:0">I\u2019m putting together a live August workshop about what you can actually do with MCP Scraper\u2014and how research, Memory, connected accounts, Scheduled Actions, and reusable workflows fit together around real work.</p><p>I\u2019ll send the date, time, and registration details to this email as soon as the live session is scheduled.</p>'
@@ -27222,14 +27530,17 @@ workshopRegistrationApp.post("/:event/register", async (c) => {
27222
27530
  lastName: parsed.data.lastName,
27223
27531
  capacity: config.capacity
27224
27532
  });
27533
+ const reminderSchedule = config.mode === "registration" ? workshopReminderSchedule(config.startAt) : null;
27225
27534
  if (registration.confirmationEmailId && registration.status !== "pending") {
27226
27535
  const waitlisted = registration.status === "waitlisted";
27227
27536
  const interested = registration.status === "interested";
27228
- return c.json({
27229
- ok: true,
27230
- status: interested ? "interested" : waitlisted ? "waitlisted" : "confirmed",
27231
- message: interested ? "You are on the early list. Check your inbox for confirmation." : waitlisted ? "The live room is full, but you are on the waitlist. Check your inbox for details." : "You are registered. Check your inbox for the confirmation and workshop access details."
27232
- });
27537
+ if (interested || waitlisted || registration.fieldNotesEmailId && reminderSchedule && workshopRemindersComplete(registration, reminderSchedule)) {
27538
+ return c.json({
27539
+ ok: true,
27540
+ status: interested ? "interested" : waitlisted ? "waitlisted" : "confirmed",
27541
+ message: interested ? "You are on the early list. Check your inbox for confirmation." : waitlisted ? "The live room is full, but you are on the waitlist. Check your inbox for details." : "You are registered. Check your inbox for the confirmation and workshop access details."
27542
+ });
27543
+ }
27233
27544
  }
27234
27545
  const resend = new Resend2(config.resendApiKey);
27235
27546
  try {
@@ -27278,7 +27589,17 @@ workshopRegistrationApp.post("/:event/register", async (c) => {
27278
27589
  });
27279
27590
  current = { ...current, resendContactId: contactId, confirmationEmailId: emailId };
27280
27591
  }
27281
- const schedule = workshopReminderSchedule(config.startAt);
27592
+ if (!current.fieldNotesEmailId) {
27593
+ const fieldNotesEmailId = await sendFieldNotesGuide(resend, current);
27594
+ await updateWorkshopRegistration(current.id, { fieldNotesEmailId, lastError: null });
27595
+ current = { ...current, fieldNotesEmailId };
27596
+ }
27597
+ const schedule = reminderSchedule;
27598
+ if (!current.reminder48hEmailId && Date.parse(schedule.reminder48h) > Date.now()) {
27599
+ const reminder48hEmailId = await scheduleReminder(resend, current, config, "48h", schedule.reminder48h);
27600
+ await updateWorkshopRegistration(current.id, { reminder48hEmailId, lastError: null });
27601
+ current = { ...current, reminder48hEmailId };
27602
+ }
27282
27603
  if (!current.reminder24hEmailId && Date.parse(schedule.reminder24h) > Date.now()) {
27283
27604
  const reminder24hEmailId = await scheduleReminder(resend, current, config, "24h", schedule.reminder24h);
27284
27605
  await updateWorkshopRegistration(current.id, { reminder24hEmailId, lastError: null });
@@ -27342,7 +27663,7 @@ function readAsset(name) {
27342
27663
  assetCache.set(name, value);
27343
27664
  return value;
27344
27665
  }
27345
- function escapeHtml4(value) {
27666
+ function escapeHtml5(value) {
27346
27667
  return value.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#39;");
27347
27668
  }
27348
27669
  function safeUrl(value, allowImage = false) {
@@ -27358,20 +27679,20 @@ function safeUrl(value, allowImage = false) {
27358
27679
  }
27359
27680
  function markdownRenderer() {
27360
27681
  const renderer = new Renderer();
27361
- renderer.html = ({ text: text2 }) => `<pre><code>${escapeHtml4(text2)}</code></pre>`;
27682
+ renderer.html = ({ text: text2 }) => `<pre><code>${escapeHtml5(text2)}</code></pre>`;
27362
27683
  renderer.link = function({ href, title, tokens }) {
27363
27684
  const label = this.parser.parseInline(tokens);
27364
27685
  const safe = safeUrl(href);
27365
27686
  if (!safe) return label;
27366
- const titleAttribute = title ? ` title="${escapeHtml4(title)}"` : "";
27687
+ const titleAttribute = title ? ` title="${escapeHtml5(title)}"` : "";
27367
27688
  const external = safe.startsWith("http://") || safe.startsWith("https://") ? ' target="_blank" rel="noopener noreferrer"' : "";
27368
- return `<a href="${escapeHtml4(safe)}"${titleAttribute}${external}>${label}</a>`;
27689
+ return `<a href="${escapeHtml5(safe)}"${titleAttribute}${external}>${label}</a>`;
27369
27690
  };
27370
27691
  renderer.image = ({ href, title, text: text2 }) => {
27371
27692
  const safe = safeUrl(href, true);
27372
- if (!safe) return `<em>[Image omitted: ${escapeHtml4(text2)}]</em>`;
27373
- const titleAttribute = title ? ` title="${escapeHtml4(title)}"` : "";
27374
- return `<img src="${escapeHtml4(safe)}" alt="${escapeHtml4(text2)}"${titleAttribute} loading="lazy">`;
27693
+ if (!safe) return `<em>[Image omitted: ${escapeHtml5(text2)}]</em>`;
27694
+ const titleAttribute = title ? ` title="${escapeHtml5(title)}"` : "";
27695
+ return `<img src="${escapeHtml5(safe)}" alt="${escapeHtml5(text2)}"${titleAttribute} loading="lazy">`;
27375
27696
  };
27376
27697
  return new Marked({ gfm: true, breaks: false, renderer });
27377
27698
  }
@@ -27394,7 +27715,7 @@ function addHeadingIds(html) {
27394
27715
  used.set(base, count + 1);
27395
27716
  const id = count ? `${base}-${count + 1}` : base;
27396
27717
  toc.push({ id, title, level: Number(level) });
27397
- return `<h${level} id="${id}" data-jump-title="${escapeHtml4(title)}">${inner}</h${level}>`;
27718
+ return `<h${level} id="${id}" data-jump-title="${escapeHtml5(title)}">${inner}</h${level}>`;
27398
27719
  });
27399
27720
  return { rendered, toc };
27400
27721
  }
@@ -27437,7 +27758,7 @@ function renderEditorialReadingRoom(input, now = /* @__PURE__ */ new Date()) {
27437
27758
  const generatedAt = now.toISOString();
27438
27759
  const data = { ...input, generatedAt, articles };
27439
27760
  const template = readAsset("index.html");
27440
- const html = template.replace("<title>Editorial Reading Room</title>", `<title>${escapeHtml4(input.site.title)}</title>`).replace('content="A mobile-first editorial reading room."', `content="${escapeHtml4(input.deck || input.site.heroTitle)}"`).replace("/*__READING_ROOM_STYLES__*/", readAsset("styles.css")).replace("/*__READING_ROOM_DATA__*/", escapedJsonForHtml(data)).replace("/*__READING_ROOM_SCRIPT__*/", readAsset("app.js"));
27761
+ const html = template.replace("<title>Editorial Reading Room</title>", `<title>${escapeHtml5(input.site.title)}</title>`).replace('content="A mobile-first editorial reading room."', `content="${escapeHtml5(input.deck || input.site.heroTitle)}"`).replace("/*__READING_ROOM_STYLES__*/", readAsset("styles.css")).replace("/*__READING_ROOM_DATA__*/", escapedJsonForHtml(data)).replace("/*__READING_ROOM_SCRIPT__*/", readAsset("app.js"));
27441
27762
  const bytes = Buffer.byteLength(html);
27442
27763
  return {
27443
27764
  html,
@@ -34100,7 +34421,7 @@ app.get("/cron/tick", async (c) => {
34100
34421
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
34101
34422
  return c.json({ error: "Unauthorized" }, 401);
34102
34423
  }
34103
- const { drainQueue } = await import("./worker-YTIMPE57.js");
34424
+ const { drainQueue } = await import("./worker-FXAGFYOE.js");
34104
34425
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
34105
34426
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
34106
34427
  const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup, connectedAccountBilling, directoryWorkflowDispatch, directoryWorkflowReconciliation, directoryArtifactCleanup, siteExtractArtifactCleanup, siteExtractRedispatch, siteExtractReconciliation] = await Promise.all([
@@ -34149,8 +34470,8 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
34149
34470
  finishExtractJob: finishExtractJob2,
34150
34471
  terminalExtractJobStatus: terminalExtractJobStatus2,
34151
34472
  extractJobLimitInfo: extractJobLimitInfo2
34152
- } = await import("./site-extract-repository-QZKZSQLQ.js");
34153
- const { assembleExtractArtifacts } = await import("./extract-bundle-CWG35SKV.js");
34473
+ } = await import("./site-extract-repository-2SMMFKKL.js");
34474
+ const { assembleExtractArtifacts } = await import("./extract-bundle-K4PG3RZJ.js");
34154
34475
  const existing = await getExtractJob2(jobId2);
34155
34476
  if (!existing) return c.json({ error: "job not found" }, 404);
34156
34477
  if (existing.status !== "failed") {
@@ -34177,7 +34498,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
34177
34498
  if (!updated) return c.json({ error: "job state changed during re-finalization" }, 409);
34178
34499
  let settlement = "already_settled_or_refunded";
34179
34500
  if (job.billedMc == null && job.userId != null) {
34180
- const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-QZKZSQLQ.js");
34501
+ const { settleExtractJob: settleExtractJob3, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-2SMMFKKL.js");
34181
34502
  const heldMc = Number(job.options.heldMc ?? 0);
34182
34503
  const successful = await countSuccessfulPages2(jobId2);
34183
34504
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -34350,4 +34671,4 @@ app.get("/blog/:slug/", (c) => {
34350
34671
  export {
34351
34672
  app
34352
34673
  };
34353
- //# sourceMappingURL=server-BXNVNAMT.js.map
34674
+ //# sourceMappingURL=server-KM3CFWCF.js.map