mcp-researchpowerpack-http 3.11.0 → 3.11.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/mcp-use.json +2 -2
- package/dist/src/schemas/web-search.js +7 -1
- package/dist/src/schemas/web-search.js.map +2 -2
- package/dist/src/tools/search.js +32 -72
- package/dist/src/tools/search.js.map +2 -2
- package/dist/src/utils/url-aggregator.js +97 -48
- package/dist/src/utils/url-aggregator.js.map +2 -2
- package/package.json +1 -1
package/dist/mcp-use.json
CHANGED
|
@@ -18,7 +18,13 @@ const webSearchOutputSchema = z.object({
|
|
|
18
18
|
execution_time_ms: z.number().int().nonnegative().describe("Elapsed execution time in milliseconds."),
|
|
19
19
|
total_unique_urls: z.number().int().nonnegative().optional().describe("Unique URL count observed across all searches."),
|
|
20
20
|
consensus_url_count: z.number().int().nonnegative().optional().describe("Count of URLs that met the consensus threshold."),
|
|
21
|
-
frequency_threshold: z.number().int().nonnegative().optional().describe("Minimum frequency required for a URL to be considered consensus.")
|
|
21
|
+
frequency_threshold: z.number().int().nonnegative().optional().describe("Minimum frequency required for a URL to be considered consensus."),
|
|
22
|
+
coverage_summary: z.array(z.object({
|
|
23
|
+
keyword: z.string().describe("The search keyword."),
|
|
24
|
+
result_count: z.number().int().nonnegative().describe("Number of results returned for this keyword."),
|
|
25
|
+
top_url: z.string().optional().describe("Domain of the top-ranked result for this keyword.")
|
|
26
|
+
})).optional().describe("Per-keyword result counts and top URLs for coverage analysis."),
|
|
27
|
+
low_yield_keywords: z.array(z.string()).optional().describe("Keywords that produced 0-1 results.")
|
|
22
28
|
}).strict().describe("Structured metadata about the completed web search batch.")
|
|
23
29
|
}).strict();
|
|
24
30
|
export {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../../../src/schemas/web-search.ts"],
|
|
4
|
-
"sourcesContent": ["import { z } from 'zod';\n\n// Keyword schema with validation\nconst keywordSchema = z\n .string({ error: 'web-search: Keyword is required' })\n .min(1, { message: 'web-search: Keyword cannot be empty' })\n .max(500, { message: 'web-search: Keyword too long (max 500 characters)' })\n .refine(\n k => k.trim().length > 0,\n { message: 'web-search: Keyword cannot be whitespace only' }\n )\n .describe('A single Google search query (1\u2013500 chars). Each keyword runs as a separate parallel search. Use varied angles: direct topic, comparisons, \"best of\" lists, year-specific, site-specific (e.g., \"site:github.com topic\").');\n\n// Input schema for web-search tool\nconst keywordsSchema = z\n .array(keywordSchema, {\n error: 'web-search: Keywords must be an array',\n })\n .min(1, { message: 'web-search: At least 1 keyword required' })\n .max(100, { message: 'web-search: Maximum 100 keywords allowed per request' })\n .describe('Array of 1\u2013100 search keywords. RECOMMENDED: 3\u20137 for solid consensus ranking, up to 20 for thorough coverage. Each keyword runs as a separate Google search in parallel. Results are aggregated and URLs appearing in multiple searches are flagged as high-confidence consensus matches. Supply <1 and you get an error; >100 is rejected.');\n\nconst webSearchParamsShape = {\n keywords: keywordsSchema,\n};\n\nexport const webSearchParamsSchema = z.object(webSearchParamsShape).strict();\nexport type WebSearchParams = z.infer<typeof webSearchParamsSchema>;\n\nexport const webSearchOutputSchema = z.object({\n content: z\n .string()\n .describe('Formatted markdown report containing ranked URLs with consensus markers and per-query results.'),\n metadata: z.object({\n total_keywords: z\n .number()\n .int()\n .nonnegative()\n .describe('Total number of keyword queries executed.'),\n total_results: z\n .number()\n .int()\n .nonnegative()\n .describe('Total number of ranked search results included across shown queries.'),\n execution_time_ms: z\n .number()\n .int()\n .nonnegative()\n .describe('Elapsed execution time in milliseconds.'),\n total_unique_urls: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Unique URL count observed across all searches.'),\n consensus_url_count: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Count of URLs that met the consensus threshold.'),\n frequency_threshold: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Minimum frequency required for a URL to be considered consensus.'),\n }).strict().describe('Structured metadata about the completed web search batch.'),\n}).strict();\n\nexport type WebSearchOutput = z.infer<typeof webSearchOutputSchema>;\n"],
|
|
5
|
-
"mappings": "AAAA,SAAS,SAAS;AAGlB,MAAM,gBAAgB,EACnB,OAAO,EAAE,OAAO,kCAAkC,CAAC,EACnD,IAAI,GAAG,EAAE,SAAS,sCAAsC,CAAC,EACzD,IAAI,KAAK,EAAE,SAAS,oDAAoD,CAAC,EACzE;AAAA,EACC,OAAK,EAAE,KAAK,EAAE,SAAS;AAAA,EACvB,EAAE,SAAS,gDAAgD;AAC7D,EACC,SAAS,gOAA2N;AAGvO,MAAM,iBAAiB,EACpB,MAAM,eAAe;AAAA,EACpB,OAAO;AACT,CAAC,EACA,IAAI,GAAG,EAAE,SAAS,0CAA0C,CAAC,EAC7D,IAAI,KAAK,EAAE,SAAS,uDAAuD,CAAC,EAC5E,SAAS,uVAA6U;AAEzV,MAAM,uBAAuB;AAAA,EAC3B,UAAU;AACZ;AAEO,MAAM,wBAAwB,EAAE,OAAO,oBAAoB,EAAE,OAAO;AAGpE,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,SAAS,EACN,OAAO,EACP,SAAS,gGAAgG;AAAA,EAC5G,UAAU,EAAE,OAAO;AAAA,IACjB,gBAAgB,EACb,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,2CAA2C;AAAA,IACvD,eAAe,EACZ,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,sEAAsE;AAAA,IAClF,mBAAmB,EAChB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,yCAAyC;AAAA,IACrD,mBAAmB,EAChB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,gDAAgD;AAAA,IAC5D,qBAAqB,EAClB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,iDAAiD;AAAA,IAC7D,qBAAqB,EAClB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,kEAAkE;AAAA,
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod';\n\n// Keyword schema with validation\nconst keywordSchema = z\n .string({ error: 'web-search: Keyword is required' })\n .min(1, { message: 'web-search: Keyword cannot be empty' })\n .max(500, { message: 'web-search: Keyword too long (max 500 characters)' })\n .refine(\n k => k.trim().length > 0,\n { message: 'web-search: Keyword cannot be whitespace only' }\n )\n .describe('A single Google search query (1\u2013500 chars). Each keyword runs as a separate parallel search. Use varied angles: direct topic, comparisons, \"best of\" lists, year-specific, site-specific (e.g., \"site:github.com topic\").');\n\n// Input schema for web-search tool\nconst keywordsSchema = z\n .array(keywordSchema, {\n error: 'web-search: Keywords must be an array',\n })\n .min(1, { message: 'web-search: At least 1 keyword required' })\n .max(100, { message: 'web-search: Maximum 100 keywords allowed per request' })\n .describe('Array of 1\u2013100 search keywords. RECOMMENDED: 3\u20137 for solid consensus ranking, up to 20 for thorough coverage. Each keyword runs as a separate Google search in parallel. Results are aggregated and URLs appearing in multiple searches are flagged as high-confidence consensus matches. Supply <1 and you get an error; >100 is rejected.');\n\nconst webSearchParamsShape = {\n keywords: keywordsSchema,\n};\n\nexport const webSearchParamsSchema = z.object(webSearchParamsShape).strict();\nexport type WebSearchParams = z.infer<typeof webSearchParamsSchema>;\n\nexport const webSearchOutputSchema = z.object({\n content: z\n .string()\n .describe('Formatted markdown report containing ranked URLs with consensus markers and per-query results.'),\n metadata: z.object({\n total_keywords: z\n .number()\n .int()\n .nonnegative()\n .describe('Total number of keyword queries executed.'),\n total_results: z\n .number()\n .int()\n .nonnegative()\n .describe('Total number of ranked search results included across shown queries.'),\n execution_time_ms: z\n .number()\n .int()\n .nonnegative()\n .describe('Elapsed execution time in milliseconds.'),\n total_unique_urls: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Unique URL count observed across all searches.'),\n consensus_url_count: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Count of URLs that met the consensus threshold.'),\n frequency_threshold: z\n .number()\n .int()\n .nonnegative()\n .optional()\n .describe('Minimum frequency required for a URL to be considered consensus.'),\n coverage_summary: z\n .array(z.object({\n keyword: z.string().describe('The search keyword.'),\n result_count: z.number().int().nonnegative().describe('Number of results returned for this keyword.'),\n top_url: z.string().optional().describe('Domain of the top-ranked result for this keyword.'),\n }))\n .optional()\n .describe('Per-keyword result counts and top URLs for coverage analysis.'),\n low_yield_keywords: z\n .array(z.string())\n .optional()\n .describe('Keywords that produced 0-1 results.'),\n }).strict().describe('Structured metadata about the completed web search batch.'),\n}).strict();\n\nexport type WebSearchOutput = z.infer<typeof webSearchOutputSchema>;\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAGlB,MAAM,gBAAgB,EACnB,OAAO,EAAE,OAAO,kCAAkC,CAAC,EACnD,IAAI,GAAG,EAAE,SAAS,sCAAsC,CAAC,EACzD,IAAI,KAAK,EAAE,SAAS,oDAAoD,CAAC,EACzE;AAAA,EACC,OAAK,EAAE,KAAK,EAAE,SAAS;AAAA,EACvB,EAAE,SAAS,gDAAgD;AAC7D,EACC,SAAS,gOAA2N;AAGvO,MAAM,iBAAiB,EACpB,MAAM,eAAe;AAAA,EACpB,OAAO;AACT,CAAC,EACA,IAAI,GAAG,EAAE,SAAS,0CAA0C,CAAC,EAC7D,IAAI,KAAK,EAAE,SAAS,uDAAuD,CAAC,EAC5E,SAAS,uVAA6U;AAEzV,MAAM,uBAAuB;AAAA,EAC3B,UAAU;AACZ;AAEO,MAAM,wBAAwB,EAAE,OAAO,oBAAoB,EAAE,OAAO;AAGpE,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,SAAS,EACN,OAAO,EACP,SAAS,gGAAgG;AAAA,EAC5G,UAAU,EAAE,OAAO;AAAA,IACjB,gBAAgB,EACb,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,2CAA2C;AAAA,IACvD,eAAe,EACZ,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,sEAAsE;AAAA,IAClF,mBAAmB,EAChB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,yCAAyC;AAAA,IACrD,mBAAmB,EAChB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,gDAAgD;AAAA,IAC5D,qBAAqB,EAClB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,iDAAiD;AAAA,IAC7D,qBAAqB,EAClB,OAAO,EACP,IAAI,EACJ,YAAY,EACZ,SAAS,EACT,SAAS,kEAAkE;AAAA,IAC9E,kBAAkB,EACf,MAAM,EAAE,OAAO;AAAA,MACd,SAAS,EAAE,OAAO,EAAE,SAAS,qBAAqB;AAAA,MAClD,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,8CAA8C;AAAA,MACpG,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,mDAAmD;AAAA,IAC7F,CAAC,CAAC,EACD,SAAS,EACT,SAAS,+DAA+D;AAAA,IAC3E,oBAAoB,EACjB,MAAM,EAAE,OAAO,CAAC,EAChB,SAAS,EACT,SAAS,qCAAqC;AAAA,EACnD,CAAC,EAAE,OAAO,EAAE,SAAS,2DAA2D;AAClF,CAAC,EAAE,OAAO;",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/dist/src/tools/search.js
CHANGED
|
@@ -6,12 +6,8 @@ import {
|
|
|
6
6
|
import { SearchClient } from "../clients/search.js";
|
|
7
7
|
import {
|
|
8
8
|
aggregateAndRank,
|
|
9
|
-
|
|
10
|
-
lookupUrl,
|
|
11
|
-
generateEnhancedOutput,
|
|
12
|
-
markConsensus
|
|
9
|
+
generateUnifiedOutput
|
|
13
10
|
} from "../utils/url-aggregator.js";
|
|
14
|
-
import { CTR_WEIGHTS } from "../config/index.js";
|
|
15
11
|
import { classifyError } from "../utils/errors.js";
|
|
16
12
|
import {
|
|
17
13
|
mcpLog,
|
|
@@ -25,84 +21,50 @@ import {
|
|
|
25
21
|
toolSuccess,
|
|
26
22
|
toToolResponse
|
|
27
23
|
} 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
24
|
async function executeSearches(keywords) {
|
|
35
25
|
const client = new SearchClient();
|
|
36
26
|
return client.searchMultiple(keywords);
|
|
37
27
|
}
|
|
38
|
-
function
|
|
28
|
+
function processResults(response) {
|
|
39
29
|
const aggregation = aggregateAndRank(response.searches, 5);
|
|
40
|
-
const urlLookup = buildUrlLookup(aggregation.rankedUrls);
|
|
41
30
|
const consensusUrls = aggregation.rankedUrls.filter((u) => u.isConsensus);
|
|
42
|
-
return { aggregation,
|
|
31
|
+
return { aggregation, consensusUrls };
|
|
43
32
|
}
|
|
44
|
-
function
|
|
45
|
-
return
|
|
33
|
+
function buildOutputMarkdown(keywords, aggregation, searches) {
|
|
34
|
+
return generateUnifiedOutput(
|
|
46
35
|
aggregation.rankedUrls,
|
|
47
36
|
keywords,
|
|
37
|
+
searches,
|
|
48
38
|
aggregation.totalUniqueUrls,
|
|
49
39
|
aggregation.frequencyThreshold,
|
|
50
40
|
aggregation.thresholdNote
|
|
51
|
-
)
|
|
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;
|
|
41
|
+
);
|
|
68
42
|
}
|
|
69
|
-
function
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
});
|
|
82
|
-
if (search.related && search.related.length > 0) {
|
|
83
|
-
const relatedSuggestions = search.related.map((r) => `\`${r}\``).join(", ");
|
|
84
|
-
markdown += `*Related:* ${relatedSuggestions}
|
|
85
|
-
|
|
86
|
-
`;
|
|
43
|
+
function formatSearchOutput(outputMarkdown, aggregation, consensusUrlCount, executionTime, totalKeywords, searches) {
|
|
44
|
+
const markdown = outputMarkdown + `
|
|
45
|
+
---
|
|
46
|
+
*${formatDuration(executionTime)} | ${aggregation.totalUniqueUrls} unique URLs | ${consensusUrlCount} consensus | threshold \u2265${aggregation.frequencyThreshold}*`;
|
|
47
|
+
const coverageSummary = searches.map((s) => {
|
|
48
|
+
let topDomain;
|
|
49
|
+
const topResult = s.results[0];
|
|
50
|
+
if (topResult) {
|
|
51
|
+
try {
|
|
52
|
+
topDomain = new URL(topResult.link).hostname.replace(/^www\./, "");
|
|
53
|
+
} catch {
|
|
54
|
+
}
|
|
87
55
|
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
`;
|
|
56
|
+
return { keyword: s.keyword, result_count: s.results.length, top_url: topDomain };
|
|
91
57
|
});
|
|
92
|
-
|
|
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*`;
|
|
58
|
+
const lowYieldKeywords = searches.filter((s) => s.results.length <= 1).map((s) => s.keyword);
|
|
99
59
|
const metadata = {
|
|
100
60
|
total_keywords: totalKeywords,
|
|
101
|
-
total_results:
|
|
61
|
+
total_results: aggregation.rankedUrls.length,
|
|
102
62
|
execution_time_ms: executionTime,
|
|
103
63
|
total_unique_urls: aggregation.totalUniqueUrls,
|
|
104
64
|
consensus_url_count: consensusUrlCount,
|
|
105
|
-
frequency_threshold: aggregation.frequencyThreshold
|
|
65
|
+
frequency_threshold: aggregation.frequencyThreshold,
|
|
66
|
+
coverage_summary: coverageSummary,
|
|
67
|
+
...lowYieldKeywords.length > 0 ? { low_yield_keywords: lowYieldKeywords } : {}
|
|
106
68
|
};
|
|
107
69
|
return toolSuccess(markdown, { content: markdown, metadata });
|
|
108
70
|
}
|
|
@@ -136,28 +98,26 @@ async function handleWebSearch(params, reporter = NOOP_REPORTER) {
|
|
|
136
98
|
await reporter.progress(15, 100, "Submitting search queries");
|
|
137
99
|
const response = await executeSearches(params.keywords);
|
|
138
100
|
await reporter.progress(50, 100, "Collected search results");
|
|
139
|
-
const { aggregation,
|
|
101
|
+
const { aggregation, consensusUrls } = processResults(response);
|
|
140
102
|
await reporter.log(
|
|
141
103
|
"info",
|
|
142
104
|
`Collected ${aggregation.totalUniqueUrls} unique URLs across ${response.totalKeywords} queries`
|
|
143
105
|
);
|
|
144
|
-
const
|
|
145
|
-
const { markdown: perQuerySection, totalResults } = buildPerQuerySection(response, urlLookup);
|
|
106
|
+
const outputMarkdown = buildOutputMarkdown(params.keywords, aggregation, response.searches);
|
|
146
107
|
await reporter.progress(80, 100, "Ranking and formatting search results");
|
|
147
108
|
const executionTime = Date.now() - startTime;
|
|
148
|
-
mcpLog("info", `Search completed: ${
|
|
109
|
+
mcpLog("info", `Search completed: ${aggregation.rankedUrls.length} unique URLs, ${consensusUrls.length} consensus`, "search");
|
|
149
110
|
await reporter.log(
|
|
150
111
|
"info",
|
|
151
|
-
`Search completed with ${
|
|
112
|
+
`Search completed with ${aggregation.rankedUrls.length} ranked URLs and ${consensusUrls.length} consensus`
|
|
152
113
|
);
|
|
153
114
|
return formatSearchOutput(
|
|
154
|
-
|
|
155
|
-
perQuerySection,
|
|
156
|
-
totalResults,
|
|
115
|
+
outputMarkdown,
|
|
157
116
|
aggregation,
|
|
158
117
|
consensusUrls.length,
|
|
159
118
|
executionTime,
|
|
160
|
-
response.totalKeywords
|
|
119
|
+
response.totalKeywords,
|
|
120
|
+
response.searches
|
|
161
121
|
);
|
|
162
122
|
} catch (error) {
|
|
163
123
|
return buildWebSearchError(error, params, startTime);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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
|
|
5
|
-
"mappings": "AAOA,SAAS,iBAAiB,4BAA4B;AACtD;AAAA,EACE;AAAA,EACA;AAAA,OAGK;AACP,SAAS,oBAAoB;AAC7B;AAAA,EACE;AAAA,EACA;AAAA,
|
|
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 generateUnifiedOutput,\n} from '../utils/url-aggregator.js';\nimport { classifyError } from '../utils/errors.js';\nimport {\n mcpLog,\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\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 processResults(response: SearchResponse): {\n aggregation: SearchAggregation;\n consensusUrls: SearchAggregation['rankedUrls'];\n} {\n const aggregation = aggregateAndRank(response.searches, 5);\n const consensusUrls = aggregation.rankedUrls.filter(u => u.isConsensus);\n return { aggregation, consensusUrls };\n}\n\nfunction buildOutputMarkdown(\n keywords: string[],\n aggregation: SearchAggregation,\n searches: SearchResponse['searches'],\n): string {\n return generateUnifiedOutput(\n aggregation.rankedUrls, keywords, searches,\n aggregation.totalUniqueUrls,\n aggregation.frequencyThreshold, aggregation.thresholdNote,\n );\n}\n\nfunction formatSearchOutput(\n outputMarkdown: string,\n aggregation: SearchAggregation,\n consensusUrlCount: number,\n executionTime: number,\n totalKeywords: number,\n searches: SearchResponse['searches'],\n): ToolExecutionResult<WebSearchOutput> {\n const markdown = outputMarkdown + `\\n---\\n*${formatDuration(executionTime)} | ${aggregation.totalUniqueUrls} unique URLs | ${consensusUrlCount} consensus | threshold \u2265${aggregation.frequencyThreshold}*`;\n\n const coverageSummary = searches.map(s => {\n let topDomain: string | undefined;\n const topResult = s.results[0];\n if (topResult) {\n try { topDomain = new URL(topResult.link).hostname.replace(/^www\\./, ''); } catch { /* ignore */ }\n }\n return { keyword: s.keyword, result_count: s.results.length, top_url: topDomain };\n });\n const lowYieldKeywords = searches\n .filter(s => s.results.length <= 1)\n .map(s => s.keyword);\n\n const metadata = {\n total_keywords: totalKeywords,\n total_results: aggregation.rankedUrls.length,\n execution_time_ms: executionTime,\n total_unique_urls: aggregation.totalUniqueUrls,\n consensus_url_count: consensusUrlCount,\n frequency_threshold: aggregation.frequencyThreshold,\n coverage_summary: coverageSummary,\n ...(lowYieldKeywords.length > 0 ? { low_yield_keywords: lowYieldKeywords } : {}),\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 '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, consensusUrls } = processResults(response);\n await reporter.log(\n 'info',\n `Collected ${aggregation.totalUniqueUrls} unique URLs across ${response.totalKeywords} queries`,\n );\n\n const outputMarkdown = buildOutputMarkdown(params.keywords, aggregation, response.searches);\n await reporter.progress(80, 100, 'Ranking and formatting search results');\n\n const executionTime = Date.now() - startTime;\n mcpLog('info', `Search completed: ${aggregation.rankedUrls.length} unique URLs, ${consensusUrls.length} consensus`, 'search');\n await reporter.log(\n 'info',\n `Search completed with ${aggregation.rankedUrls.length} ranked URLs and ${consensusUrls.length} consensus`,\n );\n\n return formatSearchOutput(\n outputMarkdown, aggregation, consensusUrls.length, executionTime, response.totalKeywords, response.searches,\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
6
|
"names": []
|
|
7
7
|
}
|
|
@@ -7,6 +7,19 @@ const BEYOND_TOP10_BASE = 10;
|
|
|
7
7
|
const DEFAULT_MIN_CONSENSUS_URLS = 5;
|
|
8
8
|
const DEFAULT_REDDIT_MIN_CONSENSUS_URLS = 3;
|
|
9
9
|
const HIGH_CONSENSUS_THRESHOLD = 4;
|
|
10
|
+
const MAX_ALT_SNIPPETS = 3;
|
|
11
|
+
const MAX_CONSISTENCY_PENALTY = 0.15;
|
|
12
|
+
const CONSISTENCY_STDDEV_SCALE = 5;
|
|
13
|
+
function computePositionStats(positions) {
|
|
14
|
+
if (positions.length <= 1) {
|
|
15
|
+
return { mean: positions[0] ?? 0, stdDev: 0, consistencyMultiplier: 1 };
|
|
16
|
+
}
|
|
17
|
+
const mean = positions.reduce((a, b) => a + b, 0) / positions.length;
|
|
18
|
+
const variance = positions.reduce((sum, p) => sum + (p - mean) ** 2, 0) / (positions.length - 1);
|
|
19
|
+
const stdDev = Math.sqrt(variance);
|
|
20
|
+
const consistencyMultiplier = 1 - MAX_CONSISTENCY_PENALTY * Math.min(stdDev / CONSISTENCY_STDDEV_SCALE, 1);
|
|
21
|
+
return { mean, stdDev, consistencyMultiplier };
|
|
22
|
+
}
|
|
10
23
|
function getCtrWeight(position) {
|
|
11
24
|
if (position >= 1 && position <= 10) {
|
|
12
25
|
return CTR_WEIGHTS[position] ?? 0;
|
|
@@ -26,6 +39,9 @@ function aggregateResults(searches) {
|
|
|
26
39
|
const prevBest = existing.bestPosition;
|
|
27
40
|
existing.bestPosition = Math.min(existing.bestPosition, result.position);
|
|
28
41
|
existing.totalScore += getCtrWeight(result.position);
|
|
42
|
+
if (result.snippet && existing.allSnippets.length < MAX_ALT_SNIPPETS && !existing.allSnippets.some((s) => s === result.snippet)) {
|
|
43
|
+
existing.allSnippets.push(result.snippet);
|
|
44
|
+
}
|
|
29
45
|
if (result.position < prevBest) {
|
|
30
46
|
existing.title = result.title;
|
|
31
47
|
existing.snippet = result.snippet;
|
|
@@ -35,6 +51,7 @@ function aggregateResults(searches) {
|
|
|
35
51
|
url: result.link,
|
|
36
52
|
title: result.title,
|
|
37
53
|
snippet: result.snippet,
|
|
54
|
+
allSnippets: result.snippet ? [result.snippet] : [],
|
|
38
55
|
frequency: 1,
|
|
39
56
|
positions: [result.position],
|
|
40
57
|
queries: [search.keyword],
|
|
@@ -63,76 +80,108 @@ function countByFrequency(urlMap, minFrequency) {
|
|
|
63
80
|
}
|
|
64
81
|
return count;
|
|
65
82
|
}
|
|
66
|
-
function calculateWeightedScores(urls, consensusThreshold) {
|
|
83
|
+
function calculateWeightedScores(urls, consensusThreshold, totalQueries) {
|
|
67
84
|
if (urls.length === 0) return [];
|
|
68
|
-
const
|
|
69
|
-
|
|
70
|
-
|
|
85
|
+
const scored = urls.map((url) => {
|
|
86
|
+
const stats = computePositionStats(url.positions);
|
|
87
|
+
const compositeScore = url.totalScore * stats.consistencyMultiplier;
|
|
88
|
+
return { url, compositeScore, stats };
|
|
89
|
+
});
|
|
90
|
+
scored.sort((a, b) => b.compositeScore - a.compositeScore);
|
|
91
|
+
const maxScore = scored[0].compositeScore;
|
|
92
|
+
return scored.map(({ url, compositeScore, stats }, index) => ({
|
|
71
93
|
url: url.url,
|
|
72
94
|
title: url.title,
|
|
73
95
|
snippet: url.snippet,
|
|
96
|
+
allSnippets: url.allSnippets,
|
|
74
97
|
rank: index + 1,
|
|
75
|
-
score: maxScore > 0 ?
|
|
98
|
+
score: maxScore > 0 ? compositeScore / maxScore * 100 : 0,
|
|
76
99
|
frequency: url.frequency,
|
|
77
100
|
positions: url.positions,
|
|
78
101
|
queries: url.queries,
|
|
79
102
|
bestPosition: url.bestPosition,
|
|
80
|
-
isConsensus: url.frequency >= consensusThreshold
|
|
103
|
+
isConsensus: url.frequency >= consensusThreshold,
|
|
104
|
+
coverageRatio: totalQueries > 0 ? url.frequency / totalQueries : 0,
|
|
105
|
+
positionStdDev: stats.stdDev,
|
|
106
|
+
consistencyMultiplier: stats.consistencyMultiplier
|
|
81
107
|
}));
|
|
82
108
|
}
|
|
83
|
-
function markConsensus(frequency) {
|
|
84
|
-
return frequency >=
|
|
109
|
+
function markConsensus(frequency, threshold = WEB_CONSENSUS_THRESHOLD) {
|
|
110
|
+
return frequency >= threshold ? "CONSENSUS" : "";
|
|
85
111
|
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
if (
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
} else {
|
|
93
|
-
parts.push(`Appeared in ${url.frequency} search${url.frequency > 1 ? "es" : ""}`);
|
|
94
|
-
}
|
|
95
|
-
if (url.bestPosition === 1) {
|
|
96
|
-
parts.push("ranked #1 in at least one search");
|
|
97
|
-
} else if (url.bestPosition <= 3) {
|
|
98
|
-
parts.push(`best position was top-3 (#${url.bestPosition})`);
|
|
99
|
-
}
|
|
100
|
-
return parts.join(", ") + ".";
|
|
112
|
+
const COVERAGE_TABLE_MAX_ROWS = 20;
|
|
113
|
+
function consistencyLabel(stdDev, frequency) {
|
|
114
|
+
if (frequency <= 1) return "n/a";
|
|
115
|
+
if (stdDev < 1.5) return "high";
|
|
116
|
+
if (stdDev < 3.5) return "medium";
|
|
117
|
+
return "variable";
|
|
101
118
|
}
|
|
102
|
-
function
|
|
119
|
+
function generateUnifiedOutput(rankedUrls, allKeywords, keywordResults, totalUniqueUrls, frequencyThreshold, thresholdNote) {
|
|
103
120
|
const lines = [];
|
|
104
121
|
const consensusCount = rankedUrls.filter((u) => u.isConsensus).length;
|
|
105
|
-
lines.push(`##
|
|
106
|
-
lines.push("");
|
|
107
|
-
lines.push(`Based on ${allKeywords.length} distinct searches, we found **${rankedUrls.length} unique resources** (${consensusCount} appear in multiple queries).`);
|
|
122
|
+
lines.push(`## Web Search Results (${allKeywords.length} queries, ${totalUniqueUrls} unique URLs)`);
|
|
108
123
|
lines.push("");
|
|
109
124
|
if (thresholdNote) {
|
|
110
125
|
lines.push(`> ${thresholdNote}`);
|
|
111
126
|
lines.push("");
|
|
112
127
|
}
|
|
113
|
-
lines.push("### \u{1F947} Ranked Resources");
|
|
114
|
-
lines.push("");
|
|
115
128
|
for (const url of rankedUrls) {
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
const
|
|
119
|
-
lines.push(
|
|
120
|
-
lines.push(
|
|
121
|
-
lines.push(
|
|
122
|
-
lines.push(
|
|
123
|
-
|
|
129
|
+
const consensusTag = url.frequency >= HIGH_CONSENSUS_THRESHOLD ? " CONSENSUS+++" : url.isConsensus ? " CONSENSUS" : "";
|
|
130
|
+
const coveragePct = Math.round(url.coverageRatio * 100);
|
|
131
|
+
const consistency = consistencyLabel(url.positionStdDev, url.frequency);
|
|
132
|
+
lines.push(`**${url.rank}. [${url.title}](${url.url})**${consensusTag}`);
|
|
133
|
+
lines.push(`Score: ${url.score.toFixed(1)} | Seen in: ${url.frequency}/${allKeywords.length} queries (${coveragePct}%) | Best pos: #${url.bestPosition} | Consistency: ${consistency}`);
|
|
134
|
+
lines.push(`Queries: ${url.queries.map((q) => `"${q}"`).join(", ")}`);
|
|
135
|
+
lines.push(`> ${url.snippet}`);
|
|
136
|
+
if (url.allSnippets.length > 1) {
|
|
137
|
+
const alts = url.allSnippets.filter((s) => s !== url.snippet).slice(0, 3).map((s) => s.length > 100 ? s.slice(0, 97) + "..." : s);
|
|
138
|
+
if (alts.length > 0) {
|
|
139
|
+
lines.push(`Alt: ${alts.map((s) => `"${s}"`).join(" | ")}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
124
142
|
lines.push("");
|
|
125
143
|
}
|
|
126
144
|
lines.push("---");
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
145
|
+
if (allKeywords.length <= COVERAGE_TABLE_MAX_ROWS) {
|
|
146
|
+
lines.push("### Keyword Coverage");
|
|
147
|
+
lines.push("| Keyword | Results | Top URL | Top Pos |");
|
|
148
|
+
lines.push("|---------|---------|---------|---------|");
|
|
149
|
+
for (const search of keywordResults) {
|
|
150
|
+
const topResult = search.results[0];
|
|
151
|
+
let topDomain = "";
|
|
152
|
+
if (topResult) {
|
|
153
|
+
try {
|
|
154
|
+
topDomain = new URL(topResult.link).hostname.replace(/^www\./, "");
|
|
155
|
+
} catch {
|
|
156
|
+
topDomain = topResult.link;
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
lines.push(`| "${search.keyword}" | ${search.results.length} | ${topDomain || "\u2014"} | ${topResult ? `#${topResult.position}` : "\u2014"} |`);
|
|
160
|
+
}
|
|
161
|
+
lines.push("");
|
|
162
|
+
} else {
|
|
163
|
+
const goodCount = keywordResults.filter((s) => s.results.length >= 3).length;
|
|
164
|
+
lines.push(`### Keyword Coverage: ${goodCount}/${allKeywords.length} keywords returned 3+ results`);
|
|
165
|
+
lines.push("");
|
|
166
|
+
}
|
|
167
|
+
const lowYield = keywordResults.filter((s) => s.results.length <= 1);
|
|
168
|
+
if (lowYield.length > 0) {
|
|
169
|
+
lines.push(`**Low-yield keywords** (0-1 results): ${lowYield.map((s) => `\`${s.keyword}\``).join(", ")}`);
|
|
170
|
+
lines.push("");
|
|
171
|
+
}
|
|
172
|
+
const allRelated = /* @__PURE__ */ new Set();
|
|
173
|
+
for (const search of keywordResults) {
|
|
174
|
+
if (search.related) {
|
|
175
|
+
for (const r of search.related) {
|
|
176
|
+
allRelated.add(r);
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
if (allRelated.size > 0) {
|
|
181
|
+
const related = [...allRelated].slice(0, 10);
|
|
182
|
+
lines.push(`**Related searches:** ${related.map((r) => `\`${r}\``).join(", ")}`);
|
|
183
|
+
lines.push("");
|
|
184
|
+
}
|
|
136
185
|
return lines.join("\n");
|
|
137
186
|
}
|
|
138
187
|
function aggregateAndRank(searches, minConsensusUrls = DEFAULT_MIN_CONSENSUS_URLS) {
|
|
@@ -153,7 +202,7 @@ function aggregateAndRank(searches, minConsensusUrls = DEFAULT_MIN_CONSENSUS_URL
|
|
|
153
202
|
}
|
|
154
203
|
}
|
|
155
204
|
const allUrls = [...urlMap.values()];
|
|
156
|
-
const rankedUrls = calculateWeightedScores(allUrls, usedThreshold);
|
|
205
|
+
const rankedUrls = calculateWeightedScores(allUrls, usedThreshold, totalQueries);
|
|
157
206
|
return {
|
|
158
207
|
rankedUrls,
|
|
159
208
|
totalUniqueUrls,
|
|
@@ -349,8 +398,8 @@ export {
|
|
|
349
398
|
aggregateAndRank,
|
|
350
399
|
aggregateAndRankReddit,
|
|
351
400
|
buildUrlLookup,
|
|
352
|
-
generateEnhancedOutput,
|
|
353
401
|
generateRedditEnhancedOutput,
|
|
402
|
+
generateUnifiedOutput,
|
|
354
403
|
lookupUrl,
|
|
355
404
|
markConsensus
|
|
356
405
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
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 { KeywordSearchResult, RedditSearchResult } from '../clients/search.js';\n\n/** Minimum frequency for web search consensus marking */\nconst WEB_CONSENSUS_THRESHOLD = 3 as const;\n\n/** Minimum frequency for Reddit consensus marking (lower due to fewer overlapping results) */\nconst REDDIT_CONSENSUS_THRESHOLD = 2 as const;\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/** Default minimum consensus URLs before lowering threshold (Reddit) */\nconst DEFAULT_REDDIT_MIN_CONSENSUS_URLS = 3 as const;\n\n/** High consensus frequency threshold for enhanced output labeling */\nconst HIGH_CONSENSUS_THRESHOLD = 4 as const;\n\n/**\n * Aggregated URL data structure\n */\ninterface AggregatedUrl {\n readonly url: string;\n title: string;\n snippet: string;\n frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n bestPosition: number;\n totalScore: number;\n}\n\n/**\n * Ranked URL with normalized score\n */\ninterface RankedUrl {\n readonly url: string;\n readonly title: string;\n readonly snippet: 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}\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: KeywordSearchResult[]): 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.keyword);\n const prevBest = existing.bestPosition;\n existing.bestPosition = Math.min(existing.bestPosition, result.position);\n existing.totalScore += getCtrWeight(result.position);\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 frequency: 1,\n positions: [result.position],\n queries: [search.keyword],\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 and normalize to 100.0\n * Returns ALL URLs sorted by score with rank assignments and consensus marking\n */\nfunction calculateWeightedScores(urls: AggregatedUrl[], consensusThreshold: number): RankedUrl[] {\n if (urls.length === 0) return [];\n\n // Sort by total score descending\n const sorted = [...urls].sort((a, b) => b.totalScore - a.totalScore);\n\n // Find max score for normalization\n const maxScore = sorted[0]!.totalScore;\n\n // Map to ranked URLs with normalized scores\n return sorted.map((url, index) => ({\n url: url.url,\n title: url.title,\n snippet: url.snippet,\n rank: index + 1,\n score: maxScore > 0 ? (url.totalScore / 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 }));\n}\n\n/**\n * Mark consensus status for a URL\n * Returns \"\u2713\" if frequency >= threshold, else \"\u2717\"\n */\nexport function markConsensus(frequency: number): string {\n return frequency >= WEB_CONSENSUS_THRESHOLD ? '\u2713' : '\u2717';\n}\n\n/**\n * Generate justification for why a URL is ranked at its position\n */\nfunction generateJustification(url: RankedUrl, rank: number): string {\n const parts: string[] = [];\n \n if (url.frequency >= HIGH_CONSENSUS_THRESHOLD) {\n parts.push(`Appeared in ${url.frequency} different searches showing strong cross-query relevance`);\n } else if (url.frequency >= WEB_CONSENSUS_THRESHOLD) {\n parts.push(`Found across ${url.frequency} searches indicating solid topical coverage`);\n } else {\n parts.push(`Appeared in ${url.frequency} search${url.frequency > 1 ? 'es' : ''}`);\n }\n \n if (url.bestPosition === 1) {\n parts.push('ranked #1 in at least one search');\n } else if (url.bestPosition <= 3) {\n parts.push(`best position was top-3 (#${url.bestPosition})`);\n }\n \n return parts.join(', ') + '.';\n}\n\n/**\n * Generate enhanced narrative output for consensus URLs\n */\nexport function generateEnhancedOutput(\n rankedUrls: RankedUrl[],\n allKeywords: string[],\n totalUniqueUrls: number,\n frequencyThreshold: number,\n thresholdNote?: string\n): string {\n const lines: string[] = [];\n \n // Header\n const consensusCount = rankedUrls.filter(u => u.isConsensus).length;\n lines.push(`## Aggregated Search Results (${allKeywords.length} Queries \u2192 ${rankedUrls.length} Unique URLs)`);\n lines.push('');\n lines.push(`Based on ${allKeywords.length} distinct searches, we found **${rankedUrls.length} unique resources** (${consensusCount} appear in multiple queries).`);\n lines.push('');\n\n if (thresholdNote) {\n lines.push(`> ${thresholdNote}`);\n lines.push('');\n }\n\n // All ranked resources\n lines.push('### \uD83E\uDD47 Ranked Resources');\n lines.push('');\n\n for (const url of rankedUrls) {\n const highConsensus = url.frequency >= HIGH_CONSENSUS_THRESHOLD ? ' \u2B50 HIGHEST CONSENSUS' : url.isConsensus ? ' \u2713 CONSENSUS' : '';\n lines.push(`#### #${url.rank}: ${url.title} (Score: ${url.score.toFixed(1)})${highConsensus}`);\n \n // Appeared in queries\n const queriesList = url.queries.map(q => `\"${q}\"`).join(', ');\n lines.push(`- **Appeared in:** ${url.frequency} queries (${queriesList})`);\n \n // Best ranking\n lines.push(`- **Best ranking:** Position ${url.bestPosition}`);\n \n // Description\n lines.push(`- **Description:** ${url.snippet}`);\n \n // Justification\n lines.push(`- **Why it's #${url.rank}:** ${generateJustification(url, url.rank)}`);\n \n // URL\n lines.push(`- **URL:** ${url.url}`);\n lines.push('');\n }\n \n // Metadata section\n lines.push('---');\n lines.push('');\n lines.push('### \uD83D\uDCC8 Metadata');\n lines.push('');\n lines.push(`- **Total Queries:** ${allKeywords.length} (${allKeywords.join(', ')})`);\n \n // Sort all URLs by frequency for the unique URLs list\n const sortedByFreq = [...rankedUrls].sort((a, b) => b.frequency - a.frequency);\n const urlFreqList = sortedByFreq\n .map(u => `${u.url} (${u.frequency}x)`)\n .join(', ');\n \n lines.push(`- **Unique URLs Found:** ${totalUniqueUrls} \u2014 top by frequency: ${urlFreqList}`);\n lines.push(`- **Consensus Threshold:** \u2265${frequencyThreshold} appearances`);\n lines.push('');\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: KeywordSearchResult[],\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);\n\n return {\n rankedUrls,\n totalUniqueUrls,\n totalQueries,\n frequencyThreshold: usedThreshold,\n thresholdNote,\n };\n}\n\n/**\n * Build URL lookup map for quick consensus checking during result formatting\n */\nexport function buildUrlLookup(rankedUrls: RankedUrl[]): Map<string, RankedUrl> {\n const lookup = new Map<string, RankedUrl>();\n \n for (const url of rankedUrls) {\n const normalized = normalizeUrl(url.url);\n lookup.set(normalized, url);\n // Also store original URL\n lookup.set(url.url.toLowerCase(), url);\n }\n\n return lookup;\n}\n\n/**\n * Look up a URL in the ranked results\n */\nexport function lookupUrl(url: string, lookup: Map<string, RankedUrl>): RankedUrl | undefined {\n const normalized = normalizeUrl(url);\n return lookup.get(normalized) || lookup.get(url.toLowerCase());\n}\n\n// ============================================================================\n// Reddit-Specific Aggregation\n// ============================================================================\n\n/**\n * Aggregated Reddit URL data structure\n */\ninterface AggregatedRedditUrl {\n readonly url: string;\n title: string;\n snippet: string;\n date?: string;\n frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n bestPosition: number;\n totalScore: number;\n}\n\n/**\n * Ranked Reddit URL with normalized score\n */\ninterface RankedRedditUrl {\n readonly url: string;\n readonly title: string;\n readonly snippet: string;\n readonly date?: 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}\n\n/**\n * Reddit aggregation result\n */\ninterface RedditAggregationResult {\n readonly rankedUrls: RankedRedditUrl[];\n readonly totalUniqueUrls: number;\n readonly totalQueries: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\n/**\n * Aggregate Reddit search results from multiple queries\n */\nfunction aggregateRedditResults(\n searches: Map<string, RedditSearchResult[]>\n): Map<string, AggregatedRedditUrl> {\n const urlMap = new Map<string, AggregatedRedditUrl>();\n\n for (const [query, results] of searches) {\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n if (!result) continue;\n const position = i + 1;\n const normalizedUrl = normalizeUrl(result.url);\n const existing = urlMap.get(normalizedUrl);\n\n if (existing) {\n existing.frequency += 1;\n existing.positions.push(position);\n existing.queries.push(query);\n const prevBest = existing.bestPosition;\n existing.bestPosition = Math.min(existing.bestPosition, position);\n existing.totalScore += getCtrWeight(position);\n // Keep best title/snippet (from highest ranking position)\n if (position < prevBest) {\n existing.title = result.title;\n existing.snippet = result.snippet;\n existing.date = result.date;\n }\n } else {\n urlMap.set(normalizedUrl, {\n url: result.url,\n title: result.title,\n snippet: result.snippet,\n date: result.date,\n frequency: 1,\n positions: [position],\n queries: [query],\n bestPosition: position,\n totalScore: getCtrWeight(position),\n });\n }\n }\n }\n\n return urlMap;\n}\n\n/**\n * Count Reddit URLs meeting a frequency threshold\n */\nfunction countRedditByFrequency(\n urlMap: Map<string, AggregatedRedditUrl>,\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 for Reddit URLs\n * Returns ALL URLs sorted by score with consensus marking\n */\nfunction calculateRedditWeightedScores(urls: AggregatedRedditUrl[], consensusThreshold: number): RankedRedditUrl[] {\n if (urls.length === 0) return [];\n\n // Sort by total score descending\n const sorted = [...urls].sort((a, b) => b.totalScore - a.totalScore);\n\n // Find max score for normalization\n const maxScore = sorted[0]!.totalScore;\n\n // Map to ranked URLs with normalized scores\n return sorted.map((url, index) => ({\n url: url.url,\n title: url.title,\n snippet: url.snippet,\n date: url.date,\n rank: index + 1,\n score: maxScore > 0 ? (url.totalScore / 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 }));\n}\n\n/**\n * Full Reddit aggregation pipeline \u2014 returns ALL URLs ranked by CTR score.\n * Determines a consensus threshold for labeling, never drops URLs.\n */\nexport function aggregateAndRankReddit(\n searches: Map<string, RedditSearchResult[]>,\n minConsensusUrls: number = DEFAULT_REDDIT_MIN_CONSENSUS_URLS\n): RedditAggregationResult {\n const urlMap = aggregateRedditResults(searches);\n const totalUniqueUrls = urlMap.size;\n const totalQueries = searches.size;\n\n // Determine consensus threshold for labeling (not filtering)\n const thresholds = [2, 1];\n let usedThreshold = 1;\n let thresholdNote: string | undefined;\n\n for (const threshold of thresholds) {\n const count = countRedditByFrequency(urlMap, threshold);\n if (count >= minConsensusUrls || threshold === 1) {\n usedThreshold = threshold;\n if (threshold < 2 && totalQueries > 1) {\n thresholdNote = `Note: Consensus threshold set to \u2265${threshold} due to result diversity across queries.`;\n }\n break;\n }\n }\n\n // Rank ALL URLs, marking consensus based on determined threshold\n const allUrls = [...urlMap.values()];\n const rankedUrls = calculateRedditWeightedScores(allUrls, usedThreshold);\n\n return {\n rankedUrls,\n totalUniqueUrls,\n totalQueries,\n frequencyThreshold: usedThreshold,\n thresholdNote,\n };\n}\n\n/**\n * Generate enhanced output for Reddit aggregated results\n * Now includes both aggregated view AND per-query raw results\n */\nexport function generateRedditEnhancedOutput(\n aggregation: RedditAggregationResult,\n allQueries: string[],\n rawResults?: Map<string, RedditSearchResult[]>\n): string {\n const { rankedUrls, totalUniqueUrls, frequencyThreshold, thresholdNote } = aggregation;\n const lines: string[] = [];\n\n // Header\n lines.push(`# \uD83D\uDD0D Reddit Search Results (Aggregated from ${allQueries.length} Queries)`);\n lines.push('');\n lines.push(`**Total Unique Posts:** ${totalUniqueUrls} | **Consensus Threshold:** \u2265${frequencyThreshold} appearances`);\n lines.push('');\n\n if (thresholdNote) {\n lines.push(`> ${thresholdNote}`);\n lines.push('');\n }\n\n // Consensus section (URLs appearing in multiple queries)\n const consensusUrls = rankedUrls.filter(u => u.frequency >= frequencyThreshold && u.frequency > 1);\n if (consensusUrls.length > 0) {\n lines.push('## \u2B50 High-Consensus Posts (Multiple Queries)');\n lines.push('');\n lines.push('*These posts appeared across multiple search queries, indicating high relevance:*');\n lines.push('');\n\n for (const url of consensusUrls) {\n const dateStr = url.date ? ` \u2022 \uD83D\uDCC5 ${url.date}` : '';\n const queriesList = url.queries.map(q => `\"${q}\"`).join(', ');\n lines.push(`### #${url.rank}: ${url.title}`);\n lines.push(`**Score:** ${url.score.toFixed(1)} | **Found in:** ${url.frequency} queries (${queriesList})${dateStr}`);\n lines.push(`${url.url}`);\n lines.push(`> ${url.snippet}`);\n lines.push('');\n }\n\n lines.push('---');\n lines.push('');\n }\n\n // All results ranked by CTR score\n lines.push('## \uD83D\uDCCA All Results (CTR-Ranked)');\n lines.push('');\n\n for (const url of rankedUrls) {\n const dateStr = url.date ? ` \u2022 \uD83D\uDCC5 ${url.date}` : '';\n const consensusMarker = url.frequency > 1 ? ' \u2B50' : '';\n lines.push(`**${url.rank}. ${url.title}**${consensusMarker}${dateStr}`);\n lines.push(`${url.url}`);\n lines.push(`> ${url.snippet}`);\n if (url.frequency > 1) {\n lines.push(`_Found in ${url.frequency} queries: ${url.queries.map(q => `\"${q}\"`).join(', ')}_`);\n }\n lines.push('');\n }\n\n // Per-Query Raw Results Section (NEW)\n if (rawResults && rawResults.size > 0) {\n lines.push('---');\n lines.push('');\n lines.push('## \uD83D\uDCCB Per-Query Raw Results');\n lines.push('');\n lines.push('*Complete results for each individual query before aggregation:*');\n lines.push('');\n\n for (const [query, results] of rawResults) {\n lines.push(`### \uD83D\uDD0E Query: \"${query}\"`);\n lines.push(`**Results:** ${results.length} posts`);\n lines.push('');\n\n if (results.length === 0) {\n lines.push('_No results found for this query._');\n lines.push('');\n continue;\n }\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n if (!result) continue;\n const position = i + 1;\n const dateStr = result.date ? ` \u2022 \uD83D\uDCC5 ${result.date}` : '';\n lines.push(`${position}. **${result.title}**${dateStr}`);\n lines.push(` ${result.url}`);\n lines.push(` > ${result.snippet}`);\n lines.push('');\n }\n }\n }\n\n // Metadata\n lines.push('---');\n lines.push('');\n lines.push('### \uD83D\uDCC8 Search Metadata');\n lines.push('');\n lines.push(`- **Queries:** ${allQueries.map(q => `\"${q}\"`).join(', ')}`);\n lines.push(`- **Unique Posts Found:** ${totalUniqueUrls}`);\n lines.push(`- **High-Consensus Posts:** ${consensusUrls.length}`);\n lines.push('');\n\n return lines.join('\\n');\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
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 { KeywordSearchResult, RedditSearchResult } from '../clients/search.js';\n\n/** Minimum frequency for web search consensus marking */\nconst WEB_CONSENSUS_THRESHOLD = 3 as const;\n\n/** Minimum frequency for Reddit consensus marking (lower due to fewer overlapping results) */\nconst REDDIT_CONSENSUS_THRESHOLD = 2 as const;\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/** Default minimum consensus URLs before lowering threshold (Reddit) */\nconst DEFAULT_REDDIT_MIN_CONSENSUS_URLS = 3 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: KeywordSearchResult[]): 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.keyword);\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.keyword],\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/**\n * Mark consensus status for a URL against a given threshold.\n * Returns \"CONSENSUS\" if frequency >= threshold, else empty string.\n */\nexport function markConsensus(frequency: number, threshold: number = WEB_CONSENSUS_THRESHOLD): string {\n return frequency >= threshold ? 'CONSENSUS' : '';\n}\n\n/** Maximum keywords 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 allKeywords: string[],\n keywordResults: KeywordSearchResult[],\n totalUniqueUrls: number,\n frequencyThreshold: number,\n thresholdNote?: string,\n): string {\n const lines: string[] = [];\n const consensusCount = rankedUrls.filter(u => u.isConsensus).length;\n\n // Header\n lines.push(`## Web Search Results (${allKeywords.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 for (const url of rankedUrls) {\n const consensusTag = url.frequency >= HIGH_CONSENSUS_THRESHOLD\n ? ' CONSENSUS+++'\n : 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 lines.push(`Score: ${url.score.toFixed(1)} | Seen in: ${url.frequency}/${allKeywords.length} queries (${coveragePct}%) | Best pos: #${url.bestPosition} | Consistency: ${consistency}`);\n lines.push(`Queries: ${url.queries.map(q => `\"${q}\"`).join(', ')}`);\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 (allKeywords.length <= COVERAGE_TABLE_MAX_ROWS) {\n // Full table for \u226420 keywords\n lines.push('### Keyword Coverage');\n lines.push('| Keyword | Results | Top URL | Top Pos |');\n lines.push('|---------|---------|---------|---------|');\n\n for (const search of keywordResults) {\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.keyword}\" | ${search.results.length} | ${topDomain || '\u2014'} | ${topResult ? `#${topResult.position}` : '\u2014'} |`);\n }\n lines.push('');\n } else {\n // Collapsed summary for >20 keywords\n const goodCount = keywordResults.filter(s => s.results.length >= 3).length;\n lines.push(`### Keyword Coverage: ${goodCount}/${allKeywords.length} keywords returned 3+ results`);\n lines.push('');\n }\n\n // Low-yield keywords\n const lowYield = keywordResults.filter(s => s.results.length <= 1);\n if (lowYield.length > 0) {\n lines.push(`**Low-yield keywords** (0-1 results): ${lowYield.map(s => `\\`${s.keyword}\\``).join(', ')}`);\n lines.push('');\n }\n\n // Related searches (merged and deduplicated)\n const allRelated = new Set<string>();\n for (const search of keywordResults) {\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: KeywordSearchResult[],\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/**\n * Build URL lookup map for quick consensus checking during result formatting\n */\nexport function buildUrlLookup(rankedUrls: RankedUrl[]): Map<string, RankedUrl> {\n const lookup = new Map<string, RankedUrl>();\n \n for (const url of rankedUrls) {\n const normalized = normalizeUrl(url.url);\n lookup.set(normalized, url);\n // Also store original URL\n lookup.set(url.url.toLowerCase(), url);\n }\n\n return lookup;\n}\n\n/**\n * Look up a URL in the ranked results\n */\nexport function lookupUrl(url: string, lookup: Map<string, RankedUrl>): RankedUrl | undefined {\n const normalized = normalizeUrl(url);\n return lookup.get(normalized) || lookup.get(url.toLowerCase());\n}\n\n// ============================================================================\n// Reddit-Specific Aggregation\n// ============================================================================\n\n/**\n * Aggregated Reddit URL data structure\n */\ninterface AggregatedRedditUrl {\n readonly url: string;\n title: string;\n snippet: string;\n date?: string;\n frequency: number;\n readonly positions: number[];\n readonly queries: string[];\n bestPosition: number;\n totalScore: number;\n}\n\n/**\n * Ranked Reddit URL with normalized score\n */\ninterface RankedRedditUrl {\n readonly url: string;\n readonly title: string;\n readonly snippet: string;\n readonly date?: 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}\n\n/**\n * Reddit aggregation result\n */\ninterface RedditAggregationResult {\n readonly rankedUrls: RankedRedditUrl[];\n readonly totalUniqueUrls: number;\n readonly totalQueries: number;\n readonly frequencyThreshold: number;\n readonly thresholdNote?: string;\n}\n\n/**\n * Aggregate Reddit search results from multiple queries\n */\nfunction aggregateRedditResults(\n searches: Map<string, RedditSearchResult[]>\n): Map<string, AggregatedRedditUrl> {\n const urlMap = new Map<string, AggregatedRedditUrl>();\n\n for (const [query, results] of searches) {\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n if (!result) continue;\n const position = i + 1;\n const normalizedUrl = normalizeUrl(result.url);\n const existing = urlMap.get(normalizedUrl);\n\n if (existing) {\n existing.frequency += 1;\n existing.positions.push(position);\n existing.queries.push(query);\n const prevBest = existing.bestPosition;\n existing.bestPosition = Math.min(existing.bestPosition, position);\n existing.totalScore += getCtrWeight(position);\n // Keep best title/snippet (from highest ranking position)\n if (position < prevBest) {\n existing.title = result.title;\n existing.snippet = result.snippet;\n existing.date = result.date;\n }\n } else {\n urlMap.set(normalizedUrl, {\n url: result.url,\n title: result.title,\n snippet: result.snippet,\n date: result.date,\n frequency: 1,\n positions: [position],\n queries: [query],\n bestPosition: position,\n totalScore: getCtrWeight(position),\n });\n }\n }\n }\n\n return urlMap;\n}\n\n/**\n * Count Reddit URLs meeting a frequency threshold\n */\nfunction countRedditByFrequency(\n urlMap: Map<string, AggregatedRedditUrl>,\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 for Reddit URLs\n * Returns ALL URLs sorted by score with consensus marking\n */\nfunction calculateRedditWeightedScores(urls: AggregatedRedditUrl[], consensusThreshold: number): RankedRedditUrl[] {\n if (urls.length === 0) return [];\n\n // Sort by total score descending\n const sorted = [...urls].sort((a, b) => b.totalScore - a.totalScore);\n\n // Find max score for normalization\n const maxScore = sorted[0]!.totalScore;\n\n // Map to ranked URLs with normalized scores\n return sorted.map((url, index) => ({\n url: url.url,\n title: url.title,\n snippet: url.snippet,\n date: url.date,\n rank: index + 1,\n score: maxScore > 0 ? (url.totalScore / 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 }));\n}\n\n/**\n * Full Reddit aggregation pipeline \u2014 returns ALL URLs ranked by CTR score.\n * Determines a consensus threshold for labeling, never drops URLs.\n */\nexport function aggregateAndRankReddit(\n searches: Map<string, RedditSearchResult[]>,\n minConsensusUrls: number = DEFAULT_REDDIT_MIN_CONSENSUS_URLS\n): RedditAggregationResult {\n const urlMap = aggregateRedditResults(searches);\n const totalUniqueUrls = urlMap.size;\n const totalQueries = searches.size;\n\n // Determine consensus threshold for labeling (not filtering)\n const thresholds = [2, 1];\n let usedThreshold = 1;\n let thresholdNote: string | undefined;\n\n for (const threshold of thresholds) {\n const count = countRedditByFrequency(urlMap, threshold);\n if (count >= minConsensusUrls || threshold === 1) {\n usedThreshold = threshold;\n if (threshold < 2 && totalQueries > 1) {\n thresholdNote = `Note: Consensus threshold set to \u2265${threshold} due to result diversity across queries.`;\n }\n break;\n }\n }\n\n // Rank ALL URLs, marking consensus based on determined threshold\n const allUrls = [...urlMap.values()];\n const rankedUrls = calculateRedditWeightedScores(allUrls, usedThreshold);\n\n return {\n rankedUrls,\n totalUniqueUrls,\n totalQueries,\n frequencyThreshold: usedThreshold,\n thresholdNote,\n };\n}\n\n/**\n * Generate enhanced output for Reddit aggregated results\n * Now includes both aggregated view AND per-query raw results\n */\nexport function generateRedditEnhancedOutput(\n aggregation: RedditAggregationResult,\n allQueries: string[],\n rawResults?: Map<string, RedditSearchResult[]>\n): string {\n const { rankedUrls, totalUniqueUrls, frequencyThreshold, thresholdNote } = aggregation;\n const lines: string[] = [];\n\n // Header\n lines.push(`# \uD83D\uDD0D Reddit Search Results (Aggregated from ${allQueries.length} Queries)`);\n lines.push('');\n lines.push(`**Total Unique Posts:** ${totalUniqueUrls} | **Consensus Threshold:** \u2265${frequencyThreshold} appearances`);\n lines.push('');\n\n if (thresholdNote) {\n lines.push(`> ${thresholdNote}`);\n lines.push('');\n }\n\n // Consensus section (URLs appearing in multiple queries)\n const consensusUrls = rankedUrls.filter(u => u.frequency >= frequencyThreshold && u.frequency > 1);\n if (consensusUrls.length > 0) {\n lines.push('## \u2B50 High-Consensus Posts (Multiple Queries)');\n lines.push('');\n lines.push('*These posts appeared across multiple search queries, indicating high relevance:*');\n lines.push('');\n\n for (const url of consensusUrls) {\n const dateStr = url.date ? ` \u2022 \uD83D\uDCC5 ${url.date}` : '';\n const queriesList = url.queries.map(q => `\"${q}\"`).join(', ');\n lines.push(`### #${url.rank}: ${url.title}`);\n lines.push(`**Score:** ${url.score.toFixed(1)} | **Found in:** ${url.frequency} queries (${queriesList})${dateStr}`);\n lines.push(`${url.url}`);\n lines.push(`> ${url.snippet}`);\n lines.push('');\n }\n\n lines.push('---');\n lines.push('');\n }\n\n // All results ranked by CTR score\n lines.push('## \uD83D\uDCCA All Results (CTR-Ranked)');\n lines.push('');\n\n for (const url of rankedUrls) {\n const dateStr = url.date ? ` \u2022 \uD83D\uDCC5 ${url.date}` : '';\n const consensusMarker = url.frequency > 1 ? ' \u2B50' : '';\n lines.push(`**${url.rank}. ${url.title}**${consensusMarker}${dateStr}`);\n lines.push(`${url.url}`);\n lines.push(`> ${url.snippet}`);\n if (url.frequency > 1) {\n lines.push(`_Found in ${url.frequency} queries: ${url.queries.map(q => `\"${q}\"`).join(', ')}_`);\n }\n lines.push('');\n }\n\n // Per-Query Raw Results Section (NEW)\n if (rawResults && rawResults.size > 0) {\n lines.push('---');\n lines.push('');\n lines.push('## \uD83D\uDCCB Per-Query Raw Results');\n lines.push('');\n lines.push('*Complete results for each individual query before aggregation:*');\n lines.push('');\n\n for (const [query, results] of rawResults) {\n lines.push(`### \uD83D\uDD0E Query: \"${query}\"`);\n lines.push(`**Results:** ${results.length} posts`);\n lines.push('');\n\n if (results.length === 0) {\n lines.push('_No results found for this query._');\n lines.push('');\n continue;\n }\n\n for (let i = 0; i < results.length; i++) {\n const result = results[i];\n if (!result) continue;\n const position = i + 1;\n const dateStr = result.date ? ` \u2022 \uD83D\uDCC5 ${result.date}` : '';\n lines.push(`${position}. **${result.title}**${dateStr}`);\n lines.push(` ${result.url}`);\n lines.push(` > ${result.snippet}`);\n lines.push('');\n }\n }\n }\n\n // Metadata\n lines.push('---');\n lines.push('');\n lines.push('### \uD83D\uDCC8 Search Metadata');\n lines.push('');\n lines.push(`- **Queries:** ${allQueries.map(q => `\"${q}\"`).join(', ')}`);\n lines.push(`- **Unique Posts Found:** ${totalUniqueUrls}`);\n lines.push(`- **High-Consensus Posts:** ${consensusUrls.length}`);\n lines.push('');\n\n return lines.join('\\n');\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-researchpowerpack-http",
|
|
3
|
-
"version": "3.11.
|
|
3
|
+
"version": "3.11.1",
|
|
4
4
|
"description": "The ultimate research MCP toolkit: Reddit mining, web search with CTR aggregation, and intelligent web scraping - all in one modular package",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|