mcp-researchpowerpack 3.9.4 → 4.3.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 (188) hide show
  1. package/README.md +116 -208
  2. package/dist/index.js +280 -337
  3. package/dist/index.js.map +7 -1
  4. package/dist/mcp-use.json +7 -0
  5. package/dist/src/clients/reddit.js +278 -0
  6. package/dist/src/clients/reddit.js.map +7 -0
  7. package/dist/src/clients/scraper.js +326 -0
  8. package/dist/src/clients/scraper.js.map +7 -0
  9. package/dist/src/clients/search.js +217 -0
  10. package/dist/src/clients/search.js.map +7 -0
  11. package/dist/src/config/index.js +138 -0
  12. package/dist/src/config/index.js.map +7 -0
  13. package/dist/src/prompts/deep-research.js +41 -0
  14. package/dist/src/prompts/deep-research.js.map +7 -0
  15. package/dist/src/prompts/reddit-sentiment.js +47 -0
  16. package/dist/src/prompts/reddit-sentiment.js.map +7 -0
  17. package/dist/src/schemas/reddit.js +21 -0
  18. package/dist/src/schemas/reddit.js.map +7 -0
  19. package/dist/src/schemas/scrape-links.js +26 -0
  20. package/dist/src/schemas/scrape-links.js.map +7 -0
  21. package/dist/src/schemas/start-research.js +17 -0
  22. package/dist/src/schemas/start-research.js.map +7 -0
  23. package/dist/src/schemas/web-search.js +53 -0
  24. package/dist/src/schemas/web-search.js.map +7 -0
  25. package/dist/src/services/llm-processor.js +675 -0
  26. package/dist/src/services/llm-processor.js.map +7 -0
  27. package/dist/src/services/markdown-cleaner.js +62 -0
  28. package/dist/src/services/markdown-cleaner.js.map +7 -0
  29. package/dist/src/services/workflow-state.js +116 -0
  30. package/dist/src/services/workflow-state.js.map +7 -0
  31. package/dist/src/tools/mcp-helpers.js +50 -0
  32. package/dist/src/tools/mcp-helpers.js.map +7 -0
  33. package/dist/src/tools/reddit.js +277 -0
  34. package/dist/src/tools/reddit.js.map +7 -0
  35. package/dist/src/tools/registry.js +18 -0
  36. package/dist/src/tools/registry.js.map +7 -0
  37. package/dist/src/tools/scrape.js +334 -0
  38. package/dist/src/tools/scrape.js.map +7 -0
  39. package/dist/src/tools/search.js +423 -0
  40. package/dist/src/tools/search.js.map +7 -0
  41. package/dist/src/tools/start-research.js +199 -0
  42. package/dist/src/tools/start-research.js.map +7 -0
  43. package/dist/src/tools/utils.js +29 -0
  44. package/dist/src/tools/utils.js.map +7 -0
  45. package/dist/src/utils/bootstrap-guard.js +27 -0
  46. package/dist/src/utils/bootstrap-guard.js.map +7 -0
  47. package/dist/src/utils/concurrency.js +62 -0
  48. package/dist/src/utils/concurrency.js.map +7 -0
  49. package/dist/src/utils/content-extractor.js +61 -0
  50. package/dist/src/utils/content-extractor.js.map +7 -0
  51. package/dist/src/utils/errors.js +211 -0
  52. package/dist/src/utils/errors.js.map +7 -0
  53. package/dist/src/utils/logger.js +25 -0
  54. package/dist/src/utils/logger.js.map +7 -0
  55. package/dist/src/utils/markdown-formatter.js +15 -0
  56. package/dist/src/utils/markdown-formatter.js.map +7 -0
  57. package/dist/src/utils/reddit-keyword-guard.js +29 -0
  58. package/dist/src/utils/reddit-keyword-guard.js.map +7 -0
  59. package/dist/src/utils/response.js +81 -0
  60. package/dist/src/utils/response.js.map +7 -0
  61. package/dist/src/utils/retry.js +13 -0
  62. package/dist/src/utils/retry.js.map +7 -0
  63. package/dist/src/utils/sanitize.js +10 -0
  64. package/dist/src/utils/sanitize.js.map +7 -0
  65. package/dist/src/utils/source-type.js +41 -0
  66. package/dist/src/utils/source-type.js.map +7 -0
  67. package/dist/src/utils/url-aggregator.js +227 -0
  68. package/dist/src/utils/url-aggregator.js.map +7 -0
  69. package/dist/src/utils/workflow-key.js +14 -0
  70. package/dist/src/utils/workflow-key.js.map +7 -0
  71. package/dist/src/version.js +32 -0
  72. package/dist/src/version.js.map +7 -0
  73. package/package.json +33 -28
  74. package/dist/clients/reddit.d.ts +0 -69
  75. package/dist/clients/reddit.d.ts.map +0 -1
  76. package/dist/clients/reddit.js +0 -369
  77. package/dist/clients/reddit.js.map +0 -1
  78. package/dist/clients/research.d.ts +0 -67
  79. package/dist/clients/research.d.ts.map +0 -1
  80. package/dist/clients/research.js +0 -290
  81. package/dist/clients/research.js.map +0 -1
  82. package/dist/clients/scraper.d.ts +0 -72
  83. package/dist/clients/scraper.d.ts.map +0 -1
  84. package/dist/clients/scraper.js +0 -338
  85. package/dist/clients/scraper.js.map +0 -1
  86. package/dist/clients/search.d.ts +0 -57
  87. package/dist/clients/search.d.ts.map +0 -1
  88. package/dist/clients/search.js +0 -223
  89. package/dist/clients/search.js.map +0 -1
  90. package/dist/config/index.d.ts +0 -78
  91. package/dist/config/index.d.ts.map +0 -1
  92. package/dist/config/index.js +0 -201
  93. package/dist/config/index.js.map +0 -1
  94. package/dist/config/loader.d.ts +0 -40
  95. package/dist/config/loader.d.ts.map +0 -1
  96. package/dist/config/loader.js +0 -322
  97. package/dist/config/loader.js.map +0 -1
  98. package/dist/config/types.d.ts +0 -81
  99. package/dist/config/types.d.ts.map +0 -1
  100. package/dist/config/types.js +0 -6
  101. package/dist/config/types.js.map +0 -1
  102. package/dist/config/yaml/tools.yaml +0 -146
  103. package/dist/index.d.ts +0 -7
  104. package/dist/index.d.ts.map +0 -1
  105. package/dist/schemas/deep-research.d.ts +0 -64
  106. package/dist/schemas/deep-research.d.ts.map +0 -1
  107. package/dist/schemas/deep-research.js +0 -224
  108. package/dist/schemas/deep-research.js.map +0 -1
  109. package/dist/schemas/scrape-links.d.ts +0 -32
  110. package/dist/schemas/scrape-links.d.ts.map +0 -1
  111. package/dist/schemas/scrape-links.js +0 -34
  112. package/dist/schemas/scrape-links.js.map +0 -1
  113. package/dist/schemas/web-search.d.ts +0 -22
  114. package/dist/schemas/web-search.d.ts.map +0 -1
  115. package/dist/schemas/web-search.js +0 -21
  116. package/dist/schemas/web-search.js.map +0 -1
  117. package/dist/services/file-attachment.d.ts +0 -30
  118. package/dist/services/file-attachment.d.ts.map +0 -1
  119. package/dist/services/file-attachment.js +0 -207
  120. package/dist/services/file-attachment.js.map +0 -1
  121. package/dist/services/llm-processor.d.ts +0 -29
  122. package/dist/services/llm-processor.d.ts.map +0 -1
  123. package/dist/services/llm-processor.js +0 -244
  124. package/dist/services/llm-processor.js.map +0 -1
  125. package/dist/services/markdown-cleaner.d.ts +0 -8
  126. package/dist/services/markdown-cleaner.d.ts.map +0 -1
  127. package/dist/services/markdown-cleaner.js +0 -74
  128. package/dist/services/markdown-cleaner.js.map +0 -1
  129. package/dist/tools/definitions.d.ts +0 -16
  130. package/dist/tools/definitions.d.ts.map +0 -1
  131. package/dist/tools/definitions.js +0 -17
  132. package/dist/tools/definitions.js.map +0 -1
  133. package/dist/tools/reddit.d.ts +0 -14
  134. package/dist/tools/reddit.d.ts.map +0 -1
  135. package/dist/tools/reddit.js +0 -265
  136. package/dist/tools/reddit.js.map +0 -1
  137. package/dist/tools/registry.d.ts +0 -71
  138. package/dist/tools/registry.d.ts.map +0 -1
  139. package/dist/tools/registry.js +0 -252
  140. package/dist/tools/registry.js.map +0 -1
  141. package/dist/tools/research.d.ts +0 -14
  142. package/dist/tools/research.d.ts.map +0 -1
  143. package/dist/tools/research.js +0 -196
  144. package/dist/tools/research.js.map +0 -1
  145. package/dist/tools/scrape.d.ts +0 -14
  146. package/dist/tools/scrape.d.ts.map +0 -1
  147. package/dist/tools/scrape.js +0 -234
  148. package/dist/tools/scrape.js.map +0 -1
  149. package/dist/tools/search.d.ts +0 -10
  150. package/dist/tools/search.d.ts.map +0 -1
  151. package/dist/tools/search.js +0 -158
  152. package/dist/tools/search.js.map +0 -1
  153. package/dist/tools/utils.d.ts +0 -105
  154. package/dist/tools/utils.d.ts.map +0 -1
  155. package/dist/tools/utils.js +0 -159
  156. package/dist/tools/utils.js.map +0 -1
  157. package/dist/utils/concurrency.d.ts +0 -28
  158. package/dist/utils/concurrency.d.ts.map +0 -1
  159. package/dist/utils/concurrency.js +0 -92
  160. package/dist/utils/concurrency.js.map +0 -1
  161. package/dist/utils/errors.d.ts +0 -95
  162. package/dist/utils/errors.d.ts.map +0 -1
  163. package/dist/utils/errors.js +0 -390
  164. package/dist/utils/errors.js.map +0 -1
  165. package/dist/utils/logger.d.ts +0 -39
  166. package/dist/utils/logger.d.ts.map +0 -1
  167. package/dist/utils/logger.js +0 -57
  168. package/dist/utils/logger.js.map +0 -1
  169. package/dist/utils/markdown-formatter.d.ts +0 -5
  170. package/dist/utils/markdown-formatter.d.ts.map +0 -1
  171. package/dist/utils/markdown-formatter.js +0 -15
  172. package/dist/utils/markdown-formatter.js.map +0 -1
  173. package/dist/utils/response.d.ts +0 -93
  174. package/dist/utils/response.d.ts.map +0 -1
  175. package/dist/utils/response.js +0 -170
  176. package/dist/utils/response.js.map +0 -1
  177. package/dist/utils/retry.d.ts +0 -43
  178. package/dist/utils/retry.d.ts.map +0 -1
  179. package/dist/utils/retry.js +0 -57
  180. package/dist/utils/retry.js.map +0 -1
  181. package/dist/utils/url-aggregator.d.ts +0 -90
  182. package/dist/utils/url-aggregator.d.ts.map +0 -1
  183. package/dist/utils/url-aggregator.js +0 -538
  184. package/dist/utils/url-aggregator.js.map +0 -1
  185. package/dist/version.d.ts +0 -29
  186. package/dist/version.d.ts.map +0 -1
  187. package/dist/version.js +0 -55
  188. package/dist/version.js.map +0 -1
@@ -0,0 +1,227 @@
1
+ import { CTR_WEIGHTS } from "../config/index.js";
2
+ const WEB_CONSENSUS_THRESHOLD = 3;
3
+ const MIN_BEYOND_TOP10_WEIGHT = 0;
4
+ const BEYOND_TOP10_DECAY = 0.5;
5
+ const BEYOND_TOP10_BASE = 10;
6
+ const DEFAULT_MIN_CONSENSUS_URLS = 5;
7
+ const HIGH_CONSENSUS_THRESHOLD = 4;
8
+ const MAX_ALT_SNIPPETS = 3;
9
+ const MAX_CONSISTENCY_PENALTY = 0.15;
10
+ const CONSISTENCY_STDDEV_SCALE = 5;
11
+ function computePositionStats(positions) {
12
+ if (positions.length <= 1) {
13
+ return { mean: positions[0] ?? 0, stdDev: 0, consistencyMultiplier: 1 };
14
+ }
15
+ const mean = positions.reduce((a, b) => a + b, 0) / positions.length;
16
+ const variance = positions.reduce((sum, p) => sum + (p - mean) ** 2, 0) / (positions.length - 1);
17
+ const stdDev = Math.sqrt(variance);
18
+ const consistencyMultiplier = 1 - MAX_CONSISTENCY_PENALTY * Math.min(stdDev / CONSISTENCY_STDDEV_SCALE, 1);
19
+ return { mean, stdDev, consistencyMultiplier };
20
+ }
21
+ function getCtrWeight(position) {
22
+ if (position >= 1 && position <= 10) {
23
+ return CTR_WEIGHTS[position] ?? 0;
24
+ }
25
+ return Math.max(MIN_BEYOND_TOP10_WEIGHT, BEYOND_TOP10_BASE - (position - BEYOND_TOP10_BASE) * BEYOND_TOP10_DECAY);
26
+ }
27
+ function aggregateResults(searches) {
28
+ const urlMap = /* @__PURE__ */ new Map();
29
+ for (const search of searches) {
30
+ for (const result of search.results) {
31
+ const normalizedUrl = normalizeUrl(result.link);
32
+ const existing = urlMap.get(normalizedUrl);
33
+ if (existing) {
34
+ existing.frequency += 1;
35
+ existing.positions.push(result.position);
36
+ existing.queries.push(search.query);
37
+ const prevBest = existing.bestPosition;
38
+ existing.bestPosition = Math.min(existing.bestPosition, result.position);
39
+ existing.totalScore += getCtrWeight(result.position);
40
+ if (result.snippet && existing.allSnippets.length < MAX_ALT_SNIPPETS && !existing.allSnippets.some((s) => s === result.snippet)) {
41
+ existing.allSnippets.push(result.snippet);
42
+ }
43
+ if (result.position < prevBest) {
44
+ existing.title = result.title;
45
+ existing.snippet = result.snippet;
46
+ }
47
+ } else {
48
+ urlMap.set(normalizedUrl, {
49
+ url: result.link,
50
+ title: result.title,
51
+ snippet: result.snippet,
52
+ allSnippets: result.snippet ? [result.snippet] : [],
53
+ frequency: 1,
54
+ positions: [result.position],
55
+ queries: [search.query],
56
+ bestPosition: result.position,
57
+ totalScore: getCtrWeight(result.position)
58
+ });
59
+ }
60
+ }
61
+ }
62
+ return urlMap;
63
+ }
64
+ function normalizeUrl(url) {
65
+ try {
66
+ const parsed = new URL(url);
67
+ let host = parsed.hostname.replace(/^www\./, "");
68
+ let path = parsed.pathname.replace(/\/$/, "") || "/";
69
+ return `${host}${path}${parsed.search}`.toLowerCase();
70
+ } catch {
71
+ return url.toLowerCase().replace(/\/$/, "");
72
+ }
73
+ }
74
+ function countByFrequency(urlMap, minFrequency) {
75
+ let count = 0;
76
+ for (const url of urlMap.values()) {
77
+ if (url.frequency >= minFrequency) count++;
78
+ }
79
+ return count;
80
+ }
81
+ function calculateWeightedScores(urls, consensusThreshold, totalQueries) {
82
+ if (urls.length === 0) return [];
83
+ const scored = urls.map((url) => {
84
+ const stats = computePositionStats(url.positions);
85
+ const compositeScore = url.totalScore * stats.consistencyMultiplier;
86
+ return { url, compositeScore, stats };
87
+ });
88
+ scored.sort((a, b) => b.compositeScore - a.compositeScore);
89
+ const maxScore = scored[0].compositeScore;
90
+ return scored.map(({ url, compositeScore, stats }, index) => ({
91
+ url: url.url,
92
+ title: url.title,
93
+ snippet: url.snippet,
94
+ allSnippets: url.allSnippets,
95
+ rank: index + 1,
96
+ score: maxScore > 0 ? compositeScore / maxScore * 100 : 0,
97
+ frequency: url.frequency,
98
+ positions: url.positions,
99
+ queries: url.queries,
100
+ bestPosition: url.bestPosition,
101
+ isConsensus: url.frequency >= consensusThreshold,
102
+ coverageRatio: totalQueries > 0 ? url.frequency / totalQueries : 0,
103
+ positionStdDev: stats.stdDev,
104
+ consistencyMultiplier: stats.consistencyMultiplier
105
+ }));
106
+ }
107
+ const COVERAGE_TABLE_MAX_ROWS = 20;
108
+ function consistencyLabel(stdDev, frequency) {
109
+ if (frequency <= 1) return "n/a";
110
+ if (stdDev < 1.5) return "high";
111
+ if (stdDev < 3.5) return "medium";
112
+ return "variable";
113
+ }
114
+ function generateUnifiedOutput(rankedUrls, allQueries, queryResults, totalUniqueUrls, frequencyThreshold, thresholdNote, verbose = false) {
115
+ const lines = [];
116
+ const consensusCount = rankedUrls.filter((u) => u.isConsensus).length;
117
+ lines.push(`## Web Search Results (${allQueries.length} queries, ${totalUniqueUrls} unique URLs)`);
118
+ lines.push("");
119
+ if (thresholdNote) {
120
+ lines.push(`> ${thresholdNote}`);
121
+ lines.push("");
122
+ }
123
+ const consensusActive = frequencyThreshold > 1;
124
+ for (const url of rankedUrls) {
125
+ const consensusTag = consensusActive && url.frequency >= HIGH_CONSENSUS_THRESHOLD ? " CONSENSUS+++" : consensusActive && url.isConsensus ? " CONSENSUS" : "";
126
+ const coveragePct = Math.round(url.coverageRatio * 100);
127
+ const consistency = consistencyLabel(url.positionStdDev, url.frequency);
128
+ lines.push(`**${url.rank}. [${url.title}](${url.url})**${consensusTag}`);
129
+ const showRowMetadata = verbose || allQueries.length > 1 && url.frequency > 1 || allQueries.length === 1;
130
+ if (showRowMetadata) {
131
+ const parts = [
132
+ `Score: ${url.score.toFixed(1)}`,
133
+ `Seen in: ${url.frequency}/${allQueries.length} queries (${coveragePct}%)`,
134
+ `Best pos: #${url.bestPosition}`
135
+ ];
136
+ if (url.frequency > 1) {
137
+ parts.push(`Consistency: ${consistency}`);
138
+ }
139
+ lines.push(parts.join(" | "));
140
+ }
141
+ if (url.queries.length > 1 || verbose) {
142
+ lines.push(`Queries: ${url.queries.map((q) => `"${q}"`).join(", ")}`);
143
+ }
144
+ lines.push(`> ${url.snippet}`);
145
+ if (url.allSnippets.length > 1) {
146
+ const alts = url.allSnippets.filter((s) => s !== url.snippet).slice(0, 3).map((s) => s.length > 100 ? s.slice(0, 97) + "..." : s);
147
+ if (alts.length > 0) {
148
+ lines.push(`Alt: ${alts.map((s) => `"${s}"`).join(" | ")}`);
149
+ }
150
+ }
151
+ lines.push("");
152
+ }
153
+ lines.push("---");
154
+ if (allQueries.length <= COVERAGE_TABLE_MAX_ROWS) {
155
+ lines.push("### Query Coverage");
156
+ lines.push("| Query | Results | Top URL | Top Pos |");
157
+ lines.push("|---------|---------|---------|---------|");
158
+ for (const search of queryResults) {
159
+ const topResult = search.results[0];
160
+ let topDomain = "";
161
+ if (topResult) {
162
+ try {
163
+ topDomain = new URL(topResult.link).hostname.replace(/^www\./, "");
164
+ } catch {
165
+ topDomain = topResult.link;
166
+ }
167
+ }
168
+ lines.push(`| "${search.query}" | ${search.results.length} | ${topDomain || "\u2014"} | ${topResult ? `#${topResult.position}` : "\u2014"} |`);
169
+ }
170
+ lines.push("");
171
+ } else {
172
+ const goodCount = queryResults.filter((s) => s.results.length >= 3).length;
173
+ lines.push(`### Query Coverage: ${goodCount}/${allQueries.length} queries returned 3+ results`);
174
+ lines.push("");
175
+ }
176
+ const lowYield = queryResults.filter((s) => s.results.length <= 1);
177
+ if (lowYield.length > 0) {
178
+ lines.push(`**Low-yield queries** (0-1 results): ${lowYield.map((s) => `\`${s.query}\``).join(", ")}`);
179
+ lines.push("");
180
+ }
181
+ const allRelated = /* @__PURE__ */ new Set();
182
+ for (const search of queryResults) {
183
+ if (search.related) {
184
+ for (const r of search.related) {
185
+ allRelated.add(r);
186
+ }
187
+ }
188
+ }
189
+ if (allRelated.size > 0) {
190
+ const related = [...allRelated].slice(0, 10);
191
+ lines.push(`**Related searches:** ${related.map((r) => `\`${r}\``).join(", ")}`);
192
+ lines.push("");
193
+ }
194
+ return lines.join("\n");
195
+ }
196
+ function aggregateAndRank(searches, minConsensusUrls = DEFAULT_MIN_CONSENSUS_URLS) {
197
+ const urlMap = aggregateResults(searches);
198
+ const totalUniqueUrls = urlMap.size;
199
+ const totalQueries = searches.length;
200
+ const thresholds = [3, 2, 1];
201
+ let usedThreshold = 1;
202
+ let thresholdNote;
203
+ for (const threshold of thresholds) {
204
+ const count = countByFrequency(urlMap, threshold);
205
+ if (count >= minConsensusUrls || threshold === 1) {
206
+ usedThreshold = threshold;
207
+ if (threshold < 3) {
208
+ thresholdNote = `Note: Consensus threshold set to \u2265${threshold} due to result diversity.`;
209
+ }
210
+ break;
211
+ }
212
+ }
213
+ const allUrls = [...urlMap.values()];
214
+ const rankedUrls = calculateWeightedScores(allUrls, usedThreshold, totalQueries);
215
+ return {
216
+ rankedUrls,
217
+ totalUniqueUrls,
218
+ totalQueries,
219
+ frequencyThreshold: usedThreshold,
220
+ thresholdNote
221
+ };
222
+ }
223
+ export {
224
+ aggregateAndRank,
225
+ generateUnifiedOutput
226
+ };
227
+ //# sourceMappingURL=url-aggregator.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/utils/url-aggregator.ts"],
4
+ "sourcesContent": ["/**\n * URL Aggregator Utility\n * Aggregates search results across multiple queries, calculates CTR-weighted scores,\n * and generates consensus-based rankings.\n */\n\nimport { CTR_WEIGHTS } from '../config/index.js';\nimport type { QuerySearchResult } from '../clients/search.js';\n\n/** Minimum frequency for web search consensus marking */\nconst WEB_CONSENSUS_THRESHOLD = 3 as const;\n\n\n/** Minimum weight assigned to positions beyond top 10 */\nconst MIN_BEYOND_TOP10_WEIGHT = 0 as const;\n\n/** Weight decay per position beyond top 10 */\nconst BEYOND_TOP10_DECAY = 0.5 as const;\n\n/** Base position for beyond-top-10 weight calculation */\nconst BEYOND_TOP10_BASE = 10 as const;\n\n/** Default minimum consensus URLs before lowering threshold (web search) */\nconst DEFAULT_MIN_CONSENSUS_URLS = 5 as const;\n\n/** High consensus frequency threshold for enhanced output labeling */\nconst HIGH_CONSENSUS_THRESHOLD = 4 as const;\n\n/** Maximum number of alternative snippets to retain per URL */\nconst MAX_ALT_SNIPPETS = 3 as const;\n\n/** Consistency penalty cap \u2014 bounds the impact of position variance */\nconst MAX_CONSISTENCY_PENALTY = 0.15 as const;\n\n/** Standard deviation normalizer \u2014 stdDev of 5+ gets full penalty */\nconst CONSISTENCY_STDDEV_SCALE = 5 as const;\n\n/**\n * Aggregated URL data structure\n */\ninterface AggregatedUrl {\n readonly url: string;\n title: string;\n snippet: string;\n readonly allSnippets: string[];\n frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n bestPosition: number;\n totalScore: number;\n}\n\n/**\n * Compute position statistics for consistency scoring\n */\nfunction computePositionStats(positions: number[]): { mean: number; stdDev: number; consistencyMultiplier: number } {\n if (positions.length <= 1) {\n return { mean: positions[0] ?? 0, stdDev: 0, consistencyMultiplier: 1.0 };\n }\n const mean = positions.reduce((a, b) => a + b, 0) / positions.length;\n const variance = positions.reduce((sum, p) => sum + (p - mean) ** 2, 0) / (positions.length - 1);\n const stdDev = Math.sqrt(variance);\n const consistencyMultiplier = 1.0 - MAX_CONSISTENCY_PENALTY * Math.min(stdDev / CONSISTENCY_STDDEV_SCALE, 1.0);\n return { mean, stdDev, consistencyMultiplier };\n}\n\n/**\n * Ranked URL with normalized score and enriched signals\n */\ninterface RankedUrl {\n readonly url: string;\n readonly title: string;\n readonly snippet: string;\n readonly allSnippets: string[];\n readonly rank: number;\n readonly score: number;\n readonly frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n readonly bestPosition: number;\n readonly isConsensus: boolean;\n readonly coverageRatio: number;\n readonly positionStdDev: number;\n readonly consistencyMultiplier: number;\n}\n\n/**\n * Aggregation result containing all processed data\n */\ninterface AggregationResult {\n readonly rankedUrls: RankedUrl[];\n readonly totalUniqueUrls: number;\n readonly totalQueries: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\n/**\n * Get CTR weight for a position (1-10)\n * Positions beyond 10 get minimal weight\n */\nfunction getCtrWeight(position: number): number {\n if (position >= 1 && position <= 10) {\n return CTR_WEIGHTS[position] ?? 0;\n }\n // Positions beyond 10 get diminishing returns\n return Math.max(MIN_BEYOND_TOP10_WEIGHT, BEYOND_TOP10_BASE - (position - BEYOND_TOP10_BASE) * BEYOND_TOP10_DECAY);\n}\n\n/**\n * Aggregate results from multiple searches\n * Flattens all results, deduplicates by URL, and tracks frequency/positions\n */\nfunction aggregateResults(searches: QuerySearchResult[]): Map<string, AggregatedUrl> {\n const urlMap = new Map<string, AggregatedUrl>();\n\n for (const search of searches) {\n for (const result of search.results) {\n const normalizedUrl = normalizeUrl(result.link);\n const existing = urlMap.get(normalizedUrl);\n\n if (existing) {\n existing.frequency += 1;\n existing.positions.push(result.position);\n existing.queries.push(search.query);\n const prevBest = existing.bestPosition;\n existing.bestPosition = Math.min(existing.bestPosition, result.position);\n existing.totalScore += getCtrWeight(result.position);\n // Collect distinct snippets (up to MAX_ALT_SNIPPETS)\n if (\n result.snippet &&\n existing.allSnippets.length < MAX_ALT_SNIPPETS &&\n !existing.allSnippets.some(s => s === result.snippet)\n ) {\n existing.allSnippets.push(result.snippet);\n }\n // Keep best title/snippet (from highest ranking position)\n if (result.position < prevBest) {\n existing.title = result.title;\n existing.snippet = result.snippet;\n }\n } else {\n urlMap.set(normalizedUrl, {\n url: result.link,\n title: result.title,\n snippet: result.snippet,\n allSnippets: result.snippet ? [result.snippet] : [],\n frequency: 1,\n positions: [result.position],\n queries: [search.query],\n bestPosition: result.position,\n totalScore: getCtrWeight(result.position),\n });\n }\n }\n }\n\n return urlMap;\n}\n\n/**\n * Normalize URL for deduplication\n * Removes trailing slashes, www prefix, and normalizes protocol\n */\nfunction normalizeUrl(url: string): string {\n try {\n const parsed = new URL(url);\n let host = parsed.hostname.replace(/^www\\./, '');\n let path = parsed.pathname.replace(/\\/$/, '') || '/';\n return `${host}${path}${parsed.search}`.toLowerCase();\n } catch {\n return url.toLowerCase().replace(/\\/$/, '');\n }\n}\n\n/**\n * Count URLs meeting a frequency threshold\n */\nfunction countByFrequency(\n urlMap: Map<string, AggregatedUrl>,\n minFrequency: number\n): number {\n let count = 0;\n for (const url of urlMap.values()) {\n if (url.frequency >= minFrequency) count++;\n }\n return count;\n}\n\n/**\n * Calculate weighted scores with consistency multiplier, normalize to 100.0.\n * Returns ALL URLs sorted by composite score with rank assignments and consensus marking.\n */\nfunction calculateWeightedScores(urls: AggregatedUrl[], consensusThreshold: number, totalQueries: number): RankedUrl[] {\n if (urls.length === 0) return [];\n\n // Compute composite scores (base CTR \u00D7 consistency multiplier)\n const scored = urls.map(url => {\n const stats = computePositionStats(url.positions);\n const compositeScore = url.totalScore * stats.consistencyMultiplier;\n return { url, compositeScore, stats };\n });\n\n // Sort by composite score descending\n scored.sort((a, b) => b.compositeScore - a.compositeScore);\n\n // Find max for normalization\n const maxScore = scored[0]!.compositeScore;\n\n // Map to ranked URLs with all signals\n return scored.map(({ url, compositeScore, stats }, index) => ({\n url: url.url,\n title: url.title,\n snippet: url.snippet,\n allSnippets: url.allSnippets,\n rank: index + 1,\n score: maxScore > 0 ? (compositeScore / maxScore) * 100 : 0,\n frequency: url.frequency,\n positions: url.positions,\n queries: url.queries,\n bestPosition: url.bestPosition,\n isConsensus: url.frequency >= consensusThreshold,\n coverageRatio: totalQueries > 0 ? url.frequency / totalQueries : 0,\n positionStdDev: stats.stdDev,\n consistencyMultiplier: stats.consistencyMultiplier,\n }));\n}\n\n/** Maximum queries to show in the coverage table before collapsing */\nconst COVERAGE_TABLE_MAX_ROWS = 20 as const;\n\n/**\n * Consistency label based on position standard deviation\n */\nfunction consistencyLabel(stdDev: number, frequency: number): string {\n if (frequency <= 1) return 'n/a';\n if (stdDev < 1.5) return 'high';\n if (stdDev < 3.5) return 'medium';\n return 'variable';\n}\n\n/**\n * Generate a unified output where every URL appears exactly once.\n * Replaces the old generateEnhancedOutput + per-query section combo.\n */\nexport function generateUnifiedOutput(\n rankedUrls: RankedUrl[],\n allQueries: string[],\n queryResults: QuerySearchResult[],\n totalUniqueUrls: number,\n frequencyThreshold: number,\n thresholdNote?: string,\n verbose: boolean = false,\n): string {\n const lines: string[] = [];\n const consensusCount = rankedUrls.filter(u => u.isConsensus).length;\n\n // Header\n lines.push(`## Web Search Results (${allQueries.length} queries, ${totalUniqueUrls} unique URLs)`);\n lines.push('');\n if (thresholdNote) {\n lines.push(`> ${thresholdNote}`);\n lines.push('');\n }\n\n // Ranked URL list \u2014 every URL exactly once.\n //\n // Per-row metadata is gated:\n // - CONSENSUS labels only appear when the effective threshold is >1 (a\n // threshold of 1 means *every* row gets the label, so it carries no\n // signal). See: docs/code-review/context/02-current-tool-surface.md.\n // - The Score/Seen/Consistency line is suppressed for rows that were\n // seen in exactly one query in a multi-query call (Seen=1/N is common\n // and Consistency is always \"n/a\" in that case).\n // - Verbose mode restores both for callers that explicitly want them.\n const consensusActive = frequencyThreshold > 1;\n\n for (const url of rankedUrls) {\n const consensusTag = consensusActive && url.frequency >= HIGH_CONSENSUS_THRESHOLD\n ? ' CONSENSUS+++'\n : consensusActive && url.isConsensus\n ? ' CONSENSUS'\n : '';\n const coveragePct = Math.round(url.coverageRatio * 100);\n const consistency = consistencyLabel(url.positionStdDev, url.frequency);\n\n lines.push(`**${url.rank}. [${url.title}](${url.url})**${consensusTag}`);\n\n const showRowMetadata = verbose\n || (allQueries.length > 1 && url.frequency > 1)\n || allQueries.length === 1;\n if (showRowMetadata) {\n const parts = [\n `Score: ${url.score.toFixed(1)}`,\n `Seen in: ${url.frequency}/${allQueries.length} queries (${coveragePct}%)`,\n `Best pos: #${url.bestPosition}`,\n ];\n if (url.frequency > 1) {\n parts.push(`Consistency: ${consistency}`);\n }\n lines.push(parts.join(' | '));\n }\n if (url.queries.length > 1 || verbose) {\n lines.push(`Queries: ${url.queries.map(q => `\"${q}\"`).join(', ')}`);\n }\n lines.push(`> ${url.snippet}`);\n\n // Alt snippets (if multiple distinct snippets were collected)\n if (url.allSnippets.length > 1) {\n const alts = url.allSnippets\n .filter(s => s !== url.snippet)\n .slice(0, 3)\n .map(s => s.length > 100 ? s.slice(0, 97) + '...' : s);\n if (alts.length > 0) {\n lines.push(`Alt: ${alts.map(s => `\"${s}\"`).join(' | ')}`);\n }\n }\n\n lines.push('');\n }\n\n // Keyword coverage section\n lines.push('---');\n\n if (allQueries.length <= COVERAGE_TABLE_MAX_ROWS) {\n // Full table for \u226420 queries\n lines.push('### Query Coverage');\n lines.push('| Query | Results | Top URL | Top Pos |');\n lines.push('|---------|---------|---------|---------|');\n\n for (const search of queryResults) {\n const topResult = search.results[0];\n let topDomain = '';\n if (topResult) {\n try {\n topDomain = new URL(topResult.link).hostname.replace(/^www\\./, '');\n } catch {\n topDomain = topResult.link;\n }\n }\n lines.push(`| \"${search.query}\" | ${search.results.length} | ${topDomain || '\u2014'} | ${topResult ? `#${topResult.position}` : '\u2014'} |`);\n }\n lines.push('');\n } else {\n // Collapsed summary for >20 queries\n const goodCount = queryResults.filter(s => s.results.length >= 3).length;\n lines.push(`### Query Coverage: ${goodCount}/${allQueries.length} queries returned 3+ results`);\n lines.push('');\n }\n\n // Low-yield queries\n const lowYield = queryResults.filter(s => s.results.length <= 1);\n if (lowYield.length > 0) {\n lines.push(`**Low-yield queries** (0-1 results): ${lowYield.map(s => `\\`${s.query}\\``).join(', ')}`);\n lines.push('');\n }\n\n // Related searches (merged and deduplicated)\n const allRelated = new Set<string>();\n for (const search of queryResults) {\n if (search.related) {\n for (const r of search.related) {\n allRelated.add(r);\n }\n }\n }\n if (allRelated.size > 0) {\n const related = [...allRelated].slice(0, 10);\n lines.push(`**Related searches:** ${related.map(r => `\\`${r}\\``).join(', ')}`);\n lines.push('');\n }\n\n return lines.join('\\n');\n}\n\n/**\n * Full aggregation pipeline \u2014 returns ALL URLs ranked by CTR score.\n * Determines a consensus threshold (\u22653, \u22652, or \u22651) for labeling, but never\n * drops URLs below the threshold. Every collected URL appears in the output.\n */\nexport function aggregateAndRank(\n searches: QuerySearchResult[],\n minConsensusUrls: number = DEFAULT_MIN_CONSENSUS_URLS\n): AggregationResult {\n const urlMap = aggregateResults(searches);\n const totalUniqueUrls = urlMap.size;\n const totalQueries = searches.length;\n\n // Determine consensus threshold for labeling (not filtering)\n const thresholds = [3, 2, 1];\n let usedThreshold = 1;\n let thresholdNote: string | undefined;\n\n for (const threshold of thresholds) {\n const count = countByFrequency(urlMap, threshold);\n if (count >= minConsensusUrls || threshold === 1) {\n usedThreshold = threshold;\n if (threshold < 3) {\n thresholdNote = `Note: Consensus threshold set to \u2265${threshold} due to result diversity.`;\n }\n break;\n }\n }\n\n // Rank ALL URLs, marking consensus based on determined threshold\n const allUrls = [...urlMap.values()];\n const rankedUrls = calculateWeightedScores(allUrls, usedThreshold, totalQueries);\n\n return {\n rankedUrls,\n totalUniqueUrls,\n totalQueries,\n frequencyThreshold: usedThreshold,\n thresholdNote,\n };\n}\n\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,14 @@
1
+ function buildWorkflowKey(ctx) {
2
+ const caller = ctx.client.user();
3
+ if (caller?.subject && caller.conversationId) {
4
+ return `chatgpt:${caller.subject}:${caller.conversationId}`;
5
+ }
6
+ if (caller?.conversationId) {
7
+ return `conversation:${caller.conversationId}`;
8
+ }
9
+ return `session:${ctx.session.sessionId}`;
10
+ }
11
+ export {
12
+ buildWorkflowKey
13
+ };
14
+ //# sourceMappingURL=workflow-key.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/utils/workflow-key.ts"],
4
+ "sourcesContent": ["export interface WorkflowCaller {\n subject?: string;\n conversationId?: string;\n}\n\nexport interface WorkflowKeyContext {\n session: { sessionId: string };\n client: {\n user(): WorkflowCaller | undefined;\n };\n}\n\nexport function buildWorkflowKey(ctx: WorkflowKeyContext): string {\n const caller = ctx.client.user();\n\n if (caller?.subject && caller.conversationId) {\n return `chatgpt:${caller.subject}:${caller.conversationId}`;\n }\n\n if (caller?.conversationId) {\n return `conversation:${caller.conversationId}`;\n }\n\n return `session:${ctx.session.sessionId}`;\n}\n"],
5
+ "mappings": "AAYO,SAAS,iBAAiB,KAAiC;AAChE,QAAM,SAAS,IAAI,OAAO,KAAK;AAE/B,MAAI,QAAQ,WAAW,OAAO,gBAAgB;AAC5C,WAAO,WAAW,OAAO,OAAO,IAAI,OAAO,cAAc;AAAA,EAC3D;AAEA,MAAI,QAAQ,gBAAgB;AAC1B,WAAO,gBAAgB,OAAO,cAAc;AAAA,EAC9C;AAEA,SAAO,WAAW,IAAI,QAAQ,SAAS;AACzC;",
6
+ "names": []
7
+ }
@@ -0,0 +1,32 @@
1
+ import { createRequire } from "module";
2
+ import { fileURLToPath } from "url";
3
+ import { dirname, join } from "path";
4
+ const DEFAULT_PACKAGE_INFO = {
5
+ version: "3.9.5",
6
+ name: "mcp-researchpowerpack-http",
7
+ description: "Research Powerpack MCP Server"
8
+ };
9
+ let packageJson = { ...DEFAULT_PACKAGE_INFO };
10
+ try {
11
+ if (typeof import.meta.url === "string" && import.meta.url.startsWith("file:")) {
12
+ const _require = createRequire(import.meta.url);
13
+ const _dirname = dirname(fileURLToPath(import.meta.url));
14
+ try {
15
+ packageJson = _require(join(_dirname, "..", "package.json"));
16
+ } catch {
17
+ packageJson = _require(join(_dirname, "..", "..", "package.json"));
18
+ }
19
+ }
20
+ } catch {
21
+ }
22
+ const VERSION = packageJson.version;
23
+ const PACKAGE_NAME = packageJson.name;
24
+ const PACKAGE_DESCRIPTION = packageJson.description;
25
+ const USER_AGENT_VERSION = `${PACKAGE_NAME}/${VERSION}`;
26
+ export {
27
+ PACKAGE_DESCRIPTION,
28
+ PACKAGE_NAME,
29
+ USER_AGENT_VERSION,
30
+ VERSION
31
+ };
32
+ //# sourceMappingURL=version.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../src/version.ts"],
4
+ "sourcesContent": ["/**\n * Version Module - Single Source of Truth\n * \n * This module reads package metadata from package.json at runtime and keeps a\n * fallback copy for environments where package.json cannot be resolved.\n * \n * Usage:\n * import { VERSION, PACKAGE_NAME } from './version.js';\n */\n\nimport { createRequire } from 'module';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// Defaults used if package.json cannot be loaded at runtime.\nconst DEFAULT_PACKAGE_INFO = {\n version: '3.9.5',\n name: 'mcp-researchpowerpack-http',\n description: 'Research Powerpack MCP Server',\n} as const;\n\nlet packageJson: { version: string; name: string; description: string } = { ...DEFAULT_PACKAGE_INFO };\n\ntry {\n if (typeof import.meta.url === 'string' && import.meta.url.startsWith('file:')) {\n const _require = createRequire(import.meta.url);\n const _dirname = dirname(fileURLToPath(import.meta.url));\n try {\n packageJson = _require(join(_dirname, '..', 'package.json'));\n } catch {\n packageJson = _require(join(_dirname, '..', '..', 'package.json'));\n }\n }\n} catch {\n // Keep hardcoded defaults when package.json is unavailable\n}\n\n/**\n * Package version from package.json\n * This is the single source of truth for versioning\n */\nexport const VERSION: string = packageJson.version;\n\n/**\n * Package name from package.json\n */\nexport const PACKAGE_NAME: string = packageJson.name;\n\n/**\n * Package description from package.json\n */\nexport const PACKAGE_DESCRIPTION: string = packageJson.description;\n\n/**\n * Formatted version string for user agents and logging\n * Example: \"mcp-researchpowerpack-http/3.2.0\"\n */\nexport const USER_AGENT_VERSION: string = `${PACKAGE_NAME}/${VERSION}`;\n\n// VERSION_INFO removed - unused, individual exports sufficient\n"],
5
+ "mappings": "AAUA,SAAS,qBAAqB;AAC9B,SAAS,qBAAqB;AAC9B,SAAS,SAAS,YAAY;AAG9B,MAAM,uBAAuB;AAAA,EAC3B,SAAS;AAAA,EACT,MAAM;AAAA,EACN,aAAa;AACf;AAEA,IAAI,cAAsE,EAAE,GAAG,qBAAqB;AAEpG,IAAI;AACF,MAAI,OAAO,YAAY,QAAQ,YAAY,YAAY,IAAI,WAAW,OAAO,GAAG;AAC9E,UAAM,WAAW,cAAc,YAAY,GAAG;AAC9C,UAAM,WAAW,QAAQ,cAAc,YAAY,GAAG,CAAC;AACvD,QAAI;AACF,oBAAc,SAAS,KAAK,UAAU,MAAM,cAAc,CAAC;AAAA,IAC7D,QAAQ;AACN,oBAAc,SAAS,KAAK,UAAU,MAAM,MAAM,cAAc,CAAC;AAAA,IACnE;AAAA,EACF;AACF,QAAQ;AAER;AAMO,MAAM,UAAkB,YAAY;AAKpC,MAAM,eAAuB,YAAY;AAKzC,MAAM,sBAA8B,YAAY;AAMhD,MAAM,qBAA6B,GAAG,YAAY,IAAI,OAAO;",
6
+ "names": []
7
+ }
package/package.json CHANGED
@@ -1,15 +1,17 @@
1
1
  {
2
2
  "name": "mcp-researchpowerpack",
3
- "version": "3.9.4",
4
- "description": "The ultimate research MCP toolkit: Reddit mining, web search with CTR aggregation, AI-powered deep research, and intelligent web scraping - all in one modular package",
3
+ "version": "4.3.0",
4
+ "description": "HTTP-first MCP research server: web-search (with Reddit scope), scrape-links (chrome-stripped), get-reddit-post built on mcp-use, deployed at research.yigitkonur.com/mcp.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "bin": {
8
- "mcp-research-powerpack": "dist/index.js",
9
- "research-powerpack-mcp": "dist/index.js"
8
+ "mcp-researchpowerpack": "dist/index.js",
9
+ "mcp-researchpowerpack-http": "dist/index.js",
10
+ "mcp-research-powerpack-http": "dist/index.js"
10
11
  },
11
12
  "files": [
12
- "dist"
13
+ "dist",
14
+ "!dist/tests"
13
15
  ],
14
16
  "keywords": [
15
17
  "mcp",
@@ -19,54 +21,57 @@
19
21
  "search",
20
22
  "ai",
21
23
  "model-context-protocol",
22
- "deep-research",
23
24
  "web-scraping",
24
25
  "claude",
25
26
  "anthropic",
26
27
  "openrouter",
27
28
  "scraping",
28
- "research-powerpack"
29
+ "research-powerpack",
30
+ "http-mcp"
29
31
  ],
30
32
  "author": "Yiğit Konur <yigit35@gmail.com>",
31
33
  "license": "MIT",
32
34
  "repository": {
33
35
  "type": "git",
34
- "url": "git+https://github.com/yigitkonur/mcp-research-powerpack.git"
36
+ "url": "git+https://github.com/yigitkonur/mcp-researchpowerpack.git"
35
37
  },
36
- "homepage": "https://github.com/yigitkonur/mcp-research-powerpack#readme",
38
+ "homepage": "https://github.com/yigitkonur/mcp-researchpowerpack#readme",
37
39
  "bugs": {
38
- "url": "https://github.com/yigitkonur/mcp-research-powerpack/issues"
40
+ "url": "https://github.com/yigitkonur/mcp-researchpowerpack/issues"
39
41
  },
40
42
  "dependencies": {
41
- "@modelcontextprotocol/sdk": "^1.18.1",
42
- "agents": "^0.3.10",
43
+ "@mozilla/readability": "^0.6.0",
44
+ "jsdom": "^29.0.2",
45
+ "mcp-use": "1.22.0",
43
46
  "openai": "^4.77.0",
47
+ "redis": "^5.11.0",
44
48
  "turndown": "^7.2.2",
45
- "yaml": "^2.7.0",
46
- "zod": "^3.24.1",
47
- "zod-to-json-schema": "^3.24.1"
49
+ "zod": "^4.3.6"
48
50
  },
49
51
  "devDependencies": {
52
+ "@mcp-use/cli": "2.20.0",
53
+ "@types/jsdom": "^28.0.1",
50
54
  "@types/node": "^22.0.0",
51
55
  "@types/turndown": "^5.0.6",
52
- "dotenv": "^17.2.3",
53
56
  "tsx": "^4.19.0",
54
- "typescript": "^5.6.0",
55
- "wrangler": "^4.63.0"
57
+ "typescript": "^5.6.0"
56
58
  },
57
59
  "engines": {
58
- "node": ">=20.0.0"
60
+ "node": "^20.19.0 || >=22.12.0"
61
+ },
62
+ "publishConfig": {
63
+ "access": "public"
59
64
  },
60
65
  "scripts": {
61
- "build": "tsc && cp -r src/config/yaml dist/config/",
62
- "dev": "tsx src/index.ts",
63
- "start": "node dist/index.js",
66
+ "build": "mcp-use build",
67
+ "dev": "mcp-use dev",
68
+ "start": "mcp-use start",
69
+ "deploy": "mcp-use deploy",
70
+ "inspect": "npx @mcp-use/inspector --url http://localhost:3000/mcp",
64
71
  "typecheck": "tsc --noEmit",
65
- "test:web-search": "tsx tests/test-web-search.ts",
66
- "test:reddit-search": "tsx tests/test-reddit-search.ts",
67
- "test:scrape-links": "tsx tests/test-scrape-links.ts",
68
- "test:deep-research": "tsx tests/test-deep-research.ts",
69
- "test:all": "pnpm test:web-search && pnpm test:reddit-search && pnpm test:scrape-links && pnpm test:deep-research",
70
- "test:check": "tsx tests/check-setup.ts"
72
+ "test": "pnpm test:unit && pnpm test:http",
73
+ "test:unit": "tsx --test tests/**/*.test.ts",
74
+ "test:http": "tsx tests/http-server.ts",
75
+ "test:evals": "tsx tests/agent-behavior.ts"
71
76
  }
72
77
  }
@@ -1,69 +0,0 @@
1
- /**
2
- * Reddit OAuth API Client
3
- * Fetches posts and comments sorted by score (most upvoted first)
4
- * Implements robust error handling that NEVER crashes
5
- */
6
- interface Post {
7
- readonly title: string;
8
- readonly author: string;
9
- readonly subreddit: string;
10
- readonly body: string;
11
- readonly score: number;
12
- readonly commentCount: number;
13
- readonly url: string;
14
- readonly created: Date;
15
- readonly flair?: string;
16
- readonly isNsfw: boolean;
17
- readonly isPinned: boolean;
18
- }
19
- export interface Comment {
20
- readonly author: string;
21
- readonly body: string;
22
- readonly score: number;
23
- readonly depth: number;
24
- readonly isOP: boolean;
25
- }
26
- export interface PostResult {
27
- readonly post: Post;
28
- readonly comments: Comment[];
29
- readonly allocatedComments: number;
30
- readonly actualComments: number;
31
- }
32
- interface BatchPostResult {
33
- readonly results: Map<string, PostResult | Error>;
34
- readonly batchesProcessed: number;
35
- readonly totalPosts: number;
36
- readonly rateLimitHits: number;
37
- readonly commentAllocation: CommentAllocation;
38
- }
39
- interface CommentAllocation {
40
- readonly totalBudget: number;
41
- readonly perPostBase: number;
42
- readonly perPostCapped: number;
43
- redistributed: boolean;
44
- }
45
- export declare function calculateCommentAllocation(postCount: number): CommentAllocation;
46
- export declare class RedditClient {
47
- private clientId;
48
- private clientSecret;
49
- private userAgent;
50
- constructor(clientId: string, clientSecret: string);
51
- /**
52
- * Authenticate with Reddit API with retry logic
53
- * Uses module-level token cache and promise deduplication to prevent
54
- * concurrent auth calls from firing multiple token requests
55
- * Returns null on failure instead of throwing
56
- */
57
- private auth;
58
- private performAuth;
59
- private parseUrl;
60
- /**
61
- * Get a single Reddit post with comments
62
- * Returns PostResult or throws Error (for use with Promise.allSettled)
63
- */
64
- getPost(url: string, maxComments?: number): Promise<PostResult>;
65
- getPosts(urls: string[], maxComments?: number): Promise<Map<string, PostResult | Error>>;
66
- batchGetPosts(urls: string[], maxCommentsOverride?: number, fetchComments?: boolean, onBatchComplete?: (batchNum: number, totalBatches: number, processed: number) => void): Promise<BatchPostResult>;
67
- }
68
- export {};
69
- //# sourceMappingURL=reddit.d.ts.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"reddit.d.ts","sourceRoot":"","sources":["../../src/clients/reddit.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAuBH,UAAU,IAAI;IACZ,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,GAAG,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,OAAO,EAAE,IAAI,CAAC;IACvB,QAAQ,CAAC,KAAK,CAAC,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,OAAO,CAAC;IACzB,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;CAC5B;AAED,MAAM,WAAW,OAAO;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACxB;AAED,MAAM,WAAW,UAAU;IACzB,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC;IACpB,QAAQ,CAAC,QAAQ,EAAE,OAAO,EAAE,CAAC;IAC7B,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;CACjC;AAED,UAAU,eAAe;IACvB,QAAQ,CAAC,OAAO,EAAE,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,KAAK,CAAC,CAAC;IAClD,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,iBAAiB,EAAE,iBAAiB,CAAC;CAC/C;AAED,UAAU,iBAAiB;IACzB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,aAAa,EAAE,OAAO,CAAC;CACxB;AA4CD,wBAAgB,0BAA0B,CAAC,SAAS,EAAE,MAAM,GAAG,iBAAiB,CAK/E;AAgOD,qBAAa,YAAY;IAGX,OAAO,CAAC,QAAQ;IAAU,OAAO,CAAC,YAAY;IAF1D,OAAO,CAAC,SAAS,CAA6D;gBAE1D,QAAQ,EAAE,MAAM,EAAU,YAAY,EAAE,MAAM;IAElE;;;;;OAKG;YACW,IAAI;YAmBJ,WAAW;IAmEzB,OAAO,CAAC,QAAQ;IAKhB;;;OAGG;IACG,OAAO,CAAC,GAAG,EAAE,MAAM,EAAE,WAAW,SAAM,GAAG,OAAO,CAAC,UAAU,CAAC;IAkD5D,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,EAAE,WAAW,SAAM,GAAG,OAAO,CAAC,GAAG,CAAC,MAAM,EAAE,UAAU,GAAG,KAAK,CAAC,CAAC;IAYrF,aAAa,CACjB,IAAI,EAAE,MAAM,EAAE,EACd,mBAAmB,CAAC,EAAE,MAAM,EAC5B,aAAa,UAAO,EACpB,eAAe,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,EAAE,YAAY,EAAE,MAAM,EAAE,SAAS,EAAE,MAAM,KAAK,IAAI,GACpF,OAAO,CAAC,eAAe,CAAC;CA2C5B"}