mcp-scraper 0.4.6 → 0.4.12

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 +1532 -1351
  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 +1 -1
  5. package/dist/bin/mcp-scraper-cli.cjs.map +1 -1
  6. package/dist/bin/mcp-scraper-cli.js +1 -1
  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 +1 -1
  11. package/dist/bin/mcp-stdio-server.cjs.map +1 -1
  12. package/dist/bin/mcp-stdio-server.js +2 -2
  13. package/dist/bin/paa-harvest.cjs +13 -4
  14. package/dist/bin/paa-harvest.cjs.map +1 -1
  15. package/dist/bin/paa-harvest.js +1 -1
  16. package/dist/{chunk-5GAD2AN2.js → chunk-2YY46QYT.js} +27 -10
  17. package/dist/chunk-2YY46QYT.js.map +1 -0
  18. package/dist/{chunk-IFXACD4K.js → chunk-6EXP6DQG.js} +2 -2
  19. package/dist/{chunk-IVQYNY45.js → chunk-EMY7ELRU.js} +23 -6
  20. package/dist/chunk-EMY7ELRU.js.map +1 -0
  21. package/dist/{chunk-NYAWN7CZ.js → chunk-ONIOF5XW.js} +2 -2
  22. package/dist/chunk-SHXJQQOH.js +7 -0
  23. package/dist/chunk-SHXJQQOH.js.map +1 -0
  24. package/dist/index.cjs +59 -4
  25. package/dist/index.cjs.map +1 -1
  26. package/dist/index.js +8 -2
  27. package/dist/index.js.map +1 -1
  28. package/dist/{server-R3KVQNOT.js → server-UNID3SJU.js} +409 -272
  29. package/dist/server-UNID3SJU.js.map +1 -0
  30. package/dist/{site-extract-repository-LI2S3S6E.js → site-extract-repository-HTIY52MW.js} +3 -3
  31. package/dist/{worker-VASAJ7FZ.js → worker-HTYZAYGB.js} +16 -12
  32. package/dist/worker-HTYZAYGB.js.map +1 -0
  33. package/package.json +1 -2
  34. package/dist/chunk-5BB6Y3BB.js +0 -7
  35. package/dist/chunk-5BB6Y3BB.js.map +0 -1
  36. package/dist/chunk-5GAD2AN2.js.map +0 -1
  37. package/dist/chunk-IVQYNY45.js.map +0 -1
  38. package/dist/server-R3KVQNOT.js.map +0 -1
  39. package/dist/worker-VASAJ7FZ.js.map +0 -1
  40. /package/dist/{chunk-IFXACD4K.js.map → chunk-6EXP6DQG.js.map} +0 -0
  41. /package/dist/{chunk-NYAWN7CZ.js.map → chunk-ONIOF5XW.js.map} +0 -0
  42. /package/dist/{site-extract-repository-LI2S3S6E.js.map → site-extract-repository-HTIY52MW.js.map} +0 -0
@@ -16,7 +16,7 @@ import {
16
16
  recordVendorUsage,
17
17
  resolveKernelProxyId,
18
18
  runWithCostContext
19
- } from "./chunk-5GAD2AN2.js";
19
+ } from "./chunk-2YY46QYT.js";
20
20
  import {
21
21
  HttpMcpToolExecutor,
22
22
  MemoryMcpToolExecutor,
@@ -31,7 +31,7 @@ import {
31
31
  registerSerpIntelligenceCaptureTools,
32
32
  sanitizeAttempts,
33
33
  sanitizeHarvestResult
34
- } from "./chunk-IFXACD4K.js";
34
+ } from "./chunk-6EXP6DQG.js";
35
35
  import {
36
36
  auditImages,
37
37
  getBlobStore
@@ -65,7 +65,7 @@ import {
65
65
  RawMapsOverviewSchema,
66
66
  RawMapsReviewStatsSchema
67
67
  } from "./chunk-XGIPATLV.js";
68
- import "./chunk-5BB6Y3BB.js";
68
+ import "./chunk-SHXJQQOH.js";
69
69
  import {
70
70
  completeExtractJob,
71
71
  countSuccessfulPages,
@@ -76,7 +76,7 @@ import {
76
76
  saveExtractPages,
77
77
  setExtractJobTotal,
78
78
  settleExtractJob
79
- } from "./chunk-NYAWN7CZ.js";
79
+ } from "./chunk-ONIOF5XW.js";
80
80
  import {
81
81
  BROWSER_OPEN_MIN_BALANCE_MC,
82
82
  CONCURRENCY_PRICE_ID,
@@ -90,12 +90,14 @@ import {
90
90
  MEMORY_PLANS,
91
91
  MEMORY_PLAN_QUOTA,
92
92
  SCHEDULING_PLANS,
93
+ SITE_AUDIT_LLM_MARGIN_MULTIPLE,
93
94
  SUBSCRIPTION_TIERS,
94
95
  SUBSCRIPTION_TIER_BY_KEY,
96
+ VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE,
95
97
  browserActiveCostMc,
96
98
  concurrencySlotBillingInfo,
97
99
  insufficientBalanceResponse
98
- } from "./chunk-IVQYNY45.js";
100
+ } from "./chunk-EMY7ELRU.js";
99
101
  import {
100
102
  CaptchaError,
101
103
  RECAPTCHA_INSTRUCTIONS,
@@ -5339,6 +5341,7 @@ function extractJson(raw) {
5339
5341
  var DeepInfraLlmClient = class {
5340
5342
  apiKey;
5341
5343
  model;
5344
+ totalCostUsd = 0;
5342
5345
  constructor(apiKey, model = "Qwen/Qwen3.6-35B-A3B") {
5343
5346
  this.apiKey = apiKey;
5344
5347
  this.model = model;
@@ -5371,6 +5374,7 @@ ${raw}`
5371
5374
  var OpenRouterLlmClient = class {
5372
5375
  apiKey;
5373
5376
  model;
5377
+ totalCostUsd = 0;
5374
5378
  constructor(apiKey, model = "qwen/qwen3-235b-a22b") {
5375
5379
  this.apiKey = apiKey;
5376
5380
  this.model = model;
@@ -5383,6 +5387,9 @@ var OpenRouterLlmClient = class {
5383
5387
  });
5384
5388
  if (!resp.ok) throw new Error(`OpenRouter error ${resp.status}: ${await resp.text()}`);
5385
5389
  const data = await resp.json();
5390
+ const cost = data.usage?.cost ?? 0;
5391
+ this.totalCostUsd += cost;
5392
+ void recordVendorUsage({ vendor: "openrouter", model: this.model, units: cost, unitType: "usd" });
5386
5393
  return data.choices[0].message.content;
5387
5394
  }
5388
5395
  async completeJson(prompt) {
@@ -5408,6 +5415,9 @@ var FallbackLlmClient = class {
5408
5415
  secondary;
5409
5416
  lastServedBy = null;
5410
5417
  lastPrimaryError = null;
5418
+ get totalCostUsd() {
5419
+ return this.primary.totalCostUsd + this.secondary.totalCostUsd;
5420
+ }
5411
5421
  async complete(prompt) {
5412
5422
  try {
5413
5423
  const result = await this.primary.complete(prompt);
@@ -6894,8 +6904,23 @@ var SiteAuditService = class {
6894
6904
  async getJobsForUser(userId) {
6895
6905
  return this.deps.repo.listSiteAuditJobs(userId);
6896
6906
  }
6907
+ async runLlmBilled(userId, source, fn) {
6908
+ const before = this.deps.llm.totalCostUsd;
6909
+ const result = await runWithCostContext({ ...currentCostContext(), op: "audit_site", userId }, fn);
6910
+ const deltaCostUsd = this.deps.llm.totalCostUsd - before;
6911
+ if (deltaCostUsd <= 0) return result;
6912
+ const mc = Math.round(deltaCostUsd * SITE_AUDIT_LLM_MARGIN_MULTIPLE * MC_PER_USD);
6913
+ if (mc <= 0) return result;
6914
+ const { ok } = await debitMc(userId, mc, LedgerOperation.SITE_AUDIT_LLM, source);
6915
+ if (!ok) {
6916
+ console.warn(`[site-audit-service] SITE_AUDIT_LLM surcharge debit failed for user ${userId} (${source}): insufficient balance for ${mc}mc`);
6917
+ }
6918
+ return result;
6919
+ }
6897
6920
  async runIngestPhase(jobId, payload) {
6898
6921
  const { request } = payload;
6922
+ const job = await this.deps.repo.getSiteAuditJob(jobId);
6923
+ if (!job) throw new Error(`[site-audit-service] runIngestPhase: job ${jobId} not found`);
6899
6924
  const input = {
6900
6925
  clientDomain: request.clientDomain,
6901
6926
  sfInternalPath: request.sfInternalPath,
@@ -6910,14 +6935,18 @@ var SiteAuditService = class {
6910
6935
  businessContext: request.businessContext,
6911
6936
  priorAuditMemoryEnabled: request.priorAuditMemoryEnabled
6912
6937
  };
6913
- const result = await llmParseWithRetry(
6914
- ValidatedFileManifestSchema,
6915
- (attempt, error) => {
6916
- void attempt;
6917
- void error;
6918
- return buildIngestValidatePrompt(input);
6919
- },
6920
- this.deps.llm
6938
+ const result = await this.runLlmBilled(
6939
+ job.user_id,
6940
+ `${jobId}:phase1-ingest`,
6941
+ () => llmParseWithRetry(
6942
+ ValidatedFileManifestSchema,
6943
+ (attempt, error) => {
6944
+ void attempt;
6945
+ void error;
6946
+ return buildIngestValidatePrompt(input);
6947
+ },
6948
+ this.deps.llm
6949
+ )
6921
6950
  );
6922
6951
  await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase1-ingest", result);
6923
6952
  }
@@ -6932,23 +6961,27 @@ var SiteAuditService = class {
6932
6961
  const competitorDomainsCsvPath = request.competitorDomainsCsvPath;
6933
6962
  void competitorDomainsCsvPath;
6934
6963
  const competitors = await this._readCompetitorDomains(request);
6935
- const results = await Promise.all(
6936
- competitors.map(
6937
- (domain, idx) => limit(async () => {
6938
- const competitorIndex = idx + 1;
6939
- const rawPrompt = buildEnrichCompetitorPrompt({ domain, sessionPath });
6940
- const prompt = rawPrompt.replace("<competitor-index>", String(competitorIndex));
6941
- const result = await llmParseWithRetry(
6942
- EnrichCompetitorOutputSchema,
6943
- (attempt, error) => {
6944
- void attempt;
6945
- void error;
6946
- return prompt;
6947
- },
6948
- this.deps.llm
6949
- );
6950
- return result;
6951
- })
6964
+ const results = await this.runLlmBilled(
6965
+ job.user_id,
6966
+ `${jobId}:phase1-enrich`,
6967
+ () => Promise.all(
6968
+ competitors.map(
6969
+ (domain, idx) => limit(async () => {
6970
+ const competitorIndex = idx + 1;
6971
+ const rawPrompt = buildEnrichCompetitorPrompt({ domain, sessionPath });
6972
+ const prompt = rawPrompt.replace("<competitor-index>", String(competitorIndex));
6973
+ const result = await llmParseWithRetry(
6974
+ EnrichCompetitorOutputSchema,
6975
+ (attempt, error) => {
6976
+ void attempt;
6977
+ void error;
6978
+ return prompt;
6979
+ },
6980
+ this.deps.llm
6981
+ );
6982
+ return result;
6983
+ })
6984
+ )
6952
6985
  )
6953
6986
  );
6954
6987
  await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase1-ingest", { competitors: results });
@@ -6962,19 +6995,23 @@ var SiteAuditService = class {
6962
6995
  const clientDomain = request.clientDomain;
6963
6996
  const urlInventory = [];
6964
6997
  const directedEdgeCount = 0;
6965
- const orphanAnnotation = await llmParseWithRetry(
6966
- OrphanAnnotationOutputSchema,
6967
- (attempt, error) => {
6968
- void attempt;
6969
- void error;
6970
- return buildGraphPerSitePrompt({
6971
- siteId: "client",
6972
- domain: clientDomain,
6973
- urlInventory,
6974
- directedEdgeCount
6975
- });
6976
- },
6977
- this.deps.llm
6998
+ const orphanAnnotation = await this.runLlmBilled(
6999
+ job.user_id,
7000
+ `${jobId}:phase2-build-graph`,
7001
+ () => llmParseWithRetry(
7002
+ OrphanAnnotationOutputSchema,
7003
+ (attempt, error) => {
7004
+ void attempt;
7005
+ void error;
7006
+ return buildGraphPerSitePrompt({
7007
+ siteId: "client",
7008
+ domain: clientDomain,
7009
+ urlInventory,
7010
+ directedEdgeCount
7011
+ });
7012
+ },
7013
+ this.deps.llm
7014
+ )
6978
7015
  );
6979
7016
  const graphMetrics = await this.deps.python.runGraphMetrics({
6980
7017
  directedLinkGraphPath: `${sessionPath}/phase1/directed_link_graph.json`,
@@ -7019,50 +7056,52 @@ var SiteAuditService = class {
7019
7056
  }
7020
7057
  if (currentBatch.length > 0) batches.push(currentBatch);
7021
7058
  const usZipsCsvPath = process.env["US_ZIPS_CSV_PATH"] ?? "/Users/vilovieta/Downloads/sales-magician-api-leads-magician-01c6cff78e31/tools/analytics/data/uszips.csv";
7022
- for (const batch of batches) {
7023
- let classified = [];
7024
- let unclassifiedUrls = batch.map((u) => u.url);
7025
- try {
7026
- const pythonResult = await this.deps.python.runLocationClassifier({
7027
- urls: batch.map((u) => u.url),
7028
- usZipsCsvPath
7029
- });
7030
- classified = pythonResult.classified;
7031
- unclassifiedUrls = pythonResult.unclassified;
7032
- } catch (err) {
7033
- console.warn(
7034
- "[site-audit-service] runClassifyPhase: python.runLocationClassifier failed (falling back to LLM):",
7035
- err instanceof Error ? err.message : String(err)
7036
- );
7037
- unclassifiedUrls = batch.map((u) => u.url);
7038
- }
7039
- for (const c of classified) {
7040
- allClassifications.push({
7041
- url: c.url,
7042
- page_type: c.pageType,
7043
- confidence: c.confidence > 0.6 ? "high" : c.confidence > 0.4 ? "medium" : "low",
7044
- _classification_label: "[COMPUTED:python/location-classifier]",
7045
- topic_cluster: null,
7046
- location_signals: c.locationSignals
7047
- });
7048
- }
7049
- if (unclassifiedUrls.length > 0) {
7050
- const unclassifiedBatch = batch.filter((u) => unclassifiedUrls.includes(u.url));
7051
- const llmResults = await llmParseWithRetry(
7052
- z9.array(PageTypeClassificationRowSchema),
7053
- (attempt, error) => {
7054
- void attempt;
7055
- void error;
7056
- return buildMetricsClassifyPrompt({
7057
- urls: unclassifiedBatch,
7058
- siteDomain: clientDomain
7059
- });
7060
- },
7061
- this.deps.llm
7062
- );
7063
- allClassifications.push(...llmResults);
7059
+ await this.runLlmBilled(job.user_id, `${jobId}:phase3-classify`, async () => {
7060
+ for (const batch of batches) {
7061
+ let classified = [];
7062
+ let unclassifiedUrls = batch.map((u) => u.url);
7063
+ try {
7064
+ const pythonResult = await this.deps.python.runLocationClassifier({
7065
+ urls: batch.map((u) => u.url),
7066
+ usZipsCsvPath
7067
+ });
7068
+ classified = pythonResult.classified;
7069
+ unclassifiedUrls = pythonResult.unclassified;
7070
+ } catch (err) {
7071
+ console.warn(
7072
+ "[site-audit-service] runClassifyPhase: python.runLocationClassifier failed (falling back to LLM):",
7073
+ err instanceof Error ? err.message : String(err)
7074
+ );
7075
+ unclassifiedUrls = batch.map((u) => u.url);
7076
+ }
7077
+ for (const c of classified) {
7078
+ allClassifications.push({
7079
+ url: c.url,
7080
+ page_type: c.pageType,
7081
+ confidence: c.confidence > 0.6 ? "high" : c.confidence > 0.4 ? "medium" : "low",
7082
+ _classification_label: "[COMPUTED:python/location-classifier]",
7083
+ topic_cluster: null,
7084
+ location_signals: c.locationSignals
7085
+ });
7086
+ }
7087
+ if (unclassifiedUrls.length > 0) {
7088
+ const unclassifiedBatch = batch.filter((u) => unclassifiedUrls.includes(u.url));
7089
+ const llmResults = await llmParseWithRetry(
7090
+ z9.array(PageTypeClassificationRowSchema),
7091
+ (attempt, error) => {
7092
+ void attempt;
7093
+ void error;
7094
+ return buildMetricsClassifyPrompt({
7095
+ urls: unclassifiedBatch,
7096
+ siteDomain: clientDomain
7097
+ });
7098
+ },
7099
+ this.deps.llm
7100
+ );
7101
+ allClassifications.push(...llmResults);
7102
+ }
7064
7103
  }
7065
- }
7104
+ });
7066
7105
  void sessionPath;
7067
7106
  await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase3-classify", {
7068
7107
  classifications: allClassifications,
@@ -7075,20 +7114,24 @@ var SiteAuditService = class {
7075
7114
  if (!job) throw new Error(`[site-audit-service] runComparePhase: job ${jobId} not found`);
7076
7115
  const request = JSON.parse(job.request);
7077
7116
  const clientDomain = request.clientDomain;
7078
- const result = await llmParseWithRetry(
7079
- CompareRecommendOutputSchema,
7080
- (attempt, error) => {
7081
- void attempt;
7082
- void error;
7083
- return buildCompareRecommendPrompt({
7084
- clientDomain,
7085
- clientMoneyPages: [],
7086
- competitorSites: [],
7087
- clientUrlMetrics: [],
7088
- clientPageTypes: []
7089
- });
7090
- },
7091
- this.deps.llm
7117
+ const result = await this.runLlmBilled(
7118
+ job.user_id,
7119
+ `${jobId}:phase4-compare`,
7120
+ () => llmParseWithRetry(
7121
+ CompareRecommendOutputSchema,
7122
+ (attempt, error) => {
7123
+ void attempt;
7124
+ void error;
7125
+ return buildCompareRecommendPrompt({
7126
+ clientDomain,
7127
+ clientMoneyPages: [],
7128
+ competitorSites: [],
7129
+ clientUrlMetrics: [],
7130
+ clientPageTypes: []
7131
+ });
7132
+ },
7133
+ this.deps.llm
7134
+ )
7092
7135
  );
7093
7136
  await this.deps.repo.logSiteAuditPhaseComplete(jobId, "phase4-compare", result);
7094
7137
  return result;
@@ -7118,20 +7161,24 @@ var SiteAuditService = class {
7118
7161
  total: ilesTotal,
7119
7162
  components: ilesComponents
7120
7163
  };
7121
- const reportMarkdown = await llmParseWithRetry(
7122
- z9.string(),
7123
- (attempt, error) => {
7124
- void attempt;
7125
- void error;
7126
- return buildScoreSynthesizePrompt({
7127
- preComputedArchitectureScore,
7128
- preComputedIlesScore,
7129
- rankedBacklog: [],
7130
- clientDomain,
7131
- sessionPath
7132
- });
7133
- },
7134
- this.deps.llm
7164
+ const reportMarkdown = await this.runLlmBilled(
7165
+ job.user_id,
7166
+ `${jobId}:phase5-synthesize`,
7167
+ () => llmParseWithRetry(
7168
+ z9.string(),
7169
+ (attempt, error) => {
7170
+ void attempt;
7171
+ void error;
7172
+ return buildScoreSynthesizePrompt({
7173
+ preComputedArchitectureScore,
7174
+ preComputedIlesScore,
7175
+ rankedBacklog: [],
7176
+ clientDomain,
7177
+ sessionPath
7178
+ });
7179
+ },
7180
+ this.deps.llm
7181
+ )
7135
7182
  );
7136
7183
  const auditResult = SiteAuditResultSchema.parse({
7137
7184
  jobId,
@@ -8140,6 +8187,11 @@ internalMemoryApp.post("/ai-debit", async (c) => {
8140
8187
  }
8141
8188
  const user = await getUserByEmail(body.identity.trim().toLowerCase());
8142
8189
  if (!user) return c.json({ ok: false, reason: "no_user" }, 200);
8190
+ if (body.costUsd > 0) {
8191
+ await runWithCostContext({ ...currentCostContext(), op: "memory_ai", userId: user.id }, async () => {
8192
+ void recordVendorUsage({ vendor: "mcp_memory_ai", model: null, units: body.costUsd, unitType: "usd", error: null });
8193
+ });
8194
+ }
8143
8195
  const mc = Math.round(body.costUsd * MEMORY_AI_MARGIN_MULTIPLE * MC_PER_USD);
8144
8196
  if (mc <= 0) return c.json({ ok: true, mc: 0, balance_mc: null }, 200);
8145
8197
  const { ok, balance_mc } = await debitMc(user.id, mc, LedgerOperation.MEMORY_AI, `${body.source ?? "ai"}:${body.op ?? ""}`);
@@ -8660,6 +8712,11 @@ async function fetchViaYoutubeTranscript(videoId) {
8660
8712
  }));
8661
8713
  const text = chunks.map((c) => c.text).join(" ");
8662
8714
  const last = chunks[chunks.length - 1];
8715
+ const now = Date.now();
8716
+ await runWithCostContext(
8717
+ { ...currentCostContext(), op: "yt_transcription", subOp: "youtube-transcript" },
8718
+ () => recordKernelSession({ kernelSessionId: null, source: "yt_caption_tier", headlessSent: null, openedAtMs: now, closedAtMs: now })
8719
+ );
8663
8720
  return { videoId, text, chunks, durationMs: last ? last.timestamp[1] * 1e3 : 0, method: "youtube-transcript" };
8664
8721
  } catch {
8665
8722
  return null;
@@ -8711,163 +8768,221 @@ function parseTimedtextXml(xml) {
8711
8768
  }
8712
8769
  return results;
8713
8770
  }
8714
- async function fetchViaKernelInnertube(videoId) {
8715
- const kernelApiKey = browserServiceApiKey();
8716
- if (!kernelApiKey) return null;
8717
- const kernelProxyId = browserServiceProxyId();
8718
- const driver = new BrowserDriver();
8719
- const start = Date.now();
8720
- try {
8721
- await driver.launch({ kernelApiKey, kernelProxyId, headless: true, viewport: { width: 1280, height: 800 }, locale: "en-US" });
8722
- const page = driver.getPage();
8723
- await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
8724
- await page.waitForFunction(
8725
- () => !!window.ytcfg,
8726
- { timeout: 2e4 }
8727
- );
8728
- const xml = await page.evaluate(async (vid) => {
8729
- const ANDROID_VERSION = "20.10.38";
8730
- const resp = await fetch("/youtubei/v1/player?prettyPrint=false", {
8731
- method: "POST",
8732
- headers: {
8733
- "Content-Type": "application/json",
8734
- "User-Agent": `com.google.android.youtube/${ANDROID_VERSION} (Linux; U; Android 14)`
8735
- },
8736
- body: JSON.stringify({
8737
- context: { client: { clientName: "ANDROID", clientVersion: ANDROID_VERSION } },
8738
- videoId: vid
8739
- })
8771
+ var INNERTUBE_CLIENTS = [
8772
+ { name: "ANDROID", version: "20.10.38", userAgent: "com.google.android.youtube/20.10.38 (Linux; U; Android 14)" },
8773
+ { name: "IOS", version: "20.10.4", userAgent: "com.google.ios.youtube/20.10.4 (iPhone16,2; U; CPU iOS 18_3_2 like Mac OS X;)" },
8774
+ { name: "ANDROID_VR", version: "1.62.27", userAgent: "com.google.android.apps.youtube.vr.oculus/1.62.27 (Linux; U; Android 12L) gzip" }
8775
+ ];
8776
+ function isBotBlockStatus(status) {
8777
+ return status === "LOGIN_REQUIRED" || status === "AGE_VERIFICATION_REQUIRED";
8778
+ }
8779
+ async function probePlayer(page, videoId) {
8780
+ return page.evaluate(
8781
+ async ({ vid, clients }) => {
8782
+ let lastStatus = null;
8783
+ for (const client2 of clients) {
8784
+ const resp = await fetch("/youtubei/v1/player?prettyPrint=false", {
8785
+ method: "POST",
8786
+ headers: { "Content-Type": "application/json", "User-Agent": client2.userAgent },
8787
+ body: JSON.stringify({
8788
+ context: { client: { clientName: client2.name, clientVersion: client2.version } },
8789
+ videoId: vid
8790
+ })
8791
+ }).catch(() => null);
8792
+ if (!resp?.ok) {
8793
+ lastStatus = resp ? `player_http_${resp.status}` : "player_fetch_failed";
8794
+ continue;
8795
+ }
8796
+ const data = await resp.json();
8797
+ lastStatus = data?.playabilityStatus?.status ?? null;
8798
+ const audioFormats = (data?.streamingData?.adaptiveFormats ?? []).filter((f) => typeof f.url === "string" && f.url && typeof f.mimeType === "string" && f.mimeType.startsWith("audio/")).sort((a, b) => (Number(a.contentLength) || Infinity) - (Number(b.contentLength) || Infinity));
8799
+ const pick = audioFormats[0] ?? null;
8800
+ let captions = null;
8801
+ const tracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
8802
+ if (tracks.length > 0) {
8803
+ const track = tracks.find((t) => t.languageCode === "en") ?? tracks[0];
8804
+ const sep = track.baseUrl.includes("?") ? "&" : "?";
8805
+ const xmlResp = await fetch(`${track.baseUrl}${sep}fmt=json3`).catch(() => null);
8806
+ if (xmlResp?.ok) captions = await xmlResp.text();
8807
+ }
8808
+ if (!captions && !pick) continue;
8809
+ return {
8810
+ captions,
8811
+ status: lastStatus,
8812
+ audio: pick ? {
8813
+ url: pick.url,
8814
+ mimeType: pick.mimeType,
8815
+ bitrate: pick.bitrate ?? null,
8816
+ contentLength: pick.contentLength ? Number(pick.contentLength) || null : null,
8817
+ approxDurationMs: pick.approxDurationMs ? Number(pick.approxDurationMs) || null : null
8818
+ } : null
8819
+ };
8820
+ }
8821
+ return { captions: null, audio: null, status: lastStatus };
8822
+ },
8823
+ { vid: videoId, clients: INNERTUBE_CLIENTS }
8824
+ ).catch(() => null);
8825
+ }
8826
+ var MAX_AUDIO_BYTES = 96 * 1024 * 1024;
8827
+ async function downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, audio) {
8828
+ if (audio.contentLength && audio.contentLength > MAX_AUDIO_BYTES) {
8829
+ console.warn(`[CaptionFetcher] audio stream ${audio.contentLength} bytes exceeds ${MAX_AUDIO_BYTES} byte cap`);
8830
+ return null;
8831
+ }
8832
+ const sep = audio.url.includes("?") ? "&" : "?";
8833
+ const url = audio.contentLength ? `${audio.url}${sep}range=0-${audio.contentLength - 1}` : audio.url;
8834
+ const result = await page.evaluate(async (u) => {
8835
+ try {
8836
+ const resp = await fetch(u);
8837
+ if (!resp.ok) return { ok: false, error: `http_${resp.status}` };
8838
+ const blob = await resp.blob();
8839
+ const dataUrl = await new Promise((resolve, reject) => {
8840
+ const reader = new FileReader();
8841
+ reader.onload = () => resolve(reader.result);
8842
+ reader.onerror = () => reject(reader.error);
8843
+ reader.readAsDataURL(blob);
8740
8844
  });
8741
- if (!resp.ok) return null;
8742
- const data = await resp.json();
8743
- const tracks = data?.captions?.playerCaptionsTracklistRenderer?.captionTracks ?? [];
8744
- if (tracks.length === 0) return null;
8745
- const track = tracks.find((t) => t.languageCode === "en") ?? tracks[0];
8746
- const sep = track.baseUrl.includes("?") ? "&" : "?";
8747
- const xmlResp = await fetch(`${track.baseUrl}${sep}fmt=json3`);
8748
- return xmlResp.ok ? xmlResp.text() : null;
8749
- }, videoId);
8750
- if (!xml) return null;
8751
- let entries = parseTimedtextJson3(xml);
8752
- if (entries.length === 0) entries = parseTimedtextXml(xml);
8753
- if (entries.length === 0) return null;
8754
- const chunks = entries.map((e) => ({
8755
- timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
8756
- text: e.text
8757
- }));
8758
- const text = chunks.map((c) => c.text).join(" ");
8759
- const last = chunks[chunks.length - 1];
8760
- return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
8761
- } catch {
8845
+ return { ok: true, base64: dataUrl.slice(dataUrl.indexOf(",") + 1) };
8846
+ } catch (e) {
8847
+ return { ok: false, error: String(e) };
8848
+ }
8849
+ }, url).catch((e) => ({ ok: false, error: String(e) }));
8850
+ if (!result.ok || !result.base64) {
8851
+ console.warn(`[CaptionFetcher] audio fetch failed (${result.error ?? "unknown"}) for a ${audio.contentLength ?? 0} byte stream`);
8762
8852
  return null;
8763
- } finally {
8764
- await driver.close();
8765
8853
  }
8854
+ const bytes = Buffer.from(result.base64, "base64");
8855
+ if (audio.contentLength && bytes.length < audio.contentLength) {
8856
+ console.warn(`[CaptionFetcher] partial audio: ${bytes.length} of ${audio.contentLength} bytes`);
8857
+ return null;
8858
+ }
8859
+ return { bytes, mimeType: audio.mimeType.split(";")[0].trim() };
8766
8860
  }
8767
- var WHISPER_RECORD_SECONDS = 90;
8768
- async function captureAudioViaMediaRecorder(page) {
8769
- const result = await page.evaluate((recordSecs) => {
8770
- return new Promise((resolve) => {
8771
- const video = document.querySelector("video");
8772
- if (!video) {
8773
- resolve({ ok: false, error: "no video element" });
8774
- return;
8775
- }
8776
- video.muted = false;
8777
- video.volume = 1;
8778
- try {
8779
- const ctx = new AudioContext();
8780
- const src = ctx.createMediaElementSource(video);
8781
- const dest = ctx.createMediaStreamDestination();
8782
- src.connect(dest);
8783
- src.connect(ctx.destination);
8784
- const mimeType = MediaRecorder.isTypeSupported("audio/webm;codecs=opus") ? "audio/webm;codecs=opus" : MediaRecorder.isTypeSupported("audio/webm") ? "audio/webm" : "audio/ogg";
8785
- const recorder = new MediaRecorder(dest.stream, { mimeType, audioBitsPerSecond: 48e3 });
8786
- const chunks = [];
8787
- recorder.ondataavailable = (e) => {
8788
- if (e.data.size > 0) chunks.push(e.data);
8789
- };
8790
- recorder.onstop = async () => {
8791
- const blob = new Blob(chunks, { type: mimeType });
8792
- const ab = await blob.arrayBuffer();
8793
- const arr = new Uint8Array(ab);
8794
- let binary = "";
8795
- const chunkSize = 8192;
8796
- for (let i = 0; i < arr.length; i += chunkSize) {
8797
- binary += String.fromCharCode(...arr.slice(i, Math.min(i + chunkSize, arr.length)));
8798
- }
8799
- resolve({ ok: true, base64: btoa(binary), size: ab.byteLength, mimeType });
8800
- };
8801
- recorder.start(1e3);
8802
- video.playbackRate = 2;
8803
- video.play().catch(() => void 0);
8804
- setTimeout(() => {
8805
- recorder.stop();
8806
- ctx.close();
8807
- }, recordSecs * 1e3);
8808
- } catch (e) {
8809
- resolve({ ok: false, error: String(e) });
8810
- }
8811
- });
8812
- }, WHISPER_RECORD_SECONDS);
8813
- if (!result.ok || !result.base64 || !result.mimeType) return null;
8814
- return { bytes: Buffer.from(result.base64, "base64"), mimeType: result.mimeType };
8815
- }
8816
- async function attemptKernelWhisper(videoId, kernelApiKey, kernelProxyId, falKey, start) {
8817
- const driver = new BrowserDriver();
8861
+ async function transcribeWithWizper(videoId, audio, startMs) {
8862
+ const falKey = process.env.FAL_KEY;
8863
+ if (!falKey) return null;
8818
8864
  try {
8819
- await driver.launch({ kernelApiKey, kernelProxyId, headless: true, viewport: { width: 1280, height: 800 }, locale: "en-US" });
8820
- const page = driver.getPage();
8821
- await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
8822
- await page.waitForFunction(
8823
- () => !!window.ytcfg,
8824
- { timeout: 2e4 }
8825
- );
8826
- const playStatus = await page.evaluate(() => {
8827
- const w = window;
8828
- return w.ytInitialPlayerResponse?.playabilityStatus?.status;
8829
- });
8830
- if (playStatus !== "OK") {
8831
- await driver.close();
8832
- return null;
8833
- }
8834
- const audioInfo = await captureAudioViaMediaRecorder(page);
8835
- await driver.close();
8836
- if (!audioInfo || audioInfo.bytes.length < 1e4) return null;
8837
8865
  fal.config({ credentials: falKey });
8838
- const ext = audioInfo.mimeType.includes("ogg") ? "ogg" : "webm";
8839
- const audioFile = new File([new Uint8Array(audioInfo.bytes)], `audio.${ext}`, { type: audioInfo.mimeType });
8866
+ const ext = audio.mimeType.includes("webm") ? "webm" : audio.mimeType.includes("mp4") ? "m4a" : "audio";
8867
+ const audioFile = new File([new Uint8Array(audio.bytes)], `audio.${ext}`, { type: audio.mimeType });
8840
8868
  const uploadedUrl = await fal.storage.upload(audioFile);
8869
+ const subscribeStartMs = Date.now();
8841
8870
  const result = await fal.subscribe("fal-ai/wizper", {
8842
- input: { audio_url: uploadedUrl, task: "transcribe", language: "en" },
8871
+ input: { audio_url: uploadedUrl, task: "transcribe", language: "en", chunk_level: "segment" },
8843
8872
  logs: false,
8844
8873
  pollInterval: 3e3
8845
8874
  });
8875
+ const subscribeDurationMs = Date.now() - subscribeStartMs;
8846
8876
  const data = result.data;
8847
8877
  const chunks = (data.chunks ?? []).map((c) => ({
8848
8878
  timestamp: c.timestamp,
8849
8879
  text: c.text.trim()
8850
8880
  }));
8851
- const audioSec = chunks.length ? chunks[chunks.length - 1]?.timestamp?.[1] ?? 0 : 0;
8852
- void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: audioSec / 60, unitType: "audio_min" });
8881
+ void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: subscribeDurationMs / 1e3, unitType: "compute_sec" });
8853
8882
  const text = data.text ?? chunks.map((c) => c.text).join(" ");
8854
8883
  if (!text) return null;
8855
- return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-whisper" };
8856
- } catch {
8857
- await driver.close();
8884
+ return { videoId, text, chunks, durationMs: Date.now() - startMs, method: "browser-whisper" };
8885
+ } catch (err) {
8886
+ console.warn(`[CaptionFetcher] wizper transcription failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
8858
8887
  return null;
8859
8888
  }
8860
8889
  }
8861
- async function fetchViaKernelWhisper(videoId) {
8890
+ var YT_RESIDENTIAL_LOCATION = process.env.YOUTUBE_PROXY_LOCATION ?? "Austin, Texas";
8891
+ async function resolveResidentialProxy(kernelApiKey) {
8892
+ try {
8893
+ const resolved = await resolveKernelProxyId({
8894
+ kernelApiKey,
8895
+ proxyMode: "location",
8896
+ location: YT_RESIDENTIAL_LOCATION,
8897
+ gl: "us",
8898
+ fresh: true
8899
+ });
8900
+ return { kernelProxyId: resolved.kernelProxyId, disposableProxyId: resolved.disposableProxyId };
8901
+ } catch (err) {
8902
+ console.warn(`[CaptionFetcher] residential proxy resolution failed: ${err instanceof Error ? err.message : String(err)}`);
8903
+ return {};
8904
+ }
8905
+ }
8906
+ async function probeAndExtract(videoId, opts) {
8862
8907
  const kernelApiKey = browserServiceApiKey();
8863
- const kernelProxyId = browserServiceProxyId();
8864
- const falKey = process.env.FAL_KEY;
8865
- if (!kernelApiKey || !falKey) return null;
8908
+ if (!kernelApiKey) return null;
8909
+ const kernelProxyId = opts?.kernelProxyId ?? browserServiceProxyId();
8910
+ const driver = new BrowserDriver();
8911
+ return runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: "browser-innertube" }, async () => {
8912
+ let entries = [];
8913
+ let audio = null;
8914
+ let retryWithFreshIp = false;
8915
+ try {
8916
+ await driver.launch({ kernelApiKey, kernelProxyId, headless: true, headlessMode: "headless", viewport: { width: 1280, height: 800 }, locale: "en-US" });
8917
+ const page = driver.getPage();
8918
+ await driver.navigateTo(`https://www.youtube.com/watch?v=${videoId}`);
8919
+ await page.waitForFunction(
8920
+ () => !!window.ytcfg,
8921
+ { timeout: 2e4 }
8922
+ );
8923
+ const probe = await probePlayer(page, videoId);
8924
+ if (!probe) return null;
8925
+ if (probe.captions && !opts?.skipCaptions) {
8926
+ entries = parseTimedtextJson3(probe.captions);
8927
+ if (entries.length === 0) entries = parseTimedtextXml(probe.captions);
8928
+ }
8929
+ if (entries.length === 0) {
8930
+ if (!probe.audio) {
8931
+ retryWithFreshIp = isBotBlockStatus(probe.status);
8932
+ console.warn(`[CaptionFetcher] no captions and no audio stream for ${videoId} (playability=${probe.status ?? "unknown"}, proxied=${Boolean(kernelProxyId)})`);
8933
+ return { entries, audio: null, retryWithFreshIp };
8934
+ }
8935
+ audio = await downloadWholeAudioStreamInOneRequestBecauseYouTubeAllowsOnlyOnePerIp(page, probe.audio);
8936
+ if (!audio) return { entries, audio: null, retryWithFreshIp: true };
8937
+ }
8938
+ } catch (err) {
8939
+ console.warn(`[CaptionFetcher] kernel session failed for ${videoId}: ${err instanceof Error ? err.message : String(err)}`);
8940
+ return null;
8941
+ } finally {
8942
+ const servedSubOp = audio ? "browser-whisper" : "browser-innertube";
8943
+ await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", subOp: servedSubOp }, () => driver.close());
8944
+ }
8945
+ return { entries, audio, retryWithFreshIp };
8946
+ });
8947
+ }
8948
+ async function extractWithProxyEscalation(videoId, opts) {
8949
+ const direct = await probeAndExtract(videoId, opts);
8950
+ if (direct && (direct.entries.length > 0 || direct.audio)) return direct;
8951
+ const shouldEscalate = direct === null || direct.retryWithFreshIp;
8952
+ if (!shouldEscalate) return direct;
8953
+ const kernelApiKey = browserServiceApiKey();
8954
+ if (!kernelApiKey) return direct;
8955
+ const { kernelProxyId, disposableProxyId } = await resolveResidentialProxy(kernelApiKey);
8956
+ if (!kernelProxyId) return direct;
8957
+ try {
8958
+ console.warn(`[CaptionFetcher] ${videoId}: retrying through residential proxy after bot block`);
8959
+ const viaProxy = await probeAndExtract(videoId, { ...opts, kernelProxyId });
8960
+ return viaProxy ?? direct;
8961
+ } finally {
8962
+ if (disposableProxyId) {
8963
+ await deleteKernelProxyId(kernelApiKey, disposableProxyId).catch(
8964
+ (err) => console.warn(`[CaptionFetcher] failed to delete disposable proxy: ${err instanceof Error ? err.message : String(err)}`)
8965
+ );
8966
+ }
8967
+ }
8968
+ }
8969
+ async function fetchViaKernelSession(videoId) {
8866
8970
  const start = Date.now();
8867
- for (let attempt = 0; attempt < 4; attempt++) {
8868
- if (attempt > 0) await new Promise((r) => setTimeout(r, 2e3 * attempt));
8869
- const result = await attemptKernelWhisper(videoId, kernelApiKey, kernelProxyId, falKey, start);
8870
- if (result) return result;
8971
+ const outcome = await extractWithProxyEscalation(videoId);
8972
+ if (!outcome) return null;
8973
+ if (outcome.entries.length > 0) {
8974
+ const chunks = outcome.entries.map((e) => ({
8975
+ timestamp: [e.start / 1e3, (e.start + e.dur) / 1e3],
8976
+ text: e.text
8977
+ }));
8978
+ const text = chunks.map((c) => c.text).join(" ");
8979
+ return { videoId, text, chunks, durationMs: Date.now() - start, method: "browser-innertube" };
8980
+ }
8981
+ if (outcome.audio) {
8982
+ return runWithCostContext(
8983
+ { ...currentCostContext(), op: "yt_transcription", subOp: "browser-whisper" },
8984
+ () => transcribeWithWizper(videoId, outcome.audio, start)
8985
+ );
8871
8986
  }
8872
8987
  return null;
8873
8988
  }
@@ -8889,11 +9004,11 @@ function finalizeCaptions(result) {
8889
9004
  async function fetchCaptions(videoId) {
8890
9005
  const primary = await fetchViaYoutubeTranscript(videoId);
8891
9006
  if (primary) return finalizeCaptions(primary);
8892
- const kernel = await fetchViaKernelInnertube(videoId);
8893
- if (kernel) return finalizeCaptions(kernel);
8894
- const whisper = await fetchViaKernelWhisper(videoId);
8895
- if (whisper) return finalizeCaptions(whisper);
8896
- throw new Error(`No captions available for ${videoId} \u2014 all three tiers failed`);
9007
+ const result = await fetchViaKernelSession(videoId);
9008
+ if (result) return finalizeCaptions(result);
9009
+ throw new Error(
9010
+ `Could not transcribe ${videoId}. It has no caption track, and its audio stream could not be downloaded \u2014 YouTube permits a single media request per IP, so streams that do not fit in one response are unreachable. Captioned videos of any length still work.`
9011
+ );
8897
9012
  }
8898
9013
 
8899
9014
  // src/api/api-auth.ts
@@ -9221,7 +9336,7 @@ youtubeApp.post("/transcribe", createApiKeyAuth(), async (c) => {
9221
9336
  const { ok: trOk, balance_mc: trBal } = await debitMc(user.id, holdMc, LedgerOperation.TRANSCRIPTION_HOLD, id);
9222
9337
  if (!trOk) return c.json(insufficientBalanceResponse(trBal, holdMc), 402);
9223
9338
  debited = true;
9224
- const result = await fetchCaptions(id);
9339
+ const result = await runWithCostContext({ ...currentCostContext(), op: "yt_transcription", userId: user.id }, () => fetchCaptions(id));
9225
9340
  const lastChunk = result.chunks?.[result.chunks.length - 1];
9226
9341
  const chunkSecs = lastChunk && Number.isFinite(lastChunk.timestamp[1]) ? lastChunk.timestamp[1] : 0;
9227
9342
  const durationSecs = Number.isFinite(result.durationMs) ? result.durationMs / 1e3 : 0;
@@ -10284,8 +10399,7 @@ async function transcribeMediaUrl(mediaUrl, markdownTitle = "# Media Transcript"
10284
10399
  const text = data.text ?? "";
10285
10400
  const chunks = data.chunks ?? [];
10286
10401
  const durationMs = Date.now() - startMs;
10287
- const audioSec = chunks.length ? chunks[chunks.length - 1]?.timestamp?.[1] ?? 0 : 0;
10288
- void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: audioSec / 60, unitType: "audio_min" });
10402
+ void recordVendorUsage({ vendor: "fal_wizper", model: "fal-ai/wizper", units: durationMs / 1e3, unitType: "compute_sec" });
10289
10403
  return {
10290
10404
  text,
10291
10405
  chunks,
@@ -11760,6 +11874,7 @@ redditApp.post("/thread", createApiKeyAuth(), async (c) => {
11760
11874
  if (!gate.ok) return c.json(concurrencyLimitExceededResponse(gate), 429, { "Retry-After": String(gate.retryAfterSeconds) });
11761
11875
  let debited = false;
11762
11876
  let refunded = false;
11877
+ let commentDebitMc = 0;
11763
11878
  try {
11764
11879
  const { ok, balance_mc } = await debitMc(user.id, MC_COSTS.reddit_thread, LedgerOperation.REDDIT_THREAD, body.url);
11765
11880
  if (!ok) return c.json(insufficientBalanceResponse(balance_mc, MC_COSTS.reddit_thread), 402);
@@ -12077,6 +12192,21 @@ videoApp.post("/status", createApiKeyAuth(), async (c) => {
12077
12192
  effectiveFrames: st.frameCount ?? null
12078
12193
  };
12079
12194
  }
12195
+ if ((st.status === "done" || st.status === "failed") && typeof st.costUsd === "number" && st.costUsd > 0) {
12196
+ const llmOp = `${LedgerOperation.VIDEO_ANALYSIS_LLM}:${runId}`;
12197
+ if (!await ledgerExistsForOperation(user.id, llmOp)) {
12198
+ await runWithCostContext({ ...currentCostContext(), op: "video_analysis", userId: user.id }, async () => {
12199
+ void recordVendorUsage({ vendor: "mcp_memory_video", model: null, units: st.costUsd, unitType: "usd", error: null });
12200
+ });
12201
+ const surchargeMc = Math.round(st.costUsd * VIDEO_ANALYSIS_LLM_MARGIN_MULTIPLE * MC_PER_USD);
12202
+ if (surchargeMc > 0) {
12203
+ const { ok } = await debitMc(user.id, surchargeMc, llmOp, `real-cost-surcharge:${runId}`);
12204
+ if (!ok) {
12205
+ console.warn(`[video-routes] VIDEO_ANALYSIS_LLM surcharge debit failed for user ${user.id} (run ${runId}): insufficient balance for ${surchargeMc}mc`);
12206
+ }
12207
+ }
12208
+ }
12209
+ }
12080
12210
  return c.json({
12081
12211
  ok: true,
12082
12212
  runId,
@@ -13247,7 +13377,7 @@ trustpilotApp.post("/reviews", createApiKeyAuth(), async (c) => {
13247
13377
  let pagesFetched = 0;
13248
13378
  try {
13249
13379
  await driver.launch({
13250
- headless: true,
13380
+ headless: false,
13251
13381
  kernelApiKey: browserServiceApiKey(),
13252
13382
  viewport: { width: 1280, height: 900 },
13253
13383
  locale: "en-US"
@@ -13440,7 +13570,7 @@ g2App.post("/reviews", createApiKeyAuth(), async (c) => {
13440
13570
  const driver = new BrowserDriver();
13441
13571
  try {
13442
13572
  await driver.launch({
13443
- headless: true,
13573
+ headless: false,
13444
13574
  kernelApiKey: browserServiceApiKey(),
13445
13575
  kernelProxyId: proxyId,
13446
13576
  viewport: { width: 1280, height: 900 },
@@ -20279,9 +20409,12 @@ function opForCostPath(p) {
20279
20409
  }
20280
20410
  if (p.startsWith("/instagram/")) return "instagram";
20281
20411
  if (p === "/reddit/thread") return "reddit_thread";
20412
+ if (p.startsWith("/trustpilot/")) return "trustpilot_reviews";
20413
+ if (p.startsWith("/g2/")) return "g2_reviews";
20282
20414
  if (p.startsWith("/api/internal/site-architecture-auditor")) return "audit_site";
20283
20415
  if (p.startsWith("/api/internal/memory")) return "memory_ai";
20284
20416
  if (p.startsWith("/agent/")) return "browser_session";
20417
+ if (p.startsWith("/video/")) return "video_analysis";
20285
20418
  return null;
20286
20419
  }
20287
20420
  app.use("*", (c, next) => {
@@ -20993,16 +21126,20 @@ app.post("/harvest/sync", auth2, async (c) => {
20993
21126
  AbortSignal.timeout(syncTimeoutMs)
20994
21127
  ]);
20995
21128
  try {
20996
- const result = await harvest({
20997
- ...options,
20998
- kernelApiKey: browserServiceApiKey(),
20999
- headless: true,
21000
- format: "json",
21001
- outputDir: "/tmp/paa-output-api",
21002
- signal: syncSignal,
21003
- softDeadlineMs: Date.now() + Math.floor(syncTimeoutMs * 0.8),
21004
- onAttemptEvent: recordAttempt
21005
- });
21129
+ const harvestCtx = currentCostContext();
21130
+ const result = await runWithCostContext(
21131
+ { ...harvestCtx, op: options.serpOnly ? "serp" : "paa", userId: user.id },
21132
+ () => harvest({
21133
+ ...options,
21134
+ kernelApiKey: browserServiceApiKey(),
21135
+ headless: true,
21136
+ format: "json",
21137
+ outputDir: "/tmp/paa-output-api",
21138
+ signal: syncSignal,
21139
+ softDeadlineMs: Date.now() + Math.floor(syncTimeoutMs * 0.8),
21140
+ onAttemptEvent: recordAttempt
21141
+ })
21142
+ );
21006
21143
  await completeJob(jobId, result);
21007
21144
  const attempts = await listHarvestAttempts(jobId, user.id);
21008
21145
  if (!options.serpOnly) {
@@ -21681,7 +21818,7 @@ app.get("/cron/tick", async (c) => {
21681
21818
  if (!process.env.CRON_SECRET || secret2 !== `Bearer ${process.env.CRON_SECRET}`) {
21682
21819
  return c.json({ error: "Unauthorized" }, 401);
21683
21820
  }
21684
- const { drainQueue } = await import("./worker-VASAJ7FZ.js");
21821
+ const { drainQueue } = await import("./worker-HTYZAYGB.js");
21685
21822
  const budget = { maxJobs: 10, deadlineMs: Date.now() + 28e4 };
21686
21823
  const workflowDispatchResult = await dispatchDueWorkflowSchedules(`${new URL(c.req.url).protocol}//${new URL(c.req.url).host}`);
21687
21824
  const [results, sweepResult, reapResult, expiredResult, blobCleanup] = await Promise.all([
@@ -21699,7 +21836,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
21699
21836
  return c.json({ error: "Unauthorized" }, 401);
21700
21837
  }
21701
21838
  const jobId = c.req.param("id");
21702
- const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-LI2S3S6E.js");
21839
+ const { getExtractJob: getExtractJob2, completeExtractJob: completeExtractJob2 } = await import("./site-extract-repository-HTIY52MW.js");
21703
21840
  const { assembleExtractArtifacts } = await import("./extract-bundle-COS56ZDO.js");
21704
21841
  const job = await getExtractJob2(jobId);
21705
21842
  if (!job) return c.json({ error: "job not found" }, 404);
@@ -21707,7 +21844,7 @@ app.post("/api/internal/extract-refinalize/:id", async (c) => {
21707
21844
  await completeExtractJob2(jobId, stored);
21708
21845
  let settlement = "already_settled_or_refunded";
21709
21846
  if (job.billedMc == null && job.userId != null) {
21710
- const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-LI2S3S6E.js");
21847
+ const { settleExtractJob: settleExtractJob2, countSuccessfulPages: countSuccessfulPages2 } = await import("./site-extract-repository-HTIY52MW.js");
21711
21848
  const heldMc = Number(job.options.heldMc ?? 0);
21712
21849
  const successful = await countSuccessfulPages2(jobId);
21713
21850
  const usedMc = Math.min(successful * MC_COSTS.page_scrape, heldMc);
@@ -21871,4 +22008,4 @@ app.get("/blog/:slug/", (c) => {
21871
22008
  export {
21872
22009
  app
21873
22010
  };
21874
- //# sourceMappingURL=server-R3KVQNOT.js.map
22011
+ //# sourceMappingURL=server-UNID3SJU.js.map