mcp-researchpowerpack 3.9.4 → 4.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -208
- package/dist/index.js +280 -337
- package/dist/index.js.map +7 -1
- package/dist/mcp-use.json +7 -0
- package/dist/src/clients/reddit.js +278 -0
- package/dist/src/clients/reddit.js.map +7 -0
- package/dist/src/clients/scraper.js +326 -0
- package/dist/src/clients/scraper.js.map +7 -0
- package/dist/src/clients/search.js +217 -0
- package/dist/src/clients/search.js.map +7 -0
- package/dist/src/config/index.js +138 -0
- package/dist/src/config/index.js.map +7 -0
- package/dist/src/prompts/deep-research.js +41 -0
- package/dist/src/prompts/deep-research.js.map +7 -0
- package/dist/src/prompts/reddit-sentiment.js +47 -0
- package/dist/src/prompts/reddit-sentiment.js.map +7 -0
- package/dist/src/schemas/reddit.js +21 -0
- package/dist/src/schemas/reddit.js.map +7 -0
- package/dist/src/schemas/scrape-links.js +26 -0
- package/dist/src/schemas/scrape-links.js.map +7 -0
- package/dist/src/schemas/start-research.js +17 -0
- package/dist/src/schemas/start-research.js.map +7 -0
- package/dist/src/schemas/web-search.js +53 -0
- package/dist/src/schemas/web-search.js.map +7 -0
- package/dist/src/services/llm-processor.js +675 -0
- package/dist/src/services/llm-processor.js.map +7 -0
- package/dist/src/services/markdown-cleaner.js +62 -0
- package/dist/src/services/markdown-cleaner.js.map +7 -0
- package/dist/src/services/workflow-state.js +116 -0
- package/dist/src/services/workflow-state.js.map +7 -0
- package/dist/src/tools/mcp-helpers.js +50 -0
- package/dist/src/tools/mcp-helpers.js.map +7 -0
- package/dist/src/tools/reddit.js +277 -0
- package/dist/src/tools/reddit.js.map +7 -0
- package/dist/src/tools/registry.js +18 -0
- package/dist/src/tools/registry.js.map +7 -0
- package/dist/src/tools/scrape.js +334 -0
- package/dist/src/tools/scrape.js.map +7 -0
- package/dist/src/tools/search.js +423 -0
- package/dist/src/tools/search.js.map +7 -0
- package/dist/src/tools/start-research.js +199 -0
- package/dist/src/tools/start-research.js.map +7 -0
- package/dist/src/tools/utils.js +29 -0
- package/dist/src/tools/utils.js.map +7 -0
- package/dist/src/utils/bootstrap-guard.js +27 -0
- package/dist/src/utils/bootstrap-guard.js.map +7 -0
- package/dist/src/utils/concurrency.js +62 -0
- package/dist/src/utils/concurrency.js.map +7 -0
- package/dist/src/utils/content-extractor.js +61 -0
- package/dist/src/utils/content-extractor.js.map +7 -0
- package/dist/src/utils/errors.js +211 -0
- package/dist/src/utils/errors.js.map +7 -0
- package/dist/src/utils/logger.js +25 -0
- package/dist/src/utils/logger.js.map +7 -0
- package/dist/src/utils/markdown-formatter.js +15 -0
- package/dist/src/utils/markdown-formatter.js.map +7 -0
- package/dist/src/utils/reddit-keyword-guard.js +29 -0
- package/dist/src/utils/reddit-keyword-guard.js.map +7 -0
- package/dist/src/utils/response.js +81 -0
- package/dist/src/utils/response.js.map +7 -0
- package/dist/src/utils/retry.js +13 -0
- package/dist/src/utils/retry.js.map +7 -0
- package/dist/src/utils/sanitize.js +10 -0
- package/dist/src/utils/sanitize.js.map +7 -0
- package/dist/src/utils/source-type.js +41 -0
- package/dist/src/utils/source-type.js.map +7 -0
- package/dist/src/utils/url-aggregator.js +227 -0
- package/dist/src/utils/url-aggregator.js.map +7 -0
- package/dist/src/utils/workflow-key.js +14 -0
- package/dist/src/utils/workflow-key.js.map +7 -0
- package/dist/src/version.js +32 -0
- package/dist/src/version.js.map +7 -0
- package/package.json +33 -28
- package/dist/clients/reddit.d.ts +0 -69
- package/dist/clients/reddit.d.ts.map +0 -1
- package/dist/clients/reddit.js +0 -369
- package/dist/clients/reddit.js.map +0 -1
- package/dist/clients/research.d.ts +0 -67
- package/dist/clients/research.d.ts.map +0 -1
- package/dist/clients/research.js +0 -290
- package/dist/clients/research.js.map +0 -1
- package/dist/clients/scraper.d.ts +0 -72
- package/dist/clients/scraper.d.ts.map +0 -1
- package/dist/clients/scraper.js +0 -338
- package/dist/clients/scraper.js.map +0 -1
- package/dist/clients/search.d.ts +0 -57
- package/dist/clients/search.d.ts.map +0 -1
- package/dist/clients/search.js +0 -223
- package/dist/clients/search.js.map +0 -1
- package/dist/config/index.d.ts +0 -78
- package/dist/config/index.d.ts.map +0 -1
- package/dist/config/index.js +0 -201
- package/dist/config/index.js.map +0 -1
- package/dist/config/loader.d.ts +0 -40
- package/dist/config/loader.d.ts.map +0 -1
- package/dist/config/loader.js +0 -322
- package/dist/config/loader.js.map +0 -1
- package/dist/config/types.d.ts +0 -81
- package/dist/config/types.d.ts.map +0 -1
- package/dist/config/types.js +0 -6
- package/dist/config/types.js.map +0 -1
- package/dist/config/yaml/tools.yaml +0 -146
- package/dist/index.d.ts +0 -7
- package/dist/index.d.ts.map +0 -1
- package/dist/schemas/deep-research.d.ts +0 -64
- package/dist/schemas/deep-research.d.ts.map +0 -1
- package/dist/schemas/deep-research.js +0 -224
- package/dist/schemas/deep-research.js.map +0 -1
- package/dist/schemas/scrape-links.d.ts +0 -32
- package/dist/schemas/scrape-links.d.ts.map +0 -1
- package/dist/schemas/scrape-links.js +0 -34
- package/dist/schemas/scrape-links.js.map +0 -1
- package/dist/schemas/web-search.d.ts +0 -22
- package/dist/schemas/web-search.d.ts.map +0 -1
- package/dist/schemas/web-search.js +0 -21
- package/dist/schemas/web-search.js.map +0 -1
- package/dist/services/file-attachment.d.ts +0 -30
- package/dist/services/file-attachment.d.ts.map +0 -1
- package/dist/services/file-attachment.js +0 -207
- package/dist/services/file-attachment.js.map +0 -1
- package/dist/services/llm-processor.d.ts +0 -29
- package/dist/services/llm-processor.d.ts.map +0 -1
- package/dist/services/llm-processor.js +0 -244
- package/dist/services/llm-processor.js.map +0 -1
- package/dist/services/markdown-cleaner.d.ts +0 -8
- package/dist/services/markdown-cleaner.d.ts.map +0 -1
- package/dist/services/markdown-cleaner.js +0 -74
- package/dist/services/markdown-cleaner.js.map +0 -1
- package/dist/tools/definitions.d.ts +0 -16
- package/dist/tools/definitions.d.ts.map +0 -1
- package/dist/tools/definitions.js +0 -17
- package/dist/tools/definitions.js.map +0 -1
- package/dist/tools/reddit.d.ts +0 -14
- package/dist/tools/reddit.d.ts.map +0 -1
- package/dist/tools/reddit.js +0 -265
- package/dist/tools/reddit.js.map +0 -1
- package/dist/tools/registry.d.ts +0 -71
- package/dist/tools/registry.d.ts.map +0 -1
- package/dist/tools/registry.js +0 -252
- package/dist/tools/registry.js.map +0 -1
- package/dist/tools/research.d.ts +0 -14
- package/dist/tools/research.d.ts.map +0 -1
- package/dist/tools/research.js +0 -196
- package/dist/tools/research.js.map +0 -1
- package/dist/tools/scrape.d.ts +0 -14
- package/dist/tools/scrape.d.ts.map +0 -1
- package/dist/tools/scrape.js +0 -234
- package/dist/tools/scrape.js.map +0 -1
- package/dist/tools/search.d.ts +0 -10
- package/dist/tools/search.d.ts.map +0 -1
- package/dist/tools/search.js +0 -158
- package/dist/tools/search.js.map +0 -1
- package/dist/tools/utils.d.ts +0 -105
- package/dist/tools/utils.d.ts.map +0 -1
- package/dist/tools/utils.js +0 -159
- package/dist/tools/utils.js.map +0 -1
- package/dist/utils/concurrency.d.ts +0 -28
- package/dist/utils/concurrency.d.ts.map +0 -1
- package/dist/utils/concurrency.js +0 -92
- package/dist/utils/concurrency.js.map +0 -1
- package/dist/utils/errors.d.ts +0 -95
- package/dist/utils/errors.d.ts.map +0 -1
- package/dist/utils/errors.js +0 -390
- package/dist/utils/errors.js.map +0 -1
- package/dist/utils/logger.d.ts +0 -39
- package/dist/utils/logger.d.ts.map +0 -1
- package/dist/utils/logger.js +0 -57
- package/dist/utils/logger.js.map +0 -1
- package/dist/utils/markdown-formatter.d.ts +0 -5
- package/dist/utils/markdown-formatter.d.ts.map +0 -1
- package/dist/utils/markdown-formatter.js +0 -15
- package/dist/utils/markdown-formatter.js.map +0 -1
- package/dist/utils/response.d.ts +0 -93
- package/dist/utils/response.d.ts.map +0 -1
- package/dist/utils/response.js +0 -170
- package/dist/utils/response.js.map +0 -1
- package/dist/utils/retry.d.ts +0 -43
- package/dist/utils/retry.d.ts.map +0 -1
- package/dist/utils/retry.js +0 -57
- package/dist/utils/retry.js.map +0 -1
- package/dist/utils/url-aggregator.d.ts +0 -90
- package/dist/utils/url-aggregator.d.ts.map +0 -1
- package/dist/utils/url-aggregator.js +0 -538
- package/dist/utils/url-aggregator.js.map +0 -1
- package/dist/version.d.ts +0 -29
- package/dist/version.d.ts.map +0 -1
- package/dist/version.js +0 -55
- package/dist/version.js.map +0 -1
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
import { Logger } from "mcp-use";
|
|
2
|
+
import { VERSION, PACKAGE_NAME, PACKAGE_DESCRIPTION } from "../version.js";
|
|
3
|
+
function safeParseInt(value, defaultVal, min, max) {
|
|
4
|
+
const logger = Logger.get("config");
|
|
5
|
+
if (!value) {
|
|
6
|
+
return defaultVal;
|
|
7
|
+
}
|
|
8
|
+
const parsed = parseInt(value, 10);
|
|
9
|
+
if (isNaN(parsed)) {
|
|
10
|
+
logger.warn(`Invalid number "${value}", using default ${defaultVal}`);
|
|
11
|
+
return defaultVal;
|
|
12
|
+
}
|
|
13
|
+
if (parsed < min) {
|
|
14
|
+
logger.warn(`Value ${parsed} below minimum ${min}, clamping to ${min}`);
|
|
15
|
+
return min;
|
|
16
|
+
}
|
|
17
|
+
if (parsed > max) {
|
|
18
|
+
logger.warn(`Value ${parsed} above maximum ${max}, clamping to ${max}`);
|
|
19
|
+
return max;
|
|
20
|
+
}
|
|
21
|
+
return parsed;
|
|
22
|
+
}
|
|
23
|
+
const VALID_REASONING_EFFORTS = ["low", "medium", "high"];
|
|
24
|
+
let cachedEnv = null;
|
|
25
|
+
function parseEnv() {
|
|
26
|
+
if (cachedEnv) return cachedEnv;
|
|
27
|
+
cachedEnv = {
|
|
28
|
+
SCRAPER_API_KEY: process.env.SCRAPEDO_API_KEY || "",
|
|
29
|
+
SEARCH_API_KEY: process.env.SERPER_API_KEY || void 0,
|
|
30
|
+
REDDIT_CLIENT_ID: process.env.REDDIT_CLIENT_ID || void 0,
|
|
31
|
+
REDDIT_CLIENT_SECRET: process.env.REDDIT_CLIENT_SECRET || void 0
|
|
32
|
+
};
|
|
33
|
+
return cachedEnv;
|
|
34
|
+
}
|
|
35
|
+
const SERVER = {
|
|
36
|
+
NAME: PACKAGE_NAME,
|
|
37
|
+
VERSION,
|
|
38
|
+
DESCRIPTION: PACKAGE_DESCRIPTION
|
|
39
|
+
};
|
|
40
|
+
function getCapabilities() {
|
|
41
|
+
const env = parseEnv();
|
|
42
|
+
return {
|
|
43
|
+
reddit: !!(env.REDDIT_CLIENT_ID && env.REDDIT_CLIENT_SECRET),
|
|
44
|
+
search: !!env.SEARCH_API_KEY,
|
|
45
|
+
scraping: !!env.SCRAPER_API_KEY,
|
|
46
|
+
llmExtraction: !!LLM_EXTRACTION.API_KEY
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function getMissingEnvMessage(capability) {
|
|
50
|
+
const messages = {
|
|
51
|
+
reddit: '\u274C **Reddit tools unavailable.** Set `REDDIT_CLIENT_ID` and `REDDIT_CLIENT_SECRET` to enable `get-reddit-post`.\n\n\u{1F449} Create a Reddit app at: https://www.reddit.com/prefs/apps (select "script" type)',
|
|
52
|
+
search: '\u274C **Search unavailable.** Set `SERPER_API_KEY` to enable `web-search` (including `scope: "reddit"`).\n\n\u{1F449} Get your free API key at: https://serper.dev (2,500 free queries)',
|
|
53
|
+
scraping: "\u274C **Web scraping unavailable.** Set `SCRAPEDO_API_KEY` to enable `scrape-links`.\n\n\u{1F449} Sign up at: https://scrape.do (1,000 free credits)",
|
|
54
|
+
llmExtraction: "\u26A0\uFE0F **AI extraction disabled.** Set `LLM_API_KEY` to enable AI-powered content extraction and search classification.\n\nScraping will work but without intelligent content filtering."
|
|
55
|
+
};
|
|
56
|
+
return messages[capability];
|
|
57
|
+
}
|
|
58
|
+
const CONCURRENCY = {
|
|
59
|
+
SEARCH: safeParseInt(process.env.CONCURRENCY_SEARCH, 50, 1, 200),
|
|
60
|
+
SCRAPER: safeParseInt(process.env.CONCURRENCY_SCRAPER, 50, 1, 200),
|
|
61
|
+
REDDIT: safeParseInt(process.env.CONCURRENCY_REDDIT, 50, 1, 200),
|
|
62
|
+
LLM_EXTRACTION: safeParseInt(
|
|
63
|
+
process.env.LLM_CONCURRENCY || process.env.LLM_EXTRACTION_CONCURRENCY,
|
|
64
|
+
50,
|
|
65
|
+
1,
|
|
66
|
+
200
|
|
67
|
+
)
|
|
68
|
+
};
|
|
69
|
+
const SCRAPER = {
|
|
70
|
+
BATCH_SIZE: 30,
|
|
71
|
+
EXTRACTION_PREFIX: "Extract from document only \u2014 never hallucinate or add external knowledge.",
|
|
72
|
+
EXTRACTION_SUFFIX: "First line = content, not preamble. No confirmation messages."
|
|
73
|
+
};
|
|
74
|
+
const REDDIT = {
|
|
75
|
+
BATCH_SIZE: 10,
|
|
76
|
+
MAX_WORDS_PER_POST: 5e4,
|
|
77
|
+
MAX_WORDS_TOTAL: 5e5,
|
|
78
|
+
MIN_POSTS: 1,
|
|
79
|
+
MAX_POSTS: 50,
|
|
80
|
+
RETRY_COUNT: 5,
|
|
81
|
+
RETRY_DELAYS: [2e3, 4e3, 8e3, 16e3, 32e3]
|
|
82
|
+
};
|
|
83
|
+
const CTR_WEIGHTS = {
|
|
84
|
+
1: 100,
|
|
85
|
+
2: 60,
|
|
86
|
+
3: 48.89,
|
|
87
|
+
4: 33.33,
|
|
88
|
+
5: 28.89,
|
|
89
|
+
6: 26.44,
|
|
90
|
+
7: 24.44,
|
|
91
|
+
8: 17.78,
|
|
92
|
+
9: 13.33,
|
|
93
|
+
10: 12.56
|
|
94
|
+
};
|
|
95
|
+
function parseLlmReasoningEffort(value) {
|
|
96
|
+
if (value === "none") return "none";
|
|
97
|
+
if (value && VALID_REASONING_EFFORTS.includes(value)) {
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
return "low";
|
|
101
|
+
}
|
|
102
|
+
function envWithFallback(...names) {
|
|
103
|
+
for (const name of names) {
|
|
104
|
+
const val = process.env[name]?.trim();
|
|
105
|
+
if (val) return val;
|
|
106
|
+
}
|
|
107
|
+
return void 0;
|
|
108
|
+
}
|
|
109
|
+
let cachedLlmExtraction = null;
|
|
110
|
+
function getLlmExtraction() {
|
|
111
|
+
if (cachedLlmExtraction) return cachedLlmExtraction;
|
|
112
|
+
cachedLlmExtraction = {
|
|
113
|
+
API_KEY: envWithFallback("LLM_API_KEY", "LLM_EXTRACTION_API_KEY", "OPENROUTER_API_KEY") || "",
|
|
114
|
+
BASE_URL: envWithFallback("LLM_BASE_URL", "LLM_EXTRACTION_BASE_URL", "OPENROUTER_BASE_URL") || "https://openrouter.ai/api/v1",
|
|
115
|
+
MODEL: envWithFallback("LLM_MODEL", "LLM_EXTRACTION_MODEL") || "openai/gpt-5.4-mini",
|
|
116
|
+
FALLBACK_MODEL: envWithFallback("LLM_FALLBACK_MODEL", "RESEARCH_FALLBACK_MODEL") || "google/gemini-2.5-flash",
|
|
117
|
+
MAX_TOKENS: safeParseInt(envWithFallback("LLM_MAX_TOKENS", "LLM_EXTRACTION_MAX_TOKENS"), 8e3, 1e3, 32e3),
|
|
118
|
+
REASONING_EFFORT: parseLlmReasoningEffort(envWithFallback("LLM_REASONING", "LLM_EXTRACTION_REASONING"))
|
|
119
|
+
};
|
|
120
|
+
return cachedLlmExtraction;
|
|
121
|
+
}
|
|
122
|
+
const LLM_EXTRACTION = new Proxy({}, {
|
|
123
|
+
get(_target, prop) {
|
|
124
|
+
return getLlmExtraction()[prop];
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
export {
|
|
128
|
+
CONCURRENCY,
|
|
129
|
+
CTR_WEIGHTS,
|
|
130
|
+
LLM_EXTRACTION,
|
|
131
|
+
REDDIT,
|
|
132
|
+
SCRAPER,
|
|
133
|
+
SERVER,
|
|
134
|
+
getCapabilities,
|
|
135
|
+
getMissingEnvMessage,
|
|
136
|
+
parseEnv
|
|
137
|
+
};
|
|
138
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/config/index.ts"],
|
|
4
|
+
"sourcesContent": ["/**\n * Consolidated configuration\n * All environment variables, constants, and LLM config in one place\n */\n\nimport { Logger } from 'mcp-use';\n\nimport { VERSION, PACKAGE_NAME, PACKAGE_DESCRIPTION } from '../version.js';\n\n// ============================================================================\n// Safe Integer Parsing Helper\n// ============================================================================\n\n/**\n * Safely parse an integer from environment variable with bounds checking\n */\nfunction safeParseInt(\n value: string | undefined,\n defaultVal: number,\n min: number,\n max: number\n): number {\n const logger = Logger.get('config');\n\n if (!value) {\n return defaultVal;\n }\n\n const parsed = parseInt(value, 10);\n\n if (isNaN(parsed)) {\n logger.warn(`Invalid number \"${value}\", using default ${defaultVal}`);\n return defaultVal;\n }\n\n if (parsed < min) {\n logger.warn(`Value ${parsed} below minimum ${min}, clamping to ${min}`);\n return min;\n }\n\n if (parsed > max) {\n logger.warn(`Value ${parsed} above maximum ${max}, clamping to ${max}`);\n return max;\n }\n\n return parsed;\n}\n\n// ============================================================================\n// Reasoning Effort Validation\n// ============================================================================\n\nconst VALID_REASONING_EFFORTS = ['low', 'medium', 'high'] as const;\ntype ReasoningEffort = typeof VALID_REASONING_EFFORTS[number];\n\n// ============================================================================\n// Environment Parsing\n// ============================================================================\n\ninterface EnvConfig {\n SCRAPER_API_KEY: string;\n SEARCH_API_KEY: string | undefined;\n REDDIT_CLIENT_ID: string | undefined;\n REDDIT_CLIENT_SECRET: string | undefined;\n}\n\nlet cachedEnv: EnvConfig | null = null;\n\nexport function parseEnv(): EnvConfig {\n if (cachedEnv) return cachedEnv;\n cachedEnv = {\n SCRAPER_API_KEY: process.env.SCRAPEDO_API_KEY || '',\n SEARCH_API_KEY: process.env.SERPER_API_KEY || undefined,\n REDDIT_CLIENT_ID: process.env.REDDIT_CLIENT_ID || undefined,\n REDDIT_CLIENT_SECRET: process.env.REDDIT_CLIENT_SECRET || undefined,\n };\n return cachedEnv;\n}\n\n// ============================================================================\n// MCP Server Configuration\n// ============================================================================\n\nexport const SERVER = {\n NAME: PACKAGE_NAME,\n VERSION: VERSION,\n DESCRIPTION: PACKAGE_DESCRIPTION,\n} as const;\n\n// ============================================================================\n// Capability Detection (which features are available based on ENV)\n// ============================================================================\n\nexport interface Capabilities {\n reddit: boolean; // REDDIT_CLIENT_ID + REDDIT_CLIENT_SECRET\n search: boolean; // SERPER_API_KEY\n scraping: boolean; // SCRAPEDO_API_KEY\n llmExtraction: boolean; // LLM_API_KEY (or legacy: LLM_EXTRACTION_API_KEY, OPENROUTER_API_KEY)\n}\n\nexport function getCapabilities(): Capabilities {\n const env = parseEnv();\n return {\n reddit: !!(env.REDDIT_CLIENT_ID && env.REDDIT_CLIENT_SECRET),\n search: !!env.SEARCH_API_KEY,\n scraping: !!env.SCRAPER_API_KEY,\n llmExtraction: !!LLM_EXTRACTION.API_KEY,\n };\n}\n\nexport function getMissingEnvMessage(capability: keyof Capabilities): string {\n const messages: Record<keyof Capabilities, string> = {\n reddit: '\u274C **Reddit tools unavailable.** Set `REDDIT_CLIENT_ID` and `REDDIT_CLIENT_SECRET` to enable `get-reddit-post`.\\n\\n\uD83D\uDC49 Create a Reddit app at: https://www.reddit.com/prefs/apps (select \"script\" type)',\n search: '\u274C **Search unavailable.** Set `SERPER_API_KEY` to enable `web-search` (including `scope: \"reddit\"`).\\n\\n\uD83D\uDC49 Get your free API key at: https://serper.dev (2,500 free queries)',\n scraping: '\u274C **Web scraping unavailable.** Set `SCRAPEDO_API_KEY` to enable `scrape-links`.\\n\\n\uD83D\uDC49 Sign up at: https://scrape.do (1,000 free credits)',\n llmExtraction: '\u26A0\uFE0F **AI extraction disabled.** Set `LLM_API_KEY` to enable AI-powered content extraction and search classification.\\n\\nScraping will work but without intelligent content filtering.',\n };\n return messages[capability];\n}\n\n// ============================================================================\n// Concurrency Limits\n// ============================================================================\n\nexport const CONCURRENCY = {\n SEARCH: safeParseInt(process.env.CONCURRENCY_SEARCH, 50, 1, 200),\n SCRAPER: safeParseInt(process.env.CONCURRENCY_SCRAPER, 50, 1, 200),\n REDDIT: safeParseInt(process.env.CONCURRENCY_REDDIT, 50, 1, 200),\n LLM_EXTRACTION: safeParseInt(\n process.env.LLM_CONCURRENCY || process.env.LLM_EXTRACTION_CONCURRENCY,\n 50, 1, 200,\n ),\n} as const;\n\nexport const SCRAPER = {\n BATCH_SIZE: 30,\n EXTRACTION_PREFIX: 'Extract from document only \u2014 never hallucinate or add external knowledge.',\n EXTRACTION_SUFFIX: 'First line = content, not preamble. No confirmation messages.',\n} as const;\n\n// ============================================================================\n// Reddit Configuration\n// ============================================================================\n\nexport const REDDIT = {\n BATCH_SIZE: 10,\n MAX_WORDS_PER_POST: 50_000,\n MAX_WORDS_TOTAL: 500_000,\n MIN_POSTS: 1,\n MAX_POSTS: 50,\n RETRY_COUNT: 5,\n RETRY_DELAYS: [2000, 4000, 8000, 16000, 32000] as const,\n} as const;\n\n// ============================================================================\n// CTR Weights for URL Ranking (inspired from CTR research)\n// ============================================================================\n\nexport const CTR_WEIGHTS: Record<number, number> = {\n 1: 100.00,\n 2: 60.00,\n 3: 48.89,\n 4: 33.33,\n 5: 28.89,\n 6: 26.44,\n 7: 24.44,\n 8: 17.78,\n 9: 13.33,\n 10: 12.56,\n} as const;\n\n// ============================================================================\n// LLM Configuration\n//\n// Env var naming: LLM_* (canonical) with backwards-compatible fallbacks.\n// Fallback chain per variable:\n// LLM_API_KEY \u2190 LLM_EXTRACTION_API_KEY \u2190 OPENROUTER_API_KEY\n// LLM_BASE_URL \u2190 LLM_EXTRACTION_BASE_URL \u2190 OPENROUTER_BASE_URL \u2190 default\n// LLM_MODEL \u2190 LLM_EXTRACTION_MODEL \u2190 default\n// LLM_MAX_TOKENS \u2190 LLM_EXTRACTION_MAX_TOKENS \u2190 8000\n// LLM_REASONING \u2190 LLM_EXTRACTION_REASONING \u2190 'low'\n// LLM_CONCURRENCY \u2190 LLM_EXTRACTION_CONCURRENCY \u2190 10\n// ============================================================================\n\ntype LlmReasoningEffort = ReasoningEffort | 'none';\n\nfunction parseLlmReasoningEffort(value: string | undefined): LlmReasoningEffort {\n if (value === 'none') return 'none';\n if (value && VALID_REASONING_EFFORTS.includes(value as ReasoningEffort)) {\n return value as ReasoningEffort;\n }\n return 'low';\n}\n\ninterface LlmExtractionConfig {\n readonly MODEL: string;\n readonly FALLBACK_MODEL: string;\n readonly BASE_URL: string;\n readonly API_KEY: string;\n readonly MAX_TOKENS: number;\n readonly REASONING_EFFORT: LlmReasoningEffort;\n}\n\n/** Read an env var with a backwards-compatible fallback chain */\nfunction envWithFallback(...names: string[]): string | undefined {\n for (const name of names) {\n const val = process.env[name]?.trim();\n if (val) return val;\n }\n return undefined;\n}\n\nlet cachedLlmExtraction: LlmExtractionConfig | null = null;\n\nfunction getLlmExtraction(): LlmExtractionConfig {\n if (cachedLlmExtraction) return cachedLlmExtraction;\n cachedLlmExtraction = {\n API_KEY: envWithFallback('LLM_API_KEY', 'LLM_EXTRACTION_API_KEY', 'OPENROUTER_API_KEY') || '',\n BASE_URL: envWithFallback('LLM_BASE_URL', 'LLM_EXTRACTION_BASE_URL', 'OPENROUTER_BASE_URL') || 'https://openrouter.ai/api/v1',\n MODEL: envWithFallback('LLM_MODEL', 'LLM_EXTRACTION_MODEL') || 'openai/gpt-5.4-mini',\n FALLBACK_MODEL: envWithFallback('LLM_FALLBACK_MODEL', 'RESEARCH_FALLBACK_MODEL') || 'google/gemini-2.5-flash',\n MAX_TOKENS: safeParseInt(envWithFallback('LLM_MAX_TOKENS', 'LLM_EXTRACTION_MAX_TOKENS'), 8000, 1000, 32000),\n REASONING_EFFORT: parseLlmReasoningEffort(envWithFallback('LLM_REASONING', 'LLM_EXTRACTION_REASONING')),\n };\n return cachedLlmExtraction;\n}\n\nexport const LLM_EXTRACTION: LlmExtractionConfig = new Proxy({} as LlmExtractionConfig, {\n get(_target, prop: string) {\n return getLlmExtraction()[prop as keyof LlmExtractionConfig];\n },\n});\n"],
|
|
5
|
+
"mappings": "AAKA,SAAS,cAAc;AAEvB,SAAS,SAAS,cAAc,2BAA2B;AAS3D,SAAS,aACP,OACA,YACA,KACA,KACQ;AACR,QAAM,SAAS,OAAO,IAAI,QAAQ;AAElC,MAAI,CAAC,OAAO;AACV,WAAO;AAAA,EACT;AAEA,QAAM,SAAS,SAAS,OAAO,EAAE;AAEjC,MAAI,MAAM,MAAM,GAAG;AACjB,WAAO,KAAK,mBAAmB,KAAK,oBAAoB,UAAU,EAAE;AACpE,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,KAAK;AAChB,WAAO,KAAK,SAAS,MAAM,kBAAkB,GAAG,iBAAiB,GAAG,EAAE;AACtE,WAAO;AAAA,EACT;AAEA,MAAI,SAAS,KAAK;AAChB,WAAO,KAAK,SAAS,MAAM,kBAAkB,GAAG,iBAAiB,GAAG,EAAE;AACtE,WAAO;AAAA,EACT;AAEA,SAAO;AACT;AAMA,MAAM,0BAA0B,CAAC,OAAO,UAAU,MAAM;AAcxD,IAAI,YAA8B;AAE3B,SAAS,WAAsB;AACpC,MAAI,UAAW,QAAO;AACtB,cAAY;AAAA,IACV,iBAAiB,QAAQ,IAAI,oBAAoB;AAAA,IACjD,gBAAgB,QAAQ,IAAI,kBAAkB;AAAA,IAC9C,kBAAkB,QAAQ,IAAI,oBAAoB;AAAA,IAClD,sBAAsB,QAAQ,IAAI,wBAAwB;AAAA,EAC5D;AACA,SAAO;AACT;AAMO,MAAM,SAAS;AAAA,EACpB,MAAM;AAAA,EACN;AAAA,EACA,aAAa;AACf;AAaO,SAAS,kBAAgC;AAC9C,QAAM,MAAM,SAAS;AACrB,SAAO;AAAA,IACL,QAAQ,CAAC,EAAE,IAAI,oBAAoB,IAAI;AAAA,IACvC,QAAQ,CAAC,CAAC,IAAI;AAAA,IACd,UAAU,CAAC,CAAC,IAAI;AAAA,IAChB,eAAe,CAAC,CAAC,eAAe;AAAA,EAClC;AACF;AAEO,SAAS,qBAAqB,YAAwC;AAC3E,QAAM,WAA+C;AAAA,IACnD,QAAQ;AAAA,IACR,QAAQ;AAAA,IACR,UAAU;AAAA,IACV,eAAe;AAAA,EACjB;AACA,SAAO,SAAS,UAAU;AAC5B;AAMO,MAAM,cAAc;AAAA,EACzB,QAAQ,aAAa,QAAQ,IAAI,oBAAoB,IAAI,GAAG,GAAG;AAAA,EAC/D,SAAS,aAAa,QAAQ,IAAI,qBAAqB,IAAI,GAAG,GAAG;AAAA,EACjE,QAAQ,aAAa,QAAQ,IAAI,oBAAoB,IAAI,GAAG,GAAG;AAAA,EAC/D,gBAAgB;AAAA,IACd,QAAQ,IAAI,mBAAmB,QAAQ,IAAI;AAAA,IAC3C;AAAA,IAAI;AAAA,IAAG;AAAA,EACT;AACF;AAEO,MAAM,UAAU;AAAA,EACrB,YAAY;AAAA,EACZ,mBAAmB;AAAA,EACnB,mBAAmB;AACrB;AAMO,MAAM,SAAS;AAAA,EACpB,YAAY;AAAA,EACZ,oBAAoB;AAAA,EACpB,iBAAiB;AAAA,EACjB,WAAW;AAAA,EACX,WAAW;AAAA,EACX,aAAa;AAAA,EACb,cAAc,CAAC,KAAM,KAAM,KAAM,MAAO,IAAK;AAC/C;AAMO,MAAM,cAAsC;AAAA,EACjD,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,GAAG;AAAA,EACH,IAAI;AACN;AAiBA,SAAS,wBAAwB,OAA+C;AAC9E,MAAI,UAAU,OAAQ,QAAO;AAC7B,MAAI,SAAS,wBAAwB,SAAS,KAAwB,GAAG;AACvE,WAAO;AAAA,EACT;AACA,SAAO;AACT;AAYA,SAAS,mBAAmB,OAAqC;AAC/D,aAAW,QAAQ,OAAO;AACxB,UAAM,MAAM,QAAQ,IAAI,IAAI,GAAG,KAAK;AACpC,QAAI,IAAK,QAAO;AAAA,EAClB;AACA,SAAO;AACT;AAEA,IAAI,sBAAkD;AAEtD,SAAS,mBAAwC;AAC/C,MAAI,oBAAqB,QAAO;AAChC,wBAAsB;AAAA,IACpB,SAAS,gBAAgB,eAAe,0BAA0B,oBAAoB,KAAK;AAAA,IAC3F,UAAU,gBAAgB,gBAAgB,2BAA2B,qBAAqB,KAAK;AAAA,IAC/F,OAAO,gBAAgB,aAAa,sBAAsB,KAAK;AAAA,IAC/D,gBAAgB,gBAAgB,sBAAsB,yBAAyB,KAAK;AAAA,IACpF,YAAY,aAAa,gBAAgB,kBAAkB,2BAA2B,GAAG,KAAM,KAAM,IAAK;AAAA,IAC1G,kBAAkB,wBAAwB,gBAAgB,iBAAiB,0BAA0B,CAAC;AAAA,EACxG;AACA,SAAO;AACT;AAEO,MAAM,iBAAsC,IAAI,MAAM,CAAC,GAA0B;AAAA,EACtF,IAAI,SAAS,MAAc;AACzB,WAAO,iBAAiB,EAAE,IAAiC;AAAA,EAC7D;AACF,CAAC;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { text } from "mcp-use/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
function registerDeepResearchPrompt(server) {
|
|
4
|
+
server.prompt(
|
|
5
|
+
{
|
|
6
|
+
name: "deep-research",
|
|
7
|
+
title: "Deep Research",
|
|
8
|
+
description: "Multi-pass research loop on a topic using the research-powerpack tools.",
|
|
9
|
+
schema: z.object({
|
|
10
|
+
topic: z.string().describe('Topic to research. Be specific about what "done" looks like \u2014 the first tool call will generate a goal-tailored research brief from it.')
|
|
11
|
+
})
|
|
12
|
+
},
|
|
13
|
+
async ({ topic }) => text(
|
|
14
|
+
[
|
|
15
|
+
"You are a research agent using the research-powerpack MCP tools. You are running a research LOOP, not answering from memory \u2014 every claim in your final answer must be traceable to a scraped page or expanded Reddit thread. Never cite a URL from a search snippet alone.",
|
|
16
|
+
"",
|
|
17
|
+
`Research goal: ${topic}`,
|
|
18
|
+
"",
|
|
19
|
+
"## Workflow",
|
|
20
|
+
"",
|
|
21
|
+
"1. **Call `start-research` with `goal` = the research goal above.** The server returns a goal-tailored brief: classified goal type, source priorities, a Reddit-branch recommendation, anticipated gaps, 3\u20138 pre-built concept groups (25\u201350 concrete queries ready to fire), first-pass scrape targets, and success criteria. Read it carefully \u2014 it tells you exactly what pass 1 should look like.",
|
|
22
|
+
'2. **Fire ONE `web-search` call** with every query from every concept group concatenated into the flat `queries` array. Set `extract` to a specific description of what "relevant" means for this goal (not just a keyword).',
|
|
23
|
+
"3. **Read the classifier output**: `synthesis` (grounded in `[rank]` citations), `gaps` (what is missing \u2014 each with an id), `refine_queries` (follow-ups linked to gap ids). If confidence is `low`, trust the `gaps` list more than the synthesis.",
|
|
24
|
+
"4. **Scrape with `scrape-links`** \u2014 every HIGHLY_RELEVANT plus the 2\u20133 best MAYBE_RELEVANT, batched in one call. Write `extract` as facets separated by `|` (e.g. `root cause | affected versions | fix | workarounds`). Each page returns a structured extract with `## Matches`, `## Not found` (admitted gaps), and `## Follow-up signals` (new terms + referenced-but-unscraped URLs that should seed the next search pass).",
|
|
25
|
+
'5. **Reddit branch** \u2014 only if the brief says `fire_reddit_branch: true`. Call `web-search` again with `scope: "reddit"` for post-permalink discovery, then `get-reddit-post` on the 3\u201310 strongest threads. Never cite a Reddit thread you have not expanded.',
|
|
26
|
+
"6. **Loop**: build new concept groups for the unclosed gaps, fire another `web-search`, scrape, read. Stop when every gap is closed AND no new terms appear, OR after 4 passes \u2014 whichever comes first.",
|
|
27
|
+
"",
|
|
28
|
+
"## Output discipline",
|
|
29
|
+
"",
|
|
30
|
+
"- Cite URL (or Reddit thread permalink) for every non-trivial claim.",
|
|
31
|
+
"- Separate documented facts from inferred conclusions explicitly.",
|
|
32
|
+
"- Include scrape dates for time-sensitive claims.",
|
|
33
|
+
"- If any success criterion from the brief is unmet, say so \u2014 do not paper over it."
|
|
34
|
+
].join("\n")
|
|
35
|
+
)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
export {
|
|
39
|
+
registerDeepResearchPrompt
|
|
40
|
+
};
|
|
41
|
+
//# sourceMappingURL=deep-research.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/prompts/deep-research.ts"],
|
|
4
|
+
"sourcesContent": ["import { text, type MCPServer } from 'mcp-use/server';\nimport { z } from 'zod';\n\nexport function registerDeepResearchPrompt(server: MCPServer): void {\n server.prompt(\n {\n name: 'deep-research',\n title: 'Deep Research',\n description: 'Multi-pass research loop on a topic using the research-powerpack tools.',\n schema: z.object({\n topic: z.string().describe('Topic to research. Be specific about what \"done\" looks like \u2014 the first tool call will generate a goal-tailored research brief from it.'),\n }),\n },\n async ({ topic }) => text(\n [\n 'You are a research agent using the research-powerpack MCP tools. You are running a research LOOP, not answering from memory \u2014 every claim in your final answer must be traceable to a scraped page or expanded Reddit thread. Never cite a URL from a search snippet alone.',\n '',\n `Research goal: ${topic}`,\n '',\n '## Workflow',\n '',\n '1. **Call `start-research` with `goal` = the research goal above.** The server returns a goal-tailored brief: classified goal type, source priorities, a Reddit-branch recommendation, anticipated gaps, 3\u20138 pre-built concept groups (25\u201350 concrete queries ready to fire), first-pass scrape targets, and success criteria. Read it carefully \u2014 it tells you exactly what pass 1 should look like.',\n '2. **Fire ONE `web-search` call** with every query from every concept group concatenated into the flat `queries` array. Set `extract` to a specific description of what \"relevant\" means for this goal (not just a keyword).',\n '3. **Read the classifier output**: `synthesis` (grounded in `[rank]` citations), `gaps` (what is missing \u2014 each with an id), `refine_queries` (follow-ups linked to gap ids). If confidence is `low`, trust the `gaps` list more than the synthesis.',\n '4. **Scrape with `scrape-links`** \u2014 every HIGHLY_RELEVANT plus the 2\u20133 best MAYBE_RELEVANT, batched in one call. Write `extract` as facets separated by `|` (e.g. `root cause | affected versions | fix | workarounds`). Each page returns a structured extract with `## Matches`, `## Not found` (admitted gaps), and `## Follow-up signals` (new terms + referenced-but-unscraped URLs that should seed the next search pass).',\n '5. **Reddit branch** \u2014 only if the brief says `fire_reddit_branch: true`. Call `web-search` again with `scope: \"reddit\"` for post-permalink discovery, then `get-reddit-post` on the 3\u201310 strongest threads. Never cite a Reddit thread you have not expanded.',\n '6. **Loop**: build new concept groups for the unclosed gaps, fire another `web-search`, scrape, read. Stop when every gap is closed AND no new terms appear, OR after 4 passes \u2014 whichever comes first.',\n '',\n '## Output discipline',\n '',\n '- Cite URL (or Reddit thread permalink) for every non-trivial claim.',\n '- Separate documented facts from inferred conclusions explicitly.',\n '- Include scrape dates for time-sensitive claims.',\n '- If any success criterion from the brief is unmet, say so \u2014 do not paper over it.',\n ].join('\\n'),\n ),\n );\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,YAA4B;AACrC,SAAS,SAAS;AAEX,SAAS,2BAA2B,QAAyB;AAClE,SAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,aAAa;AAAA,MACb,QAAQ,EAAE,OAAO;AAAA,QACf,OAAO,EAAE,OAAO,EAAE,SAAS,8IAAyI;AAAA,MACtK,CAAC;AAAA,IACH;AAAA,IACA,OAAO,EAAE,MAAM,MAAM;AAAA,MACnB;AAAA,QACE;AAAA,QACA;AAAA,QACA,kBAAkB,KAAK;AAAA,QACvB;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,MACF,EAAE,KAAK,IAAI;AAAA,IACb;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { text } from "mcp-use/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
function registerRedditSentimentPrompt(server) {
|
|
4
|
+
server.prompt(
|
|
5
|
+
{
|
|
6
|
+
name: "reddit-sentiment",
|
|
7
|
+
title: "Reddit Sentiment",
|
|
8
|
+
description: "Research Reddit sentiment for a topic using the research-powerpack tools \u2014 lived experience, migration stories, agreement/dissent distribution.",
|
|
9
|
+
schema: z.object({
|
|
10
|
+
topic: z.string().describe('Topic to evaluate. Phrase it as a sentiment question \u2014 "what developers actually think about X", "why teams moved from X to Y".'),
|
|
11
|
+
subreddits: z.string().optional().describe('Optional comma-separated subreddit filters, e.g. "webdev,javascript".')
|
|
12
|
+
})
|
|
13
|
+
},
|
|
14
|
+
async ({ topic, subreddits }) => {
|
|
15
|
+
const subredditList = subreddits ? subreddits.split(",").map((value) => value.trim().replace(/^\/?r\//i, "")).filter(Boolean) : [];
|
|
16
|
+
const subredditScope = subredditList.length ? ` Scope Reddit searches to ${subredditList.map((s) => `r/${s}`).join(", ")} when possible.` : "";
|
|
17
|
+
return text(
|
|
18
|
+
[
|
|
19
|
+
"You are a research agent using the research-powerpack MCP tools to characterize Reddit sentiment. You are running a research LOOP, not answering from memory. Sentiment claims must be traceable to specific expanded Reddit threads with verbatim quotes \u2014 never cite a thread you have not expanded with `get-reddit-post`.",
|
|
20
|
+
"",
|
|
21
|
+
`Research goal: Reddit sentiment on "${topic}" \u2014 agreement distribution, dissent distribution, representative verbatim quotes with attribution, and the strongest causal explanations.${subredditScope}`,
|
|
22
|
+
"",
|
|
23
|
+
"## Workflow",
|
|
24
|
+
"",
|
|
25
|
+
`1. **Call \`start-research\` with \`goal\` = the research goal above.** The server returns a goal-tailored brief. For a sentiment goal you should see \`goal_class: sentiment\`, \`fire_reddit_branch: true\`, source priorities led by \`reddit\`/\`hackernews\`/\`blogs\`, and concept groups that include \`site:reddit.com\` queries.`,
|
|
26
|
+
'2. **Fire `web-search` twice in parallel** \u2014 one call with the open-web concept-group queries, one with `scope: "reddit"` (server appends `site:reddit.com` and filters to post permalinks; no need to add it to the query yourself). For both, set `extract` to describe the shape of the sentiment answer (e.g. "agreement reasons | dissent reasons | representative quotes | migration drivers").',
|
|
27
|
+
"3. **Shortlist the strongest Reddit threads** \u2014 those with (a) high comment count, (b) visible disagreement in replies, (c) specific stack/environment details from the OP. Avoid single-comment threads.",
|
|
28
|
+
"4. **Fetch with `get-reddit-post`** \u2014 batch 3\u201310 threads in one call. Read every comment tree end-to-end, not just the top-voted reply.",
|
|
29
|
+
'5. **Scrape supporting evidence with `scrape-links`** \u2014 blog post-mortems, GitHub issues, HN discussions referenced in the threads. Use `extract` = "concrete reasons | stack details | version numbers | outcome". The scraper will preserve verbatim quotes and surface referenced-but-unscraped URLs under `## Follow-up signals`.',
|
|
30
|
+
'6. **Loop**: if the classifier flags gaps ("no dissent voices captured", "no migration timeline") or the anticipated_gaps from the brief are unmet, build new concept groups and run another pass. Stop after 4 passes or when sentiment distribution stabilizes across two passes.',
|
|
31
|
+
"",
|
|
32
|
+
"## Output discipline",
|
|
33
|
+
"",
|
|
34
|
+
'- Report sentiment as a distribution ("~N of M replies agreed / ~K dissented / rest off-topic"), not a single mood label.',
|
|
35
|
+
"- Cite every quote with the Reddit thread permalink plus `u/username` attribution.",
|
|
36
|
+
"- Separate OP claims from reply-thread consensus \u2014 they often diverge.",
|
|
37
|
+
"- If dissent is present, surface the strongest dissenting quote verbatim, even if the majority view dominates.",
|
|
38
|
+
"- Include the scrape date on every time-sensitive claim."
|
|
39
|
+
].join("\n")
|
|
40
|
+
);
|
|
41
|
+
}
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
export {
|
|
45
|
+
registerRedditSentimentPrompt
|
|
46
|
+
};
|
|
47
|
+
//# sourceMappingURL=reddit-sentiment.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/prompts/reddit-sentiment.ts"],
|
|
4
|
+
"sourcesContent": ["import { text, type MCPServer } from 'mcp-use/server';\nimport { z } from 'zod';\n\nexport function registerRedditSentimentPrompt(server: MCPServer): void {\n server.prompt(\n {\n name: 'reddit-sentiment',\n title: 'Reddit Sentiment',\n description: 'Research Reddit sentiment for a topic using the research-powerpack tools \u2014 lived experience, migration stories, agreement/dissent distribution.',\n schema: z.object({\n topic: z.string().describe('Topic to evaluate. Phrase it as a sentiment question \u2014 \"what developers actually think about X\", \"why teams moved from X to Y\".'),\n subreddits: z.string().optional().describe('Optional comma-separated subreddit filters, e.g. \"webdev,javascript\".'),\n }),\n },\n async ({ topic, subreddits }) => {\n const subredditList = subreddits\n ? subreddits\n .split(',')\n .map((value) => value.trim().replace(/^\\/?r\\//i, ''))\n .filter(Boolean)\n : [];\n const subredditScope = subredditList.length\n ? ` Scope Reddit searches to ${subredditList.map((s) => `r/${s}`).join(', ')} when possible.`\n : '';\n\n return text(\n [\n 'You are a research agent using the research-powerpack MCP tools to characterize Reddit sentiment. You are running a research LOOP, not answering from memory. Sentiment claims must be traceable to specific expanded Reddit threads with verbatim quotes \u2014 never cite a thread you have not expanded with `get-reddit-post`.',\n '',\n `Research goal: Reddit sentiment on \"${topic}\" \u2014 agreement distribution, dissent distribution, representative verbatim quotes with attribution, and the strongest causal explanations.${subredditScope}`,\n '',\n '## Workflow',\n '',\n `1. **Call \\`start-research\\` with \\`goal\\` = the research goal above.** The server returns a goal-tailored brief. For a sentiment goal you should see \\`goal_class: sentiment\\`, \\`fire_reddit_branch: true\\`, source priorities led by \\`reddit\\`/\\`hackernews\\`/\\`blogs\\`, and concept groups that include \\`site:reddit.com\\` queries.`,\n '2. **Fire `web-search` twice in parallel** \u2014 one call with the open-web concept-group queries, one with `scope: \"reddit\"` (server appends `site:reddit.com` and filters to post permalinks; no need to add it to the query yourself). For both, set `extract` to describe the shape of the sentiment answer (e.g. \"agreement reasons | dissent reasons | representative quotes | migration drivers\").',\n '3. **Shortlist the strongest Reddit threads** \u2014 those with (a) high comment count, (b) visible disagreement in replies, (c) specific stack/environment details from the OP. Avoid single-comment threads.',\n '4. **Fetch with `get-reddit-post`** \u2014 batch 3\u201310 threads in one call. Read every comment tree end-to-end, not just the top-voted reply.',\n '5. **Scrape supporting evidence with `scrape-links`** \u2014 blog post-mortems, GitHub issues, HN discussions referenced in the threads. Use `extract` = \"concrete reasons | stack details | version numbers | outcome\". The scraper will preserve verbatim quotes and surface referenced-but-unscraped URLs under `## Follow-up signals`.',\n '6. **Loop**: if the classifier flags gaps (\"no dissent voices captured\", \"no migration timeline\") or the anticipated_gaps from the brief are unmet, build new concept groups and run another pass. Stop after 4 passes or when sentiment distribution stabilizes across two passes.',\n '',\n '## Output discipline',\n '',\n '- Report sentiment as a distribution (\"~N of M replies agreed / ~K dissented / rest off-topic\"), not a single mood label.',\n '- Cite every quote with the Reddit thread permalink plus `u/username` attribution.',\n '- Separate OP claims from reply-thread consensus \u2014 they often diverge.',\n '- If dissent is present, surface the strongest dissenting quote verbatim, even if the majority view dominates.',\n '- Include the scrape date on every time-sensitive claim.',\n ].join('\\n'),\n );\n },\n );\n}\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,YAA4B;AACrC,SAAS,SAAS;AAEX,SAAS,8BAA8B,QAAyB;AACrE,SAAO;AAAA,IACL;AAAA,MACE,MAAM;AAAA,MACN,OAAO;AAAA,MACP,aAAa;AAAA,MACb,QAAQ,EAAE,OAAO;AAAA,QACf,OAAO,EAAE,OAAO,EAAE,SAAS,sIAAiI;AAAA,QAC5J,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,uEAAuE;AAAA,MACpH,CAAC;AAAA,IACH;AAAA,IACA,OAAO,EAAE,OAAO,WAAW,MAAM;AAC/B,YAAM,gBAAgB,aAClB,WACG,MAAM,GAAG,EACT,IAAI,CAAC,UAAU,MAAM,KAAK,EAAE,QAAQ,YAAY,EAAE,CAAC,EACnD,OAAO,OAAO,IACjB,CAAC;AACL,YAAM,iBAAiB,cAAc,SACjC,6BAA6B,cAAc,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,EAAE,KAAK,IAAI,CAAC,oBAC1E;AAEJ,aAAO;AAAA,QACL;AAAA,UACE;AAAA,UACA;AAAA,UACA,uCAAuC,KAAK,iJAA4I,cAAc;AAAA,UACtM;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,UACA;AAAA,QACF,EAAE,KAAK,IAAI;AAAA,MACb;AAAA,IACF;AAAA,EACF;AACF;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const getRedditPostParamsSchema = z.object({
|
|
3
|
+
urls: z.array(
|
|
4
|
+
z.string().url({ message: "get-reddit-post: Each URL must be valid" }).describe("A Reddit post URL.")
|
|
5
|
+
).min(1, { message: "get-reddit-post: At least 1 URL required" }).describe("Reddit post URLs to fetch. Each post is returned with its full threaded comment tree.")
|
|
6
|
+
}).strict();
|
|
7
|
+
const getRedditPostOutputSchema = z.object({
|
|
8
|
+
content: z.string().describe("Raw Reddit posts with threaded comments including author, score, and OP markers."),
|
|
9
|
+
metadata: z.object({
|
|
10
|
+
total_items: z.number().int().nonnegative().describe("Number of URLs processed."),
|
|
11
|
+
successful: z.number().int().nonnegative().describe("Posts fetched successfully."),
|
|
12
|
+
failed: z.number().int().nonnegative().describe("Posts that failed to fetch."),
|
|
13
|
+
execution_time_ms: z.number().int().nonnegative().describe("Wall clock time in milliseconds."),
|
|
14
|
+
rate_limit_hits: z.number().int().nonnegative().describe("Reddit API rate-limit retries.")
|
|
15
|
+
}).strict()
|
|
16
|
+
}).strict();
|
|
17
|
+
export {
|
|
18
|
+
getRedditPostOutputSchema,
|
|
19
|
+
getRedditPostParamsSchema
|
|
20
|
+
};
|
|
21
|
+
//# sourceMappingURL=reddit.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/schemas/reddit.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod';\n\n// Note: search-reddit was deleted in mcp-revisions/tool-surface/01.\n// Reddit discovery now flows through `web-search` with `scope: \"reddit\"`.\n// See src/schemas/web-search.ts for the scope param.\n\n// ============================================================================\n// get-reddit-post\n// ============================================================================\n\nexport const getRedditPostParamsSchema = z.object({\n urls: z\n .array(\n z.string()\n .url({ message: 'get-reddit-post: Each URL must be valid' })\n .describe('A Reddit post URL.'),\n )\n .min(1, { message: 'get-reddit-post: At least 1 URL required' })\n .describe('Reddit post URLs to fetch. Each post is returned with its full threaded comment tree.'),\n}).strict();\n\nexport type GetRedditPostParams = z.infer<typeof getRedditPostParamsSchema>;\n\nexport const getRedditPostOutputSchema = z.object({\n content: z\n .string()\n .describe('Raw Reddit posts with threaded comments including author, score, and OP markers.'),\n metadata: z.object({\n total_items: z.number().int().nonnegative().describe('Number of URLs processed.'),\n successful: z.number().int().nonnegative().describe('Posts fetched successfully.'),\n failed: z.number().int().nonnegative().describe('Posts that failed to fetch.'),\n execution_time_ms: z.number().int().nonnegative().describe('Wall clock time in milliseconds.'),\n rate_limit_hits: z.number().int().nonnegative().describe('Reddit API rate-limit retries.'),\n }).strict(),\n}).strict();\n\nexport type GetRedditPostOutput = z.infer<typeof getRedditPostOutputSchema>;\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAUX,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,MAAM,EACH;AAAA,IACC,EAAE,OAAO,EACN,IAAI,EAAE,SAAS,0CAA0C,CAAC,EAC1D,SAAS,oBAAoB;AAAA,EAClC,EACC,IAAI,GAAG,EAAE,SAAS,2CAA2C,CAAC,EAC9D,SAAS,uFAAuF;AACrG,CAAC,EAAE,OAAO;AAIH,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,SAAS,EACN,OAAO,EACP,SAAS,kFAAkF;AAAA,EAC9F,UAAU,EAAE,OAAO;AAAA,IACjB,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,2BAA2B;AAAA,IAChF,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,6BAA6B;AAAA,IACjF,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,6BAA6B;AAAA,IAC7E,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,kCAAkC;AAAA,IAC7F,iBAAiB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,gCAAgC;AAAA,EAC3F,CAAC,EAAE,OAAO;AACZ,CAAC,EAAE,OAAO;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const urlSchema = z.string().url({ message: "scrape-links: Invalid URL format" }).refine(
|
|
3
|
+
(url) => url.startsWith("http://") || url.startsWith("https://"),
|
|
4
|
+
{ message: "scrape-links: URL must use http:// or https://" }
|
|
5
|
+
).describe("A fully-qualified HTTP or HTTPS URL to scrape.");
|
|
6
|
+
const scrapeLinksParamsSchema = z.object({
|
|
7
|
+
urls: z.array(urlSchema).min(1, { message: "scrape-links: At least 1 URL required" }).describe("Web page URLs to scrape and extract content from. Reddit URLs (`reddit.com/...`) are rejected with `UNSUPPORTED_URL_TYPE` \u2014 use get-reddit-post for `reddit.com/r/.../comments/...` permalinks."),
|
|
8
|
+
extract: z.string().min(1, { message: "scrape-links: extract cannot be empty" }).describe(
|
|
9
|
+
'Semantic extraction instruction. Describe the SHAPE of what you want, separated by `|`. The extractor classifies each page (docs / github-thread / reddit / marketing / cve / paper / announcement / qa / blog / changelog / release-notes) and adjusts emphasis per type: preserves numbers/versions/stacktraces verbatim from docs and CVE pages, quotes Reddit/HN with attribution plus sentiment distribution, flags what the page did NOT answer in a "Not found" section, and surfaces referenced-but-unscraped URLs in a "Follow-up signals" bulletin that feeds the next research loop. Good examples: "root cause | affected versions | fix | workarounds | timeline"; "pricing tiers | rate limits | enterprise contact | free-tier quotas"; "maintainer decisions | accepted fix commits | stacktraces | resolved version".'
|
|
10
|
+
)
|
|
11
|
+
}).strict();
|
|
12
|
+
const scrapeLinksOutputSchema = z.object({
|
|
13
|
+
content: z.string().describe("LLM-extracted content from scraped pages per the extract instructions."),
|
|
14
|
+
metadata: z.object({
|
|
15
|
+
total_items: z.number().int().nonnegative().describe("Number of URLs processed."),
|
|
16
|
+
successful: z.number().int().nonnegative().describe("URLs fetched successfully."),
|
|
17
|
+
failed: z.number().int().nonnegative().describe("URLs that failed."),
|
|
18
|
+
execution_time_ms: z.number().int().nonnegative().describe("Wall clock time in milliseconds."),
|
|
19
|
+
total_credits: z.number().int().nonnegative().describe("External scraping credits consumed.")
|
|
20
|
+
}).strict()
|
|
21
|
+
}).strict();
|
|
22
|
+
export {
|
|
23
|
+
scrapeLinksOutputSchema,
|
|
24
|
+
scrapeLinksParamsSchema
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=scrape-links.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/schemas/scrape-links.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod';\n\nconst urlSchema = z\n .string()\n .url({ message: 'scrape-links: Invalid URL format' })\n .refine(\n url => url.startsWith('http://') || url.startsWith('https://'),\n { message: 'scrape-links: URL must use http:// or https://' }\n )\n .describe('A fully-qualified HTTP or HTTPS URL to scrape.');\n\nexport const scrapeLinksParamsSchema = z.object({\n urls: z\n .array(urlSchema)\n .min(1, { message: 'scrape-links: At least 1 URL required' })\n .describe('Web page URLs to scrape and extract content from. Reddit URLs (`reddit.com/...`) are rejected with `UNSUPPORTED_URL_TYPE` \u2014 use get-reddit-post for `reddit.com/r/.../comments/...` permalinks.'),\n extract: z\n .string()\n .min(1, { message: 'scrape-links: extract cannot be empty' })\n .describe(\n 'Semantic extraction instruction. Describe the SHAPE of what you want, separated by `|`. The extractor classifies each page (docs / github-thread / reddit / marketing / cve / paper / announcement / qa / blog / changelog / release-notes) and adjusts emphasis per type: preserves numbers/versions/stacktraces verbatim from docs and CVE pages, quotes Reddit/HN with attribution plus sentiment distribution, flags what the page did NOT answer in a \"Not found\" section, and surfaces referenced-but-unscraped URLs in a \"Follow-up signals\" bulletin that feeds the next research loop. Good examples: \"root cause | affected versions | fix | workarounds | timeline\"; \"pricing tiers | rate limits | enterprise contact | free-tier quotas\"; \"maintainer decisions | accepted fix commits | stacktraces | resolved version\".',\n ),\n}).strict();\n\nexport type ScrapeLinksParams = z.infer<typeof scrapeLinksParamsSchema>;\n\nexport const scrapeLinksOutputSchema = z.object({\n content: z\n .string()\n .describe('LLM-extracted content from scraped pages per the extract instructions.'),\n metadata: z.object({\n total_items: z.number().int().nonnegative().describe('Number of URLs processed.'),\n successful: z.number().int().nonnegative().describe('URLs fetched successfully.'),\n failed: z.number().int().nonnegative().describe('URLs that failed.'),\n execution_time_ms: z.number().int().nonnegative().describe('Wall clock time in milliseconds.'),\n total_credits: z.number().int().nonnegative().describe('External scraping credits consumed.'),\n }).strict(),\n}).strict();\n\nexport type ScrapeLinksOutput = z.infer<typeof scrapeLinksOutputSchema>;\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAElB,MAAM,YAAY,EACf,OAAO,EACP,IAAI,EAAE,SAAS,mCAAmC,CAAC,EACnD;AAAA,EACC,SAAO,IAAI,WAAW,SAAS,KAAK,IAAI,WAAW,UAAU;AAAA,EAC7D,EAAE,SAAS,iDAAiD;AAC9D,EACC,SAAS,gDAAgD;AAErD,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,MAAM,EACH,MAAM,SAAS,EACf,IAAI,GAAG,EAAE,SAAS,wCAAwC,CAAC,EAC3D,SAAS,sMAAiM;AAAA,EAC7M,SAAS,EACN,OAAO,EACP,IAAI,GAAG,EAAE,SAAS,wCAAwC,CAAC,EAC3D;AAAA,IACC;AAAA,EACF;AACJ,CAAC,EAAE,OAAO;AAIH,MAAM,0BAA0B,EAAE,OAAO;AAAA,EAC9C,SAAS,EACN,OAAO,EACP,SAAS,wEAAwE;AAAA,EACpF,UAAU,EAAE,OAAO;AAAA,IACjB,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,2BAA2B;AAAA,IAChF,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,4BAA4B;AAAA,IAChF,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,mBAAmB;AAAA,IACnE,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,kCAAkC;AAAA,IAC7F,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,qCAAqC;AAAA,EAC9F,CAAC,EAAE,OAAO;AACZ,CAAC,EAAE,OAAO;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const startResearchParamsSchema = z.object({
|
|
3
|
+
goal: z.string().min(1, { message: "start-research: goal cannot be empty" }).optional().describe(
|
|
4
|
+
'Research goal for this session. When provided AND the LLM planner (LLM_API_KEY) is available, the server runs a one-shot planner that returns a **goal-tailored research brief**: classified goal type (spec / bug / migration / sentiment / pricing / security / synthesis / product_launch), ordered source priorities, a Reddit-branch recommendation with reason, freshness target, 3\u20138 pre-built concept groups (25\u201350 total queries ready to fire in one web-search call), anticipated gaps, first-pass scrape targets, and success criteria. If no goal is provided, or the planner fails/is unavailable, you get the generic research-loop playbook and an explicit note that the tailored brief is not available. Write the goal as you would to a human researcher \u2014 one or two sentences, specific about what "done" looks like.'
|
|
5
|
+
),
|
|
6
|
+
include_playbook: z.boolean().default(false).describe(
|
|
7
|
+
"Include the full ~1100-token research playbook (concept groups, research loop, Reddit branch, output discipline). Default false \u2014 when the LLM planner is offline the server emits a compact ~300-token stub that already names the loop. Pass true only if the agent needs the verbose tactic reference, or to override the degraded-mode shrink."
|
|
8
|
+
)
|
|
9
|
+
}).strict();
|
|
10
|
+
const startResearchOutputSchema = z.object({
|
|
11
|
+
content: z.string().describe("Orientation markdown for the current research session.")
|
|
12
|
+
}).strict();
|
|
13
|
+
export {
|
|
14
|
+
startResearchOutputSchema,
|
|
15
|
+
startResearchParamsSchema
|
|
16
|
+
};
|
|
17
|
+
//# sourceMappingURL=start-research.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/schemas/start-research.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod';\n\nexport const startResearchParamsSchema = z.object({\n goal: z\n .string()\n .min(1, { message: 'start-research: goal cannot be empty' })\n .optional()\n .describe(\n 'Research goal for this session. When provided AND the LLM planner (LLM_API_KEY) is available, the server runs a one-shot planner that returns a **goal-tailored research brief**: classified goal type (spec / bug / migration / sentiment / pricing / security / synthesis / product_launch), ordered source priorities, a Reddit-branch recommendation with reason, freshness target, 3\u20138 pre-built concept groups (25\u201350 total queries ready to fire in one web-search call), anticipated gaps, first-pass scrape targets, and success criteria. If no goal is provided, or the planner fails/is unavailable, you get the generic research-loop playbook and an explicit note that the tailored brief is not available. Write the goal as you would to a human researcher \u2014 one or two sentences, specific about what \"done\" looks like.',\n ),\n include_playbook: z\n .boolean()\n .default(false)\n .describe(\n 'Include the full ~1100-token research playbook (concept groups, research loop, Reddit branch, output discipline). Default false \u2014 when the LLM planner is offline the server emits a compact ~300-token stub that already names the loop. Pass true only if the agent needs the verbose tactic reference, or to override the degraded-mode shrink.',\n ),\n}).strict();\n\nexport const startResearchOutputSchema = z.object({\n content: z\n .string()\n .describe('Orientation markdown for the current research session.'),\n}).strict();\n\nexport type StartResearchParams = z.infer<typeof startResearchParamsSchema>;\nexport type StartResearchOutput = z.infer<typeof startResearchOutputSchema>;\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAEX,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,MAAM,EACH,OAAO,EACP,IAAI,GAAG,EAAE,SAAS,uCAAuC,CAAC,EAC1D,SAAS,EACT;AAAA,IACC;AAAA,EACF;AAAA,EACF,kBAAkB,EACf,QAAQ,EACR,QAAQ,KAAK,EACb;AAAA,IACC;AAAA,EACF;AACJ,CAAC,EAAE,OAAO;AAEH,MAAM,4BAA4B,EAAE,OAAO;AAAA,EAChD,SAAS,EACN,OAAO,EACP,SAAS,wDAAwD;AACtE,CAAC,EAAE,OAAO;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
const webSearchParamsSchema = z.object({
|
|
3
|
+
queries: z.array(
|
|
4
|
+
z.string().min(1, { message: "web-search: Query cannot be empty" }).describe("A single Google search query. Each query runs as a separate parallel search. Use operators (site:, quotes, verbatim version numbers) to sharpen retrieval.")
|
|
5
|
+
).min(1, { message: "web-search: At least 1 query required" }).describe(
|
|
6
|
+
"Search queries to run in parallel via Google. Think of these as **concept groups** \u2014 clusters of semantically distinct facets of your research goal, each probing a DIFFERENT angle (official spec, implementation, failures, comparison, sentiment, changelog, CVE, pricing). Fire all groups in ONE call as a flat array. Overlapping queries waste budget; orthogonal facets multiply coverage. A narrow bug needs 10\u201320 queries across 2\u20133 facets; a comparison needs 25\u201335 across 4\u20136 facets; open-ended synthesis needs 40\u201380 across 8+ facets."
|
|
7
|
+
),
|
|
8
|
+
extract: z.string().min(1, { message: "web-search: extract cannot be empty" }).describe(
|
|
9
|
+
'Semantic instruction for the relevance classifier \u2014 what "relevant" means for THIS goal. Drives tiering (HIGHLY_RELEVANT / MAYBE_RELEVANT / OTHER), synthesis, gap analysis, and refine-query suggestions. Be specific: "OAuth 2.1 support in TypeScript MCP frameworks \u2014 runnable code, not marketing", not "MCP OAuth". The classifier uses this to choose a source-of-truth rubric (vendor_doc for spec, github for bugs, reddit/blog for migration/sentiment, cve_databases for security).'
|
|
10
|
+
),
|
|
11
|
+
raw: z.boolean().default(false).describe("Skip LLM classification and return the raw ranked URL list. Use when you need unprocessed results."),
|
|
12
|
+
scope: z.enum(["web", "reddit", "both"]).default("web").describe(
|
|
13
|
+
'Search scope. "web" (default) = open web, no augmentation. "reddit" = server appends `site:reddit.com` to every query and filters results to post permalinks (`/r/.+/comments/[a-z0-9]+/`); subreddit homepages are dropped. "both" = runs every query twice (open web + reddit-scoped), merges the result set, and tags each row with its source. Use "reddit" for sentiment/migration/lived-experience research; use "both" when you want one call to cover both branches.'
|
|
14
|
+
),
|
|
15
|
+
verbose: z.boolean().default(false).describe(
|
|
16
|
+
"Include the per-row scoring/coverage metadata, the trailing Signals block, and the CONSENSUS labels even when they carry little signal (single-query hits, threshold of 1). Default false \u2014 most agents do not need this and it costs ~1.5KB per call on a typical 3-query fan-out."
|
|
17
|
+
)
|
|
18
|
+
}).strict();
|
|
19
|
+
const webSearchOutputSchema = z.object({
|
|
20
|
+
content: z.string().describe("Markdown report with tiered results (LLM mode) or ranked URL list (raw mode)."),
|
|
21
|
+
results: z.array(z.object({
|
|
22
|
+
rank: z.number().int().positive().describe("1-based rank in the merged ranking."),
|
|
23
|
+
url: z.string().describe("Result URL."),
|
|
24
|
+
title: z.string().describe("Page title from the result."),
|
|
25
|
+
snippet: z.string().describe("Search snippet from the result."),
|
|
26
|
+
source_type: z.enum(["reddit", "github", "docs", "blog", "paper", "qa", "cve", "news", "video", "web"]).describe(
|
|
27
|
+
"Heuristic source kind from the URL. When the LLM classifier is online its tag overrides this."
|
|
28
|
+
),
|
|
29
|
+
score: z.number().describe("Composite CTR-weighted score, normalized to 100."),
|
|
30
|
+
seen_in: z.number().int().nonnegative().describe("Number of input queries this URL appeared in."),
|
|
31
|
+
best_position: z.number().int().nonnegative().describe("Best (lowest) SERP position observed.")
|
|
32
|
+
})).optional().describe("Per-result structured payload \u2014 same data the markdown table renders, machine-readable."),
|
|
33
|
+
metadata: z.object({
|
|
34
|
+
total_items: z.number().int().nonnegative().describe("Number of queries executed."),
|
|
35
|
+
successful: z.number().int().nonnegative().describe("Queries that returned results."),
|
|
36
|
+
failed: z.number().int().nonnegative().describe("Queries that failed."),
|
|
37
|
+
execution_time_ms: z.number().int().nonnegative().describe("Wall clock time in milliseconds."),
|
|
38
|
+
llm_classified: z.boolean().describe("Whether LLM classification was applied."),
|
|
39
|
+
llm_error: z.string().optional().describe("LLM error if classification failed and fell back to raw."),
|
|
40
|
+
scope: z.enum(["web", "reddit", "both"]).optional().describe("Search scope used."),
|
|
41
|
+
coverage_summary: z.array(z.object({
|
|
42
|
+
query: z.string().describe("The search query."),
|
|
43
|
+
result_count: z.number().int().nonnegative().describe("Results returned for this query."),
|
|
44
|
+
top_url: z.string().optional().describe("Domain of the top result.")
|
|
45
|
+
})).optional().describe("Per-query result counts and top URLs."),
|
|
46
|
+
low_yield_queries: z.array(z.string()).optional().describe("Queries that produced 0-1 results.")
|
|
47
|
+
}).strict()
|
|
48
|
+
}).strict();
|
|
49
|
+
export {
|
|
50
|
+
webSearchOutputSchema,
|
|
51
|
+
webSearchParamsSchema
|
|
52
|
+
};
|
|
53
|
+
//# sourceMappingURL=web-search.js.map
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
{
|
|
2
|
+
"version": 3,
|
|
3
|
+
"sources": ["../../../src/schemas/web-search.ts"],
|
|
4
|
+
"sourcesContent": ["import { z } from 'zod';\n\nexport const webSearchParamsSchema = z.object({\n queries: z\n .array(\n z.string()\n .min(1, { message: 'web-search: Query cannot be empty' })\n .describe('A single Google search query. Each query runs as a separate parallel search. Use operators (site:, quotes, verbatim version numbers) to sharpen retrieval.'),\n )\n .min(1, { message: 'web-search: At least 1 query required' })\n .describe(\n 'Search queries to run in parallel via Google. Think of these as **concept groups** \u2014 clusters of semantically distinct facets of your research goal, each probing a DIFFERENT angle (official spec, implementation, failures, comparison, sentiment, changelog, CVE, pricing). Fire all groups in ONE call as a flat array. Overlapping queries waste budget; orthogonal facets multiply coverage. A narrow bug needs 10\u201320 queries across 2\u20133 facets; a comparison needs 25\u201335 across 4\u20136 facets; open-ended synthesis needs 40\u201380 across 8+ facets.',\n ),\n extract: z\n .string()\n .min(1, { message: 'web-search: extract cannot be empty' })\n .describe(\n 'Semantic instruction for the relevance classifier \u2014 what \"relevant\" means for THIS goal. Drives tiering (HIGHLY_RELEVANT / MAYBE_RELEVANT / OTHER), synthesis, gap analysis, and refine-query suggestions. Be specific: \"OAuth 2.1 support in TypeScript MCP frameworks \u2014 runnable code, not marketing\", not \"MCP OAuth\". The classifier uses this to choose a source-of-truth rubric (vendor_doc for spec, github for bugs, reddit/blog for migration/sentiment, cve_databases for security).',\n ),\n raw: z\n .boolean()\n .default(false)\n .describe('Skip LLM classification and return the raw ranked URL list. Use when you need unprocessed results.'),\n scope: z\n .enum(['web', 'reddit', 'both'])\n .default('web')\n .describe(\n 'Search scope. \"web\" (default) = open web, no augmentation. \"reddit\" = server appends `site:reddit.com` to every query and filters results to post permalinks (`/r/.+/comments/[a-z0-9]+/`); subreddit homepages are dropped. \"both\" = runs every query twice (open web + reddit-scoped), merges the result set, and tags each row with its source. Use \"reddit\" for sentiment/migration/lived-experience research; use \"both\" when you want one call to cover both branches.',\n ),\n verbose: z\n .boolean()\n .default(false)\n .describe(\n 'Include the per-row scoring/coverage metadata, the trailing Signals block, and the CONSENSUS labels even when they carry little signal (single-query hits, threshold of 1). Default false \u2014 most agents do not need this and it costs ~1.5KB per call on a typical 3-query fan-out.',\n ),\n}).strict();\n\nexport type WebSearchParams = z.infer<typeof webSearchParamsSchema>;\n\nexport const webSearchOutputSchema = z.object({\n content: z\n .string()\n .describe('Markdown report with tiered results (LLM mode) or ranked URL list (raw mode).'),\n results: z\n .array(z.object({\n rank: z.number().int().positive().describe('1-based rank in the merged ranking.'),\n url: z.string().describe('Result URL.'),\n title: z.string().describe('Page title from the result.'),\n snippet: z.string().describe('Search snippet from the result.'),\n source_type: z\n .enum(['reddit', 'github', 'docs', 'blog', 'paper', 'qa', 'cve', 'news', 'video', 'web'])\n .describe(\n 'Heuristic source kind from the URL. When the LLM classifier is online its tag overrides this.',\n ),\n score: z.number().describe('Composite CTR-weighted score, normalized to 100.'),\n seen_in: z.number().int().nonnegative().describe('Number of input queries this URL appeared in.'),\n best_position: z.number().int().nonnegative().describe('Best (lowest) SERP position observed.'),\n }))\n .optional()\n .describe('Per-result structured payload \u2014 same data the markdown table renders, machine-readable.'),\n metadata: z.object({\n total_items: z.number().int().nonnegative().describe('Number of queries executed.'),\n successful: z.number().int().nonnegative().describe('Queries that returned results.'),\n failed: z.number().int().nonnegative().describe('Queries that failed.'),\n execution_time_ms: z.number().int().nonnegative().describe('Wall clock time in milliseconds.'),\n llm_classified: z.boolean().describe('Whether LLM classification was applied.'),\n llm_error: z.string().optional().describe('LLM error if classification failed and fell back to raw.'),\n scope: z.enum(['web', 'reddit', 'both']).optional().describe('Search scope used.'),\n coverage_summary: z\n .array(z.object({\n query: z.string().describe('The search query.'),\n result_count: z.number().int().nonnegative().describe('Results returned for this query.'),\n top_url: z.string().optional().describe('Domain of the top result.'),\n }))\n .optional()\n .describe('Per-query result counts and top URLs.'),\n low_yield_queries: z\n .array(z.string())\n .optional()\n .describe('Queries that produced 0-1 results.'),\n }).strict(),\n}).strict();\n\nexport type WebSearchOutput = z.infer<typeof webSearchOutputSchema>;\n"],
|
|
5
|
+
"mappings": "AAAA,SAAS,SAAS;AAEX,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,SAAS,EACN;AAAA,IACC,EAAE,OAAO,EACN,IAAI,GAAG,EAAE,SAAS,oCAAoC,CAAC,EACvD,SAAS,4JAA4J;AAAA,EAC1K,EACC,IAAI,GAAG,EAAE,SAAS,wCAAwC,CAAC,EAC3D;AAAA,IACC;AAAA,EACF;AAAA,EACF,SAAS,EACN,OAAO,EACP,IAAI,GAAG,EAAE,SAAS,sCAAsC,CAAC,EACzD;AAAA,IACC;AAAA,EACF;AAAA,EACF,KAAK,EACF,QAAQ,EACR,QAAQ,KAAK,EACb,SAAS,oGAAoG;AAAA,EAChH,OAAO,EACJ,KAAK,CAAC,OAAO,UAAU,MAAM,CAAC,EAC9B,QAAQ,KAAK,EACb;AAAA,IACC;AAAA,EACF;AAAA,EACF,SAAS,EACN,QAAQ,EACR,QAAQ,KAAK,EACb;AAAA,IACC;AAAA,EACF;AACJ,CAAC,EAAE,OAAO;AAIH,MAAM,wBAAwB,EAAE,OAAO;AAAA,EAC5C,SAAS,EACN,OAAO,EACP,SAAS,+EAA+E;AAAA,EAC3F,SAAS,EACN,MAAM,EAAE,OAAO;AAAA,IACd,MAAM,EAAE,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,SAAS,qCAAqC;AAAA,IAChF,KAAK,EAAE,OAAO,EAAE,SAAS,aAAa;AAAA,IACtC,OAAO,EAAE,OAAO,EAAE,SAAS,6BAA6B;AAAA,IACxD,SAAS,EAAE,OAAO,EAAE,SAAS,iCAAiC;AAAA,IAC9D,aAAa,EACV,KAAK,CAAC,UAAU,UAAU,QAAQ,QAAQ,SAAS,MAAM,OAAO,QAAQ,SAAS,KAAK,CAAC,EACvF;AAAA,MACC;AAAA,IACF;AAAA,IACF,OAAO,EAAE,OAAO,EAAE,SAAS,kDAAkD;AAAA,IAC7E,SAAS,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,+CAA+C;AAAA,IAChG,eAAe,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,uCAAuC;AAAA,EAChG,CAAC,CAAC,EACD,SAAS,EACT,SAAS,8FAAyF;AAAA,EACrG,UAAU,EAAE,OAAO;AAAA,IACjB,aAAa,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,6BAA6B;AAAA,IAClF,YAAY,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,gCAAgC;AAAA,IACpF,QAAQ,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,sBAAsB;AAAA,IACtE,mBAAmB,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,kCAAkC;AAAA,IAC7F,gBAAgB,EAAE,QAAQ,EAAE,SAAS,yCAAyC;AAAA,IAC9E,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,0DAA0D;AAAA,IACpG,OAAO,EAAE,KAAK,CAAC,OAAO,UAAU,MAAM,CAAC,EAAE,SAAS,EAAE,SAAS,oBAAoB;AAAA,IACjF,kBAAkB,EACf,MAAM,EAAE,OAAO;AAAA,MACd,OAAO,EAAE,OAAO,EAAE,SAAS,mBAAmB;AAAA,MAC9C,cAAc,EAAE,OAAO,EAAE,IAAI,EAAE,YAAY,EAAE,SAAS,kCAAkC;AAAA,MACxF,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,SAAS,2BAA2B;AAAA,IACrE,CAAC,CAAC,EACD,SAAS,EACT,SAAS,uCAAuC;AAAA,IACnD,mBAAmB,EAChB,MAAM,EAAE,OAAO,CAAC,EAChB,SAAS,EACT,SAAS,oCAAoC;AAAA,EAClD,CAAC,EAAE,OAAO;AACZ,CAAC,EAAE,OAAO;",
|
|
6
|
+
"names": []
|
|
7
|
+
}
|