mcp-scraper 0.33.7 → 0.34.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.
Files changed (42) hide show
  1. package/dist/bin/api-server.cjs +10063 -10012
  2. package/dist/bin/api-server.cjs.map +1 -1
  3. package/dist/bin/api-server.js +2 -2
  4. package/dist/bin/mcp-scraper-cli.cjs +2 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +3 -3
  7. package/dist/bin/mcp-scraper-install.cjs +1 -1
  8. package/dist/bin/mcp-scraper-install.cjs.map +1 -1
  9. package/dist/bin/mcp-scraper-install.js +1 -1
  10. package/dist/bin/mcp-stdio-server.cjs +15 -8
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +3 -3
  13. package/dist/bin/paa-harvest.cjs +22 -0
  14. package/dist/bin/paa-harvest.cjs.map +1 -1
  15. package/dist/bin/paa-harvest.js +2 -2
  16. package/dist/{chunk-XVCETVYY.js → chunk-4KQVIYIH.js} +11 -3
  17. package/dist/chunk-4KQVIYIH.js.map +1 -0
  18. package/dist/{chunk-V73MPRU6.js → chunk-CB5C3BPB.js} +17 -1
  19. package/dist/chunk-CB5C3BPB.js.map +1 -0
  20. package/dist/{chunk-H6QECE2U.js → chunk-J32XSMJI.js} +16 -10
  21. package/dist/chunk-J32XSMJI.js.map +1 -0
  22. package/dist/chunk-NFMIHEYE.js +7 -0
  23. package/dist/chunk-NFMIHEYE.js.map +1 -0
  24. package/dist/{chunk-AT2PFWHW.js → chunk-ZID3WQID.js} +2 -2
  25. package/dist/index.cjs +22 -0
  26. package/dist/index.cjs.map +1 -1
  27. package/dist/index.d.cts +3 -0
  28. package/dist/index.d.ts +3 -0
  29. package/dist/index.js +2 -2
  30. package/dist/{server-B35CQJB3.js → server-LKVPEQFE.js} +130 -108
  31. package/dist/server-LKVPEQFE.js.map +1 -0
  32. package/dist/{worker-3LYNVGLF.js → worker-ZZHSYI3Y.js} +3 -3
  33. package/docs/mcp-tool-manifest.generated.json +36 -8
  34. package/package.json +1 -1
  35. package/dist/chunk-H6QECE2U.js.map +0 -1
  36. package/dist/chunk-HAMN42SP.js +0 -7
  37. package/dist/chunk-HAMN42SP.js.map +0 -1
  38. package/dist/chunk-V73MPRU6.js.map +0 -1
  39. package/dist/chunk-XVCETVYY.js.map +0 -1
  40. package/dist/server-B35CQJB3.js.map +0 -1
  41. /package/dist/{chunk-AT2PFWHW.js.map → chunk-ZID3WQID.js.map} +0 -0
  42. /package/dist/{worker-3LYNVGLF.js.map → worker-ZZHSYI3Y.js.map} +0 -0
package/dist/index.d.cts CHANGED
@@ -24,6 +24,7 @@ declare const HarvestOptionsSchema: z.ZodObject<{
24
24
  format: z.ZodDefault<z.ZodEnum<["json", "csv", "both"]>>;
25
25
  serpOnly: z.ZodDefault<z.ZodBoolean>;
26
26
  pages: z.ZodDefault<z.ZodNumber>;
27
+ recency: z.ZodOptional<z.ZodEnum<["day", "week", "month", "year"]>>;
27
28
  softDeadlineMs: z.ZodOptional<z.ZodNumber>;
28
29
  }, "strip", z.ZodTypeAny, {
29
30
  query: string;
@@ -49,6 +50,7 @@ declare const HarvestOptionsSchema: z.ZodObject<{
49
50
  kernelApiKey?: string | undefined;
50
51
  kernelProxyId?: string | undefined;
51
52
  kernelProxyResolution?: unknown;
53
+ recency?: "day" | "week" | "month" | "year" | undefined;
52
54
  softDeadlineMs?: number | undefined;
53
55
  }, {
54
56
  query: string;
@@ -74,6 +76,7 @@ declare const HarvestOptionsSchema: z.ZodObject<{
74
76
  format?: "json" | "csv" | "both" | undefined;
75
77
  serpOnly?: boolean | undefined;
76
78
  pages?: number | undefined;
79
+ recency?: "day" | "week" | "month" | "year" | undefined;
77
80
  softDeadlineMs?: number | undefined;
78
81
  }>;
79
82
 
package/dist/index.d.ts CHANGED
@@ -24,6 +24,7 @@ declare const HarvestOptionsSchema: z.ZodObject<{
24
24
  format: z.ZodDefault<z.ZodEnum<["json", "csv", "both"]>>;
25
25
  serpOnly: z.ZodDefault<z.ZodBoolean>;
26
26
  pages: z.ZodDefault<z.ZodNumber>;
27
+ recency: z.ZodOptional<z.ZodEnum<["day", "week", "month", "year"]>>;
27
28
  softDeadlineMs: z.ZodOptional<z.ZodNumber>;
28
29
  }, "strip", z.ZodTypeAny, {
29
30
  query: string;
@@ -49,6 +50,7 @@ declare const HarvestOptionsSchema: z.ZodObject<{
49
50
  kernelApiKey?: string | undefined;
50
51
  kernelProxyId?: string | undefined;
51
52
  kernelProxyResolution?: unknown;
53
+ recency?: "day" | "week" | "month" | "year" | undefined;
52
54
  softDeadlineMs?: number | undefined;
53
55
  }, {
54
56
  query: string;
@@ -74,6 +76,7 @@ declare const HarvestOptionsSchema: z.ZodObject<{
74
76
  format?: "json" | "csv" | "both" | undefined;
75
77
  serpOnly?: boolean | undefined;
76
78
  pages?: number | undefined;
79
+ recency?: "day" | "week" | "month" | "year" | undefined;
77
80
  softDeadlineMs?: number | undefined;
78
81
  }>;
79
82
 
package/dist/index.js CHANGED
@@ -1,11 +1,11 @@
1
1
  import {
2
2
  harvest
3
- } from "./chunk-XVCETVYY.js";
3
+ } from "./chunk-4KQVIYIH.js";
4
4
  import {
5
5
  recordVendorUsage
6
6
  } from "./chunk-V36LS5YV.js";
7
7
  import "./chunk-K443GQY5.js";
8
- import "./chunk-V73MPRU6.js";
8
+ import "./chunk-CB5C3BPB.js";
9
9
  import "./chunk-M2S27J6Z.js";
10
10
  import "./chunk-62DQAWPF.js";
11
11
 
@@ -20,7 +20,7 @@ import {
20
20
  resolveBackupKernelProxyId,
21
21
  resolveKernelProxyId,
22
22
  serpArrivalUrl
23
- } from "./chunk-XVCETVYY.js";
23
+ } from "./chunk-4KQVIYIH.js";
24
24
  import {
25
25
  HttpMcpToolExecutor,
26
26
  assessTranscriptSignal,
@@ -40,7 +40,7 @@ import {
40
40
  sanitizeAttempts,
41
41
  sanitizeHarvestResult,
42
42
  transcribeMediaUrl
43
- } from "./chunk-H6QECE2U.js";
43
+ } from "./chunk-J32XSMJI.js";
44
44
  import {
45
45
  auditImages,
46
46
  buildLinkReport,
@@ -69,7 +69,7 @@ import {
69
69
  workflowOutputBaseDir,
70
70
  workflowStepCount,
71
71
  workflowSupportsSteps
72
- } from "./chunk-AT2PFWHW.js";
72
+ } from "./chunk-ZID3WQID.js";
73
73
  import "./chunk-K443GQY5.js";
74
74
  import {
75
75
  DEFAULT_MAPS_PROXY_MODE,
@@ -80,8 +80,8 @@ import {
80
80
  RawMapsHoursRowSchema,
81
81
  RawMapsOverviewSchema,
82
82
  RawMapsReviewStatsSchema
83
- } from "./chunk-V73MPRU6.js";
84
- import "./chunk-HAMN42SP.js";
83
+ } from "./chunk-CB5C3BPB.js";
84
+ import "./chunk-NFMIHEYE.js";
85
85
  import {
86
86
  completeExtractJob,
87
87
  countSuccessfulPages,
@@ -10367,7 +10367,8 @@ var HarvestBodySchema = z14.object({
10367
10367
  proxyZip: z14.string().regex(/^\d{5}$/).optional(),
10368
10368
  debug: z14.boolean().optional(),
10369
10369
  serpOnly: z14.boolean().optional(),
10370
- pages: z14.number().int().min(1).max(2).optional()
10370
+ pages: z14.number().int().min(1).max(2).optional(),
10371
+ recency: z14.enum(["day", "week", "month", "year"]).optional()
10371
10372
  });
10372
10373
  var ExtractUrlBodySchema = z14.object({
10373
10374
  url: z14.string().min(1, "url is required"),
@@ -13163,33 +13164,44 @@ import { Hono as Hono10 } from "hono";
13163
13164
  import { z as z19 } from "zod";
13164
13165
 
13165
13166
  // src/api/reddit-trending.ts
13166
- var PARSE_REDDIT_SEARCH = `(() => {
13167
- const txt = el => ((el && el.innerText) || '').trim();
13168
- const bodyText = (document.body && document.body.innerText) || '';
13169
- const blocked = /whoa there|blocked by network|you've been blocked|network (policy|security)|log in to your reddit account/i.test(bodyText);
13170
- const listing = document.querySelector('.search-result-listing');
13171
- const rows = [];
13172
- document.querySelectorAll('.search-result.search-result-link').forEach(r => {
13173
- const titleEl = r.querySelector('header.search-result-header a.search-title');
13174
- rows.push({
13175
- title: txt(titleEl),
13176
- url: (titleEl && titleEl.getAttribute('href')) || '',
13177
- subreddit: txt(r.querySelector('a.search-subreddit-link')),
13178
- scoreText: txt(r.querySelector('.search-score')),
13179
- commentsText: txt(r.querySelector('a.search-comments')),
13180
- ageText: txt(r.querySelector('.search-time time')),
13167
+ function buildSerpQuery(topic, subreddit) {
13168
+ const scope = subreddit ? `site:reddit.com/r/${subreddit}` : "site:reddit.com";
13169
+ return `${scope} ${topic}`.trim();
13170
+ }
13171
+ function subredditFromUrl(url) {
13172
+ const m = url.match(/\/r\/([^/]+)\//i);
13173
+ return m ? m[1] : "";
13174
+ }
13175
+ function extractRedditThreadUrls(organicResults, limit) {
13176
+ if (!Array.isArray(organicResults)) return [];
13177
+ const seen = /* @__PURE__ */ new Set();
13178
+ const out = [];
13179
+ for (const raw of organicResults) {
13180
+ if (!raw || typeof raw !== "object") continue;
13181
+ const r = raw;
13182
+ const url = canonicalThreadUrl(r.url);
13183
+ if (!url || seen.has(url)) continue;
13184
+ seen.add(url);
13185
+ out.push({
13186
+ title: typeof r.title === "string" ? r.title.trim() : "",
13187
+ url,
13188
+ subreddit: subredditFromUrl(url)
13181
13189
  });
13190
+ if (out.length >= limit) break;
13191
+ }
13192
+ return out;
13193
+ }
13194
+ async function mapWithConcurrency(items, concurrency, fn) {
13195
+ const results = new Array(items.length);
13196
+ let next = 0;
13197
+ const workers = Array.from({ length: Math.max(1, Math.min(concurrency, items.length)) }, async () => {
13198
+ while (next < items.length) {
13199
+ const i = next++;
13200
+ results[i] = await fn(items[i], i);
13201
+ }
13182
13202
  });
13183
- return { blocked: blocked, listingFound: Boolean(listing), rowCount: rows.length, rows: rows };
13184
- })()`;
13185
- function buildRedditSearchUrl(topic, subreddit) {
13186
- const base = subreddit ? `https://old.reddit.com/r/${encodeURIComponent(subreddit)}/search` : "https://old.reddit.com/search";
13187
- const params = new URLSearchParams();
13188
- params.set("q", topic);
13189
- params.set("sort", "top");
13190
- params.set("t", "month");
13191
- if (subreddit) params.set("restrict_sr", "on");
13192
- return `${base}?${params.toString()}`;
13203
+ await Promise.all(workers);
13204
+ return results;
13193
13205
  }
13194
13206
  function parseCountText(text2) {
13195
13207
  if (typeof text2 !== "string") return 0;
@@ -13214,35 +13226,6 @@ function canonicalThreadUrl(href) {
13214
13226
  function engagementScore(score, commentCount) {
13215
13227
  return score + 2 * commentCount;
13216
13228
  }
13217
- function normalizeSearchRows(rows) {
13218
- if (!Array.isArray(rows)) return [];
13219
- const seen = /* @__PURE__ */ new Set();
13220
- const out = [];
13221
- for (const raw of rows) {
13222
- if (!raw || typeof raw !== "object") continue;
13223
- const r = raw;
13224
- const url = canonicalThreadUrl(r.url);
13225
- const title = typeof r.title === "string" ? r.title.trim() : "";
13226
- if (!url || !title || seen.has(url)) continue;
13227
- seen.add(url);
13228
- const score = parseCountText(r.scoreText);
13229
- const commentCount = parseCountText(r.commentsText);
13230
- out.push({
13231
- title,
13232
- url,
13233
- subreddit: typeof r.subreddit === "string" ? r.subreddit.trim() : "",
13234
- score,
13235
- commentCount,
13236
- engagementScore: engagementScore(score, commentCount),
13237
- ageText: typeof r.ageText === "string" ? r.ageText.trim() : "",
13238
- topQuestions: []
13239
- });
13240
- }
13241
- return out;
13242
- }
13243
- function rankThreads(rows, maxThreads) {
13244
- return [...rows].sort((a, b) => b.engagementScore - a.engagementScore).slice(0, maxThreads);
13245
- }
13246
13229
  var INTERROGATIVE_STARTERS = /* @__PURE__ */ new Set([
13247
13230
  "what",
13248
13231
  "whats",
@@ -13335,7 +13318,8 @@ var RedditThreadBodySchema = z19.object({
13335
13318
  var RedditTrendingBodySchema = z19.object({
13336
13319
  topic: z19.string().trim().min(1, "topic is required"),
13337
13320
  subreddit: z19.string().trim().regex(/^(r\/)?[A-Za-z0-9_]{2,24}$/i, "subreddit must be a bare name like SEO").optional(),
13338
- maxThreads: z19.number().int().min(1).max(25).default(10),
13321
+ window: z19.enum(["week", "month"]).default("month"),
13322
+ maxThreads: z19.number().int().min(1).max(40).default(20),
13339
13323
  includeComments: z19.boolean().default(true),
13340
13324
  maxCommentsPerThread: z19.number().int().min(1).max(200).default(50)
13341
13325
  });
@@ -13486,50 +13470,76 @@ redditApp.post("/thread", createApiKeyAuth(), async (c) => {
13486
13470
  await releaseConcurrencyGate(gate.lockId);
13487
13471
  }
13488
13472
  });
13473
+ var TRENDING_SOFT_DEADLINE_MS = 24e4;
13474
+ var TRENDING_SCRAPE_CONCURRENCY = 4;
13489
13475
  redditApp.post("/trending", createApiKeyAuth(), async (c) => {
13490
13476
  const raw = await c.req.json().catch(() => ({}));
13491
13477
  const parsed = RedditTrendingBodySchema.safeParse(raw);
13492
13478
  if (!parsed.success) return c.json(invalidRequest4(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
13493
13479
  const body = parsed.data;
13494
13480
  const subreddit = body.subreddit ? body.subreddit.replace(/^r\//i, "") : void 0;
13495
- const searchUrl = buildRedditSearchUrl(body.topic, subreddit);
13481
+ const serpQuery = buildSerpQuery(body.topic, subreddit);
13496
13482
  const user = c.get("user");
13497
13483
  const gate = await acquireConcurrencyGate(user, "reddit_trending", {
13498
13484
  reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
13499
13485
  metadata: { topic: body.topic }
13500
13486
  });
13501
13487
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
13488
+ const softDeadlineMs = Date.now() + TRENDING_SOFT_DEADLINE_MS;
13502
13489
  let discoveryDebited = false;
13503
13490
  let discoveryRefunded = false;
13504
- let threadDebitMcOutstanding = 0;
13505
- let commentDebitMcOutstanding = 0;
13491
+ const outstanding = { thread: 0, comment: 0 };
13506
13492
  try {
13507
- const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD, searchUrl);
13493
+ const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD, serpQuery);
13508
13494
  if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.reddit_thread), 402);
13509
13495
  discoveryDebited = true;
13510
- const discovery = await scrapeOldRedditWithRetries(
13511
- searchUrl,
13512
- PARSE_REDDIT_SEARCH,
13513
- (data) => !data.blocked && data.listingFound
13514
- );
13515
- if (!discovery) {
13516
- await creditMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD_REFUND, "reddit blocked/empty after retries");
13517
- discoveryRefunded = true;
13518
- await logRequestEvent({ userId: user.id, source: "reddit_trending", status: "failed", query: body.topic, error: "reddit blocked/empty after retries" });
13519
- return c.json({ error: "Reddit blocked the request after retries (refunded)" }, 503);
13496
+ let candidates = [];
13497
+ try {
13498
+ const serp = await harvest({
13499
+ query: serpQuery,
13500
+ serpOnly: true,
13501
+ proxyMode: "none",
13502
+ pages: 2,
13503
+ recency: body.window,
13504
+ kernelApiKey: browserServiceApiKey(),
13505
+ format: "json",
13506
+ outputDir: "/tmp/reddit-trending-serp",
13507
+ softDeadlineMs
13508
+ });
13509
+ candidates = extractRedditThreadUrls(serp.organicResults, body.maxThreads);
13510
+ } catch {
13511
+ candidates = [];
13520
13512
  }
13521
- const rows = normalizeSearchRows(discovery.data.rows);
13522
- const ranked = rankThreads(rows, body.maxThreads);
13523
- let threadsScraped = 0;
13524
- if (body.includeComments) {
13525
- let insufficient = false;
13526
- for (const thread of ranked) {
13527
- if (insufficient) break;
13528
- const oldUrl = toOldReddit(thread.url);
13529
- if (!oldUrl) continue;
13530
- const threadDebit = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD, thread.url);
13531
- if (!threadDebit.ok) break;
13532
- threadDebitMcOutstanding += MC_COSTS.reddit_thread;
13513
+ if (candidates.length === 0) {
13514
+ await creditMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD_REFUND, "no reddit threads discovered");
13515
+ discoveryRefunded = true;
13516
+ await logRequestEvent({ userId: user.id, source: "reddit_trending", status: "failed", query: body.topic, error: "no reddit threads discovered" });
13517
+ return c.json({ error: "No Reddit threads found for that topic and window (refunded)" }, 404);
13518
+ }
13519
+ let ranked;
13520
+ if (!body.includeComments) {
13521
+ ranked = candidates.map((cand) => ({
13522
+ title: cand.title,
13523
+ url: cand.url,
13524
+ subreddit: cand.subreddit,
13525
+ score: 0,
13526
+ commentCount: 0,
13527
+ engagementScore: 0,
13528
+ ageText: "",
13529
+ topQuestions: []
13530
+ }));
13531
+ } else {
13532
+ let balanceExhausted = false;
13533
+ const scrapedThreads = await mapWithConcurrency(candidates, TRENDING_SCRAPE_CONCURRENCY, async (cand) => {
13534
+ if (balanceExhausted || Date.now() >= softDeadlineMs) return null;
13535
+ const oldUrl = toOldReddit(cand.url);
13536
+ if (!oldUrl) return null;
13537
+ const threadDebit = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD, cand.url);
13538
+ if (!threadDebit.ok) {
13539
+ balanceExhausted = true;
13540
+ return null;
13541
+ }
13542
+ outstanding.thread += MC_COSTS.reddit_thread;
13533
13543
  const scraped = await scrapeOldRedditWithRetries(
13534
13544
  oldUrl,
13535
13545
  PARSE_REDDIT,
@@ -13537,46 +13547,57 @@ redditApp.post("/trending", createApiKeyAuth(), async (c) => {
13537
13547
  );
13538
13548
  if (!scraped) {
13539
13549
  await creditMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD_REFUND, "reddit blocked/empty after retries");
13540
- threadDebitMcOutstanding -= MC_COSTS.reddit_thread;
13541
- continue;
13550
+ outstanding.thread -= MC_COSTS.reddit_thread;
13551
+ return null;
13542
13552
  }
13553
+ const score = parseCountText(scraped.data.score);
13543
13554
  let comments = Array.isArray(scraped.data.comments) ? scraped.data.comments : [];
13555
+ const commentCount = comments.length;
13544
13556
  comments = comments.slice(0, body.maxCommentsPerThread);
13545
13557
  let commentMc = comments.length * MC_COSTS.reddit_comment;
13546
13558
  if (commentMc > 0) {
13547
- const commentDebit = await debitMc(user.id, commentMc, LedgerOperation.REDDIT_COMMENT, thread.url);
13559
+ const commentDebit = await debitMc(user.id, commentMc, LedgerOperation.REDDIT_COMMENT, cand.url);
13548
13560
  if (!commentDebit.ok) {
13561
+ balanceExhausted = true;
13549
13562
  const affordable = Math.floor((commentDebit.balance_mc ?? 0) / MC_COSTS.reddit_comment);
13550
13563
  comments = comments.slice(0, affordable);
13551
13564
  commentMc = affordable * MC_COSTS.reddit_comment;
13552
- if (commentMc > 0) await debitMc(user.id, commentMc, LedgerOperation.REDDIT_COMMENT, thread.url);
13553
- insufficient = true;
13565
+ if (commentMc > 0) await debitMc(user.id, commentMc, LedgerOperation.REDDIT_COMMENT, cand.url);
13554
13566
  }
13555
- commentDebitMcOutstanding += commentMc;
13567
+ outstanding.comment += commentMc;
13556
13568
  }
13557
13569
  const topLevelBodies = comments.filter((cm) => cm && cm.depth === 0).map((cm) => cm.body);
13558
- thread.topQuestions = extractQuestions([scraped.data.postBody, ...topLevelBodies]);
13559
- threadsScraped++;
13560
- }
13570
+ return {
13571
+ title: cand.title || scraped.data.title || "",
13572
+ url: cand.url,
13573
+ subreddit: cand.subreddit,
13574
+ score,
13575
+ commentCount,
13576
+ engagementScore: engagementScore(score, commentCount),
13577
+ ageText: "",
13578
+ topQuestions: extractQuestions([scraped.data.postBody, ...topLevelBodies])
13579
+ };
13580
+ });
13581
+ ranked = scrapedThreads.filter((t) => t !== null).sort((a, b) => b.engagementScore - a.engagementScore);
13561
13582
  }
13562
13583
  const result = {
13563
13584
  topic: body.topic,
13564
13585
  subreddit: subreddit ?? null,
13565
- window: "30d",
13586
+ window: body.window === "week" ? "7d" : "30d",
13566
13587
  totals: trendingTotals(ranked),
13567
13588
  rankedThreads: ranked,
13568
13589
  questions: flattenQuestions(ranked),
13569
- threadsScraped,
13570
- searchUrl,
13571
- attempts: discovery.attempts,
13572
- proxied: discovery.proxied
13590
+ threadsScraped: body.includeComments ? ranked.length : 0,
13591
+ candidatesFound: candidates.length,
13592
+ partial: body.includeComments && ranked.length < candidates.length,
13593
+ searchQuery: serpQuery
13573
13594
  };
13574
13595
  await logRequestEvent({ userId: user.id, source: "reddit_trending", status: "done", query: body.topic, resultCount: ranked.length, result });
13575
13596
  return c.json(result);
13576
13597
  } catch (err) {
13577
13598
  if (discoveryDebited && !discoveryRefunded) await creditMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD_REFUND, "failed call");
13578
- if (threadDebitMcOutstanding > 0) await creditMc(user.id, threadDebitMcOutstanding, LedgerOperation.REDDIT_THREAD_REFUND, "failed call");
13579
- if (commentDebitMcOutstanding > 0) await creditMc(user.id, commentDebitMcOutstanding, LedgerOperation.REDDIT_COMMENT_REFUND, "failed call");
13599
+ if (outstanding.thread > 0) await creditMc(user.id, outstanding.thread, LedgerOperation.REDDIT_THREAD_REFUND, "failed call");
13600
+ if (outstanding.comment > 0) await creditMc(user.id, outstanding.comment, LedgerOperation.REDDIT_COMMENT_REFUND, "failed call");
13580
13601
  const msg = err instanceof Error ? err.message : String(err);
13581
13602
  await logRequestEvent({ userId: user.id, source: "reddit_trending", status: "failed", query: body.topic, error: msg });
13582
13603
  return c.json({ error: msg }, 500);
@@ -21923,7 +21944,7 @@ async function readMemory(tool, args, token) {
21923
21944
  }
21924
21945
  return result;
21925
21946
  }
21926
- async function mapWithConcurrency(items, limit, map) {
21947
+ async function mapWithConcurrency2(items, limit, map) {
21927
21948
  const values = new Array(items.length);
21928
21949
  let cursor = 0;
21929
21950
  const worker = async () => {
@@ -21940,7 +21961,7 @@ async function readVault(vault, token) {
21940
21961
  const list = await readMemory("listTool", { vault }, token);
21941
21962
  if (!list.ok) throw new Error(list.error ?? `unable to read ${vault}`);
21942
21963
  const noteRefs = (list.notes ?? []).slice(0, MAX_VAULT_RECORDS);
21943
- const notes = await mapWithConcurrency(
21964
+ const notes = await mapWithConcurrency2(
21944
21965
  noteRefs,
21945
21966
  MAX_MEMORY_READ_CONCURRENCY,
21946
21967
  ({ path: path5 }) => readMemory("getTool", { vault, path: path5 }, token)
@@ -28160,7 +28181,8 @@ app.post("/harvest/sync", auth2, async (c) => {
28160
28181
  proxyZip: body.proxyZip,
28161
28182
  debug: body.debug ?? false,
28162
28183
  serpOnly: body.serpOnly ?? false,
28163
- pages: Math.min(2, Math.max(1, body.pages ?? 1))
28184
+ pages: Math.min(2, Math.max(1, body.pages ?? 1)),
28185
+ recency: body.recency
28164
28186
  };
28165
28187
  const syncCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount(options.maxQuestions);
28166
28188
  const { ok: syncOk, balance_mc: syncBal } = await debitMc(user.id, syncCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
@@ -28915,7 +28937,7 @@ app.get("/cron/tick", async (c) => {
28915
28937
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
28916
28938
  return c.json({ error: "Unauthorized" }, 401);
28917
28939
  }
28918
- const { drainQueue } = await import("./worker-3LYNVGLF.js");
28940
+ const { drainQueue } = await import("./worker-ZZHSYI3Y.js");
28919
28941
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
28920
28942
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
28921
28943
  const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
@@ -29120,4 +29142,4 @@ app.get("/blog/:slug/", (c) => {
29120
29142
  export {
29121
29143
  app
29122
29144
  };
29123
- //# sourceMappingURL=server-B35CQJB3.js.map
29145
+ //# sourceMappingURL=server-LKVPEQFE.js.map