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.
- package/README.md +1 -1
- package/dist/bin/api-server.cjs +382 -44
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +3 -3
- package/dist/bin/mcp-scraper-cli.cjs +1 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +1 -1
- package/dist/bin/mcp-scraper-install.cjs +1 -1
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +1 -1
- package/dist/bin/mcp-stdio-server.cjs +6 -6
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +4 -4
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-DO6WGZRS.js → chunk-3PIWJS6Y.js} +2 -2
- package/dist/{chunk-YUE46JGV.js → chunk-7XBBFBYY.js} +2 -2
- package/dist/{chunk-AFY6DNL7.js → chunk-GUVKHCKE.js} +13 -3
- package/dist/chunk-GUVKHCKE.js.map +1 -0
- package/dist/{chunk-TJRCAP7C.js → chunk-LP6E462I.js} +8 -8
- package/dist/chunk-LP6E462I.js.map +1 -0
- package/dist/{chunk-4WAE4AV5.js → chunk-NVXNEOUQ.js} +2 -2
- package/dist/{chunk-3HOANNNR.js → chunk-X2LKCX6H.js} +2 -2
- package/dist/{chunk-ATWQRW3K.js → chunk-XORPNO3Z.js} +2 -2
- package/dist/chunk-ZUJLSICT.js +7 -0
- package/dist/chunk-ZUJLSICT.js.map +1 -0
- package/dist/{db-6AJW47NF.js → db-W3CP562I.js} +2 -2
- package/dist/{extract-bundle-CWG35SKV.js → extract-bundle-K4PG3RZJ.js} +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +3 -3
- package/dist/{location-data-repository-BGKZF7DV.js → location-data-repository-RLQX6SNM.js} +3 -3
- package/dist/{server-BXNVNAMT.js → server-KM3CFWCF.js} +373 -52
- package/dist/server-KM3CFWCF.js.map +1 -0
- package/dist/{site-extract-repository-QZKZSQLQ.js → site-extract-repository-2SMMFKKL.js} +3 -3
- package/dist/{worker-YTIMPE57.js → worker-FXAGFYOE.js} +5 -5
- package/package.json +7 -7
- package/dist/chunk-6NWL3MVU.js +0 -7
- package/dist/chunk-6NWL3MVU.js.map +0 -1
- package/dist/chunk-AFY6DNL7.js.map +0 -1
- package/dist/chunk-TJRCAP7C.js.map +0 -1
- package/dist/server-BXNVNAMT.js.map +0 -1
- package/docs/adr/0001-in-page-graphql-interception-for-anti-bot-scraping.md +0 -58
- package/docs/adr/0002-hybrid-smart-rag-vault-retrieval.md +0 -62
- package/docs/adr/0003-waive-unrecoverable-scheduled-model-cost.md +0 -22
- package/docs/adr/2026-07-28-owned-aug-webinar-registration.md +0 -61
- package/docs/adr/README.md +0 -13
- package/docs/final-tooling-spec.md +0 -206
- package/docs/hosted-location-data.md +0 -108
- package/docs/kernel-proxy-future-enhancements.md +0 -80
- package/docs/mcp-tool-craft-lint.generated.md +0 -186
- package/docs/mcp-tool-design-guide.md +0 -225
- package/docs/mcp-tool-manifest.generated.json +0 -23305
- package/docs/mcp-tool-quality-spec.md +0 -240
- package/docs/oauth-legal-review.md +0 -38
- package/docs/seo-crawl-report-spec.md +0 -287
- package/docs/specs/api-forge-spec.md +0 -234
- package/docs/specs/connected-services-control-plane-decoupling-spec.md +0 -1044
- package/docs/specs/deferred-work-spec.md +0 -86
- package/docs/specs/google-drive-bulk-access-and-mcp-schema-passthrough-spec.md +0 -1689
- package/docs/specs/kernel-stealth-captcha-test-matrix.md +0 -278
- package/docs/specs/main-mcp-integration-ownership-spec.md +0 -1164
- package/docs/specs/mcp-tool-definition-quality-audit-spec.md +0 -1602
- package/docs/specs/meta-ad-creative-media-resolution-spec.md +0 -31
- package/docs/specs/multimodal-image-memory-architecture-spec.md +0 -1022
- package/docs/specs/oauth-mcp-spec.md +0 -213
- package/docs/specs/plan-visibility-and-management-roadmap.md +0 -293
- package/docs/specs/query-fanout-transport-contract-fix.md +0 -45
- package/docs/specs/relationship-workspace-ai-behavior-plan.md +0 -26
- package/docs/specs/unified-credit-and-scheduled-execution-billing-spec.md +0 -995
- package/docs/tool-catalog-spec.md +0 -388
- /package/dist/{chunk-DO6WGZRS.js.map → chunk-3PIWJS6Y.js.map} +0 -0
- /package/dist/{chunk-YUE46JGV.js.map → chunk-7XBBFBYY.js.map} +0 -0
- /package/dist/{chunk-4WAE4AV5.js.map → chunk-NVXNEOUQ.js.map} +0 -0
- /package/dist/{chunk-3HOANNNR.js.map → chunk-X2LKCX6H.js.map} +0 -0
- /package/dist/{chunk-ATWQRW3K.js.map → chunk-XORPNO3Z.js.map} +0 -0
- /package/dist/{db-6AJW47NF.js.map → db-W3CP562I.js.map} +0 -0
- /package/dist/{extract-bundle-CWG35SKV.js.map → extract-bundle-K4PG3RZJ.js.map} +0 -0
- /package/dist/{location-data-repository-BGKZF7DV.js.map → location-data-repository-RLQX6SNM.js.map} +0 -0
- /package/dist/{site-extract-repository-QZKZSQLQ.js.map → site-extract-repository-2SMMFKKL.js.map} +0 -0
- /package/dist/{worker-YTIMPE57.js.map → worker-FXAGFYOE.js.map} +0 -0
package/dist/bin/api-server.cjs
CHANGED
|
@@ -3849,6 +3849,12 @@ async function migrate() {
|
|
|
3849
3849
|
`);
|
|
3850
3850
|
await db.execute(`CREATE INDEX IF NOT EXISTS jobs_user_id ON jobs(user_id)`);
|
|
3851
3851
|
await db.execute(`CREATE INDEX IF NOT EXISTS jobs_status ON jobs(status)`);
|
|
3852
|
+
await db.execute(
|
|
3853
|
+
`CREATE INDEX IF NOT EXISTS jobs_created_at_status_user ON jobs(created_at, status, user_id)`
|
|
3854
|
+
);
|
|
3855
|
+
await db.execute(
|
|
3856
|
+
`CREATE INDEX IF NOT EXISTS jobs_user_created_at_status ON jobs(user_id, created_at, status)`
|
|
3857
|
+
);
|
|
3852
3858
|
await db.execute(`
|
|
3853
3859
|
CREATE TABLE IF NOT EXISTS harvest_attempts (
|
|
3854
3860
|
id TEXT PRIMARY KEY,
|
|
@@ -4565,6 +4571,8 @@ async function migrate() {
|
|
|
4565
4571
|
zoom_join_url TEXT,
|
|
4566
4572
|
resend_contact_id TEXT,
|
|
4567
4573
|
confirmation_email_id TEXT,
|
|
4574
|
+
field_notes_email_id TEXT,
|
|
4575
|
+
reminder_48h_email_id TEXT,
|
|
4568
4576
|
reminder_24h_email_id TEXT,
|
|
4569
4577
|
reminder_1h_email_id TEXT,
|
|
4570
4578
|
last_error TEXT,
|
|
@@ -4573,7 +4581,7 @@ async function migrate() {
|
|
|
4573
4581
|
UNIQUE(event_slug, email)
|
|
4574
4582
|
)
|
|
4575
4583
|
`);
|
|
4576
|
-
for (const col of ["reminder_24h_email_id TEXT", "reminder_1h_email_id TEXT"]) {
|
|
4584
|
+
for (const col of ["field_notes_email_id TEXT", "reminder_48h_email_id TEXT", "reminder_24h_email_id TEXT", "reminder_1h_email_id TEXT"]) {
|
|
4577
4585
|
try {
|
|
4578
4586
|
await db.execute(`ALTER TABLE workshop_registrations ADD COLUMN ${col}`);
|
|
4579
4587
|
} catch {
|
|
@@ -6555,7 +6563,9 @@ var init_db = __esm({
|
|
|
6555
6563
|
"harvest_attempts_job_id",
|
|
6556
6564
|
"harvest_attempts_user_created_at",
|
|
6557
6565
|
"inbox_messages_user_sent_at",
|
|
6566
|
+
"jobs_created_at_status_user",
|
|
6558
6567
|
"jobs_status",
|
|
6568
|
+
"jobs_user_created_at_status",
|
|
6559
6569
|
"jobs_user_id",
|
|
6560
6570
|
"kpo_jobs_status",
|
|
6561
6571
|
"kpo_jobs_user_id",
|
|
@@ -6622,7 +6632,7 @@ var init_db = __esm({
|
|
|
6622
6632
|
"subscription_id",
|
|
6623
6633
|
"subscription_tier"
|
|
6624
6634
|
],
|
|
6625
|
-
workshop_registrations: ["reminder_24h_email_id", "reminder_1h_email_id"],
|
|
6635
|
+
workshop_registrations: ["field_notes_email_id", "reminder_48h_email_id", "reminder_24h_email_id", "reminder_1h_email_id"],
|
|
6626
6636
|
workflow_artifacts: ["content"],
|
|
6627
6637
|
workflow_runs: ["step_index", "step_log_json", "step_state_json", "total_steps", "webhook_url"]
|
|
6628
6638
|
};
|
|
@@ -37981,7 +37991,7 @@ var PACKAGE_VERSION;
|
|
|
37981
37991
|
var init_version = __esm({
|
|
37982
37992
|
"src/version.ts"() {
|
|
37983
37993
|
"use strict";
|
|
37984
|
-
PACKAGE_VERSION = "0.
|
|
37994
|
+
PACKAGE_VERSION = "0.40.2";
|
|
37985
37995
|
}
|
|
37986
37996
|
});
|
|
37987
37997
|
|
|
@@ -41719,10 +41729,10 @@ var init_http_mcp_tool_executor = __esm({
|
|
|
41719
41729
|
return this.call("/billing/credits", input);
|
|
41720
41730
|
}
|
|
41721
41731
|
listServiceConnections(input) {
|
|
41722
|
-
return this.getJson("/
|
|
41732
|
+
return this.getJson("/schedule-connections");
|
|
41723
41733
|
}
|
|
41724
41734
|
testServiceConnection(input) {
|
|
41725
|
-
return this.call(`/
|
|
41735
|
+
return this.call(`/schedule-connections/${encodeURIComponent(input.connectionId)}/test`, {
|
|
41726
41736
|
...input.providerConfigKey ? { providerConfigKey: input.providerConfigKey } : {}
|
|
41727
41737
|
});
|
|
41728
41738
|
}
|
|
@@ -41742,13 +41752,13 @@ var init_http_mcp_tool_executor = __esm({
|
|
|
41742
41752
|
return this.callConnectedMutation("/schedule-connections/actions/zoom/create-meeting", input);
|
|
41743
41753
|
}
|
|
41744
41754
|
readServiceConnection(input) {
|
|
41745
|
-
return this.call("/
|
|
41755
|
+
return this.call("/schedule-connections/actions/read", input);
|
|
41746
41756
|
}
|
|
41747
41757
|
importServiceConnectionToMemory(input) {
|
|
41748
41758
|
return this.call("/schedule-connections/actions/import-memory", input);
|
|
41749
41759
|
}
|
|
41750
41760
|
describeServiceConnectionTool(input) {
|
|
41751
|
-
return this.call("/
|
|
41761
|
+
return this.call("/schedule-connections/actions/describe", input);
|
|
41752
41762
|
}
|
|
41753
41763
|
exportConnectedServiceData(input) {
|
|
41754
41764
|
const timeoutMs = this.httpTimeoutOverrideMs ?? 29e4;
|
|
@@ -41762,7 +41772,7 @@ var init_http_mcp_tool_executor = __esm({
|
|
|
41762
41772
|
return this.call("/schedule-connections/actions/export-download", input);
|
|
41763
41773
|
}
|
|
41764
41774
|
callServiceConnectionAction(input) {
|
|
41765
|
-
return this.callConnectedMutation("/
|
|
41775
|
+
return this.callConnectedMutation("/schedule-connections/actions/call", input);
|
|
41766
41776
|
}
|
|
41767
41777
|
setScheduledActionConnections(input) {
|
|
41768
41778
|
return this.call(`/schedule-actions/${encodeURIComponent(input.scheduleActionId)}/connections`, {
|
|
@@ -51911,6 +51921,259 @@ var init_resend_inbound_routes = __esm({
|
|
|
51911
51921
|
}
|
|
51912
51922
|
});
|
|
51913
51923
|
|
|
51924
|
+
// src/email/workshop-field-notes.ts
|
|
51925
|
+
function escapeHtml4(value) {
|
|
51926
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
51927
|
+
}
|
|
51928
|
+
function workshopFieldNoteUrl(slug) {
|
|
51929
|
+
return `${WORKSHOP_FIELD_NOTES_URL}?article=${encodeURIComponent(slug)}`;
|
|
51930
|
+
}
|
|
51931
|
+
function renderWorkshopFieldNotesBody() {
|
|
51932
|
+
let activeCategory = "";
|
|
51933
|
+
return [
|
|
51934
|
+
'<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>',
|
|
51935
|
+
...workshopFieldNotes.flatMap((note, index) => {
|
|
51936
|
+
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">${escapeHtml4(note.category)}</h2>`];
|
|
51937
|
+
activeCategory = note.category;
|
|
51938
|
+
const url = escapeHtml4(workshopFieldNoteUrl(note.slug));
|
|
51939
|
+
const sentences = note.snippet.map(escapeHtml4).join(" ");
|
|
51940
|
+
return [
|
|
51941
|
+
...category,
|
|
51942
|
+
`<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">${escapeHtml4(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>`
|
|
51943
|
+
];
|
|
51944
|
+
})
|
|
51945
|
+
].join("");
|
|
51946
|
+
}
|
|
51947
|
+
var WORKSHOP_FIELD_NOTES_URL, workshopFieldNotes;
|
|
51948
|
+
var init_workshop_field_notes = __esm({
|
|
51949
|
+
"src/email/workshop-field-notes.ts"() {
|
|
51950
|
+
"use strict";
|
|
51951
|
+
WORKSHOP_FIELD_NOTES_URL = "https://mcp-field-notes.vercel.app/";
|
|
51952
|
+
workshopFieldNotes = [
|
|
51953
|
+
{
|
|
51954
|
+
slug: "operating-model",
|
|
51955
|
+
category: "Start Here",
|
|
51956
|
+
title: "The Operating Model",
|
|
51957
|
+
snippet: [
|
|
51958
|
+
"MCP Scraper is an operating system for evidence work, not simply a page fetcher.",
|
|
51959
|
+
"It connects a question to the right sources, method, and durable output.",
|
|
51960
|
+
"This note gives you the mental model for everything else."
|
|
51961
|
+
]
|
|
51962
|
+
},
|
|
51963
|
+
{
|
|
51964
|
+
slug: "context-aware-vault-capture",
|
|
51965
|
+
category: "Start Here",
|
|
51966
|
+
title: "Context-Aware Vault Capture",
|
|
51967
|
+
snippet: [
|
|
51968
|
+
"This demonstrates what a governed memory write actually looks like.",
|
|
51969
|
+
"The agent inspects the vault, chooses the destination, writes with supported links and tags, then reads it back.",
|
|
51970
|
+
"The final retrieval check proves the material can be found later."
|
|
51971
|
+
]
|
|
51972
|
+
},
|
|
51973
|
+
{
|
|
51974
|
+
slug: "complete-competitive-surface-scan",
|
|
51975
|
+
category: "Start Here",
|
|
51976
|
+
title: "The Complete Competitive Surface Scan",
|
|
51977
|
+
snippet: [
|
|
51978
|
+
"A competitive scan is complete when it covers the evidence needed for a decision.",
|
|
51979
|
+
"That might combine search, sites, reviews, community language, ads, or video without scraping everything.",
|
|
51980
|
+
"This note shows how to define coverage and report gaps honestly."
|
|
51981
|
+
]
|
|
51982
|
+
},
|
|
51983
|
+
{
|
|
51984
|
+
slug: "search-serp-and-paa",
|
|
51985
|
+
category: "Research Surfaces & Systems",
|
|
51986
|
+
title: "Search, SERPs, and People Also Ask",
|
|
51987
|
+
snippet: [
|
|
51988
|
+
"Rankings, questions, local results, sources, and AI answers are different kinds of search evidence.",
|
|
51989
|
+
"A fast SERP and a deeper People Also Ask harvest answer different questions.",
|
|
51990
|
+
"This note shows how to choose the least expensive search depth that changes the decision."
|
|
51991
|
+
]
|
|
51992
|
+
},
|
|
51993
|
+
{
|
|
51994
|
+
slug: "reddit-and-community-research",
|
|
51995
|
+
category: "Research Surfaces & Systems",
|
|
51996
|
+
title: "Reddit and Community Research",
|
|
51997
|
+
snippet: [
|
|
51998
|
+
"Community research exposes language, objections, workarounds, and disagreement that polished pages often hide.",
|
|
51999
|
+
"It should preserve thread context and dissent instead of turning comments into a market-wide vote.",
|
|
52000
|
+
"This note explains discovery, deep reading, and careful synthesis."
|
|
52001
|
+
]
|
|
52002
|
+
},
|
|
52003
|
+
{
|
|
52004
|
+
slug: "browser-profiles-and-ai-fanout",
|
|
52005
|
+
category: "Research Surfaces & Systems",
|
|
52006
|
+
title: "Browser Profiles and AI Citation Fan-Out",
|
|
52007
|
+
snippet: [
|
|
52008
|
+
"A browser profile preserves delegated access to sites that require login.",
|
|
52009
|
+
"Citation fan-out studies the subqueries, browsed sources, and citations inside an AI answer.",
|
|
52010
|
+
"This note keeps the authenticated surface and the research method conceptually separate."
|
|
52011
|
+
]
|
|
52012
|
+
},
|
|
52013
|
+
{
|
|
52014
|
+
slug: "youtube-and-video-research",
|
|
52015
|
+
category: "Research Surfaces & Systems",
|
|
52016
|
+
title: "YouTube and Video Research",
|
|
52017
|
+
snippet: [
|
|
52018
|
+
"Video research can stop at discovery, continue to transcription, or go deeper into frame analysis.",
|
|
52019
|
+
"Each level costs more and answers a different question.",
|
|
52020
|
+
"This note shows how to choose the cheapest sufficient depth."
|
|
52021
|
+
]
|
|
52022
|
+
},
|
|
52023
|
+
{
|
|
52024
|
+
slug: "advertising-intelligence",
|
|
52025
|
+
category: "Research Surfaces & Systems",
|
|
52026
|
+
title: "Advertising Intelligence",
|
|
52027
|
+
snippet: [
|
|
52028
|
+
"Advertising intelligence studies the messages, offers, proof, formats, and creative patterns a company distributes.",
|
|
52029
|
+
"It does not prove spend, profitability, or performance.",
|
|
52030
|
+
"This note maps the right evidence path for Meta, Google, public libraries, and authorized account creative."
|
|
52031
|
+
]
|
|
52032
|
+
},
|
|
52033
|
+
{
|
|
52034
|
+
slug: "search-console-and-analytics-skills",
|
|
52035
|
+
category: "Connected Data & Automation",
|
|
52036
|
+
title: "Search Console and Analytics Skills",
|
|
52037
|
+
snippet: [
|
|
52038
|
+
"Search Console and Analytics supply first-party evidence, not strategy by themselves.",
|
|
52039
|
+
"A user-owned skill adds business doctrine, comparison rules, and priorities.",
|
|
52040
|
+
"This note shows how connected data, public research, Memory, and scheduling become a repeatable decision system."
|
|
52041
|
+
]
|
|
52042
|
+
},
|
|
52043
|
+
{
|
|
52044
|
+
slug: "scheduled-gmail-zoom-and-research-digestion",
|
|
52045
|
+
category: "Connected Data & Automation",
|
|
52046
|
+
title: "Scheduled Gmail, Zoom, and Research Digestion",
|
|
52047
|
+
snippet: [
|
|
52048
|
+
"Scheduled digestion turns accumulating email, meetings, and research into bounded durable updates.",
|
|
52049
|
+
"It should not silently bulk-import an account or invent people, tasks, deals, or projects.",
|
|
52050
|
+
"This note defines the source, time window, filters, classifications, and destination that make a digest safe."
|
|
52051
|
+
]
|
|
52052
|
+
},
|
|
52053
|
+
{
|
|
52054
|
+
slug: "rank-tracking-and-change-monitoring",
|
|
52055
|
+
category: "Connected Data & Automation",
|
|
52056
|
+
title: "Rank Tracking and Change Monitoring",
|
|
52057
|
+
snippet: [
|
|
52058
|
+
"A tracker compares observations, and a monitor remembers a baseline before looking again.",
|
|
52059
|
+
"Repeating the same prompt does not create either system.",
|
|
52060
|
+
"This note explains stable subjects, comparable inputs, materiality thresholds, stored history, and change receipts."
|
|
52061
|
+
]
|
|
52062
|
+
},
|
|
52063
|
+
{
|
|
52064
|
+
slug: "research-artifacts-and-reading-rooms",
|
|
52065
|
+
category: "Artifacts, Memory & Skills",
|
|
52066
|
+
title: "Research Artifacts and Reading Rooms",
|
|
52067
|
+
snippet: [
|
|
52068
|
+
"Research should end in something people can inspect, reopen, and use.",
|
|
52069
|
+
"MCP Scraper can choose among inline reports, private artifacts, tables, governed Memory, and editorial reading rooms.",
|
|
52070
|
+
"This note explains which output belongs where."
|
|
52071
|
+
]
|
|
52072
|
+
},
|
|
52073
|
+
{
|
|
52074
|
+
slug: "skills-as-user-owned-judgment",
|
|
52075
|
+
category: "Artifacts, Memory & Skills",
|
|
52076
|
+
title: "Skills as User-Owned Judgment",
|
|
52077
|
+
snippet: [
|
|
52078
|
+
"MCP Scraper supplies evidence surfaces and operating primitives.",
|
|
52079
|
+
"A skill supplies the user\u2019s doctrine, sequence, standards, rejection rules, storage choices, and verification.",
|
|
52080
|
+
"This note shows how repeated corrections become reusable judgment."
|
|
52081
|
+
]
|
|
52082
|
+
},
|
|
52083
|
+
{
|
|
52084
|
+
slug: "seo-and-content",
|
|
52085
|
+
category: "Applications & Contexts",
|
|
52086
|
+
title: "SEO & Content",
|
|
52087
|
+
snippet: [
|
|
52088
|
+
"Start with a real search decision, then inspect the live SERP, questions, and competing pages.",
|
|
52089
|
+
"Preserve the evidence before turning it into a source-backed brief.",
|
|
52090
|
+
"Memory and monitoring make the method reusable instead of disposable."
|
|
52091
|
+
]
|
|
52092
|
+
},
|
|
52093
|
+
{
|
|
52094
|
+
slug: "local-markets",
|
|
52095
|
+
category: "Applications & Contexts",
|
|
52096
|
+
title: "Local Markets",
|
|
52097
|
+
snippet: [
|
|
52098
|
+
"Local research combines businesses, services, reviews, positioning, search visibility, and geography.",
|
|
52099
|
+
"The goal is to find testable gaps without flattening every source into one score.",
|
|
52100
|
+
"This note shows how to preserve a market baseline and monitor meaningful change."
|
|
52101
|
+
]
|
|
52102
|
+
},
|
|
52103
|
+
{
|
|
52104
|
+
slug: "sales-and-client-work",
|
|
52105
|
+
category: "Applications & Contexts",
|
|
52106
|
+
title: "Sales & Client Work",
|
|
52107
|
+
snippet: [
|
|
52108
|
+
"This is a relationship graph, not a contact database.",
|
|
52109
|
+
"People, organizations, opportunities, projects, conversations, meetings, decisions, and follow-ups remain separate records connected by verified links.",
|
|
52110
|
+
"This note shows why that distinction prevents invented CRM context."
|
|
52111
|
+
]
|
|
52112
|
+
},
|
|
52113
|
+
{
|
|
52114
|
+
slug: "competitor-monitoring",
|
|
52115
|
+
category: "Applications & Contexts",
|
|
52116
|
+
title: "Competitor Monitoring",
|
|
52117
|
+
snippet: [
|
|
52118
|
+
"Monitoring begins with a dated baseline and a defined set of things worth watching.",
|
|
52119
|
+
"Each run compares trustworthy evidence and classifies only material change.",
|
|
52120
|
+
"The output reports the difference, including source failure and meaningful no-change."
|
|
52121
|
+
]
|
|
52122
|
+
},
|
|
52123
|
+
{
|
|
52124
|
+
slug: "learning-and-research",
|
|
52125
|
+
category: "Applications & Contexts",
|
|
52126
|
+
title: "Learning & Research",
|
|
52127
|
+
snippet: [
|
|
52128
|
+
"Raw articles, transcripts, pages, and datasets belong in Library with provenance intact.",
|
|
52129
|
+
"Reusable lessons belong separately in Knowledge and link back to their sources.",
|
|
52130
|
+
"This note explains recall by meaning, time, or graph without confusing relevance with truth."
|
|
52131
|
+
]
|
|
52132
|
+
},
|
|
52133
|
+
{
|
|
52134
|
+
slug: "build-your-own-tools",
|
|
52135
|
+
category: "Applications & Contexts",
|
|
52136
|
+
title: "Build Your Own Tools",
|
|
52137
|
+
snippet: [
|
|
52138
|
+
"The same acquisition, browser, connection, Memory, artifact, workflow, and scheduling primitives can sit behind something you build.",
|
|
52139
|
+
"That could be a skill, internal process, client report, monitoring service, or narrow SaaS.",
|
|
52140
|
+
"This note starts from one recurring decision and defines the product contract around it."
|
|
52141
|
+
]
|
|
52142
|
+
},
|
|
52143
|
+
{
|
|
52144
|
+
slug: "improve-originating-skill",
|
|
52145
|
+
category: "How MCP Scraper Learned",
|
|
52146
|
+
title: "Improve the Originating Skill",
|
|
52147
|
+
snippet: [
|
|
52148
|
+
"A corrected output often reveals a problem in the workflow that produced it.",
|
|
52149
|
+
"This reflection method traces the failure to the highest owning layer and improves the originating skill or validator.",
|
|
52150
|
+
"Future work then inherits the lesson instead of repeating the same repair."
|
|
52151
|
+
]
|
|
52152
|
+
},
|
|
52153
|
+
{
|
|
52154
|
+
slug: "landing-page-problem-solving",
|
|
52155
|
+
category: "How MCP Scraper Learned",
|
|
52156
|
+
title: "Problem-Solving Lessons for Use-Case Pages",
|
|
52157
|
+
snippet: [
|
|
52158
|
+
"This is the concrete receipt from applying that reflection method to MCP Scraper landing pages.",
|
|
52159
|
+
"It captures the fixes for generic copy, weak proof, wrong archetypes, visible seams, inconsistent calls to action, and missed mobile defects.",
|
|
52160
|
+
"The result is a stronger generator, not merely one repaired page."
|
|
52161
|
+
]
|
|
52162
|
+
},
|
|
52163
|
+
{
|
|
52164
|
+
slug: "workshop-playbook",
|
|
52165
|
+
category: "Workshop",
|
|
52166
|
+
title: "MCP Scraper Workshop Prompt Playbook",
|
|
52167
|
+
snippet: [
|
|
52168
|
+
"This is the practical prompt set for the live session.",
|
|
52169
|
+
"It covers research surfaces, connected data, authenticated browser work, scheduling, skills, completeness receipts, and governed Memory.",
|
|
52170
|
+
"You can replace the examples with your own business, competitors, accounts, and questions."
|
|
52171
|
+
]
|
|
52172
|
+
}
|
|
52173
|
+
];
|
|
52174
|
+
}
|
|
52175
|
+
});
|
|
52176
|
+
|
|
51914
52177
|
// src/api/workshop-registration-repository.ts
|
|
51915
52178
|
function rowToRegistration(row) {
|
|
51916
52179
|
return {
|
|
@@ -51924,6 +52187,8 @@ function rowToRegistration(row) {
|
|
|
51924
52187
|
zoomJoinUrl: row.zoom_join_url == null ? null : String(row.zoom_join_url),
|
|
51925
52188
|
resendContactId: row.resend_contact_id == null ? null : String(row.resend_contact_id),
|
|
51926
52189
|
confirmationEmailId: row.confirmation_email_id == null ? null : String(row.confirmation_email_id),
|
|
52190
|
+
fieldNotesEmailId: row.field_notes_email_id == null ? null : String(row.field_notes_email_id),
|
|
52191
|
+
reminder48hEmailId: row.reminder_48h_email_id == null ? null : String(row.reminder_48h_email_id),
|
|
51927
52192
|
reminder24hEmailId: row.reminder_24h_email_id == null ? null : String(row.reminder_24h_email_id),
|
|
51928
52193
|
reminder1hEmailId: row.reminder_1h_email_id == null ? null : String(row.reminder_1h_email_id),
|
|
51929
52194
|
lastError: row.last_error == null ? null : String(row.last_error)
|
|
@@ -52023,6 +52288,8 @@ async function updateWorkshopRegistration(id, patch) {
|
|
|
52023
52288
|
zoom_join_url: patch.zoomJoinUrl,
|
|
52024
52289
|
resend_contact_id: patch.resendContactId,
|
|
52025
52290
|
confirmation_email_id: patch.confirmationEmailId,
|
|
52291
|
+
field_notes_email_id: patch.fieldNotesEmailId,
|
|
52292
|
+
reminder_48h_email_id: patch.reminder48hEmailId,
|
|
52026
52293
|
reminder_24h_email_id: patch.reminder24hEmailId,
|
|
52027
52294
|
reminder_1h_email_id: patch.reminder1hEmailId,
|
|
52028
52295
|
last_error: patch.lastError
|
|
@@ -52082,10 +52349,19 @@ function workshopReminderSchedule(startAt) {
|
|
|
52082
52349
|
const start = Date.parse(startAt);
|
|
52083
52350
|
if (!Number.isFinite(start)) throw new Error("Workshop start time is invalid.");
|
|
52084
52351
|
return {
|
|
52352
|
+
reminder48h: new Date(start - 48 * 60 * 60 * 1e3).toISOString(),
|
|
52085
52353
|
reminder24h: new Date(start - 24 * 60 * 60 * 1e3).toISOString(),
|
|
52086
52354
|
reminder1h: new Date(start - 60 * 60 * 1e3).toISOString()
|
|
52087
52355
|
};
|
|
52088
52356
|
}
|
|
52357
|
+
function workshopRemindersComplete(registration, schedule, now = Date.now()) {
|
|
52358
|
+
const reminders = [
|
|
52359
|
+
[registration.reminder48hEmailId, schedule.reminder48h],
|
|
52360
|
+
[registration.reminder24hEmailId, schedule.reminder24h],
|
|
52361
|
+
[registration.reminder1hEmailId, schedule.reminder1h]
|
|
52362
|
+
];
|
|
52363
|
+
return reminders.every(([emailId, scheduledAt]) => Boolean(emailId) || Date.parse(scheduledAt) <= now);
|
|
52364
|
+
}
|
|
52089
52365
|
function allowedOrigins() {
|
|
52090
52366
|
const origins = /* @__PURE__ */ new Set(["https://mcpscraper.dev", "https://www.mcpscraper.dev"]);
|
|
52091
52367
|
for (const value of (process.env.ALLOWED_ORIGINS ?? process.env.APP_ORIGIN ?? "").split(",")) {
|
|
@@ -52100,12 +52376,12 @@ function requestOriginAllowed(origin) {
|
|
|
52100
52376
|
function clientIp(headers) {
|
|
52101
52377
|
return headers.get("cf-connecting-ip") || headers.get("x-real-ip") || headers.get("x-forwarded-for")?.split(",")[0]?.trim() || "unknown";
|
|
52102
52378
|
}
|
|
52103
|
-
function
|
|
52379
|
+
function escapeHtml5(value) {
|
|
52104
52380
|
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
52105
52381
|
}
|
|
52106
|
-
function
|
|
52107
|
-
const button = input.button ? `<
|
|
52108
|
-
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">${
|
|
52382
|
+
function renderWorkshopEmailHtml(input) {
|
|
52383
|
+
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="${escapeHtml5(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">${escapeHtml5(input.button.label)}</a></td></tr></table>` : "";
|
|
52384
|
+
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">${escapeHtml5(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">${escapeHtml5(input.eyebrow)}</div><h1 class="email-title" style="margin:12px 0 0;color:#ffffff;font-size:42px;line-height:1.04;letter-spacing:-.04em">${escapeHtml5(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>`;
|
|
52109
52385
|
}
|
|
52110
52386
|
function icsTimestamp(value) {
|
|
52111
52387
|
return new Date(value).toISOString().replaceAll("-", "").replaceAll(":", "").replace(/\.\d{3}Z$/, "Z");
|
|
@@ -52155,21 +52431,26 @@ async function addEventContact(resend, registration, config) {
|
|
|
52155
52431
|
email: registration.email,
|
|
52156
52432
|
segmentId: config.resendSegmentId
|
|
52157
52433
|
});
|
|
52158
|
-
if (segment.error)
|
|
52434
|
+
if (segment.error) {
|
|
52435
|
+
console.warn("[workshop-registration] event contact sync skipped:", segment.error.message);
|
|
52436
|
+
return null;
|
|
52437
|
+
}
|
|
52159
52438
|
if (config.resendTopicId) {
|
|
52160
52439
|
const topic = await resend.contacts.topics.update({
|
|
52161
52440
|
email: registration.email,
|
|
52162
52441
|
topics: [{ id: config.resendTopicId, subscription: "opt_in" }]
|
|
52163
52442
|
});
|
|
52164
|
-
if (topic.error)
|
|
52443
|
+
if (topic.error) {
|
|
52444
|
+
console.warn("[workshop-registration] event topic sync skipped:", topic.error.message);
|
|
52445
|
+
}
|
|
52165
52446
|
}
|
|
52166
52447
|
return null;
|
|
52167
52448
|
}
|
|
52168
52449
|
async function sendConfirmation(resend, registration, config) {
|
|
52169
52450
|
if (!registration.zoomJoinUrl) throw new Error("Workshop access details are not ready.");
|
|
52170
|
-
const date =
|
|
52171
|
-
const timezone =
|
|
52172
|
-
const duration =
|
|
52451
|
+
const date = escapeHtml5(config.dateLabel);
|
|
52452
|
+
const timezone = escapeHtml5(config.timezoneLabel);
|
|
52453
|
+
const duration = escapeHtml5(config.durationLabel);
|
|
52173
52454
|
const sent = await resend.emails.send({
|
|
52174
52455
|
from: "Andrew at MCP Scraper <updates@updates.mcpscraper.dev>",
|
|
52175
52456
|
replyTo: "support@updates.mcpscraper.dev",
|
|
@@ -52180,7 +52461,7 @@ async function sendConfirmation(resend, registration, config) {
|
|
|
52180
52461
|
content: workshopInvitation(registration, config),
|
|
52181
52462
|
contentType: "text/calendar; method=REQUEST; charset=UTF-8"
|
|
52182
52463
|
}],
|
|
52183
|
-
html:
|
|
52464
|
+
html: renderWorkshopEmailHtml({
|
|
52184
52465
|
eyebrow: "Your live workshop seat is saved",
|
|
52185
52466
|
title: `You\u2019re in, ${registration.firstName}.`,
|
|
52186
52467
|
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>`,
|
|
@@ -52190,21 +52471,64 @@ async function sendConfirmation(resend, registration, config) {
|
|
|
52190
52471
|
if (sent.error || !sent.data?.id) throw new Error(`Resend could not send confirmation: ${sent.error?.message ?? "unknown response"}`);
|
|
52191
52472
|
return sent.data.id;
|
|
52192
52473
|
}
|
|
52474
|
+
async function sendFieldNotesGuide(resend, registration) {
|
|
52475
|
+
const sent = await resend.emails.send({
|
|
52476
|
+
from: "Andrew at MCP Scraper <updates@updates.mcpscraper.dev>",
|
|
52477
|
+
replyTo: "support@updates.mcpscraper.dev",
|
|
52478
|
+
to: registration.email,
|
|
52479
|
+
subject: "Your MCP Scraper workshop reading guide",
|
|
52480
|
+
html: renderWorkshopEmailHtml({
|
|
52481
|
+
eyebrow: "Before the live workshop",
|
|
52482
|
+
title: "The material is already here.",
|
|
52483
|
+
body: renderWorkshopFieldNotesBody(),
|
|
52484
|
+
button: { label: "Open the Field Notes", url: WORKSHOP_FIELD_NOTES_URL }
|
|
52485
|
+
})
|
|
52486
|
+
});
|
|
52487
|
+
if (sent.error || !sent.data?.id) {
|
|
52488
|
+
throw new Error(`Resend could not send the Field Notes guide: ${sent.error?.message ?? "unknown response"}`);
|
|
52489
|
+
}
|
|
52490
|
+
return sent.data.id;
|
|
52491
|
+
}
|
|
52492
|
+
function workshopReminderMessage(input) {
|
|
52493
|
+
const isTwoDays = input.kind === "48h";
|
|
52494
|
+
const isTomorrow = input.kind === "24h";
|
|
52495
|
+
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";
|
|
52496
|
+
const eyebrow = isTwoDays ? "48 hours to go" : isTomorrow ? "Your workshop is tomorrow" : "We start in one hour";
|
|
52497
|
+
const title = isTwoDays ? `Bring one real problem, ${input.firstName}.` : isTomorrow ? "Tomorrow, we build around your work." : "Grab your coffee. Bring a real use case.";
|
|
52498
|
+
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.";
|
|
52499
|
+
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.";
|
|
52500
|
+
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">${escapeHtml5(prompt)}</p></div>`;
|
|
52501
|
+
const details = `<p style="margin:24px 0 0"><strong>${escapeHtml5(input.dateLabel)}</strong><br>${escapeHtml5(input.timezoneLabel)}<br>${escapeHtml5(input.durationLabel)}</p>`;
|
|
52502
|
+
return {
|
|
52503
|
+
subject,
|
|
52504
|
+
html: renderWorkshopEmailHtml({
|
|
52505
|
+
eyebrow,
|
|
52506
|
+
title,
|
|
52507
|
+
body: `<p style="margin:0">${escapeHtml5(opening)}</p>${promptCard}${details}`,
|
|
52508
|
+
button: {
|
|
52509
|
+
label: input.kind === "1h" ? "Join the live workshop" : "Open your workshop link",
|
|
52510
|
+
url: input.zoomJoinUrl
|
|
52511
|
+
}
|
|
52512
|
+
})
|
|
52513
|
+
};
|
|
52514
|
+
}
|
|
52193
52515
|
async function scheduleReminder(resend, registration, config, kind, scheduledAt) {
|
|
52194
52516
|
if (!registration.zoomJoinUrl) throw new Error("Workshop access details are not ready.");
|
|
52195
|
-
const
|
|
52517
|
+
const message = workshopReminderMessage({
|
|
52518
|
+
kind,
|
|
52519
|
+
firstName: registration.firstName,
|
|
52520
|
+
dateLabel: config.dateLabel,
|
|
52521
|
+
timezoneLabel: config.timezoneLabel,
|
|
52522
|
+
durationLabel: config.durationLabel,
|
|
52523
|
+
zoomJoinUrl: registration.zoomJoinUrl
|
|
52524
|
+
});
|
|
52196
52525
|
const sent = await resend.emails.send({
|
|
52197
52526
|
from: "Andrew at MCP Scraper <updates@updates.mcpscraper.dev>",
|
|
52198
52527
|
replyTo: "support@updates.mcpscraper.dev",
|
|
52199
52528
|
to: registration.email,
|
|
52200
|
-
subject:
|
|
52529
|
+
subject: message.subject,
|
|
52201
52530
|
scheduledAt,
|
|
52202
|
-
html:
|
|
52203
|
-
eyebrow: isTomorrow ? "Your workshop is tomorrow" : "We start in one hour",
|
|
52204
|
-
title: isTomorrow ? `See you tomorrow, ${registration.firstName}.` : `We\u2019re starting soon, ${registration.firstName}.`,
|
|
52205
|
-
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>${escapeHtml4(config.dateLabel)}</strong><br>${escapeHtml4(config.timezoneLabel)}<br>${escapeHtml4(config.durationLabel)}</p>`,
|
|
52206
|
-
button: { label: "Join the live workshop", url: registration.zoomJoinUrl }
|
|
52207
|
-
})
|
|
52531
|
+
html: message.html
|
|
52208
52532
|
});
|
|
52209
52533
|
if (sent.error || !sent.data?.id) {
|
|
52210
52534
|
throw new Error(`Resend could not schedule the ${kind} reminder: ${sent.error?.message ?? "unknown response"}`);
|
|
@@ -52217,10 +52541,10 @@ async function sendWaitlistEmail(resend, registration, config) {
|
|
|
52217
52541
|
replyTo: "support@updates.mcpscraper.dev",
|
|
52218
52542
|
to: registration.email,
|
|
52219
52543
|
subject: "You\u2019re on the MCP Scraper workshop waitlist",
|
|
52220
|
-
html:
|
|
52544
|
+
html: renderWorkshopEmailHtml({
|
|
52221
52545
|
eyebrow: "MCP Scraper live workshop",
|
|
52222
52546
|
title: `You\u2019re on the list, ${registration.firstName}.`,
|
|
52223
|
-
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>${
|
|
52547
|
+
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>${escapeHtml5(config.dateLabel)}</strong><br>${escapeHtml5(config.timezoneLabel)}<br>${escapeHtml5(config.durationLabel)}</p>`
|
|
52224
52548
|
})
|
|
52225
52549
|
});
|
|
52226
52550
|
if (sent.error || !sent.data?.id) throw new Error(`Resend could not send waitlist confirmation: ${sent.error?.message ?? "unknown response"}`);
|
|
@@ -52232,7 +52556,7 @@ async function sendInterestEmail(resend, registration) {
|
|
|
52232
52556
|
replyTo: "support@updates.mcpscraper.dev",
|
|
52233
52557
|
to: registration.email,
|
|
52234
52558
|
subject: "You\u2019re on the early list for the MCP Scraper workshop",
|
|
52235
|
-
html:
|
|
52559
|
+
html: renderWorkshopEmailHtml({
|
|
52236
52560
|
eyebrow: "MCP Scraper live workshop",
|
|
52237
52561
|
title: `You\u2019re on the early list, ${registration.firstName}.`,
|
|
52238
52562
|
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>'
|
|
@@ -52249,6 +52573,7 @@ var init_workshop_registration_routes = __esm({
|
|
|
52249
52573
|
import_resend2 = require("resend");
|
|
52250
52574
|
import_zod41 = require("zod");
|
|
52251
52575
|
init_db();
|
|
52576
|
+
init_workshop_field_notes();
|
|
52252
52577
|
init_workshop_registration_repository();
|
|
52253
52578
|
EVENT_SLUG = "aug-webinar";
|
|
52254
52579
|
registrationSchema = import_zod41.z.object({
|
|
@@ -52300,14 +52625,17 @@ var init_workshop_registration_routes = __esm({
|
|
|
52300
52625
|
lastName: parsed.data.lastName,
|
|
52301
52626
|
capacity: config.capacity
|
|
52302
52627
|
});
|
|
52628
|
+
const reminderSchedule = config.mode === "registration" ? workshopReminderSchedule(config.startAt) : null;
|
|
52303
52629
|
if (registration.confirmationEmailId && registration.status !== "pending") {
|
|
52304
52630
|
const waitlisted = registration.status === "waitlisted";
|
|
52305
52631
|
const interested = registration.status === "interested";
|
|
52306
|
-
|
|
52307
|
-
|
|
52308
|
-
|
|
52309
|
-
|
|
52310
|
-
|
|
52632
|
+
if (interested || waitlisted || registration.fieldNotesEmailId && reminderSchedule && workshopRemindersComplete(registration, reminderSchedule)) {
|
|
52633
|
+
return c.json({
|
|
52634
|
+
ok: true,
|
|
52635
|
+
status: interested ? "interested" : waitlisted ? "waitlisted" : "confirmed",
|
|
52636
|
+
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."
|
|
52637
|
+
});
|
|
52638
|
+
}
|
|
52311
52639
|
}
|
|
52312
52640
|
const resend = new import_resend2.Resend(config.resendApiKey);
|
|
52313
52641
|
try {
|
|
@@ -52356,7 +52684,17 @@ var init_workshop_registration_routes = __esm({
|
|
|
52356
52684
|
});
|
|
52357
52685
|
current = { ...current, resendContactId: contactId, confirmationEmailId: emailId };
|
|
52358
52686
|
}
|
|
52359
|
-
|
|
52687
|
+
if (!current.fieldNotesEmailId) {
|
|
52688
|
+
const fieldNotesEmailId = await sendFieldNotesGuide(resend, current);
|
|
52689
|
+
await updateWorkshopRegistration(current.id, { fieldNotesEmailId, lastError: null });
|
|
52690
|
+
current = { ...current, fieldNotesEmailId };
|
|
52691
|
+
}
|
|
52692
|
+
const schedule = reminderSchedule;
|
|
52693
|
+
if (!current.reminder48hEmailId && Date.parse(schedule.reminder48h) > Date.now()) {
|
|
52694
|
+
const reminder48hEmailId = await scheduleReminder(resend, current, config, "48h", schedule.reminder48h);
|
|
52695
|
+
await updateWorkshopRegistration(current.id, { reminder48hEmailId, lastError: null });
|
|
52696
|
+
current = { ...current, reminder48hEmailId };
|
|
52697
|
+
}
|
|
52360
52698
|
if (!current.reminder24hEmailId && Date.parse(schedule.reminder24h) > Date.now()) {
|
|
52361
52699
|
const reminder24hEmailId = await scheduleReminder(resend, current, config, "24h", schedule.reminder24h);
|
|
52362
52700
|
await updateWorkshopRegistration(current.id, { reminder24hEmailId, lastError: null });
|
|
@@ -52412,7 +52750,7 @@ function readAsset(name) {
|
|
|
52412
52750
|
assetCache.set(name, value);
|
|
52413
52751
|
return value;
|
|
52414
52752
|
}
|
|
52415
|
-
function
|
|
52753
|
+
function escapeHtml6(value) {
|
|
52416
52754
|
return value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
52417
52755
|
}
|
|
52418
52756
|
function safeUrl(value, allowImage = false) {
|
|
@@ -52428,20 +52766,20 @@ function safeUrl(value, allowImage = false) {
|
|
|
52428
52766
|
}
|
|
52429
52767
|
function markdownRenderer() {
|
|
52430
52768
|
const renderer = new import_marked.Renderer();
|
|
52431
|
-
renderer.html = ({ text: text2 }) => `<pre><code>${
|
|
52769
|
+
renderer.html = ({ text: text2 }) => `<pre><code>${escapeHtml6(text2)}</code></pre>`;
|
|
52432
52770
|
renderer.link = function({ href, title, tokens }) {
|
|
52433
52771
|
const label = this.parser.parseInline(tokens);
|
|
52434
52772
|
const safe2 = safeUrl(href);
|
|
52435
52773
|
if (!safe2) return label;
|
|
52436
|
-
const titleAttribute = title ? ` title="${
|
|
52774
|
+
const titleAttribute = title ? ` title="${escapeHtml6(title)}"` : "";
|
|
52437
52775
|
const external = safe2.startsWith("http://") || safe2.startsWith("https://") ? ' target="_blank" rel="noopener noreferrer"' : "";
|
|
52438
|
-
return `<a href="${
|
|
52776
|
+
return `<a href="${escapeHtml6(safe2)}"${titleAttribute}${external}>${label}</a>`;
|
|
52439
52777
|
};
|
|
52440
52778
|
renderer.image = ({ href, title, text: text2 }) => {
|
|
52441
52779
|
const safe2 = safeUrl(href, true);
|
|
52442
|
-
if (!safe2) return `<em>[Image omitted: ${
|
|
52443
|
-
const titleAttribute = title ? ` title="${
|
|
52444
|
-
return `<img src="${
|
|
52780
|
+
if (!safe2) return `<em>[Image omitted: ${escapeHtml6(text2)}]</em>`;
|
|
52781
|
+
const titleAttribute = title ? ` title="${escapeHtml6(title)}"` : "";
|
|
52782
|
+
return `<img src="${escapeHtml6(safe2)}" alt="${escapeHtml6(text2)}"${titleAttribute} loading="lazy">`;
|
|
52445
52783
|
};
|
|
52446
52784
|
return new import_marked.Marked({ gfm: true, breaks: false, renderer });
|
|
52447
52785
|
}
|
|
@@ -52464,7 +52802,7 @@ function addHeadingIds(html) {
|
|
|
52464
52802
|
used.set(base, count + 1);
|
|
52465
52803
|
const id = count ? `${base}-${count + 1}` : base;
|
|
52466
52804
|
toc.push({ id, title, level: Number(level) });
|
|
52467
|
-
return `<h${level} id="${id}" data-jump-title="${
|
|
52805
|
+
return `<h${level} id="${id}" data-jump-title="${escapeHtml6(title)}">${inner}</h${level}>`;
|
|
52468
52806
|
});
|
|
52469
52807
|
return { rendered, toc };
|
|
52470
52808
|
}
|
|
@@ -52507,7 +52845,7 @@ function renderEditorialReadingRoom(input, now = /* @__PURE__ */ new Date()) {
|
|
|
52507
52845
|
const generatedAt = now.toISOString();
|
|
52508
52846
|
const data = { ...input, generatedAt, articles };
|
|
52509
52847
|
const template = readAsset("index.html");
|
|
52510
|
-
const html = template.replace("<title>Editorial Reading Room</title>", `<title>${
|
|
52848
|
+
const html = template.replace("<title>Editorial Reading Room</title>", `<title>${escapeHtml6(input.site.title)}</title>`).replace('content="A mobile-first editorial reading room."', `content="${escapeHtml6(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"));
|
|
52511
52849
|
const bytes = Buffer.byteLength(html);
|
|
52512
52850
|
return {
|
|
52513
52851
|
html,
|