mcp-scraper 0.33.7 → 0.34.1
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 +4 -4
- package/dist/bin/api-server.cjs +7100 -7023
- package/dist/bin/api-server.cjs.map +1 -1
- package/dist/bin/api-server.js +2 -2
- package/dist/bin/mcp-scraper-cli.cjs +2 -1
- package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
- package/dist/bin/mcp-scraper-cli.js +3 -3
- package/dist/bin/mcp-scraper-install.cjs +2 -2
- package/dist/bin/mcp-scraper-install.cjs.map +1 -1
- package/dist/bin/mcp-scraper-install.js +2 -2
- package/dist/bin/mcp-stdio-server.cjs +43 -37
- package/dist/bin/mcp-stdio-server.cjs.map +1 -1
- package/dist/bin/mcp-stdio-server.js +6 -6
- package/dist/bin/paa-harvest.cjs +22 -0
- package/dist/bin/paa-harvest.cjs.map +1 -1
- package/dist/bin/paa-harvest.js +3 -3
- package/dist/{chunk-V36LS5YV.js → chunk-4ZB3X6BQ.js} +4 -3
- package/dist/chunk-4ZB3X6BQ.js.map +1 -0
- package/dist/{chunk-SUOHUXQS.js → chunk-5PZ6N2QM.js} +2 -2
- package/dist/{chunk-H6QECE2U.js → chunk-6OPHG76G.js} +30 -27
- package/dist/chunk-6OPHG76G.js.map +1 -0
- package/dist/{chunk-2EDFOQD7.js → chunk-7AYRWAEK.js} +2 -2
- package/dist/{chunk-2EDFOQD7.js.map → chunk-7AYRWAEK.js.map} +1 -1
- package/dist/chunk-7N2KYL4U.js +7 -0
- package/dist/chunk-7N2KYL4U.js.map +1 -0
- package/dist/{chunk-JBSGBSGT.js → chunk-BWXLTWF7.js} +21 -16
- package/dist/chunk-BWXLTWF7.js.map +1 -0
- package/dist/{chunk-V73MPRU6.js → chunk-CB5C3BPB.js} +17 -1
- package/dist/chunk-CB5C3BPB.js.map +1 -0
- package/dist/{chunk-XVCETVYY.js → chunk-XVVNKASZ.js} +12 -4
- package/dist/chunk-XVVNKASZ.js.map +1 -0
- package/dist/{chunk-AT2PFWHW.js → chunk-ZID3WQID.js} +2 -2
- package/dist/index.cjs +22 -0
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +3 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +3 -3
- package/dist/{server-B35CQJB3.js → server-IWDHTES2.js} +185 -139
- package/dist/server-IWDHTES2.js.map +1 -0
- package/dist/{site-extract-repository-JHMVHENZ.js → site-extract-repository-OLVWMOU2.js} +3 -3
- package/dist/{worker-3LYNVGLF.js → worker-645BZPEK.js} +5 -5
- package/docs/mcp-tool-craft-lint.generated.md +171 -49
- package/docs/mcp-tool-manifest.generated.json +46 -11
- package/docs/specs/query-fanout-transport-contract-fix.md +37 -0
- package/package.json +1 -1
- package/dist/chunk-H6QECE2U.js.map +0 -1
- package/dist/chunk-HAMN42SP.js +0 -7
- package/dist/chunk-HAMN42SP.js.map +0 -1
- package/dist/chunk-JBSGBSGT.js.map +0 -1
- package/dist/chunk-V36LS5YV.js.map +0 -1
- package/dist/chunk-V73MPRU6.js.map +0 -1
- package/dist/chunk-XVCETVYY.js.map +0 -1
- package/dist/server-B35CQJB3.js.map +0 -1
- /package/dist/{chunk-SUOHUXQS.js.map → chunk-5PZ6N2QM.js.map} +0 -0
- /package/dist/{chunk-AT2PFWHW.js.map → chunk-ZID3WQID.js.map} +0 -0
- /package/dist/{site-extract-repository-JHMVHENZ.js.map → site-extract-repository-OLVWMOU2.js.map} +0 -0
- /package/dist/{worker-3LYNVGLF.js.map → worker-645BZPEK.js.map} +0 -0
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
resolveBackupKernelProxyId,
|
|
21
21
|
resolveKernelProxyId,
|
|
22
22
|
serpArrivalUrl
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-XVVNKASZ.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-
|
|
43
|
+
} from "./chunk-6OPHG76G.js";
|
|
44
44
|
import {
|
|
45
45
|
auditImages,
|
|
46
46
|
buildLinkReport,
|
|
@@ -53,11 +53,10 @@ import {
|
|
|
53
53
|
browserServiceProfileSaveChanges,
|
|
54
54
|
browserServiceProxyId,
|
|
55
55
|
currentCostContext,
|
|
56
|
-
kernelCostUsd,
|
|
57
56
|
recordKernelSession,
|
|
58
57
|
recordVendorUsage,
|
|
59
58
|
runWithCostContext
|
|
60
|
-
} from "./chunk-
|
|
59
|
+
} from "./chunk-4ZB3X6BQ.js";
|
|
61
60
|
import {
|
|
62
61
|
csvRecords,
|
|
63
62
|
listWorkflowDefinitions,
|
|
@@ -69,7 +68,7 @@ import {
|
|
|
69
68
|
workflowOutputBaseDir,
|
|
70
69
|
workflowStepCount,
|
|
71
70
|
workflowSupportsSteps
|
|
72
|
-
} from "./chunk-
|
|
71
|
+
} from "./chunk-ZID3WQID.js";
|
|
73
72
|
import "./chunk-K443GQY5.js";
|
|
74
73
|
import {
|
|
75
74
|
DEFAULT_MAPS_PROXY_MODE,
|
|
@@ -80,8 +79,8 @@ import {
|
|
|
80
79
|
RawMapsHoursRowSchema,
|
|
81
80
|
RawMapsOverviewSchema,
|
|
82
81
|
RawMapsReviewStatsSchema
|
|
83
|
-
} from "./chunk-
|
|
84
|
-
import "./chunk-
|
|
82
|
+
} from "./chunk-CB5C3BPB.js";
|
|
83
|
+
import "./chunk-7N2KYL4U.js";
|
|
85
84
|
import {
|
|
86
85
|
completeExtractJob,
|
|
87
86
|
countSuccessfulPages,
|
|
@@ -92,7 +91,7 @@ import {
|
|
|
92
91
|
saveExtractPages,
|
|
93
92
|
setExtractJobTotal,
|
|
94
93
|
settleExtractJob
|
|
95
|
-
} from "./chunk-
|
|
94
|
+
} from "./chunk-5PZ6N2QM.js";
|
|
96
95
|
import {
|
|
97
96
|
BROWSER_OPEN_MIN_BALANCE_MC,
|
|
98
97
|
CONCURRENCY_PRICE_ID,
|
|
@@ -110,7 +109,6 @@ import {
|
|
|
110
109
|
MC_PER_CREDIT,
|
|
111
110
|
MC_PER_USD,
|
|
112
111
|
MEDIA_TRANSCRIBE_HOLD_MC,
|
|
113
|
-
MEDIA_TRANSCRIBE_MARGIN_MULTIPLE,
|
|
114
112
|
MEMORY_AI_MARGIN_MULTIPLE,
|
|
115
113
|
MEMORY_PLANS,
|
|
116
114
|
MEMORY_PLAN_QUOTA,
|
|
@@ -131,13 +129,14 @@ import {
|
|
|
131
129
|
getPublicRatesPayload,
|
|
132
130
|
insufficientBalanceResponse,
|
|
133
131
|
mcToCredits,
|
|
132
|
+
mediaTranscribeChargeMc,
|
|
134
133
|
offerClaimPathForTier,
|
|
135
134
|
serpActualCostMc,
|
|
136
135
|
tierAmountChargedUsd,
|
|
137
136
|
tierBillingInterval,
|
|
138
137
|
tierCreditsPerInterval,
|
|
139
138
|
vendorCostUsdToMc
|
|
140
|
-
} from "./chunk-
|
|
139
|
+
} from "./chunk-BWXLTWF7.js";
|
|
141
140
|
import {
|
|
142
141
|
CaptchaError,
|
|
143
142
|
ExtractionError,
|
|
@@ -10367,7 +10366,8 @@ var HarvestBodySchema = z14.object({
|
|
|
10367
10366
|
proxyZip: z14.string().regex(/^\d{5}$/).optional(),
|
|
10368
10367
|
debug: z14.boolean().optional(),
|
|
10369
10368
|
serpOnly: z14.boolean().optional(),
|
|
10370
|
-
pages: z14.number().int().min(1).max(2).optional()
|
|
10369
|
+
pages: z14.number().int().min(1).max(2).optional(),
|
|
10370
|
+
recency: z14.enum(["day", "week", "month", "year"]).optional()
|
|
10371
10371
|
});
|
|
10372
10372
|
var ExtractUrlBodySchema = z14.object({
|
|
10373
10373
|
url: z14.string().min(1, "url is required"),
|
|
@@ -10653,8 +10653,7 @@ youtubeApp.post("/transcribe", createApiKeyAuth(), async (c) => {
|
|
|
10653
10653
|
const { videoId } = parsed.data;
|
|
10654
10654
|
const id = videoId.trim();
|
|
10655
10655
|
const user = c.get("user");
|
|
10656
|
-
const
|
|
10657
|
-
const holdMc = MC_COSTS.yt_transcription * holdMins;
|
|
10656
|
+
const holdMc = MEDIA_TRANSCRIBE_HOLD_MC;
|
|
10658
10657
|
const gate = await acquireConcurrencyGate(user, "youtube_transcribe", {
|
|
10659
10658
|
reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
|
|
10660
10659
|
metadata: { videoId: id }
|
|
@@ -10670,8 +10669,7 @@ youtubeApp.post("/transcribe", createApiKeyAuth(), async (c) => {
|
|
|
10670
10669
|
const chunkSecs = lastChunk && Number.isFinite(lastChunk.timestamp[1]) ? lastChunk.timestamp[1] : 0;
|
|
10671
10670
|
const durationSecs = Number.isFinite(result.durationMs) ? result.durationMs / 1e3 : 0;
|
|
10672
10671
|
const videoSecs = Math.max(chunkSecs, durationSecs) || 60;
|
|
10673
|
-
const
|
|
10674
|
-
const actualMc = MC_COSTS.yt_transcription * actualMins;
|
|
10672
|
+
const actualMc = mediaTranscribeChargeMc(videoSecs);
|
|
10675
10673
|
const diff = holdMc - actualMc;
|
|
10676
10674
|
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.TRANSCRIPTION_REFUND, "overestimate refund");
|
|
10677
10675
|
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.TRANSCRIPTION, id);
|
|
@@ -11940,7 +11938,7 @@ facebookAdApp.post("/transcribe", createApiKeyAuth(), async (c) => {
|
|
|
11940
11938
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
|
|
11941
11939
|
debited = true;
|
|
11942
11940
|
const transcript = await transcribeFacebookVideoUrl(videoUrl, "# Facebook Ad Transcript");
|
|
11943
|
-
const settleMc =
|
|
11941
|
+
const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
|
|
11944
11942
|
const diff = holdMc - settleMc;
|
|
11945
11943
|
if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
11946
11944
|
else if (diff < 0) await debitMc(fbUser.id, -diff, LedgerOperation.FB_TRANSCRIBE, videoUrl);
|
|
@@ -11991,7 +11989,7 @@ facebookAdApp.post("/video-transcribe", createApiKeyAuth(), async (c) => {
|
|
|
11991
11989
|
const video = await extractFacebookOrganicVideoFromPage(page, sourceUrl.href, body.quality);
|
|
11992
11990
|
const kernelMs = Date.now() - kernelStartMs;
|
|
11993
11991
|
const transcript = await transcribeFacebookVideoUrl(video.videoUrl, "# Facebook Organic Video Transcript");
|
|
11994
|
-
const settleMc =
|
|
11992
|
+
const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
|
|
11995
11993
|
const diff = holdMc - settleMc;
|
|
11996
11994
|
if (diff > 0) await creditMc(fbUser.id, diff, LedgerOperation.FB_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
11997
11995
|
else if (diff < 0) await debitMc(fbUser.id, -diff, LedgerOperation.FB_TRANSCRIBE, sourceUrl.href);
|
|
@@ -12474,7 +12472,7 @@ googleAdsApp.post("/transcribe", createApiKeyAuth(), async (c) => {
|
|
|
12474
12472
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, holdMc), 402);
|
|
12475
12473
|
debited = true;
|
|
12476
12474
|
const transcript = await runWithCostContext({ ...currentCostContext(), op: "google_ads_transcribe", userId: user.id }, () => transcribeMediaUrl(videoUrl, "# Google Ad Transcript"));
|
|
12477
|
-
const settleMc =
|
|
12475
|
+
const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
|
|
12478
12476
|
const diff = holdMc - settleMc;
|
|
12479
12477
|
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
12480
12478
|
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.GOOGLE_ADS_TRANSCRIBE, videoUrl);
|
|
@@ -13129,7 +13127,7 @@ instagramApp.post("/media-download", createApiKeyAuth(), async (c) => {
|
|
|
13129
13127
|
if (!tx.ok) return c.json(insufficientBalanceResponse(tx.balance_mc, holdMc), 402);
|
|
13130
13128
|
transcriptDebited = true;
|
|
13131
13129
|
transcript = await transcribeMediaUrl(audioUrl, "# Instagram Media Transcript");
|
|
13132
|
-
const settleMc =
|
|
13130
|
+
const settleMc = mediaTranscribeChargeMc(transcript.durationMs / 1e3);
|
|
13133
13131
|
const diff = holdMc - settleMc;
|
|
13134
13132
|
if (diff > 0) await creditMc(user.id, diff, LedgerOperation.INSTAGRAM_TRANSCRIBE_REFUND, "real-cost reconcile");
|
|
13135
13133
|
else if (diff < 0) await debitMc(user.id, -diff, LedgerOperation.INSTAGRAM_TRANSCRIBE, sourceUrl.href);
|
|
@@ -13163,33 +13161,44 @@ import { Hono as Hono10 } from "hono";
|
|
|
13163
13161
|
import { z as z19 } from "zod";
|
|
13164
13162
|
|
|
13165
13163
|
// src/api/reddit-trending.ts
|
|
13166
|
-
|
|
13167
|
-
const
|
|
13168
|
-
|
|
13169
|
-
|
|
13170
|
-
|
|
13171
|
-
const
|
|
13172
|
-
|
|
13173
|
-
|
|
13174
|
-
|
|
13175
|
-
|
|
13176
|
-
|
|
13177
|
-
|
|
13178
|
-
|
|
13179
|
-
|
|
13180
|
-
|
|
13164
|
+
function buildSerpQuery(topic, subreddit) {
|
|
13165
|
+
const scope = subreddit ? `site:reddit.com/r/${subreddit}` : "site:reddit.com";
|
|
13166
|
+
return `${scope} ${topic}`.trim();
|
|
13167
|
+
}
|
|
13168
|
+
function subredditFromUrl(url) {
|
|
13169
|
+
const m = url.match(/\/r\/([^/]+)\//i);
|
|
13170
|
+
return m ? m[1] : "";
|
|
13171
|
+
}
|
|
13172
|
+
function extractRedditThreadUrls(organicResults, limit) {
|
|
13173
|
+
if (!Array.isArray(organicResults)) return [];
|
|
13174
|
+
const seen = /* @__PURE__ */ new Set();
|
|
13175
|
+
const out = [];
|
|
13176
|
+
for (const raw of organicResults) {
|
|
13177
|
+
if (!raw || typeof raw !== "object") continue;
|
|
13178
|
+
const r = raw;
|
|
13179
|
+
const url = canonicalThreadUrl(r.url);
|
|
13180
|
+
if (!url || seen.has(url)) continue;
|
|
13181
|
+
seen.add(url);
|
|
13182
|
+
out.push({
|
|
13183
|
+
title: typeof r.title === "string" ? r.title.trim() : "",
|
|
13184
|
+
url,
|
|
13185
|
+
subreddit: subredditFromUrl(url)
|
|
13181
13186
|
});
|
|
13187
|
+
if (out.length >= limit) break;
|
|
13188
|
+
}
|
|
13189
|
+
return out;
|
|
13190
|
+
}
|
|
13191
|
+
async function mapWithConcurrency(items, concurrency, fn) {
|
|
13192
|
+
const results = new Array(items.length);
|
|
13193
|
+
let next = 0;
|
|
13194
|
+
const workers = Array.from({ length: Math.max(1, Math.min(concurrency, items.length)) }, async () => {
|
|
13195
|
+
while (next < items.length) {
|
|
13196
|
+
const i = next++;
|
|
13197
|
+
results[i] = await fn(items[i], i);
|
|
13198
|
+
}
|
|
13182
13199
|
});
|
|
13183
|
-
|
|
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()}`;
|
|
13200
|
+
await Promise.all(workers);
|
|
13201
|
+
return results;
|
|
13193
13202
|
}
|
|
13194
13203
|
function parseCountText(text2) {
|
|
13195
13204
|
if (typeof text2 !== "string") return 0;
|
|
@@ -13214,35 +13223,6 @@ function canonicalThreadUrl(href) {
|
|
|
13214
13223
|
function engagementScore(score, commentCount) {
|
|
13215
13224
|
return score + 2 * commentCount;
|
|
13216
13225
|
}
|
|
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
13226
|
var INTERROGATIVE_STARTERS = /* @__PURE__ */ new Set([
|
|
13247
13227
|
"what",
|
|
13248
13228
|
"whats",
|
|
@@ -13335,7 +13315,8 @@ var RedditThreadBodySchema = z19.object({
|
|
|
13335
13315
|
var RedditTrendingBodySchema = z19.object({
|
|
13336
13316
|
topic: z19.string().trim().min(1, "topic is required"),
|
|
13337
13317
|
subreddit: z19.string().trim().regex(/^(r\/)?[A-Za-z0-9_]{2,24}$/i, "subreddit must be a bare name like SEO").optional(),
|
|
13338
|
-
|
|
13318
|
+
window: z19.enum(["week", "month"]).default("month"),
|
|
13319
|
+
maxThreads: z19.number().int().min(1).max(40).default(20),
|
|
13339
13320
|
includeComments: z19.boolean().default(true),
|
|
13340
13321
|
maxCommentsPerThread: z19.number().int().min(1).max(200).default(50)
|
|
13341
13322
|
});
|
|
@@ -13486,50 +13467,76 @@ redditApp.post("/thread", createApiKeyAuth(), async (c) => {
|
|
|
13486
13467
|
await releaseConcurrencyGate(gate.lockId);
|
|
13487
13468
|
}
|
|
13488
13469
|
});
|
|
13470
|
+
var TRENDING_SOFT_DEADLINE_MS = 24e4;
|
|
13471
|
+
var TRENDING_SCRAPE_CONCURRENCY = 4;
|
|
13489
13472
|
redditApp.post("/trending", createApiKeyAuth(), async (c) => {
|
|
13490
13473
|
const raw = await c.req.json().catch(() => ({}));
|
|
13491
13474
|
const parsed = RedditTrendingBodySchema.safeParse(raw);
|
|
13492
13475
|
if (!parsed.success) return c.json(invalidRequest4(parsed.error.issues[0]?.message ?? "Invalid request"), 400);
|
|
13493
13476
|
const body = parsed.data;
|
|
13494
13477
|
const subreddit = body.subreddit ? body.subreddit.replace(/^r\//i, "") : void 0;
|
|
13495
|
-
const
|
|
13478
|
+
const serpQuery = buildSerpQuery(body.topic, subreddit);
|
|
13496
13479
|
const user = c.get("user");
|
|
13497
13480
|
const gate = await acquireConcurrencyGate(user, "reddit_trending", {
|
|
13498
13481
|
reuseLockId: c.req.header("x-mcp-scraper-concurrency-lock"),
|
|
13499
13482
|
metadata: { topic: body.topic }
|
|
13500
13483
|
});
|
|
13501
13484
|
if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
|
|
13485
|
+
const softDeadlineMs = Date.now() + TRENDING_SOFT_DEADLINE_MS;
|
|
13502
13486
|
let discoveryDebited = false;
|
|
13503
13487
|
let discoveryRefunded = false;
|
|
13504
|
-
|
|
13505
|
-
let commentDebitMcOutstanding = 0;
|
|
13488
|
+
const outstanding = { thread: 0, comment: 0 };
|
|
13506
13489
|
try {
|
|
13507
|
-
const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD,
|
|
13490
|
+
const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD, serpQuery);
|
|
13508
13491
|
if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.reddit_thread), 402);
|
|
13509
13492
|
discoveryDebited = true;
|
|
13510
|
-
|
|
13511
|
-
|
|
13512
|
-
|
|
13513
|
-
|
|
13514
|
-
|
|
13515
|
-
|
|
13516
|
-
|
|
13517
|
-
|
|
13518
|
-
|
|
13519
|
-
|
|
13493
|
+
let candidates = [];
|
|
13494
|
+
try {
|
|
13495
|
+
const serp = await harvest({
|
|
13496
|
+
query: serpQuery,
|
|
13497
|
+
serpOnly: true,
|
|
13498
|
+
proxyMode: "none",
|
|
13499
|
+
pages: 2,
|
|
13500
|
+
recency: body.window,
|
|
13501
|
+
kernelApiKey: browserServiceApiKey(),
|
|
13502
|
+
format: "json",
|
|
13503
|
+
outputDir: "/tmp/reddit-trending-serp",
|
|
13504
|
+
softDeadlineMs
|
|
13505
|
+
});
|
|
13506
|
+
candidates = extractRedditThreadUrls(serp.organicResults, body.maxThreads);
|
|
13507
|
+
} catch {
|
|
13508
|
+
candidates = [];
|
|
13520
13509
|
}
|
|
13521
|
-
|
|
13522
|
-
|
|
13523
|
-
|
|
13524
|
-
|
|
13525
|
-
|
|
13526
|
-
|
|
13527
|
-
|
|
13528
|
-
|
|
13529
|
-
|
|
13530
|
-
|
|
13531
|
-
|
|
13532
|
-
|
|
13510
|
+
if (candidates.length === 0) {
|
|
13511
|
+
await creditMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD_REFUND, "no reddit threads discovered");
|
|
13512
|
+
discoveryRefunded = true;
|
|
13513
|
+
await logRequestEvent({ userId: user.id, source: "reddit_trending", status: "failed", query: body.topic, error: "no reddit threads discovered" });
|
|
13514
|
+
return c.json({ error: "No Reddit threads found for that topic and window (refunded)" }, 404);
|
|
13515
|
+
}
|
|
13516
|
+
let ranked;
|
|
13517
|
+
if (!body.includeComments) {
|
|
13518
|
+
ranked = candidates.map((cand) => ({
|
|
13519
|
+
title: cand.title,
|
|
13520
|
+
url: cand.url,
|
|
13521
|
+
subreddit: cand.subreddit,
|
|
13522
|
+
score: 0,
|
|
13523
|
+
commentCount: 0,
|
|
13524
|
+
engagementScore: 0,
|
|
13525
|
+
ageText: "",
|
|
13526
|
+
topQuestions: []
|
|
13527
|
+
}));
|
|
13528
|
+
} else {
|
|
13529
|
+
let balanceExhausted = false;
|
|
13530
|
+
const scrapedThreads = await mapWithConcurrency(candidates, TRENDING_SCRAPE_CONCURRENCY, async (cand) => {
|
|
13531
|
+
if (balanceExhausted || Date.now() >= softDeadlineMs) return null;
|
|
13532
|
+
const oldUrl = toOldReddit(cand.url);
|
|
13533
|
+
if (!oldUrl) return null;
|
|
13534
|
+
const threadDebit = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD, cand.url);
|
|
13535
|
+
if (!threadDebit.ok) {
|
|
13536
|
+
balanceExhausted = true;
|
|
13537
|
+
return null;
|
|
13538
|
+
}
|
|
13539
|
+
outstanding.thread += MC_COSTS.reddit_thread;
|
|
13533
13540
|
const scraped = await scrapeOldRedditWithRetries(
|
|
13534
13541
|
oldUrl,
|
|
13535
13542
|
PARSE_REDDIT,
|
|
@@ -13537,46 +13544,57 @@ redditApp.post("/trending", createApiKeyAuth(), async (c) => {
|
|
|
13537
13544
|
);
|
|
13538
13545
|
if (!scraped) {
|
|
13539
13546
|
await creditMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD_REFUND, "reddit blocked/empty after retries");
|
|
13540
|
-
|
|
13541
|
-
|
|
13547
|
+
outstanding.thread -= MC_COSTS.reddit_thread;
|
|
13548
|
+
return null;
|
|
13542
13549
|
}
|
|
13550
|
+
const score = parseCountText(scraped.data.score);
|
|
13543
13551
|
let comments = Array.isArray(scraped.data.comments) ? scraped.data.comments : [];
|
|
13552
|
+
const commentCount = comments.length;
|
|
13544
13553
|
comments = comments.slice(0, body.maxCommentsPerThread);
|
|
13545
13554
|
let commentMc = comments.length * MC_COSTS.reddit_comment;
|
|
13546
13555
|
if (commentMc > 0) {
|
|
13547
|
-
const commentDebit = await debitMc(user.id, commentMc, LedgerOperation.REDDIT_COMMENT,
|
|
13556
|
+
const commentDebit = await debitMc(user.id, commentMc, LedgerOperation.REDDIT_COMMENT, cand.url);
|
|
13548
13557
|
if (!commentDebit.ok) {
|
|
13558
|
+
balanceExhausted = true;
|
|
13549
13559
|
const affordable = Math.floor((commentDebit.balance_mc ?? 0) / MC_COSTS.reddit_comment);
|
|
13550
13560
|
comments = comments.slice(0, affordable);
|
|
13551
13561
|
commentMc = affordable * MC_COSTS.reddit_comment;
|
|
13552
|
-
if (commentMc > 0) await debitMc(user.id, commentMc, LedgerOperation.REDDIT_COMMENT,
|
|
13553
|
-
insufficient = true;
|
|
13562
|
+
if (commentMc > 0) await debitMc(user.id, commentMc, LedgerOperation.REDDIT_COMMENT, cand.url);
|
|
13554
13563
|
}
|
|
13555
|
-
|
|
13564
|
+
outstanding.comment += commentMc;
|
|
13556
13565
|
}
|
|
13557
13566
|
const topLevelBodies = comments.filter((cm) => cm && cm.depth === 0).map((cm) => cm.body);
|
|
13558
|
-
|
|
13559
|
-
|
|
13560
|
-
|
|
13567
|
+
return {
|
|
13568
|
+
title: cand.title || scraped.data.title || "",
|
|
13569
|
+
url: cand.url,
|
|
13570
|
+
subreddit: cand.subreddit,
|
|
13571
|
+
score,
|
|
13572
|
+
commentCount,
|
|
13573
|
+
engagementScore: engagementScore(score, commentCount),
|
|
13574
|
+
ageText: "",
|
|
13575
|
+
topQuestions: extractQuestions([scraped.data.postBody, ...topLevelBodies])
|
|
13576
|
+
};
|
|
13577
|
+
});
|
|
13578
|
+
ranked = scrapedThreads.filter((t) => t !== null).sort((a, b) => b.engagementScore - a.engagementScore);
|
|
13561
13579
|
}
|
|
13562
13580
|
const result = {
|
|
13563
13581
|
topic: body.topic,
|
|
13564
13582
|
subreddit: subreddit ?? null,
|
|
13565
|
-
window: "30d",
|
|
13583
|
+
window: body.window === "week" ? "7d" : "30d",
|
|
13566
13584
|
totals: trendingTotals(ranked),
|
|
13567
13585
|
rankedThreads: ranked,
|
|
13568
13586
|
questions: flattenQuestions(ranked),
|
|
13569
|
-
threadsScraped,
|
|
13570
|
-
|
|
13571
|
-
|
|
13572
|
-
|
|
13587
|
+
threadsScraped: body.includeComments ? ranked.length : 0,
|
|
13588
|
+
candidatesFound: candidates.length,
|
|
13589
|
+
partial: body.includeComments && ranked.length < candidates.length,
|
|
13590
|
+
searchQuery: serpQuery
|
|
13573
13591
|
};
|
|
13574
13592
|
await logRequestEvent({ userId: user.id, source: "reddit_trending", status: "done", query: body.topic, resultCount: ranked.length, result });
|
|
13575
13593
|
return c.json(result);
|
|
13576
13594
|
} catch (err) {
|
|
13577
13595
|
if (discoveryDebited && !discoveryRefunded) await creditMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD_REFUND, "failed call");
|
|
13578
|
-
if (
|
|
13579
|
-
if (
|
|
13596
|
+
if (outstanding.thread > 0) await creditMc(user.id, outstanding.thread, LedgerOperation.REDDIT_THREAD_REFUND, "failed call");
|
|
13597
|
+
if (outstanding.comment > 0) await creditMc(user.id, outstanding.comment, LedgerOperation.REDDIT_COMMENT_REFUND, "failed call");
|
|
13580
13598
|
const msg = err instanceof Error ? err.message : String(err);
|
|
13581
13599
|
await logRequestEvent({ userId: user.id, source: "reddit_trending", status: "failed", query: body.topic, error: msg });
|
|
13582
13600
|
return c.json({ error: msg }, 500);
|
|
@@ -20523,6 +20541,19 @@ if (state.key) { refreshSessions(); if (state.current) selectSession(state.curre
|
|
|
20523
20541
|
import Stripe from "stripe";
|
|
20524
20542
|
import { Hono as Hono20 } from "hono";
|
|
20525
20543
|
var stripe = new Stripe(process.env.STRIPE_SECRET_KEY, { apiVersion: "2026-02-25.clover" });
|
|
20544
|
+
async function cancelStandaloneMemorySub(memSubId, baseSubId) {
|
|
20545
|
+
if (!memSubId || memSubId === baseSubId) return;
|
|
20546
|
+
try {
|
|
20547
|
+
const memSub = await stripe.subscriptions.retrieve(memSubId);
|
|
20548
|
+
const priceId = memSub.items.data[0]?.price?.id;
|
|
20549
|
+
const stillOpen = memSub.status !== "canceled" && !memSub.cancel_at_period_end;
|
|
20550
|
+
if (priceId && priceId in MEMORY_PLANS && stillOpen) {
|
|
20551
|
+
await stripe.subscriptions.update(memSubId, { cancel_at_period_end: true });
|
|
20552
|
+
}
|
|
20553
|
+
} catch (err) {
|
|
20554
|
+
console.error("[stripe] could not wind down standalone memory subscription", memSubId, err instanceof Error ? err.message : err);
|
|
20555
|
+
}
|
|
20556
|
+
}
|
|
20526
20557
|
var stripeApp = new Hono20();
|
|
20527
20558
|
stripeApp.post("/webhooks", async (c) => {
|
|
20528
20559
|
const sig = c.req.header("stripe-signature");
|
|
@@ -20554,14 +20585,7 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
20554
20585
|
await creditMc(user.id, tier.credits_mc, LedgerOperation.SUBSCRIPTION, `${tier.label} subscription credits`, invoice.id, { neverExpires: tier.credits_never_expire });
|
|
20555
20586
|
await setSubscriptionTier(user.id, tier.tier, tier.concurrency, subId ?? user.subscription_id);
|
|
20556
20587
|
if (tier.includes_memory) {
|
|
20557
|
-
|
|
20558
|
-
if (standaloneMemorySub) {
|
|
20559
|
-
try {
|
|
20560
|
-
await stripe.subscriptions.update(standaloneMemorySub, { cancel_at_period_end: true });
|
|
20561
|
-
} catch (err) {
|
|
20562
|
-
console.error("[stripe] could not wind down standalone memory subscription", standaloneMemorySub, err instanceof Error ? err.message : err);
|
|
20563
|
-
}
|
|
20564
|
-
}
|
|
20588
|
+
await cancelStandaloneMemorySub(user.memory_subscription_id, subId ?? user.subscription_id);
|
|
20565
20589
|
await setMemoryPlan(user.id, "pro", null);
|
|
20566
20590
|
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: "pro", subscription_tier: tier.tier }));
|
|
20567
20591
|
}
|
|
@@ -20613,6 +20637,7 @@ stripeApp.post("/webhooks", async (c) => {
|
|
|
20613
20637
|
await setSubscriptionTier(user.id, newTier, live ? tier.concurrency : 0, live ? sub.id : null);
|
|
20614
20638
|
if (tier.includes_memory) {
|
|
20615
20639
|
const newMemoryPlan = live ? "pro" : "free";
|
|
20640
|
+
if (live) await cancelStandaloneMemorySub(user.memory_subscription_id, sub.id);
|
|
20616
20641
|
await setMemoryPlan(user.id, newMemoryPlan, live ? sub.id : null);
|
|
20617
20642
|
await syncMemoryKeyPlan(user, resolveEffectiveMemoryPlan({ ...user, memory_plan: newMemoryPlan }));
|
|
20618
20643
|
} else if (user.memory_subscription_id === sub.id) {
|
|
@@ -21923,7 +21948,7 @@ async function readMemory(tool, args, token) {
|
|
|
21923
21948
|
}
|
|
21924
21949
|
return result;
|
|
21925
21950
|
}
|
|
21926
|
-
async function
|
|
21951
|
+
async function mapWithConcurrency2(items, limit, map) {
|
|
21927
21952
|
const values = new Array(items.length);
|
|
21928
21953
|
let cursor = 0;
|
|
21929
21954
|
const worker = async () => {
|
|
@@ -21940,7 +21965,7 @@ async function readVault(vault, token) {
|
|
|
21940
21965
|
const list = await readMemory("listTool", { vault }, token);
|
|
21941
21966
|
if (!list.ok) throw new Error(list.error ?? `unable to read ${vault}`);
|
|
21942
21967
|
const noteRefs = (list.notes ?? []).slice(0, MAX_VAULT_RECORDS);
|
|
21943
|
-
const notes = await
|
|
21968
|
+
const notes = await mapWithConcurrency2(
|
|
21944
21969
|
noteRefs,
|
|
21945
21970
|
MAX_MEMORY_READ_CONCURRENCY,
|
|
21946
21971
|
({ path: path5 }) => readMemory("getTool", { vault, path: path5 }, token)
|
|
@@ -28160,7 +28185,8 @@ app.post("/harvest/sync", auth2, async (c) => {
|
|
|
28160
28185
|
proxyZip: body.proxyZip,
|
|
28161
28186
|
debug: body.debug ?? false,
|
|
28162
28187
|
serpOnly: body.serpOnly ?? false,
|
|
28163
|
-
pages: Math.min(2, Math.max(1, body.pages ?? 1))
|
|
28188
|
+
pages: Math.min(2, Math.max(1, body.pages ?? 1)),
|
|
28189
|
+
recency: body.recency
|
|
28164
28190
|
};
|
|
28165
28191
|
const syncCost = options.serpOnly ? MC_COSTS.serp_headful : paaCostForQuestionCount(options.maxQuestions);
|
|
28166
28192
|
const { ok: syncOk, balance_mc: syncBal } = await debitMc(user.id, syncCost, options.serpOnly ? LedgerOperation.SERP : LedgerOperation.PAA, options.query);
|
|
@@ -28667,15 +28693,25 @@ app.post("/billing/subscribe", requireAllowedOrigin, sessionAuth, async (c) => {
|
|
|
28667
28693
|
customerId = customer.id;
|
|
28668
28694
|
await setStripeCustomerId(user.id, customerId);
|
|
28669
28695
|
}
|
|
28670
|
-
|
|
28671
|
-
|
|
28696
|
+
let existingSubId = user.subscription_id;
|
|
28697
|
+
if (!existingSubId) {
|
|
28698
|
+
try {
|
|
28699
|
+
const active = await stripeClient.subscriptions.list({ customer: customerId, status: "active", limit: 20 });
|
|
28700
|
+
const baseSub = active.data.find((s) => findBasePlanItem(s));
|
|
28701
|
+
if (baseSub) existingSubId = baseSub.id;
|
|
28702
|
+
} catch (err) {
|
|
28703
|
+
console.warn("[billing/subscribe] could not check for existing subscription", err instanceof Error ? err.message : err);
|
|
28704
|
+
}
|
|
28705
|
+
}
|
|
28706
|
+
if (existingSubId) {
|
|
28707
|
+
const sub = await stripeClient.subscriptions.retrieve(existingSubId);
|
|
28672
28708
|
const itemId = findBasePlanItem(sub)?.id;
|
|
28673
28709
|
if (itemId) {
|
|
28674
|
-
await stripeClient.subscriptions.update(
|
|
28710
|
+
await stripeClient.subscriptions.update(existingSubId, {
|
|
28675
28711
|
items: [{ id: itemId, price: tier.price_id }],
|
|
28676
28712
|
proration_behavior: "create_prorations"
|
|
28677
28713
|
});
|
|
28678
|
-
const billed = await chargeTierChangeNow(stripeClient,
|
|
28714
|
+
const billed = await chargeTierChangeNow(stripeClient, existingSubId, customerId);
|
|
28679
28715
|
if (!billed.ok) {
|
|
28680
28716
|
return c.json({
|
|
28681
28717
|
updated: true,
|
|
@@ -28793,15 +28829,25 @@ app.post("/billing/subscribe/terminal-checkout", auth2, async (c) => {
|
|
|
28793
28829
|
customerId = customer.id;
|
|
28794
28830
|
await setStripeCustomerId(user.id, customerId);
|
|
28795
28831
|
}
|
|
28796
|
-
|
|
28797
|
-
|
|
28832
|
+
let existingSubId = user.subscription_id;
|
|
28833
|
+
if (!existingSubId) {
|
|
28834
|
+
try {
|
|
28835
|
+
const active = await stripeClient.subscriptions.list({ customer: customerId, status: "active", limit: 20 });
|
|
28836
|
+
const baseSub = active.data.find((s) => findBasePlanItem(s));
|
|
28837
|
+
if (baseSub) existingSubId = baseSub.id;
|
|
28838
|
+
} catch (err) {
|
|
28839
|
+
console.warn("[billing/subscribe] could not check for existing subscription", err instanceof Error ? err.message : err);
|
|
28840
|
+
}
|
|
28841
|
+
}
|
|
28842
|
+
if (existingSubId) {
|
|
28843
|
+
const sub = await stripeClient.subscriptions.retrieve(existingSubId);
|
|
28798
28844
|
const itemId = findBasePlanItem(sub)?.id;
|
|
28799
28845
|
if (itemId) {
|
|
28800
|
-
await stripeClient.subscriptions.update(
|
|
28846
|
+
await stripeClient.subscriptions.update(existingSubId, {
|
|
28801
28847
|
items: [{ id: itemId, price: tier.price_id }],
|
|
28802
28848
|
proration_behavior: "create_prorations"
|
|
28803
28849
|
});
|
|
28804
|
-
const billed = await chargeTierChangeNow(stripeClient,
|
|
28850
|
+
const billed = await chargeTierChangeNow(stripeClient, existingSubId, customerId);
|
|
28805
28851
|
if (!billed.ok) {
|
|
28806
28852
|
return c.json({
|
|
28807
28853
|
updated: true,
|
|
@@ -28915,7 +28961,7 @@ app.get("/cron/tick", async (c) => {
|
|
|
28915
28961
|
if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
|
|
28916
28962
|
return c.json({ error: "Unauthorized" }, 401);
|
|
28917
28963
|
}
|
|
28918
|
-
const { drainQueue } = await import("./worker-
|
|
28964
|
+
const { drainQueue } = await import("./worker-645BZPEK.js");
|
|
28919
28965
|
const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
|
|
28920
28966
|
const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
|
|
28921
28967
|
const [results, sweepResult, reapResult, expiredResult, blobCleanup, connectedDataArtifactCleanup] = await Promise.all([
|
|
@@ -28943,7 +28989,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
28943
28989
|
return c.json({ error: "Unauthorized" }, 401);
|
|
28944
28990
|
}
|
|
28945
28991
|
const jobId = c.req.param("id");
|
|
28946
|
-
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-
|
|
28992
|
+
const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-OLVWMOU2.js");
|
|
28947
28993
|
const { assembleExtractArtifacts } = await import("./extract-bundle-GPZRLBVY.js");
|
|
28948
28994
|
const job = await getExtractJob2(jobId);
|
|
28949
28995
|
if (!job) return c.json({ error: "job not found" }, 404);
|
|
@@ -28951,7 +28997,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
|
|
|
28951
28997
|
await completeExtractJob2(jobId, stored);
|
|
28952
28998
|
let settlement = "already_settled_or_refunded";
|
|
28953
28999
|
if (job.billedMc == null && job.userId != null) {
|
|
28954
|
-
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-
|
|
29000
|
+
const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-OLVWMOU2.js");
|
|
28955
29001
|
const heldMc = Number(job.options.heldMc ?? 0);
|
|
28956
29002
|
const successful = await countSuccessfulPages2(jobId);
|
|
28957
29003
|
const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
|
|
@@ -29120,4 +29166,4 @@ app.get("/blog/:slug/", (c) => {
|
|
|
29120
29166
|
export {
|
|
29121
29167
|
app
|
|
29122
29168
|
};
|
|
29123
|
-
//# sourceMappingURL=server-
|
|
29169
|
+
//# sourceMappingURL=server-IWDHTES2.js.map
|