mcp-researchpowerpack-http 3.10.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 (127) hide show
  1. package/README.md +124 -0
  2. package/dist/index.d.ts +3 -0
  3. package/dist/index.d.ts.map +1 -0
  4. package/dist/index.js +227 -0
  5. package/dist/index.js.map +7 -0
  6. package/dist/mcp-use.json +7 -0
  7. package/dist/src/clients/github.d.ts +83 -0
  8. package/dist/src/clients/github.d.ts.map +1 -0
  9. package/dist/src/clients/github.js +370 -0
  10. package/dist/src/clients/github.js.map +7 -0
  11. package/dist/src/clients/reddit.d.ts +60 -0
  12. package/dist/src/clients/reddit.d.ts.map +1 -0
  13. package/dist/src/clients/reddit.js +287 -0
  14. package/dist/src/clients/reddit.js.map +7 -0
  15. package/dist/src/clients/research.d.ts +67 -0
  16. package/dist/src/clients/research.d.ts.map +1 -0
  17. package/dist/src/clients/research.js +282 -0
  18. package/dist/src/clients/research.js.map +7 -0
  19. package/dist/src/clients/scraper.d.ts +72 -0
  20. package/dist/src/clients/scraper.d.ts.map +1 -0
  21. package/dist/src/clients/scraper.js +327 -0
  22. package/dist/src/clients/scraper.js.map +7 -0
  23. package/dist/src/clients/search.d.ts +57 -0
  24. package/dist/src/clients/search.d.ts.map +1 -0
  25. package/dist/src/clients/search.js +218 -0
  26. package/dist/src/clients/search.js.map +7 -0
  27. package/dist/src/config/index.d.ts +93 -0
  28. package/dist/src/config/index.d.ts.map +1 -0
  29. package/dist/src/config/index.js +218 -0
  30. package/dist/src/config/index.js.map +7 -0
  31. package/dist/src/schemas/deep-research.d.ts +40 -0
  32. package/dist/src/schemas/deep-research.d.ts.map +1 -0
  33. package/dist/src/schemas/deep-research.js +216 -0
  34. package/dist/src/schemas/deep-research.js.map +7 -0
  35. package/dist/src/schemas/github-score.d.ts +50 -0
  36. package/dist/src/schemas/github-score.d.ts.map +1 -0
  37. package/dist/src/schemas/github-score.js +58 -0
  38. package/dist/src/schemas/github-score.js.map +7 -0
  39. package/dist/src/schemas/scrape-links.d.ts +23 -0
  40. package/dist/src/schemas/scrape-links.d.ts.map +1 -0
  41. package/dist/src/schemas/scrape-links.js +32 -0
  42. package/dist/src/schemas/scrape-links.js.map +7 -0
  43. package/dist/src/schemas/web-search.d.ts +18 -0
  44. package/dist/src/schemas/web-search.d.ts.map +1 -0
  45. package/dist/src/schemas/web-search.js +28 -0
  46. package/dist/src/schemas/web-search.js.map +7 -0
  47. package/dist/src/scoring/github-quality.d.ts +142 -0
  48. package/dist/src/scoring/github-quality.d.ts.map +1 -0
  49. package/dist/src/scoring/github-quality.js +202 -0
  50. package/dist/src/scoring/github-quality.js.map +7 -0
  51. package/dist/src/services/file-attachment.d.ts +30 -0
  52. package/dist/src/services/file-attachment.d.ts.map +1 -0
  53. package/dist/src/services/file-attachment.js +205 -0
  54. package/dist/src/services/file-attachment.js.map +7 -0
  55. package/dist/src/services/llm-processor.d.ts +29 -0
  56. package/dist/src/services/llm-processor.d.ts.map +1 -0
  57. package/dist/src/services/llm-processor.js +206 -0
  58. package/dist/src/services/llm-processor.js.map +7 -0
  59. package/dist/src/services/markdown-cleaner.d.ts +8 -0
  60. package/dist/src/services/markdown-cleaner.d.ts.map +1 -0
  61. package/dist/src/services/markdown-cleaner.js +63 -0
  62. package/dist/src/services/markdown-cleaner.js.map +7 -0
  63. package/dist/src/tools/github-score.d.ts +12 -0
  64. package/dist/src/tools/github-score.d.ts.map +1 -0
  65. package/dist/src/tools/github-score.js +306 -0
  66. package/dist/src/tools/github-score.js.map +7 -0
  67. package/dist/src/tools/mcp-helpers.d.ts +27 -0
  68. package/dist/src/tools/mcp-helpers.d.ts.map +1 -0
  69. package/dist/src/tools/mcp-helpers.js +47 -0
  70. package/dist/src/tools/mcp-helpers.js.map +7 -0
  71. package/dist/src/tools/reddit.d.ts +54 -0
  72. package/dist/src/tools/reddit.d.ts.map +1 -0
  73. package/dist/src/tools/reddit.js +498 -0
  74. package/dist/src/tools/reddit.js.map +7 -0
  75. package/dist/src/tools/registry.d.ts +3 -0
  76. package/dist/src/tools/registry.d.ts.map +1 -0
  77. package/dist/src/tools/registry.js +17 -0
  78. package/dist/src/tools/registry.js.map +7 -0
  79. package/dist/src/tools/research.d.ts +14 -0
  80. package/dist/src/tools/research.d.ts.map +1 -0
  81. package/dist/src/tools/research.js +250 -0
  82. package/dist/src/tools/research.js.map +7 -0
  83. package/dist/src/tools/scrape.d.ts +14 -0
  84. package/dist/src/tools/scrape.d.ts.map +1 -0
  85. package/dist/src/tools/scrape.js +290 -0
  86. package/dist/src/tools/scrape.js.map +7 -0
  87. package/dist/src/tools/search.d.ts +10 -0
  88. package/dist/src/tools/search.d.ts.map +1 -0
  89. package/dist/src/tools/search.js +197 -0
  90. package/dist/src/tools/search.js.map +7 -0
  91. package/dist/src/tools/utils.d.ts +105 -0
  92. package/dist/src/tools/utils.d.ts.map +1 -0
  93. package/dist/src/tools/utils.js +96 -0
  94. package/dist/src/tools/utils.js.map +7 -0
  95. package/dist/src/utils/concurrency.d.ts +28 -0
  96. package/dist/src/utils/concurrency.d.ts.map +1 -0
  97. package/dist/src/utils/concurrency.js +62 -0
  98. package/dist/src/utils/concurrency.js.map +7 -0
  99. package/dist/src/utils/errors.d.ts +95 -0
  100. package/dist/src/utils/errors.d.ts.map +1 -0
  101. package/dist/src/utils/errors.js +289 -0
  102. package/dist/src/utils/errors.js.map +7 -0
  103. package/dist/src/utils/logger.d.ts +33 -0
  104. package/dist/src/utils/logger.d.ts.map +1 -0
  105. package/dist/src/utils/logger.js +41 -0
  106. package/dist/src/utils/logger.js.map +7 -0
  107. package/dist/src/utils/markdown-formatter.d.ts +5 -0
  108. package/dist/src/utils/markdown-formatter.d.ts.map +1 -0
  109. package/dist/src/utils/markdown-formatter.js +15 -0
  110. package/dist/src/utils/markdown-formatter.js.map +7 -0
  111. package/dist/src/utils/response.d.ts +83 -0
  112. package/dist/src/utils/response.d.ts.map +1 -0
  113. package/dist/src/utils/response.js +109 -0
  114. package/dist/src/utils/response.js.map +7 -0
  115. package/dist/src/utils/retry.d.ts +43 -0
  116. package/dist/src/utils/retry.d.ts.map +1 -0
  117. package/dist/src/utils/retry.js +37 -0
  118. package/dist/src/utils/retry.js.map +7 -0
  119. package/dist/src/utils/url-aggregator.d.ts +92 -0
  120. package/dist/src/utils/url-aggregator.d.ts.map +1 -0
  121. package/dist/src/utils/url-aggregator.js +357 -0
  122. package/dist/src/utils/url-aggregator.js.map +7 -0
  123. package/dist/src/version.d.ts +28 -0
  124. package/dist/src/version.d.ts.map +1 -0
  125. package/dist/src/version.js +32 -0
  126. package/dist/src/version.js.map +7 -0
  127. package/package.json +73 -0
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.d.ts","sourceRoot":"","sources":["../../../src/tools/search.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,gBAAgB,CAAC;AAGhD,OAAO,EAGL,KAAK,eAAe,EACpB,KAAK,eAAe,EACrB,MAAM,0BAA0B,CAAC;AAiBlC,OAAO,EAML,KAAK,mBAAmB,EACxB,KAAK,YAAY,EAClB,MAAM,kBAAkB,CAAC;AAgK1B,wBAAsB,eAAe,CACnC,MAAM,EAAE,eAAe,EACvB,QAAQ,GAAE,YAA4B,GACrC,OAAO,CAAC,mBAAmB,CAAC,eAAe,CAAC,CAAC,CAmC/C;AAED,wBAAgB,qBAAqB,CAAC,MAAM,EAAE,SAAS,GAAG,IAAI,CA4B7D"}
@@ -0,0 +1,197 @@
1
+ import { getCapabilities, getMissingEnvMessage } from "../config/index.js";
2
+ import {
3
+ webSearchOutputSchema,
4
+ webSearchParamsSchema
5
+ } from "../schemas/web-search.js";
6
+ import { SearchClient } from "../clients/search.js";
7
+ import {
8
+ aggregateAndRank,
9
+ buildUrlLookup,
10
+ lookupUrl,
11
+ generateEnhancedOutput,
12
+ markConsensus
13
+ } from "../utils/url-aggregator.js";
14
+ import { CTR_WEIGHTS } from "../config/index.js";
15
+ import { classifyError } from "../utils/errors.js";
16
+ import {
17
+ mcpLog,
18
+ formatError,
19
+ formatDuration
20
+ } from "./utils.js";
21
+ import {
22
+ createToolReporter,
23
+ NOOP_REPORTER,
24
+ toolFailure,
25
+ toolSuccess,
26
+ toToolResponse
27
+ } from "./mcp-helpers.js";
28
+ function getPositionScore(position) {
29
+ if (position >= 1 && position <= 10) {
30
+ return CTR_WEIGHTS[position] ?? 0;
31
+ }
32
+ return Math.max(0, 10 - (position - 10) * 0.5);
33
+ }
34
+ async function executeSearches(keywords) {
35
+ const client = new SearchClient();
36
+ return client.searchMultiple(keywords);
37
+ }
38
+ function processAndRankResults(response) {
39
+ const aggregation = aggregateAndRank(response.searches, 5);
40
+ const urlLookup = buildUrlLookup(aggregation.rankedUrls);
41
+ const consensusUrls = aggregation.rankedUrls.filter((u) => u.isConsensus);
42
+ return { aggregation, urlLookup, consensusUrls };
43
+ }
44
+ function buildConsensusSection(keywords, aggregation) {
45
+ return generateEnhancedOutput(
46
+ aggregation.rankedUrls,
47
+ keywords,
48
+ aggregation.totalUniqueUrls,
49
+ aggregation.frequencyThreshold,
50
+ aggregation.thresholdNote
51
+ ) + "\n---\n\n";
52
+ }
53
+ function formatSearchResultEntry(result, position, urlLookup) {
54
+ const positionScore = getPositionScore(position);
55
+ const rankedUrl = lookupUrl(result.link, urlLookup);
56
+ const frequency = rankedUrl?.frequency ?? 1;
57
+ const consensusMark = markConsensus(frequency);
58
+ const consensusInfo = rankedUrl ? `${consensusMark} (${frequency} searches)` : `${consensusMark} (1 search)`;
59
+ let entry = `${position}. **[${result.title}](${result.link})** \u2014 Position ${position} | Score: ${positionScore.toFixed(1)} | Consensus: ${consensusInfo}
60
+ `;
61
+ if (result.snippet) {
62
+ entry += result.date ? ` - *${result.date}* \u2014 ${result.snippet}
63
+ ` : ` - ${result.snippet}
64
+ `;
65
+ }
66
+ entry += "\n";
67
+ return entry;
68
+ }
69
+ function buildPerQuerySection(response, urlLookup) {
70
+ let markdown = `## \u{1F4CA} Full Search Results by Query
71
+
72
+ `;
73
+ let totalResults = 0;
74
+ response.searches.forEach((search, index) => {
75
+ markdown += `### Query ${index + 1}: "${search.keyword}"
76
+
77
+ `;
78
+ search.results.forEach((result, resultIndex) => {
79
+ markdown += formatSearchResultEntry(result, resultIndex + 1, urlLookup);
80
+ totalResults++;
81
+ });
82
+ if (search.related && search.related.length > 0) {
83
+ const relatedSuggestions = search.related.map((r) => `\`${r}\``).join(", ");
84
+ markdown += `*Related:* ${relatedSuggestions}
85
+
86
+ `;
87
+ }
88
+ if (index < response.searches.length - 1) markdown += `---
89
+
90
+ `;
91
+ });
92
+ return { markdown, totalResults };
93
+ }
94
+ function formatSearchOutput(consensusSection, perQuerySection, totalResults, aggregation, consensusUrlCount, executionTime, totalKeywords) {
95
+ let markdown = consensusSection + perQuerySection;
96
+ markdown += `
97
+ ---
98
+ *${formatDuration(executionTime)} | ${aggregation.totalUniqueUrls} unique URLs | ${consensusUrlCount} consensus*`;
99
+ const metadata = {
100
+ total_keywords: totalKeywords,
101
+ total_results: totalResults,
102
+ execution_time_ms: executionTime,
103
+ total_unique_urls: aggregation.totalUniqueUrls,
104
+ consensus_url_count: consensusUrlCount,
105
+ frequency_threshold: aggregation.frequencyThreshold
106
+ };
107
+ return toolSuccess(markdown, { content: markdown, metadata });
108
+ }
109
+ function buildWebSearchError(error, params, startTime) {
110
+ const structuredError = classifyError(error);
111
+ const executionTime = Date.now() - startTime;
112
+ mcpLog("error", `web-search: ${structuredError.message}`, "search");
113
+ const errorContent = formatError({
114
+ code: structuredError.code,
115
+ message: structuredError.message,
116
+ retryable: structuredError.retryable,
117
+ toolName: "web-search",
118
+ howToFix: ["Verify SERPER_API_KEY is set correctly"],
119
+ alternatives: [
120
+ 'search-reddit(queries=["topic recommendations", "topic best practices", "topic vs alternatives"]) \u2014 Reddit search uses the same API but may work; also provides community perspective',
121
+ 'deep-research(questions=[{question: "What are the key findings, best practices, and recommendations for [topic]?"}]) \u2014 uses OpenRouter API (different key), not affected by this error',
122
+ "scrape-links(urls=[...any URLs you already have...], use_llm=true) \u2014 if you have URLs from prior steps, scrape them now instead of searching"
123
+ ]
124
+ });
125
+ return toolFailure(
126
+ `${errorContent}
127
+
128
+ Execution time: ${formatDuration(executionTime)}
129
+ Keywords: ${params.keywords.length}`
130
+ );
131
+ }
132
+ async function handleWebSearch(params, reporter = NOOP_REPORTER) {
133
+ const startTime = Date.now();
134
+ try {
135
+ mcpLog("info", `Searching for ${params.keywords.length} keyword(s)`, "search");
136
+ await reporter.log("info", `Searching for ${params.keywords.length} keyword(s)`);
137
+ await reporter.progress(15, 100, "Submitting search queries");
138
+ const response = await executeSearches(params.keywords);
139
+ await reporter.progress(50, 100, "Collected search results");
140
+ const { aggregation, urlLookup, consensusUrls } = processAndRankResults(response);
141
+ await reporter.log(
142
+ "info",
143
+ `Collected ${aggregation.totalUniqueUrls} unique URLs across ${response.totalKeywords} queries`
144
+ );
145
+ const consensusSection = buildConsensusSection(params.keywords, aggregation);
146
+ const { markdown: perQuerySection, totalResults } = buildPerQuerySection(response, urlLookup);
147
+ await reporter.progress(80, 100, "Ranking and formatting search results");
148
+ const executionTime = Date.now() - startTime;
149
+ mcpLog("info", `Search completed: ${totalResults} results, ${aggregation.totalUniqueUrls} unique URLs, ${consensusUrls.length} consensus`, "search");
150
+ await reporter.log(
151
+ "info",
152
+ `Search completed with ${totalResults} ranked results and ${consensusUrls.length} consensus URL(s)`
153
+ );
154
+ return formatSearchOutput(
155
+ consensusSection,
156
+ perQuerySection,
157
+ totalResults,
158
+ aggregation,
159
+ consensusUrls.length,
160
+ executionTime,
161
+ response.totalKeywords
162
+ );
163
+ } catch (error) {
164
+ return buildWebSearchError(error, params, startTime);
165
+ }
166
+ }
167
+ function registerWebSearchTool(server) {
168
+ server.tool(
169
+ {
170
+ name: "web-search",
171
+ title: "Web Search",
172
+ description: "Run parallel Google searches across 1\u2013100 keywords and return CTR-weighted, consensus-ranked URLs for follow-up scraping. This is a bulk discovery tool \u2014 supply 3\u20137 keywords for solid consensus detection, or up to 100 for exhaustive coverage. Each keyword runs as a separate Google search; results are aggregated, scored by search position, and URLs appearing across multiple queries are flagged as high-confidence. Output is a ranked URL list ready to pipe into scrape-links or get-reddit-post.",
173
+ schema: webSearchParamsSchema,
174
+ outputSchema: webSearchOutputSchema,
175
+ annotations: {
176
+ readOnlyHint: true,
177
+ idempotentHint: true,
178
+ destructiveHint: false,
179
+ openWorldHint: true
180
+ }
181
+ },
182
+ async (args, ctx) => {
183
+ if (!getCapabilities().search) {
184
+ return toToolResponse(toolFailure(getMissingEnvMessage("search")));
185
+ }
186
+ const reporter = createToolReporter(ctx, "web-search");
187
+ const result = await handleWebSearch(args, reporter);
188
+ await reporter.progress(100, 100, result.isError ? "Search failed" : "Search complete");
189
+ return toToolResponse(result);
190
+ }
191
+ );
192
+ }
193
+ export {
194
+ handleWebSearch,
195
+ registerWebSearchTool
196
+ };
197
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/tools/search.ts"],
4
+ "sourcesContent": ["/**\n * Web Search Tool Handler\n * NEVER throws - always returns structured response for graceful degradation\n */\n\nimport type { MCPServer } from 'mcp-use/server';\n\nimport { getCapabilities, getMissingEnvMessage } from '../config/index.js';\nimport {\n webSearchOutputSchema,\n webSearchParamsSchema,\n type WebSearchParams,\n type WebSearchOutput,\n} from '../schemas/web-search.js';\nimport { SearchClient } from '../clients/search.js';\nimport {\n aggregateAndRank,\n buildUrlLookup,\n lookupUrl,\n generateEnhancedOutput,\n markConsensus,\n} from '../utils/url-aggregator.js';\nimport { CTR_WEIGHTS } from '../config/index.js';\nimport { classifyError } from '../utils/errors.js';\nimport {\n mcpLog,\n formatSuccess,\n formatError,\n formatDuration,\n} from './utils.js';\nimport {\n createToolReporter,\n NOOP_REPORTER,\n toolFailure,\n toolSuccess,\n toToolResponse,\n type ToolExecutionResult,\n type ToolReporter,\n} from './mcp-helpers.js';\n\nfunction getPositionScore(position: number): number {\n if (position >= 1 && position <= 10) {\n return CTR_WEIGHTS[position] ?? 0;\n }\n return Math.max(0, 10 - (position - 10) * 0.5);\n}\n\n// --- Internal types ---\n\ninterface SearchAggregation {\n readonly rankedUrls: ReturnType<typeof aggregateAndRank>['rankedUrls'];\n readonly totalUniqueUrls: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\ninterface SearchResponse {\n searches: Parameters<typeof aggregateAndRank>[0];\n totalKeywords: number;\n}\n\n// --- Helpers ---\n\nasync function executeSearches(keywords: string[]): Promise<SearchResponse> {\n const client = new SearchClient();\n return client.searchMultiple(keywords);\n}\n\nfunction processAndRankResults(response: SearchResponse): {\n aggregation: SearchAggregation;\n urlLookup: ReturnType<typeof buildUrlLookup>;\n consensusUrls: SearchAggregation['rankedUrls'];\n} {\n const aggregation = aggregateAndRank(response.searches, 5);\n // Build lookup from ALL ranked URLs so per-query entries can show consensus info\n const urlLookup = buildUrlLookup(aggregation.rankedUrls);\n const consensusUrls = aggregation.rankedUrls.filter(u => u.isConsensus);\n return { aggregation, urlLookup, consensusUrls };\n}\n\nfunction buildConsensusSection(\n keywords: string[],\n aggregation: SearchAggregation,\n): string {\n // Always show all ranked URLs (consensus-marked within)\n return generateEnhancedOutput(\n aggregation.rankedUrls, keywords, aggregation.totalUniqueUrls,\n aggregation.frequencyThreshold, aggregation.thresholdNote,\n ) + '\\n---\\n\\n';\n}\n\nfunction formatSearchResultEntry(\n result: { title: string; link: string; snippet?: string; date?: string },\n position: number,\n urlLookup: ReturnType<typeof buildUrlLookup>,\n): string {\n const positionScore = getPositionScore(position);\n const rankedUrl = lookupUrl(result.link, urlLookup);\n const frequency = rankedUrl?.frequency ?? 1;\n const consensusMark = markConsensus(frequency);\n const consensusInfo = rankedUrl\n ? `${consensusMark} (${frequency} searches)`\n : `${consensusMark} (1 search)`;\n\n let entry = `${position}. **[${result.title}](${result.link})** \u2014 Position ${position} | Score: ${positionScore.toFixed(1)} | Consensus: ${consensusInfo}\\n`;\n\n if (result.snippet) {\n entry += result.date\n ? ` - *${result.date}* \u2014 ${result.snippet}\\n`\n : ` - ${result.snippet}\\n`;\n }\n\n entry += '\\n';\n return entry;\n}\n\nfunction buildPerQuerySection(\n response: SearchResponse,\n urlLookup: ReturnType<typeof buildUrlLookup>,\n): { markdown: string; totalResults: number } {\n let markdown = `## \uD83D\uDCCA Full Search Results by Query\\n\\n`;\n\n let totalResults = 0;\n\n response.searches.forEach((search, index) => {\n markdown += `### Query ${index + 1}: \"${search.keyword}\"\\n\\n`;\n\n search.results.forEach((result, resultIndex) => {\n markdown += formatSearchResultEntry(result, resultIndex + 1, urlLookup);\n totalResults++;\n });\n\n if (search.related && search.related.length > 0) {\n const relatedSuggestions = search.related\n .map((r: string) => `\\`${r}\\``)\n .join(', ');\n markdown += `*Related:* ${relatedSuggestions}\\n\\n`;\n }\n\n if (index < response.searches.length - 1) markdown += `---\\n\\n`;\n });\n\n return { markdown, totalResults };\n}\n\nfunction formatSearchOutput(\n consensusSection: string,\n perQuerySection: string,\n totalResults: number,\n aggregation: SearchAggregation,\n consensusUrlCount: number,\n executionTime: number,\n totalKeywords: number,\n): ToolExecutionResult<WebSearchOutput> {\n let markdown = consensusSection + perQuerySection;\n\n markdown += `\\n---\\n*${formatDuration(executionTime)} | ${aggregation.totalUniqueUrls} unique URLs | ${consensusUrlCount} consensus*`;\n\n const metadata = {\n total_keywords: totalKeywords,\n total_results: totalResults,\n execution_time_ms: executionTime,\n total_unique_urls: aggregation.totalUniqueUrls,\n consensus_url_count: consensusUrlCount,\n frequency_threshold: aggregation.frequencyThreshold,\n };\n\n return toolSuccess(markdown, { content: markdown, metadata });\n}\n\nfunction buildWebSearchError(\n error: unknown,\n params: WebSearchParams,\n startTime: number,\n): ToolExecutionResult<WebSearchOutput> {\n const structuredError = classifyError(error);\n const executionTime = Date.now() - startTime;\n\n mcpLog('error', `web-search: ${structuredError.message}`, 'search');\n\n const errorContent = formatError({\n code: structuredError.code,\n message: structuredError.message,\n retryable: structuredError.retryable,\n toolName: 'web-search',\n howToFix: ['Verify SERPER_API_KEY is set correctly'],\n alternatives: [\n 'search-reddit(queries=[\"topic recommendations\", \"topic best practices\", \"topic vs alternatives\"]) \u2014 Reddit search uses the same API but may work; also provides community perspective',\n 'deep-research(questions=[{question: \"What are the key findings, best practices, and recommendations for [topic]?\"}]) \u2014 uses OpenRouter API (different key), not affected by this error',\n 'scrape-links(urls=[...any URLs you already have...], use_llm=true) \u2014 if you have URLs from prior steps, scrape them now instead of searching',\n ],\n });\n\n return toolFailure(\n `${errorContent}\\n\\nExecution time: ${formatDuration(executionTime)}\\nKeywords: ${params.keywords.length}`,\n );\n}\n\nexport async function handleWebSearch(\n params: WebSearchParams,\n reporter: ToolReporter = NOOP_REPORTER,\n): Promise<ToolExecutionResult<WebSearchOutput>> {\n const startTime = Date.now();\n\n try {\n mcpLog('info', `Searching for ${params.keywords.length} keyword(s)`, 'search');\n await reporter.log('info', `Searching for ${params.keywords.length} keyword(s)`);\n await reporter.progress(15, 100, 'Submitting search queries');\n\n const response = await executeSearches(params.keywords);\n await reporter.progress(50, 100, 'Collected search results');\n\n const { aggregation, urlLookup, consensusUrls } = processAndRankResults(response);\n await reporter.log(\n 'info',\n `Collected ${aggregation.totalUniqueUrls} unique URLs across ${response.totalKeywords} queries`,\n );\n\n const consensusSection = buildConsensusSection(params.keywords, aggregation);\n const { markdown: perQuerySection, totalResults } = buildPerQuerySection(response, urlLookup);\n await reporter.progress(80, 100, 'Ranking and formatting search results');\n\n const executionTime = Date.now() - startTime;\n mcpLog('info', `Search completed: ${totalResults} results, ${aggregation.totalUniqueUrls} unique URLs, ${consensusUrls.length} consensus`, 'search');\n await reporter.log(\n 'info',\n `Search completed with ${totalResults} ranked results and ${consensusUrls.length} consensus URL(s)`,\n );\n\n return formatSearchOutput(\n consensusSection, perQuerySection, totalResults,\n aggregation, consensusUrls.length, executionTime, response.totalKeywords,\n );\n } catch (error) {\n return buildWebSearchError(error, params, startTime);\n }\n}\n\nexport function registerWebSearchTool(server: MCPServer): void {\n server.tool(\n {\n name: 'web-search',\n title: 'Web Search',\n description:\n 'Run parallel Google searches across 1\u2013100 keywords and return CTR-weighted, consensus-ranked URLs for follow-up scraping. This is a bulk discovery tool \u2014 supply 3\u20137 keywords for solid consensus detection, or up to 100 for exhaustive coverage. Each keyword runs as a separate Google search; results are aggregated, scored by search position, and URLs appearing across multiple queries are flagged as high-confidence. Output is a ranked URL list ready to pipe into scrape-links or get-reddit-post.',\n schema: webSearchParamsSchema,\n outputSchema: webSearchOutputSchema,\n annotations: {\n readOnlyHint: true,\n idempotentHint: true,\n destructiveHint: false,\n openWorldHint: true,\n },\n },\n async (args, ctx) => {\n if (!getCapabilities().search) {\n return toToolResponse(toolFailure(getMissingEnvMessage('search')));\n }\n\n const reporter = createToolReporter(ctx, 'web-search');\n const result = await handleWebSearch(args, reporter);\n\n await reporter.progress(100, 100, result.isError ? 'Search failed' : 'Search complete');\n return toToolResponse(result);\n },\n );\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * Shared Tool Utilities
3
+ * Extracted from individual handlers to eliminate duplication
4
+ */
5
+ export { mcpLog, safeLog as safeLogSimple, createToolLogger, type LogLevel, type ToolLogger as SimpleToolLogger, } from '../utils/logger.js';
6
+ export { formatSuccess, formatError, formatBatchHeader, formatList, formatDuration, truncateText, type SuccessOptions, type ErrorOptions, type BatchHeaderOptions, type ListItem, } from '../utils/response.js';
7
+ /**
8
+ * Centralized token budgets for all tools
9
+ */
10
+ export declare const TOKEN_BUDGETS: {
11
+ /** Deep research total budget */
12
+ readonly RESEARCH: 32000;
13
+ /** Web scraper total budget */
14
+ readonly SCRAPER: 32000;
15
+ /** Reddit comment budget per batch */
16
+ readonly REDDIT_COMMENTS: 1000;
17
+ };
18
+ /**
19
+ * Logger function type used by tools
20
+ */
21
+ export type ToolLogger = (level: 'info' | 'error' | 'debug', message: string, sessionId: string) => Promise<void>;
22
+ /**
23
+ * Standard tool options passed to handlers
24
+ */
25
+ export interface ToolOptions {
26
+ readonly sessionId?: string;
27
+ readonly logger?: ToolLogger;
28
+ }
29
+ /**
30
+ * Safe logger wrapper - NEVER throws
31
+ * Logs to provided logger or falls back to console.error
32
+ *
33
+ * @param logger - Optional logger function
34
+ * @param sessionId - Session ID for logging context
35
+ * @param level - Log level
36
+ * @param message - Message to log
37
+ * @param toolName - Name of the tool for prefixing
38
+ */
39
+ export declare function safeLog(logger: ToolLogger | undefined, sessionId: string | undefined, level: 'info' | 'error' | 'debug', message: string, toolName: string): Promise<void>;
40
+ /**
41
+ * Calculate token allocation for batch operations
42
+ * Distributes a fixed budget across multiple items
43
+ *
44
+ * @param count - Number of items to distribute budget across
45
+ * @param budget - Total token budget
46
+ * @returns Tokens per item
47
+ */
48
+ export declare function calculateTokenAllocation(count: number, budget: number): number;
49
+ /**
50
+ * Format retry hint based on error retryability
51
+ *
52
+ * @param retryable - Whether the error is retryable
53
+ * @returns Hint string or empty string
54
+ */
55
+ export declare function formatRetryHint(retryable: boolean): string;
56
+ /**
57
+ * Create a standard error markdown response
58
+ *
59
+ * @param toolName - Name of the tool that errored
60
+ * @param errorCode - Error code
61
+ * @param message - Error message
62
+ * @param retryable - Whether error is retryable
63
+ * @param tip - Optional tip for resolution
64
+ * @returns Formatted markdown error string
65
+ */
66
+ export declare function formatToolError(toolName: string, errorCode: string, message: string, retryable: boolean, tip?: string): string;
67
+ /**
68
+ * Validate that a value is a non-empty array
69
+ *
70
+ * @param value - Value to check
71
+ * @param fieldName - Field name for error message
72
+ * @returns Error message or undefined if valid
73
+ */
74
+ export declare function validateNonEmptyArray(value: unknown, fieldName: string): string | undefined;
75
+ /**
76
+ * Validate array length is within bounds
77
+ *
78
+ * @param arr - Array to check
79
+ * @param min - Minimum length
80
+ * @param max - Maximum length
81
+ * @param fieldName - Field name for error message
82
+ * @returns Error message or undefined if valid
83
+ */
84
+ export declare function validateArrayBounds(arr: unknown[], min: number, max: number, fieldName: string): string | undefined;
85
+ /**
86
+ * Build standard header for batch operation results
87
+ *
88
+ * @param title - Title of the results section
89
+ * @param count - Number of items processed
90
+ * @param tokensPerItem - Tokens allocated per item
91
+ * @param totalBudget - Total token budget
92
+ * @returns Formatted header string
93
+ */
94
+ export declare function buildBatchHeader(title: string, count: number, tokensPerItem: number, totalBudget: number): string;
95
+ /**
96
+ * Build status line for batch results
97
+ *
98
+ * @param successful - Number of successful items
99
+ * @param failed - Number of failed items
100
+ * @param batches - Number of batches processed
101
+ * @param extras - Optional extra status items
102
+ * @returns Formatted status line
103
+ */
104
+ export declare function buildStatusLine(successful: number, failed: number, batches: number, extras?: string[]): string;
105
+ //# sourceMappingURL=utils.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.d.ts","sourceRoot":"","sources":["../../../src/tools/utils.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAGH,OAAO,EACL,MAAM,EACN,OAAO,IAAI,aAAa,EACxB,gBAAgB,EAChB,KAAK,QAAQ,EACb,KAAK,UAAU,IAAI,gBAAgB,GACpC,MAAM,oBAAoB,CAAC;AAE5B,OAAO,EACL,aAAa,EACb,WAAW,EACX,iBAAiB,EACjB,UAAU,EACV,cAAc,EACd,YAAY,EACZ,KAAK,cAAc,EACnB,KAAK,YAAY,EACjB,KAAK,kBAAkB,EACvB,KAAK,QAAQ,GACd,MAAM,sBAAsB,CAAC;AAM9B;;GAEG;AACH,eAAO,MAAM,aAAa;IACxB,iCAAiC;;IAEjC,+BAA+B;;IAE/B,sCAAsC;;CAE9B,CAAC;AAMX;;GAEG;AACH,MAAM,MAAM,UAAU,GAAG,CACvB,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,MAAM,KACd,OAAO,CAAC,IAAI,CAAC,CAAC;AAEnB;;GAEG;AACH,MAAM,WAAW,WAAW;IAC1B,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAC5B,QAAQ,CAAC,MAAM,CAAC,EAAE,UAAU,CAAC;CAC9B;AAMD;;;;;;;;;GASG;AACH,wBAAsB,OAAO,CAC3B,MAAM,EAAE,UAAU,GAAG,SAAS,EAC9B,SAAS,EAAE,MAAM,GAAG,SAAS,EAC7B,KAAK,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,EACjC,OAAO,EAAE,MAAM,EACf,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,IAAI,CAAC,CAQf;AAMD;;;;;;;GAOG;AACH,wBAAgB,wBAAwB,CAAC,KAAK,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,MAAM,CAG9E;AAMD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,SAAS,EAAE,OAAO,GAAG,MAAM,CAI1D;AAED;;;;;;;;;GASG;AACH,wBAAgB,eAAe,CAC7B,QAAQ,EAAE,MAAM,EAChB,SAAS,EAAE,MAAM,EACjB,OAAO,EAAE,MAAM,EACf,SAAS,EAAE,OAAO,EAClB,GAAG,CAAC,EAAE,MAAM,GACX,MAAM,CAIR;AAMD;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,KAAK,EAAE,OAAO,EACd,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAQpB;AAED;;;;;;;;GAQG;AACH,wBAAgB,mBAAmB,CACjC,GAAG,EAAE,OAAO,EAAE,EACd,GAAG,EAAE,MAAM,EACX,GAAG,EAAE,MAAM,EACX,SAAS,EAAE,MAAM,GAChB,MAAM,GAAG,SAAS,CAQpB;AAMD;;;;;;;;GAQG;AACH,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,aAAa,EAAE,MAAM,EACrB,WAAW,EAAE,MAAM,GAClB,MAAM,CAER;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAC7B,UAAU,EAAE,MAAM,EAClB,MAAM,EAAE,MAAM,EACd,OAAO,EAAE,MAAM,EACf,MAAM,CAAC,EAAE,MAAM,EAAE,GAChB,MAAM,CAMR"}
@@ -0,0 +1,96 @@
1
+ import {
2
+ mcpLog,
3
+ safeLog,
4
+ createToolLogger
5
+ } from "../utils/logger.js";
6
+ import {
7
+ formatSuccess,
8
+ formatError,
9
+ formatBatchHeader,
10
+ formatList,
11
+ formatDuration,
12
+ truncateText
13
+ } from "../utils/response.js";
14
+ const TOKEN_BUDGETS = {
15
+ /** Deep research total budget */
16
+ RESEARCH: 32e3,
17
+ /** Web scraper total budget */
18
+ SCRAPER: 32e3,
19
+ /** Reddit comment budget per batch */
20
+ REDDIT_COMMENTS: 1e3
21
+ };
22
+ async function safeLog2(logger, sessionId, level, message, toolName) {
23
+ if (!logger || !sessionId) return;
24
+ try {
25
+ await logger(level, `[${toolName}] ${message}`, sessionId);
26
+ } catch {
27
+ console.error(`[${toolName}] Logger failed: ${message}`);
28
+ }
29
+ }
30
+ function calculateTokenAllocation(count, budget) {
31
+ if (count <= 0) return budget;
32
+ return Math.floor(budget / count);
33
+ }
34
+ function formatRetryHint(retryable) {
35
+ return retryable ? "\n\n\u{1F4A1} This error may be temporary. Try again in a moment." : "";
36
+ }
37
+ function formatToolError(toolName, errorCode, message, retryable, tip) {
38
+ const retryHint = formatRetryHint(retryable);
39
+ const tipSection = tip ? `
40
+
41
+ **Tip:** ${tip}` : "";
42
+ return `# \u274C ${toolName}: Operation Failed
43
+
44
+ **${errorCode}:** ${message}${retryHint}${tipSection}`;
45
+ }
46
+ function validateNonEmptyArray(value, fieldName) {
47
+ if (!Array.isArray(value)) {
48
+ return `${fieldName} must be an array`;
49
+ }
50
+ if (value.length === 0) {
51
+ return `${fieldName} must not be empty`;
52
+ }
53
+ return void 0;
54
+ }
55
+ function validateArrayBounds(arr, min, max, fieldName) {
56
+ if (arr.length < min) {
57
+ return `${fieldName} requires at least ${min} items. Received: ${arr.length}`;
58
+ }
59
+ if (arr.length > max) {
60
+ return `${fieldName} allows at most ${max} items. Received: ${arr.length}. Please remove ${arr.length - max} item(s).`;
61
+ }
62
+ return void 0;
63
+ }
64
+ function buildBatchHeader(title, count, tokensPerItem, totalBudget) {
65
+ return `# ${title} (${count} items)
66
+
67
+ **Token Allocation:** ${tokensPerItem.toLocaleString()} tokens/item (${count} items, ${totalBudget.toLocaleString()} total budget)`;
68
+ }
69
+ function buildStatusLine(successful, failed, batches, extras) {
70
+ let status = `**Status:** \u2705 ${successful} successful | \u274C ${failed} failed | \u{1F4E6} ${batches} batch(es)`;
71
+ if (extras && extras.length > 0) {
72
+ status += ` | ${extras.join(" | ")}`;
73
+ }
74
+ return status;
75
+ }
76
+ export {
77
+ TOKEN_BUDGETS,
78
+ buildBatchHeader,
79
+ buildStatusLine,
80
+ calculateTokenAllocation,
81
+ createToolLogger,
82
+ formatBatchHeader,
83
+ formatDuration,
84
+ formatError,
85
+ formatList,
86
+ formatRetryHint,
87
+ formatSuccess,
88
+ formatToolError,
89
+ mcpLog,
90
+ safeLog2 as safeLog,
91
+ safeLog as safeLogSimple,
92
+ truncateText,
93
+ validateArrayBounds,
94
+ validateNonEmptyArray
95
+ };
96
+ //# sourceMappingURL=utils.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/tools/utils.ts"],
4
+ "sourcesContent": ["/**\n * Shared Tool Utilities\n * Extracted from individual handlers to eliminate duplication\n */\n\n// Re-export from centralized modules\nexport {\n mcpLog,\n safeLog as safeLogSimple,\n createToolLogger,\n type LogLevel,\n type ToolLogger as SimpleToolLogger,\n} from '../utils/logger.js';\n\nexport {\n formatSuccess,\n formatError,\n formatBatchHeader,\n formatList,\n formatDuration,\n truncateText,\n type SuccessOptions,\n type ErrorOptions,\n type BatchHeaderOptions,\n type ListItem,\n} from '../utils/response.js';\n\n// ============================================================================\n// Token Budget Constants\n// ============================================================================\n\n/**\n * Centralized token budgets for all tools\n */\nexport const TOKEN_BUDGETS = {\n /** Deep research total budget */\n RESEARCH: 32_000,\n /** Web scraper total budget */\n SCRAPER: 32_000,\n /** Reddit comment budget per batch */\n REDDIT_COMMENTS: 1_000,\n} as const;\n\n// ============================================================================\n// Types\n// ============================================================================\n\n/**\n * Logger function type used by tools\n */\nexport type ToolLogger = (\n level: 'info' | 'error' | 'debug',\n message: string,\n sessionId: string\n) => Promise<void>;\n\n/**\n * Standard tool options passed to handlers\n */\nexport interface ToolOptions {\n readonly sessionId?: string;\n readonly logger?: ToolLogger;\n}\n\n// ============================================================================\n// Logging Utilities\n// ============================================================================\n\n/**\n * Safe logger wrapper - NEVER throws\n * Logs to provided logger or falls back to console.error\n *\n * @param logger - Optional logger function\n * @param sessionId - Session ID for logging context\n * @param level - Log level\n * @param message - Message to log\n * @param toolName - Name of the tool for prefixing\n */\nexport async function safeLog(\n logger: ToolLogger | undefined,\n sessionId: string | undefined,\n level: 'info' | 'error' | 'debug',\n message: string,\n toolName: string\n): Promise<void> {\n if (!logger || !sessionId) return;\n try {\n await logger(level, `[${toolName}] ${message}`, sessionId);\n } catch {\n // Silently ignore logger errors - they should never crash the tool\n console.error(`[${toolName}] Logger failed: ${message}`);\n }\n}\n\n// ============================================================================\n// Token Allocation\n// ============================================================================\n\n/**\n * Calculate token allocation for batch operations\n * Distributes a fixed budget across multiple items\n *\n * @param count - Number of items to distribute budget across\n * @param budget - Total token budget\n * @returns Tokens per item\n */\nexport function calculateTokenAllocation(count: number, budget: number): number {\n if (count <= 0) return budget;\n return Math.floor(budget / count);\n}\n\n// ============================================================================\n// Error Formatting\n// ============================================================================\n\n/**\n * Format retry hint based on error retryability\n *\n * @param retryable - Whether the error is retryable\n * @returns Hint string or empty string\n */\nexport function formatRetryHint(retryable: boolean): string {\n return retryable\n ? '\\n\\n\uD83D\uDCA1 This error may be temporary. Try again in a moment.'\n : '';\n}\n\n/**\n * Create a standard error markdown response\n *\n * @param toolName - Name of the tool that errored\n * @param errorCode - Error code\n * @param message - Error message\n * @param retryable - Whether error is retryable\n * @param tip - Optional tip for resolution\n * @returns Formatted markdown error string\n */\nexport function formatToolError(\n toolName: string,\n errorCode: string,\n message: string,\n retryable: boolean,\n tip?: string\n): string {\n const retryHint = formatRetryHint(retryable);\n const tipSection = tip ? `\\n\\n**Tip:** ${tip}` : '';\n return `# \u274C ${toolName}: Operation Failed\\n\\n**${errorCode}:** ${message}${retryHint}${tipSection}`;\n}\n\n// ============================================================================\n// Validation Helpers\n// ============================================================================\n\n/**\n * Validate that a value is a non-empty array\n *\n * @param value - Value to check\n * @param fieldName - Field name for error message\n * @returns Error message or undefined if valid\n */\nexport function validateNonEmptyArray(\n value: unknown,\n fieldName: string\n): string | undefined {\n if (!Array.isArray(value)) {\n return `${fieldName} must be an array`;\n }\n if (value.length === 0) {\n return `${fieldName} must not be empty`;\n }\n return undefined;\n}\n\n/**\n * Validate array length is within bounds\n *\n * @param arr - Array to check\n * @param min - Minimum length\n * @param max - Maximum length\n * @param fieldName - Field name for error message\n * @returns Error message or undefined if valid\n */\nexport function validateArrayBounds(\n arr: unknown[],\n min: number,\n max: number,\n fieldName: string\n): string | undefined {\n if (arr.length < min) {\n return `${fieldName} requires at least ${min} items. Received: ${arr.length}`;\n }\n if (arr.length > max) {\n return `${fieldName} allows at most ${max} items. Received: ${arr.length}. Please remove ${arr.length - max} item(s).`;\n }\n return undefined;\n}\n\n// ============================================================================\n// Response Builders\n// ============================================================================\n\n/**\n * Build standard header for batch operation results\n *\n * @param title - Title of the results section\n * @param count - Number of items processed\n * @param tokensPerItem - Tokens allocated per item\n * @param totalBudget - Total token budget\n * @returns Formatted header string\n */\nexport function buildBatchHeader(\n title: string,\n count: number,\n tokensPerItem: number,\n totalBudget: number\n): string {\n return `# ${title} (${count} items)\\n\\n**Token Allocation:** ${tokensPerItem.toLocaleString()} tokens/item (${count} items, ${totalBudget.toLocaleString()} total budget)`;\n}\n\n/**\n * Build status line for batch results\n *\n * @param successful - Number of successful items\n * @param failed - Number of failed items\n * @param batches - Number of batches processed\n * @param extras - Optional extra status items\n * @returns Formatted status line\n */\nexport function buildStatusLine(\n successful: number,\n failed: number,\n batches: number,\n extras?: string[]\n): string {\n let status = `**Status:** \u2705 ${successful} successful | \u274C ${failed} failed | \uD83D\uDCE6 ${batches} batch(es)`;\n if (extras && extras.length > 0) {\n status += ` | ${extras.join(' | ')}`;\n }\n return status;\n}\n"],
5
+ "mappings": "AAMA;AAAA,EACE;AAAA,EACW;AAAA,EACX;AAAA,OAGK;AAEP;AAAA,EACE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,OAKK;AASA,MAAM,gBAAgB;AAAA;AAAA,EAE3B,UAAU;AAAA;AAAA,EAEV,SAAS;AAAA;AAAA,EAET,iBAAiB;AACnB;AAqCA,eAAsBA,SACpB,QACA,WACA,OACA,SACA,UACe;AACf,MAAI,CAAC,UAAU,CAAC,UAAW;AAC3B,MAAI;AACF,UAAM,OAAO,OAAO,IAAI,QAAQ,KAAK,OAAO,IAAI,SAAS;AAAA,EAC3D,QAAQ;AAEN,YAAQ,MAAM,IAAI,QAAQ,oBAAoB,OAAO,EAAE;AAAA,EACzD;AACF;AAcO,SAAS,yBAAyB,OAAe,QAAwB;AAC9E,MAAI,SAAS,EAAG,QAAO;AACvB,SAAO,KAAK,MAAM,SAAS,KAAK;AAClC;AAYO,SAAS,gBAAgB,WAA4B;AAC1D,SAAO,YACH,sEACA;AACN;AAYO,SAAS,gBACd,UACA,WACA,SACA,WACA,KACQ;AACR,QAAM,YAAY,gBAAgB,SAAS;AAC3C,QAAM,aAAa,MAAM;AAAA;AAAA,WAAgB,GAAG,KAAK;AACjD,SAAO,YAAO,QAAQ;AAAA;AAAA,IAA2B,SAAS,OAAO,OAAO,GAAG,SAAS,GAAG,UAAU;AACnG;AAaO,SAAS,sBACd,OACA,WACoB;AACpB,MAAI,CAAC,MAAM,QAAQ,KAAK,GAAG;AACzB,WAAO,GAAG,SAAS;AAAA,EACrB;AACA,MAAI,MAAM,WAAW,GAAG;AACtB,WAAO,GAAG,SAAS;AAAA,EACrB;AACA,SAAO;AACT;AAWO,SAAS,oBACd,KACA,KACA,KACA,WACoB;AACpB,MAAI,IAAI,SAAS,KAAK;AACpB,WAAO,GAAG,SAAS,sBAAsB,GAAG,qBAAqB,IAAI,MAAM;AAAA,EAC7E;AACA,MAAI,IAAI,SAAS,KAAK;AACpB,WAAO,GAAG,SAAS,mBAAmB,GAAG,qBAAqB,IAAI,MAAM,mBAAmB,IAAI,SAAS,GAAG;AAAA,EAC7G;AACA,SAAO;AACT;AAeO,SAAS,iBACd,OACA,OACA,eACA,aACQ;AACR,SAAO,KAAK,KAAK,KAAK,KAAK;AAAA;AAAA,wBAAoC,cAAc,eAAe,CAAC,iBAAiB,KAAK,WAAW,YAAY,eAAe,CAAC;AAC5J;AAWO,SAAS,gBACd,YACA,QACA,SACA,QACQ;AACR,MAAI,SAAS,sBAAiB,UAAU,wBAAmB,MAAM,uBAAgB,OAAO;AACxF,MAAI,UAAU,OAAO,SAAS,GAAG;AAC/B,cAAU,MAAM,OAAO,KAAK,KAAK,CAAC;AAAA,EACpC;AACA,SAAO;AACT;",
6
+ "names": ["safeLog"]
7
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * Concurrency utilities for bounded parallel execution
3
+ * Prevents CPU spikes and API rate limiting from unbounded Promise.all
4
+ */
5
+ /**
6
+ * Execute async tasks with a concurrency limit (like p-map).
7
+ * Processes items from the input array through the mapper function,
8
+ * running at most `concurrency` tasks simultaneously.
9
+ *
10
+ * @param items - Array of items to process
11
+ * @param mapper - Async function to apply to each item
12
+ * @param concurrency - Maximum number of concurrent tasks (default: 6)
13
+ * @param signal - Optional AbortSignal to cancel remaining work
14
+ * @returns Array of results in the same order as input items
15
+ */
16
+ export declare function pMap<T, R>(items: readonly T[], mapper: (item: T, index: number) => Promise<R>, concurrency?: number, signal?: AbortSignal): Promise<R[]>;
17
+ /**
18
+ * Like pMap but uses Promise.allSettled semantics — never rejects,
19
+ * returns PromiseSettledResult for each item.
20
+ *
21
+ * @param items - Array of items to process
22
+ * @param mapper - Async function to apply to each item
23
+ * @param concurrency - Maximum number of concurrent tasks (default: 6)
24
+ * @param signal - Optional AbortSignal to cancel remaining work
25
+ * @returns Array of PromiseSettledResult in the same order as input items
26
+ */
27
+ export declare function pMapSettled<T, R>(items: readonly T[], mapper: (item: T, index: number) => Promise<R>, concurrency?: number, signal?: AbortSignal): Promise<PromiseSettledResult<R>[]>;
28
+ //# sourceMappingURL=concurrency.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"concurrency.d.ts","sourceRoot":"","sources":["../../../src/utils/concurrency.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH;;;;;;;;;;GAUG;AACH,wBAAsB,IAAI,CAAC,CAAC,EAAE,CAAC,EAC7B,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EAC9C,WAAW,GAAE,MAAU,EACvB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,CAAC,EAAE,CAAC,CAkCd;AAED;;;;;;;;;GASG;AACH,wBAAsB,WAAW,CAAC,CAAC,EAAE,CAAC,EACpC,KAAK,EAAE,SAAS,CAAC,EAAE,EACnB,MAAM,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC,CAAC,CAAC,EAC9C,WAAW,GAAE,MAAU,EACvB,MAAM,CAAC,EAAE,WAAW,GACnB,OAAO,CAAC,oBAAoB,CAAC,CAAC,CAAC,EAAE,CAAC,CAoCpC"}
@@ -0,0 +1,62 @@
1
+ async function pMap(items, mapper, concurrency = 6, signal) {
2
+ if (items.length === 0) return [];
3
+ const limit = Math.max(1, Math.min(concurrency, items.length));
4
+ const results = new Array(items.length);
5
+ let nextIndex = 0;
6
+ const internalAbort = new AbortController();
7
+ async function worker() {
8
+ while (true) {
9
+ if (signal?.aborted || internalAbort.signal.aborted) {
10
+ throw new DOMException("Aborted", "AbortError");
11
+ }
12
+ const index = nextIndex++;
13
+ if (index >= items.length) break;
14
+ try {
15
+ results[index] = await mapper(items[index], index);
16
+ } catch (err) {
17
+ internalAbort.abort();
18
+ throw err;
19
+ }
20
+ }
21
+ }
22
+ const workers = [];
23
+ for (let i = 0; i < limit; i++) {
24
+ workers.push(worker());
25
+ }
26
+ await Promise.all(workers);
27
+ return results;
28
+ }
29
+ async function pMapSettled(items, mapper, concurrency = 6, signal) {
30
+ if (items.length === 0) return [];
31
+ const limit = Math.max(1, Math.min(concurrency, items.length));
32
+ const results = new Array(items.length);
33
+ let nextIndex = 0;
34
+ async function worker() {
35
+ while (nextIndex < items.length) {
36
+ if (signal?.aborted) break;
37
+ const index = nextIndex++;
38
+ try {
39
+ const value = await mapper(items[index], index);
40
+ results[index] = { status: "fulfilled", value };
41
+ } catch (reason) {
42
+ results[index] = { status: "rejected", reason };
43
+ }
44
+ }
45
+ }
46
+ const workers = [];
47
+ for (let i = 0; i < limit; i++) {
48
+ workers.push(worker());
49
+ }
50
+ await Promise.all(workers);
51
+ for (let i = 0; i < items.length; i++) {
52
+ if (results[i] === void 0) {
53
+ results[i] = { status: "rejected", reason: new DOMException("Aborted", "AbortError") };
54
+ }
55
+ }
56
+ return results;
57
+ }
58
+ export {
59
+ pMap,
60
+ pMapSettled
61
+ };
62
+ //# sourceMappingURL=concurrency.js.map
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../../../src/utils/concurrency.ts"],
4
+ "sourcesContent": ["/**\n * Concurrency utilities for bounded parallel execution\n * Prevents CPU spikes and API rate limiting from unbounded Promise.all\n */\n\n/**\n * Execute async tasks with a concurrency limit (like p-map).\n * Processes items from the input array through the mapper function,\n * running at most `concurrency` tasks simultaneously.\n *\n * @param items - Array of items to process\n * @param mapper - Async function to apply to each item\n * @param concurrency - Maximum number of concurrent tasks (default: 6)\n * @param signal - Optional AbortSignal to cancel remaining work\n * @returns Array of results in the same order as input items\n */\nexport async function pMap<T, R>(\n items: readonly T[],\n mapper: (item: T, index: number) => Promise<R>,\n concurrency: number = 6,\n signal?: AbortSignal,\n): Promise<R[]> {\n if (items.length === 0) return [];\n\n // Clamp concurrency to reasonable bounds\n const limit = Math.max(1, Math.min(concurrency, items.length));\n\n const results: R[] = new Array(items.length);\n let nextIndex = 0;\n const internalAbort = new AbortController();\n\n async function worker(): Promise<void> {\n while (true) {\n if (signal?.aborted || internalAbort.signal.aborted) {\n throw new DOMException('Aborted', 'AbortError');\n }\n const index = nextIndex++;\n if (index >= items.length) break;\n try {\n results[index] = await mapper(items[index]!, index);\n } catch (err) {\n internalAbort.abort(); // Signal other workers to stop picking up new work\n throw err;\n }\n }\n }\n\n // Spawn `limit` workers that pull from the shared index\n const workers: Promise<void>[] = [];\n for (let i = 0; i < limit; i++) {\n workers.push(worker());\n }\n\n await Promise.all(workers);\n return results;\n}\n\n/**\n * Like pMap but uses Promise.allSettled semantics \u2014 never rejects,\n * returns PromiseSettledResult for each item.\n *\n * @param items - Array of items to process\n * @param mapper - Async function to apply to each item\n * @param concurrency - Maximum number of concurrent tasks (default: 6)\n * @param signal - Optional AbortSignal to cancel remaining work\n * @returns Array of PromiseSettledResult in the same order as input items\n */\nexport async function pMapSettled<T, R>(\n items: readonly T[],\n mapper: (item: T, index: number) => Promise<R>,\n concurrency: number = 6,\n signal?: AbortSignal,\n): Promise<PromiseSettledResult<R>[]> {\n if (items.length === 0) return [];\n\n const limit = Math.max(1, Math.min(concurrency, items.length));\n\n const results: PromiseSettledResult<R>[] = new Array(items.length);\n let nextIndex = 0;\n\n async function worker(): Promise<void> {\n while (nextIndex < items.length) {\n if (signal?.aborted) break;\n const index = nextIndex++;\n try {\n const value = await mapper(items[index]!, index);\n results[index] = { status: 'fulfilled', value };\n } catch (reason) {\n results[index] = { status: 'rejected', reason };\n }\n }\n }\n\n const workers: Promise<void>[] = [];\n for (let i = 0; i < limit; i++) {\n workers.push(worker());\n }\n\n await Promise.all(workers);\n\n // Fill any unprocessed items after abort\n for (let i = 0; i < items.length; i++) {\n if (results[i] === undefined) {\n results[i] = { status: 'rejected', reason: new DOMException('Aborted', 'AbortError') };\n }\n }\n\n return results;\n}\n"],
5
+ "mappings": "AAgBA,eAAsB,KACpB,OACA,QACA,cAAsB,GACtB,QACc;AACd,MAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAGhC,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,aAAa,MAAM,MAAM,CAAC;AAE7D,QAAM,UAAe,IAAI,MAAM,MAAM,MAAM;AAC3C,MAAI,YAAY;AAChB,QAAM,gBAAgB,IAAI,gBAAgB;AAE1C,iBAAe,SAAwB;AACrC,WAAO,MAAM;AACX,UAAI,QAAQ,WAAW,cAAc,OAAO,SAAS;AACnD,cAAM,IAAI,aAAa,WAAW,YAAY;AAAA,MAChD;AACA,YAAM,QAAQ;AACd,UAAI,SAAS,MAAM,OAAQ;AAC3B,UAAI;AACF,gBAAQ,KAAK,IAAI,MAAM,OAAO,MAAM,KAAK,GAAI,KAAK;AAAA,MACpD,SAAS,KAAK;AACZ,sBAAc,MAAM;AACpB,cAAM;AAAA,MACR;AAAA,IACF;AAAA,EACF;AAGA,QAAM,UAA2B,CAAC;AAClC,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAQ,KAAK,OAAO,CAAC;AAAA,EACvB;AAEA,QAAM,QAAQ,IAAI,OAAO;AACzB,SAAO;AACT;AAYA,eAAsB,YACpB,OACA,QACA,cAAsB,GACtB,QACoC;AACpC,MAAI,MAAM,WAAW,EAAG,QAAO,CAAC;AAEhC,QAAM,QAAQ,KAAK,IAAI,GAAG,KAAK,IAAI,aAAa,MAAM,MAAM,CAAC;AAE7D,QAAM,UAAqC,IAAI,MAAM,MAAM,MAAM;AACjE,MAAI,YAAY;AAEhB,iBAAe,SAAwB;AACrC,WAAO,YAAY,MAAM,QAAQ;AAC/B,UAAI,QAAQ,QAAS;AACrB,YAAM,QAAQ;AACd,UAAI;AACF,cAAM,QAAQ,MAAM,OAAO,MAAM,KAAK,GAAI,KAAK;AAC/C,gBAAQ,KAAK,IAAI,EAAE,QAAQ,aAAa,MAAM;AAAA,MAChD,SAAS,QAAQ;AACf,gBAAQ,KAAK,IAAI,EAAE,QAAQ,YAAY,OAAO;AAAA,MAChD;AAAA,IACF;AAAA,EACF;AAEA,QAAM,UAA2B,CAAC;AAClC,WAAS,IAAI,GAAG,IAAI,OAAO,KAAK;AAC9B,YAAQ,KAAK,OAAO,CAAC;AAAA,EACvB;AAEA,QAAM,QAAQ,IAAI,OAAO;AAGzB,WAAS,IAAI,GAAG,IAAI,MAAM,QAAQ,KAAK;AACrC,QAAI,QAAQ,CAAC,MAAM,QAAW;AAC5B,cAAQ,CAAC,IAAI,EAAE,QAAQ,YAAY,QAAQ,IAAI,aAAa,WAAW,YAAY,EAAE;AAAA,IACvF;AAAA,EACF;AAEA,SAAO;AACT;",
6
+ "names": []
7
+ }