mcp-scraper 0.50.0 → 0.51.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -32,6 +32,39 @@ function escapeHtml(value = "") {
32
32
  .replace(/"/g, """);
33
33
  }
34
34
 
35
+ function socialImage(article = null) {
36
+ return article?.ogImage || article?.image || state.data.site.ogImage || null;
37
+ }
38
+
39
+ function setMeta(selector, attribute, value) {
40
+ const element = document.querySelector(selector);
41
+ if (!element) return;
42
+ if (value) element.setAttribute(attribute, value);
43
+ else element.removeAttribute(attribute);
44
+ }
45
+
46
+ function updateSocialMeta(article = null) {
47
+ const title = article ? `${article.title} — ${state.data.site.title}` : state.data.site.title;
48
+ const description = article?.summary || state.data.deck || state.data.site.heroTitle;
49
+ const image = socialImage(article);
50
+ setMeta('meta[property="og:type"]', "content", article ? "article" : "website");
51
+ setMeta('meta[property="og:title"]', "content", title);
52
+ setMeta('meta[property="og:description"]', "content", description);
53
+ setMeta('meta[name="twitter:title"]', "content", title);
54
+ setMeta('meta[name="twitter:description"]', "content", description);
55
+ setMeta('meta[property="og:image"]', "content", image?.url || "");
56
+ setMeta('meta[property="og:image:alt"]', "content", image?.alt || "");
57
+ setMeta('meta[property="og:image:width"]', "content", image?.width ? String(image.width) : "");
58
+ setMeta('meta[property="og:image:height"]', "content", image?.height ? String(image.height) : "");
59
+ setMeta('meta[name="twitter:image"]', "content", image?.url || "");
60
+ setMeta('meta[name="twitter:image:alt"]', "content", image?.alt || "");
61
+ }
62
+
63
+ function imageMarkup(image, className) {
64
+ if (!image) return "";
65
+ return `<img class="${className}" src="${escapeHtml(image.url)}" alt="${escapeHtml(image.alt)}" loading="lazy">`;
66
+ }
67
+
35
68
  function setTypeMode() {
36
69
  const mode = typeModes[state.typeIndex % typeModes.length];
37
70
  if (mode === "default") document.documentElement.removeAttribute("data-type");
@@ -132,6 +165,7 @@ function renderHome() {
132
165
  elements.article.hidden = true;
133
166
  elements.progress.style.width = "0";
134
167
  document.title = state.data.site.title;
168
+ updateSocialMeta();
135
169
  document.querySelector("#masthead-product").textContent = state.data.site.product;
136
170
  document.querySelector("#masthead-title").textContent = state.data.site.edition;
137
171
  document.querySelector("#edition-label").textContent = state.data.site.editionLabel;
@@ -151,6 +185,7 @@ function renderHome() {
151
185
  .map(
152
186
  (article) => `<article class="feature-card">
153
187
  <button data-article="${article.slug}">
188
+ ${imageMarkup(article.image, "feature-card-image")}
154
189
  <p class="card-kicker">${escapeHtml(article.kicker)}</p>
155
190
  <h2>${escapeHtml(article.title)}</h2>
156
191
  <p>${escapeHtml(article.summary)}</p>
@@ -190,6 +225,7 @@ function renderArticle(article) {
190
225
  elements.home.hidden = true;
191
226
  elements.article.hidden = false;
192
227
  document.title = `${article.title} — ${state.data.site.title}`;
228
+ updateSocialMeta(article);
193
229
  document.querySelector("#article-category").textContent = article.category;
194
230
  document.querySelector("#article-kicker").textContent = article.kicker;
195
231
  document.querySelector("#article-title").textContent = article.title;
@@ -199,6 +235,32 @@ function renderArticle(article) {
199
235
  document.querySelector("#article-revision").textContent = article.revision
200
236
  ? `Revision ${article.revision}`
201
237
  : article.sourceType || "Source material";
238
+ const imageFigure = document.querySelector("#article-image");
239
+ const imageElement = document.querySelector("#article-image-src");
240
+ const imageCaption = document.querySelector("#article-image-caption");
241
+ const imageCaptionText = document.querySelector("#article-image-caption-text");
242
+ const imageSource = document.querySelector("#article-image-source");
243
+ if (article.image) {
244
+ imageFigure.hidden = false;
245
+ imageElement.src = article.image.url;
246
+ imageElement.alt = article.image.alt;
247
+ imageCaptionText.textContent = [article.image.caption, article.image.credit].filter(Boolean).join(" — ");
248
+ if (article.image.sourceUrl) {
249
+ imageSource.href = article.image.sourceUrl;
250
+ imageSource.hidden = false;
251
+ } else {
252
+ imageSource.removeAttribute("href");
253
+ imageSource.hidden = true;
254
+ }
255
+ imageCaption.hidden = !imageCaptionText.textContent && !article.image.sourceUrl;
256
+ } else {
257
+ imageFigure.hidden = true;
258
+ imageElement.removeAttribute("src");
259
+ imageElement.alt = "";
260
+ imageCaptionText.textContent = "";
261
+ imageSource.removeAttribute("href");
262
+ imageSource.hidden = true;
263
+ }
202
264
  document.querySelector("#article-body").innerHTML = article.html;
203
265
  document.querySelector("#article-source").textContent = article.sourceLabel;
204
266
  renderDesktopJumps(article);
@@ -8,6 +8,7 @@
8
8
  content="A mobile-first editorial reading room."
9
9
  />
10
10
  <meta name="theme-color" content="#f5f1e8" />
11
+ <!--__READING_ROOM_SOCIAL_META__-->
11
12
  <title>Editorial Reading Room</title>
12
13
  <style>/*__READING_ROOM_STYLES__*/</style>
13
14
  </head>
@@ -99,6 +100,13 @@
99
100
  <span id="article-word-count"></span>
100
101
  <span id="article-revision"></span>
101
102
  </div>
103
+ <figure class="article-image" id="article-image" hidden>
104
+ <img id="article-image-src" alt="" />
105
+ <figcaption id="article-image-caption">
106
+ <span id="article-image-caption-text"></span>
107
+ <a id="article-image-source" target="_blank" rel="noopener noreferrer">Image source</a>
108
+ </figcaption>
109
+ </figure>
102
110
  </header>
103
111
  <div class="article-body" id="article-body"></div>
104
112
  <footer class="article-source">
@@ -277,6 +277,14 @@ a {
277
277
  text-align: left;
278
278
  }
279
279
 
280
+ .feature-card-image {
281
+ width: 100%;
282
+ aspect-ratio: 16 / 9;
283
+ margin-bottom: 20px;
284
+ border: 1px solid var(--rule);
285
+ object-fit: cover;
286
+ }
287
+
280
288
  .card-kicker {
281
289
  margin: 0 0 18px;
282
290
  color: var(--accent);
@@ -594,6 +602,37 @@ body.drawer-open .drawer-backdrop {
594
602
  text-transform: uppercase;
595
603
  }
596
604
 
605
+ .article-image {
606
+ margin: 34px 0 0;
607
+ }
608
+
609
+ .article-image img {
610
+ display: block;
611
+ width: 100%;
612
+ max-height: 680px;
613
+ border: 1px solid var(--rule);
614
+ object-fit: cover;
615
+ }
616
+
617
+ .article-image figcaption {
618
+ margin-top: 9px;
619
+ color: var(--muted);
620
+ font-size: 12px;
621
+ line-height: 1.45;
622
+ }
623
+
624
+ .article-image figcaption a {
625
+ margin-left: 0.45em;
626
+ color: inherit;
627
+ }
628
+
629
+ .article-body img {
630
+ display: block;
631
+ max-width: 100%;
632
+ height: auto;
633
+ margin: 1.8em auto;
634
+ }
635
+
597
636
  .article-body {
598
637
  padding-top: 28px;
599
638
  font-family: var(--serif);
@@ -158,7 +158,7 @@ import {
158
158
  resolveDeploymentProfile,
159
159
  resolveLocalSourcebookSchemaType,
160
160
  transcribeMediaUrl
161
- } from "./chunk-44LNKZTE.js";
161
+ } from "./chunk-O63VVCCE.js";
162
162
  import {
163
163
  auditImageUrls,
164
164
  auditImages,
@@ -214,7 +214,7 @@ import {
214
214
  } from "./chunk-3ZUBQQPQ.js";
215
215
  import {
216
216
  PACKAGE_VERSION
217
- } from "./chunk-C7NN57WH.js";
217
+ } from "./chunk-73MUQSWC.js";
218
218
  import {
219
219
  abandonExtractSettlement,
220
220
  countSuccessfulPages,
@@ -20000,6 +20000,34 @@ import { Hono as Hono11 } from "hono";
20000
20000
  import { z as z21 } from "zod";
20001
20001
 
20002
20002
  // src/api/reddit-trending.ts
20003
+ var PARSE_REDDIT_SEARCH = `(() => {
20004
+ const txt = el => ((el && el.innerText) || '').trim();
20005
+ const bodyText = (document.body && document.body.innerText) || '';
20006
+ const blocked = /whoa there|blocked by network|you've been blocked|network (policy|security)|log in to your reddit account/i.test(bodyText);
20007
+ const listing = document.querySelector('.search-result-listing');
20008
+ const rows = [];
20009
+ document.querySelectorAll('.search-result.search-result-link').forEach(r => {
20010
+ const titleEl = r.querySelector('header.search-result-header a.search-title');
20011
+ rows.push({
20012
+ title: txt(titleEl),
20013
+ url: (titleEl && titleEl.getAttribute('href')) || '',
20014
+ subreddit: txt(r.querySelector('a.search-subreddit-link')),
20015
+ scoreText: txt(r.querySelector('.search-score')),
20016
+ commentsText: txt(r.querySelector('a.search-comments')),
20017
+ ageText: txt(r.querySelector('.search-time time')),
20018
+ });
20019
+ });
20020
+ return { blocked: blocked, listingFound: Boolean(listing), rowCount: rows.length, rows: rows };
20021
+ })()`;
20022
+ function buildRedditSearchUrl(topic, subreddit, window2 = "month") {
20023
+ const base = subreddit ? `https://old.reddit.com/r/${encodeURIComponent(subreddit)}/search` : "https://old.reddit.com/search";
20024
+ const params = new URLSearchParams();
20025
+ params.set("q", topic);
20026
+ params.set("sort", "top");
20027
+ params.set("t", window2);
20028
+ if (subreddit) params.set("restrict_sr", "on");
20029
+ return `${base}?${params.toString()}`;
20030
+ }
20003
20031
  function buildSerpQuery(topic, subreddit) {
20004
20032
  const scope = subreddit ? `site:reddit.com/r/${subreddit}` : "site:reddit.com";
20005
20033
  return `${scope} ${topic}`.trim();
@@ -20062,6 +20090,32 @@ function canonicalThreadUrl(href) {
20062
20090
  function engagementScore(score, commentCount) {
20063
20091
  return score + 2 * commentCount;
20064
20092
  }
20093
+ function normalizeSearchRows(rows) {
20094
+ if (!Array.isArray(rows)) return [];
20095
+ const seen = /* @__PURE__ */ new Set();
20096
+ const out = [];
20097
+ for (const raw of rows) {
20098
+ if (!raw || typeof raw !== "object") continue;
20099
+ const r = raw;
20100
+ const url = canonicalThreadUrl(r.url);
20101
+ const title = typeof r.title === "string" ? r.title.trim() : "";
20102
+ if (!url || !title || seen.has(url)) continue;
20103
+ seen.add(url);
20104
+ const score = parseCountText(r.scoreText);
20105
+ const commentCount = parseCountText(r.commentsText);
20106
+ out.push({
20107
+ title,
20108
+ url,
20109
+ subreddit: typeof r.subreddit === "string" ? r.subreddit.trim() : "",
20110
+ score,
20111
+ commentCount,
20112
+ engagementScore: engagementScore(score, commentCount),
20113
+ ageText: typeof r.ageText === "string" ? r.ageText.trim() : "",
20114
+ topQuestions: []
20115
+ });
20116
+ }
20117
+ return out;
20118
+ }
20065
20119
  var INTERROGATIVE_STARTERS = /* @__PURE__ */ new Set([
20066
20120
  "what",
20067
20121
  "whats",
@@ -20213,8 +20267,8 @@ var PARSE_REDDIT = `(() => {
20213
20267
  });
20214
20268
  return { title: title, author: author, score: score, postBody: postBody, blocked: blocked, numComments: comments.length, comments: comments };
20215
20269
  })()`;
20216
- async function scrapeOldRedditWithRetries(url, script, accept) {
20217
- for (let attempt = 0; attempt < 4; attempt++) {
20270
+ async function scrapeOldRedditWithRetries(url, script, accept, maxAttempts = 4) {
20271
+ for (let attempt = 0; attempt < maxAttempts; attempt++) {
20218
20272
  const backupAttempt = attempt === 3;
20219
20273
  if (backupAttempt && !backupProxyAvailable()) break;
20220
20274
  const proxyId = backupAttempt ? await createBackupProxyIdSafe(browserServiceApiKey(), attempt) : await residentialProxyId(attempt);
@@ -20330,6 +20384,9 @@ redditApp.post("/trending", createApiKeyAuth(), async (c) => {
20330
20384
  if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.reddit_thread), 402);
20331
20385
  discoveryDebited = true;
20332
20386
  let candidates = [];
20387
+ let discoverySource = "google_serp";
20388
+ let primaryDiscoveryDegraded = false;
20389
+ let primaryDegradationReasons = [];
20333
20390
  try {
20334
20391
  const serp = await harvest({
20335
20392
  query: serpQuery,
@@ -20343,14 +20400,59 @@ redditApp.post("/trending", createApiKeyAuth(), async (c) => {
20343
20400
  softDeadlineMs
20344
20401
  });
20345
20402
  candidates = extractRedditThreadUrls(serp.organicResults, body.maxThreads);
20403
+ primaryDiscoveryDegraded = serp.diagnostics?.degradedResult === true;
20404
+ primaryDegradationReasons = Array.isArray(serp.diagnostics?.degradationReasons) ? serp.diagnostics.degradationReasons.filter((reason) => typeof reason === "string") : [];
20346
20405
  } catch {
20347
20406
  candidates = [];
20407
+ primaryDiscoveryDegraded = true;
20408
+ primaryDegradationReasons = ["primary_serp_failed"];
20409
+ }
20410
+ if (candidates.length === 0) {
20411
+ const fallback = await scrapeOldRedditWithRetries(
20412
+ buildRedditSearchUrl(body.topic, subreddit, body.window),
20413
+ PARSE_REDDIT_SEARCH,
20414
+ (data) => !data.blocked && data.listingFound,
20415
+ 2
20416
+ );
20417
+ if (fallback) {
20418
+ candidates = normalizeSearchRows(fallback.data.rows).slice(0, body.maxThreads).map((thread) => ({ title: thread.title, url: thread.url, subreddit: thread.subreddit.replace(/^r\//i, "") }));
20419
+ discoverySource = "reddit_search_fallback";
20420
+ } else {
20421
+ discoverySource = "none";
20422
+ }
20348
20423
  }
20349
20424
  if (candidates.length === 0) {
20350
20425
  await creditMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD_REFUND, "no reddit threads discovered");
20351
20426
  discoveryRefunded = true;
20352
- await logRequestEvent({ userId: user.id, source: "reddit_trending", status: "failed", query: body.topic, error: "no reddit threads discovered" });
20353
- return c.json({ error: "No Reddit threads found for that topic and window (refunded)" }, 404);
20427
+ const degradedResult = discoverySource === "none";
20428
+ const degradationReasons = degradedResult ? [.../* @__PURE__ */ new Set([...primaryDegradationReasons, "reddit_fallback_empty_or_blocked"])] : [];
20429
+ const result2 = {
20430
+ topic: body.topic,
20431
+ subreddit: subreddit ?? null,
20432
+ window: body.window === "week" ? "7d" : "30d",
20433
+ totals: trendingTotals([]),
20434
+ rankedThreads: [],
20435
+ questions: [],
20436
+ threadsScraped: 0,
20437
+ candidatesFound: 0,
20438
+ partial: false,
20439
+ searchQuery: serpQuery,
20440
+ discoverySource,
20441
+ resultQuality: degradedResult ? "degraded" : "complete",
20442
+ degradedResult,
20443
+ degradationReasons,
20444
+ retryRecommended: degradedResult,
20445
+ billingRefunded: true
20446
+ };
20447
+ await logRequestEvent({
20448
+ userId: user.id,
20449
+ source: "reddit_trending",
20450
+ status: "done",
20451
+ query: body.topic,
20452
+ resultCount: 0,
20453
+ result: result2
20454
+ });
20455
+ return c.json(result2);
20354
20456
  }
20355
20457
  let ranked;
20356
20458
  if (!body.includeComments) {
@@ -20426,7 +20528,13 @@ redditApp.post("/trending", createApiKeyAuth(), async (c) => {
20426
20528
  threadsScraped: body.includeComments ? ranked.length : 0,
20427
20529
  candidatesFound: candidates.length,
20428
20530
  partial: body.includeComments && ranked.length < candidates.length,
20429
- searchQuery: serpQuery
20531
+ searchQuery: serpQuery,
20532
+ discoverySource,
20533
+ resultQuality: primaryDiscoveryDegraded ? "partial" : "complete",
20534
+ degradedResult: false,
20535
+ degradationReasons: primaryDiscoveryDegraded ? primaryDegradationReasons : [],
20536
+ retryRecommended: false,
20537
+ billingRefunded: false
20430
20538
  };
20431
20539
  await logRequestEvent({ userId: user.id, source: "reddit_trending", status: "done", query: body.topic, resultCount: ranked.length, result });
20432
20540
  return c.json(result);
@@ -23886,6 +23994,24 @@ import { Hono as Hono21 } from "hono";
23886
23994
  import { createMcpHandler } from "@modelcontextprotocol/server";
23887
23995
 
23888
23996
  // src/mcp/memory-mcp-tool-executor.ts
23997
+ function folderInventory(result) {
23998
+ if (!Array.isArray(result.notes)) return [];
23999
+ const folders = /* @__PURE__ */ new Set();
24000
+ for (const note of result.notes) {
24001
+ if (!note || typeof note !== "object" || Array.isArray(note)) continue;
24002
+ const path5 = note.path;
24003
+ if (typeof path5 !== "string") continue;
24004
+ const segments = path5.replace(/\\/g, "/").split("/").filter(Boolean);
24005
+ for (let depth = 1; depth < segments.length; depth += 1) {
24006
+ folders.add(segments.slice(0, depth).join("/"));
24007
+ }
24008
+ }
24009
+ return [...folders].sort((a, b) => a.localeCompare(b));
24010
+ }
24011
+ function enrichMemoryResult(toolName, result) {
24012
+ if (toolName !== "listTool" || result.ok !== true) return result;
24013
+ return { ...result, folders: folderInventory(result) };
24014
+ }
23889
24015
  var MemoryMcpToolExecutor = class {
23890
24016
  baseUrl;
23891
24017
  apiKey;
@@ -23908,7 +24034,10 @@ var MemoryMcpToolExecutor = class {
23908
24034
  const message = data?.error ?? `memory ${toolName} failed (HTTP ${res.status})`;
23909
24035
  return { content: [{ type: "text", text: message }], isError: true };
23910
24036
  }
23911
- const result = data ?? { ok: false, error: `memory ${toolName} returned no result` };
24037
+ const result = enrichMemoryResult(
24038
+ toolName,
24039
+ data ?? { ok: false, error: `memory ${toolName} returned no result` }
24040
+ );
23912
24041
  return {
23913
24042
  content: [{ type: "text", text: JSON.stringify(result) }],
23914
24043
  structuredContent: result,
@@ -30749,6 +30878,24 @@ function safeFilename(requested, slug2) {
30749
30878
  function escapedJsonForHtml(value) {
30750
30879
  return JSON.stringify(value).replace(/</g, "\\u003c").replace(/\u2028/g, "\\u2028").replace(/\u2029/g, "\\u2029");
30751
30880
  }
30881
+ function socialMeta(input) {
30882
+ const image = input.site.ogImage;
30883
+ const tags = [
30884
+ '<meta property="og:type" content="website">',
30885
+ `<meta property="og:title" content="${escapeHtml5(input.site.title)}">`,
30886
+ `<meta property="og:description" content="${escapeHtml5(input.deck || input.site.heroTitle)}">`,
30887
+ '<meta name="twitter:card" content="summary_large_image">',
30888
+ `<meta name="twitter:title" content="${escapeHtml5(input.site.title)}">`,
30889
+ `<meta name="twitter:description" content="${escapeHtml5(input.deck || input.site.heroTitle)}">`,
30890
+ `<meta property="og:image" content="${image ? escapeHtml5(image.url) : ""}">`,
30891
+ `<meta property="og:image:alt" content="${image ? escapeHtml5(image.alt) : ""}">`,
30892
+ `<meta property="og:image:width" content="${image?.width ?? ""}">`,
30893
+ `<meta property="og:image:height" content="${image?.height ?? ""}">`,
30894
+ `<meta name="twitter:image" content="${image ? escapeHtml5(image.url) : ""}">`,
30895
+ `<meta name="twitter:image:alt" content="${image ? escapeHtml5(image.alt) : ""}">`
30896
+ ];
30897
+ return tags.join("\n ");
30898
+ }
30752
30899
  function renderEditorialReadingRoom(input, now = /* @__PURE__ */ new Date()) {
30753
30900
  const sourceBytes = input.articles.reduce((total, article) => total + Buffer.byteLength(article.markdown), 0);
30754
30901
  if (sourceBytes > 2e6) throw new Error("Editorial reading-room Markdown must be 2,000,000 bytes or fewer in total.");
@@ -30781,7 +30928,7 @@ function renderEditorialReadingRoom(input, now = /* @__PURE__ */ new Date()) {
30781
30928
  const generatedAt = now.toISOString();
30782
30929
  const data = { ...input, generatedAt, articles };
30783
30930
  const template = readAsset("index.html");
30784
- 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"));
30931
+ 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_SOCIAL_META__-->", socialMeta(input)).replace("/*__READING_ROOM_STYLES__*/", readAsset("styles.css")).replace("/*__READING_ROOM_DATA__*/", escapedJsonForHtml(data)).replace("/*__READING_ROOM_SCRIPT__*/", readAsset("app.js"));
30785
30932
  const bytes = Buffer.byteLength(html);
30786
30933
  return {
30787
30934
  html,
@@ -44403,4 +44550,4 @@ app.get("/blog/:slug/", (c) => {
44403
44550
  export {
44404
44551
  app
44405
44552
  };
44406
- //# sourceMappingURL=server-3ND2EXYO.js.map
44553
+ //# sourceMappingURL=server-IGIIEHXY.js.map