mcp-researchpowerpack 7.0.11 → 7.0.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/dist/index.js +4662 -21
  2. package/dist/index.js.map +4 -4
  3. package/dist/mcp-use.json +2 -2
  4. package/dist/src/clients/jina.js +202 -16
  5. package/dist/src/clients/jina.js.map +3 -3
  6. package/dist/src/clients/kernel.js +254 -7
  7. package/dist/src/clients/kernel.js.map +4 -4
  8. package/dist/src/clients/reddit.js +326 -23
  9. package/dist/src/clients/reddit.js.map +4 -4
  10. package/dist/src/clients/scraper.js +345 -22
  11. package/dist/src/clients/scraper.js.map +4 -4
  12. package/dist/src/clients/search.js +316 -20
  13. package/dist/src/clients/search.js.map +4 -4
  14. package/dist/src/config/index.js +39 -10
  15. package/dist/src/config/index.js.map +3 -3
  16. package/dist/src/effect/errors.js +130 -5
  17. package/dist/src/effect/errors.js.map +3 -3
  18. package/dist/src/effect/runtime.js +1893 -4
  19. package/dist/src/effect/runtime.js.map +4 -4
  20. package/dist/src/effect/services.js +2124 -22
  21. package/dist/src/effect/services.js.map +4 -4
  22. package/dist/src/schemas/scrape-links.js +6 -5
  23. package/dist/src/schemas/scrape-links.js.map +1 -1
  24. package/dist/src/schemas/start-research.js +2 -1
  25. package/dist/src/schemas/start-research.js.map +1 -1
  26. package/dist/src/schemas/web-search.js +9 -8
  27. package/dist/src/schemas/web-search.js.map +1 -1
  28. package/dist/src/services/llm-processor.js +406 -25
  29. package/dist/src/services/llm-processor.js.map +4 -4
  30. package/dist/src/services/markdown-cleaner.js +6 -5
  31. package/dist/src/services/markdown-cleaner.js.map +1 -1
  32. package/dist/src/tools/mcp-helpers.js +2 -1
  33. package/dist/src/tools/mcp-helpers.js.map +1 -1
  34. package/dist/src/tools/registry.js +4629 -3
  35. package/dist/src/tools/registry.js.map +4 -4
  36. package/dist/src/tools/scrape.js +2610 -80
  37. package/dist/src/tools/scrape.js.map +4 -4
  38. package/dist/src/tools/search.js +2388 -59
  39. package/dist/src/tools/search.js.map +4 -4
  40. package/dist/src/tools/start-research.js +2030 -23
  41. package/dist/src/tools/start-research.js.map +4 -4
  42. package/dist/src/tools/utils.js +98 -7
  43. package/dist/src/tools/utils.js.map +3 -3
  44. package/dist/src/utils/concurrency.js +1 -0
  45. package/dist/src/utils/concurrency.js.map +1 -1
  46. package/dist/src/utils/content-extractor.js +27 -2
  47. package/dist/src/utils/content-extractor.js.map +3 -3
  48. package/dist/src/utils/content-quality.js +4 -3
  49. package/dist/src/utils/content-quality.js.map +1 -1
  50. package/dist/src/utils/errors.js +26 -3
  51. package/dist/src/utils/errors.js.map +3 -3
  52. package/dist/src/utils/logger.js +1 -0
  53. package/dist/src/utils/logger.js.map +1 -1
  54. package/dist/src/utils/markdown-formatter.js +1 -0
  55. package/dist/src/utils/markdown-formatter.js.map +1 -1
  56. package/dist/src/utils/query-relax.js +9 -8
  57. package/dist/src/utils/query-relax.js.map +1 -1
  58. package/dist/src/utils/response.js +3 -2
  59. package/dist/src/utils/response.js.map +1 -1
  60. package/dist/src/utils/retry.js +5 -4
  61. package/dist/src/utils/retry.js.map +1 -1
  62. package/dist/src/utils/sanitize.js +4 -3
  63. package/dist/src/utils/sanitize.js.map +1 -1
  64. package/dist/src/utils/source-type.js +4 -3
  65. package/dist/src/utils/source-type.js.map +1 -1
  66. package/dist/src/utils/url-aggregator.js +112 -11
  67. package/dist/src/utils/url-aggregator.js.map +3 -3
  68. package/dist/src/version.js +7 -6
  69. package/dist/src/version.js.map +1 -1
  70. package/package.json +3 -3
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
- "sources": ["../../../src/tools/start-research.ts"],
4
- "sourcesContent": ["import type { MCPServer } from 'mcp-use/server';\nimport { Effect } from 'effect';\n\nimport {\n startResearchParamsSchema,\n type StartResearchOutput,\n type StartResearchParams,\n} from '../schemas/start-research.js';\nimport { QUERY_REWRITE_PAIR_GUIDANCE_TEXT } from '../schemas/web-search.js';\nimport {\n createLLMProcessor,\n getLLMHealth,\n renderResearchBrief,\n type LLMHealthSnapshot,\n} from '../services/llm-processor.js';\nimport { runExternalEffect } from '../effect/runtime.js';\nimport { LlmService, LlmServiceLive } from '../effect/services.js';\nimport { classifyError } from '../utils/errors.js';\nimport { mcpLog } from '../utils/logger.js';\nimport { toolFailure, toolSuccess, toToolResponse, type ToolExecutionResult } from './mcp-helpers.js';\nimport { formatError } from './utils.js';\n\nconst SKILL_INSTALL_HINT = [\n '> \uD83D\uDCA1 **Pair this server with the `run-research` skill** for the full agentic playbook',\n '> (single-agent loop, multi-agent orchestrator, mission-prompt templates, output discipline).',\n '> Install once per machine \u2014 the skill is what teaches the agent how to spend these tools well:',\n '>',\n '> ```bash',\n '> npx -y skills add -y -g https://github.com/yigitkonur/skills-by-yigitkonur --skill /run-research',\n '> ```',\n '>',\n '> Already installed? Skip this \u2014 the skill auto-loads on relevant prompts. The full pack',\n '> ships ~50 sibling skills: `npx -y skills add -y -g https://github.com/yigitkonur/skills-by-yigitkonur`.',\n].join('\\n');\n\n/**\n * Full research-loop playbook. Teaches the 5-tool mental model\n * (start-research, raw/smart search, raw/smart scrape), the aggressive multi-call\n * discipline, parallel-callability, and the cite-from-scrape rule.\n *\n * Emitted when the LLM planner is healthy OR `include_playbook: true`.\n */\nexport function buildStaticScaffolding(goal?: string, opts: { plannerAvailable?: boolean } = {}): string {\n const plannerAvailable = opts.plannerAvailable ?? true;\n const focusLine = goal\n ? `> Focus for this session: ${goal}`\n : '> Focus for this session: not yet specified \u2014 set one on the next pass';\n\n const classifierLoopStep = plannerAvailable\n ? '3. If you used `smart-web-search`, treat its output as a prioritization layer over your keyword fan-out: read `Start here`, HIGHLY/MAYBE tiers, `gaps[]`, and `refine_queries[]`, then decide what to scrape. Do not treat smart search as evidence; it only sees titles/snippets. If you used `raw-web-search`, use the ranked URLs/snippets as the candidate pool and do your own prioritization.'\n : '3. Classifier output is NOT available (LLM planner offline). Use `raw-web-search` and synthesize the terrain yourself from titles + snippets; then scrape before making claims.';\n\n return [\n SKILL_INSTALL_HINT,\n '',\n '# Research session started',\n '',\n focusLine,\n '',\n 'You are running a research LOOP, not answering from memory. Training data is stale; the web is authoritative for anything dated, versioned, priced, or contested. Every non-trivial claim in your final answer must be traceable to a raw or smart scrape excerpt you read. Never cite a URL from a search snippet alone.',\n '',\n '## The 5 tools',\n '',\n '**1. `start-research`** \u2014 you just called me. I plan this session and return the brief below. Call me again only if the goal materially shifts.',\n '',\n '**2. `raw-web-search`** \u2014 fan out search keywords in parallel and return the ranked markdown list directly. Serper is primary when configured; Jina Search is fallback when Serper is missing, fails, or yields empty query results. One call carries **up to 50 keywords** in a flat `keywords` array. Use raw search when recall matters more than interpretation: broad discovery, audit trails, exact candidate URLs, cheap second/third passes, and Reddit permalink discovery.',\n '',\n '**3. `smart-web-search`** \u2014 fan out search keywords, then always run LLM classification/synthesis with required `extract`. Its value is **prioritization, not evidence**: it ranks title/snippet results into HIGHLY_RELEVANT / MAYBE / OTHER, surfaces `## Gaps`, and suggests sharper follow-up keywords. Use smart search after you have a clear goal and a deliberately diverse keyword set; use raw search when you mainly need all candidate URLs or maximum distinct keyword coverage without classifier cost. Call search **aggressively** \u2014 2\u20134 rounds per session is normal, not 1. **Parallel-safe**: run multiple search calls in the same turn for orthogonal subtopics. `scope` values:',\n '- `\"reddit\"` \u2192 server appends `site:reddit.com` and filters to post permalinks. Use for sentiment / migration / lived experience.',\n '- `\"web\"` (default) \u2192 open web. Use for spec / bug / pricing / CVE / API / primary-source hunts.',\n '- `\"both\"` \u2192 fans each query across both. Use when the topic is opinion-heavy AND needs official sources.',\n '',\n `**Query rewrite discipline** \u2014 ${QUERY_REWRITE_PAIR_GUIDANCE_TEXT}`,\n '',\n '## Raw vs smart choice',\n '',\n '- Prefer **raw search** for exploration and breadth: maximize distinct keywords, source classes, domains, and exact-result capture. Prefer **smart search** for triage: give it the same diverse keyword fan-out plus a precise `extract` goal so it can prioritize which URLs deserve scraping.',\n '- Prefer **raw scrape** when the page/comments are the research object, when you need complete context, when extraction shape is unclear, or when preserving Reddit threads matters. Prefer **smart scrape** when you know the fields/facets you need and want compact evidence with `## Matches`, `## Not found`, and `## Follow-up signals`.',\n '- Smart search is allowed to steer attention, but final claims must come from raw or smart scrape content. Smart scrape is usually the highest-value smart tool for answer construction because it reads the page body, not just snippets.',\n '',\n '**4. `raw-scrape-links`** \u2014 fetch URLs in parallel and return full markdown directly. **Auto-detects** `reddit.com/r/.../comments/` permalinks and routes them through the Reddit API (threaded post + comments). Non-Reddit URLs use Jina Reader first, then Jina Reader through Scrape.do proxy mode when configured, then optional Kernel browser rendering for web pages. Use raw scrape when you need complete markdown/comments, when the extraction shape is not yet known, or before narrowing a long/important source.',\n '',\n '**5. `smart-scrape-links`** \u2014 fetch URLs with the same provider stack, then always run per-URL LLM extraction with required `extract`. Use smart scrape for focused extraction, final evidence packs, long pages where only specific facts matter, and cross-source comparison. Describe extraction SHAPE in `extract`, facets separated by `|`: `root cause | affected versions | fix | workarounds | timeline`. It should preserve numbers/versions/errors/quotes and explicitly say what the source did not answer.',\n '',\n '## The loop',\n '',\n '1. Read the brief below (if present). Note `primary_branch`, `keyword_seeds`, `gaps_to_watch`, `stop_criteria`.',\n '2. Fire `first_call_sequence` in order. For `primary_branch: reddit`, lead with `raw-web-search` using Reddit permalink probes, then `raw-scrape-links` on the best post permalinks for full comments. For `web`, use `raw-web-search` for maximum candidate breadth or `smart-web-search scope:\"web\"` to prioritize a strong keyword fan-out, then `smart-scrape-links` on selected URLs when the extraction shape is known. For `both`, issue parallel search calls split by source need.',\n classifierLoopStep,\n '4. Scrape every HIGHLY_RELEVANT plus the 2\u20133 best MAYBE_RELEVANT. Use `raw-scrape-links` first for full documents/comments and ambiguous sources; use `smart-scrape-links` when you can name the extraction facets precisely.',\n '5. Harvest from smart scrape `## Follow-up signals` and from raw scrape full markdown/comments \u2014 new terms, version numbers, vendor names, failure modes, referenced URLs. These seed your next search round.',\n '6. Fire the next search round with harvested terms plus any `refine_queries[]` the classifier suggested. Do NOT paraphrase queries already run \u2014 the classifier tracks them.',\n '7. **Stop** when every `gaps_to_watch` item is closed AND the last search pass surfaced no new terms, OR when you have completed 4 full passes. State remaining gaps explicitly if you hit the cap.',\n '',\n '## Output discipline',\n '',\n '- Cite URL (or Reddit permalink) for every non-trivial claim \u2014 only from a raw or smart scrape excerpt you read.',\n '- Quote verbatim: numbers, versions, API names, prices, error messages, stacktraces, people\\'s words.',\n '- Separate documented facts from inferred conclusions explicitly.',\n '- Include the scrape date for time-sensitive claims.',\n '- If you could not verify something, say so \u2014 do not paper over gaps.',\n '- Never cite a URL from a search snippet alone.',\n '',\n '## Post-cutoff discipline',\n '',\n 'For anything released / changed after your training cutoff \u2014 new products, versions, prices, benchmarks \u2014 treat your own query suggestions as hypotheses until a scraped first-party page confirms them. Include `site:<vendor-domain>` keywords in your first search call when the goal names a vendor or product.',\n ].join('\\n');\n}\n\n/**\n * Compact stub emitted when the LLM planner is offline AND the caller did\n * not opt into the full playbook. Names the 5 tools, the loop, parallel-safety,\n * Reddit branch, and cite-from-scrape \u2014 enough to keep an agent moving.\n */\nexport function buildDegradedStub(goal?: string): string {\n const focusLine = goal\n ? `> Focus for this session: ${goal}`\n : '> Focus for this session: not specified \u2014 set one on the next pass.';\n return [\n SKILL_INSTALL_HINT,\n '',\n '# Research session started (LLM planner offline \u2014 compact stub)',\n '',\n focusLine,\n '',\n '**5 tools**: `start-research` (plans), `raw-web-search` (keywords-only raw search), `smart-web-search` (search + required LLM prioritization over snippets, `scope: web|reddit|both`), `raw-scrape-links` (urls-only full markdown/comments), `smart-scrape-links` (scrape + required LLM extraction over page bodies). All are **parallel-callable** \u2014 fire multiple in the same turn when subtopics are orthogonal.',\n '',\n '**Loop**: search \u2192 scrape \u2192 harvest terms/signals \u2192 next search round \u2192 stop when gaps close OR after 4 passes. Use raw search for breadth, smart search for prioritization, raw scrape for full context, and smart scrape for focused extraction.',\n '',\n '**Reddit branch**: use `raw-web-search` with Reddit permalink probes for sentiment / migration / lived experience, then `raw-scrape-links` to capture full threaded comments. Use smart search only to prioritize many candidate posts; use smart scrape only after you know the extraction facets.',\n '',\n '**Cite**: every non-trivial claim must trace to a raw or smart scrape excerpt, never a search snippet. Quote verbatim for numbers, versions, stacktraces, people\\'s words.',\n '',\n 'Pass `include_playbook: true` to `start-research` for the full tactic reference.',\n ].join('\\n');\n}\n\n/**\n * Backward-compat alias \u2014 older call sites import `buildOrientation` directly.\n */\nexport const buildOrientation = buildStaticScaffolding;\n\n// ============================================================================\n// Planner-offline gate.\n//\n// The problem we are guarding against: a single transient LLM failure (one bad\n// 429, one malformed JSON response from the classifier) used to poison the\n// gate forever and force every subsequent `start-research` call into the\n// compact stub \u2014 even when env was fine and the next call would have\n// succeeded. That created a deadlock where the very tool that could reset\n// the health flag was the tool being blocked.\n//\n// The safer semantics implemented here:\n// 1. If env is not configured, we are offline. Hard stop.\n// 2. Otherwise, require **two consecutive failures** before gating (one\n// blip is tolerated).\n// 3. Even then, the gate only holds for PLANNER_FAILURE_TTL_MS after the\n// most recent failure. After that window we give the planner another\n// chance regardless of the counter \u2014 if it is still broken the next\n// call's failure will re-arm the gate.\n// 4. Any success resets the counter to 0, so the gate opens immediately\n// on recovery.\n// ============================================================================\n\n/** Minimum consecutive failures before the gate closes. */\nexport const PLANNER_FAILURE_THRESHOLD = 2;\n\n/** How long a recent failure burst keeps the gate closed, in ms. */\nexport const PLANNER_FAILURE_TTL_MS = 60_000;\n\ntype PlannerGateHealth = Pick<\n LLMHealthSnapshot,\n 'plannerConfigured' | 'consecutivePlannerFailures' | 'lastPlannerCheckedAt'\n>;\n\n/**\n * Pure predicate \u2014 returns true when the planner should be treated as\n * offline for the purposes of `start-research`. Kept exported and\n * dependency-free so tests can drive it without touching the LLM.\n */\nexport function isPlannerKnownOffline(\n health: PlannerGateHealth,\n nowMs: number = Date.now(),\n): boolean {\n if (!health.plannerConfigured) {\n return true;\n }\n if (health.consecutivePlannerFailures < PLANNER_FAILURE_THRESHOLD) {\n return false;\n }\n if (health.lastPlannerCheckedAt === null) {\n return false;\n }\n const lastMs = Date.parse(health.lastPlannerCheckedAt);\n if (Number.isNaN(lastMs)) {\n return false;\n }\n return nowMs - lastMs < PLANNER_FAILURE_TTL_MS;\n}\n\nasync function buildGoalAwareBrief(\n goal: string,\n signal?: AbortSignal,\n): Promise<string> {\n const processor = createLLMProcessor();\n if (!processor) {\n mcpLog('info', 'start-research: LLM unavailable, returning static orientation only', 'start-research');\n return '';\n }\n\n void signal;\n const brief = await runExternalEffect(\n Effect.gen(function* () {\n const llm = yield* LlmService;\n return yield* llm.generateResearchBrief(goal, processor);\n }),\n LlmServiceLive,\n );\n if (!brief) {\n mcpLog('warning', 'start-research: brief generation failed, returning static orientation only', 'start-research');\n return '';\n }\n\n return renderResearchBrief(brief);\n}\n\nasync function handleStartResearch(\n params: StartResearchParams,\n signal?: AbortSignal,\n): Promise<ToolExecutionResult<StartResearchOutput>> {\n try {\n const llmHealth = getLLMHealth();\n const plannerKnownOffline = isPlannerKnownOffline(llmHealth);\n\n if (plannerKnownOffline && !params.include_playbook) {\n const stub = buildDegradedStub(params.goal);\n return toolSuccess(stub);\n }\n\n const scaffolding = buildStaticScaffolding(params.goal, {\n plannerAvailable: !plannerKnownOffline,\n });\n\n let brief = '';\n if (params.goal) {\n brief = await buildGoalAwareBrief(params.goal, signal);\n }\n\n const briefFallbackNote = params.goal && !brief\n ? '\\n\\n---\\n\\n> _Goal-tailored brief unavailable: LLM planner is not configured or failed this call. The static playbook above still applies; you can proceed with it, or retry `start-research` after verifying `LLM_API_KEY`._'\n : '';\n\n const content = brief\n ? `${scaffolding}\\n\\n---\\n\\n${brief}`\n : `${scaffolding}${briefFallbackNote}`;\n\n return toolSuccess(content);\n } catch (err: unknown) {\n const structuredError = classifyError(err);\n mcpLog('error', `start-research: ${structuredError.message}`, 'start-research');\n return toolFailure(\n formatError({\n code: structuredError.code,\n message: structuredError.message,\n retryable: structuredError.retryable,\n toolName: 'start-research',\n howToFix: ['Retry start-research. If the failure persists, verify LLM_API_KEY / LLM_BASE_URL / LLM_MODEL.'],\n }),\n );\n }\n}\n\nexport function registerStartResearchTool(server: MCPServer): void {\n server.tool(\n {\n name: 'start-research',\n title: 'Start Research Session',\n description:\n `Call this FIRST every research session. Provide a \\`goal\\`; I return a goal-tailored brief naming (a) \\`primary_branch\\` (reddit for sentiment/migration, web for spec/bug/pricing, both when opinion-heavy AND needs official sources), (b) the exact \\`first_call_sequence\\` of raw/smart search and scrape calls to fire, (c) 25\u201350 keyword seeds for your first search call, (d) iteration hints, (e) gaps to watch, (f) stop criteria. ${QUERY_REWRITE_PAIR_GUIDANCE_TEXT} No goal? You still get the generic 5-tool playbook. Other tools work without calling this, but you will use them worse.`,\n schema: startResearchParamsSchema,\n annotations: {\n readOnlyHint: true,\n idempotentHint: true,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async (args) => toToolResponse(await handleStartResearch(args)),\n );\n}\n"],
5
- "mappings": "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",
6
- "names": []
3
+ "sources": ["../../../src/tools/start-research.ts", "../../../src/schemas/start-research.ts", "../../../src/schemas/web-search.ts", "../../../src/services/llm-processor.ts", "../../../src/config/index.ts", "../../../src/version.ts", "../../../src/utils/logger.ts", "../../../src/utils/errors.ts", "../../../src/effect/runtime.ts", "../../../src/effect/services.ts", "../../../src/utils/retry.ts", "../../../src/utils/concurrency.ts", "../../../src/clients/search.ts", "../../../src/clients/jina.ts", "../../../src/clients/kernel.ts", "../../../src/clients/reddit.ts", "../../../src/effect/errors.ts", "../../../src/tools/mcp-helpers.ts", "../../../src/utils/response.ts"],
4
+ "sourcesContent": ["import type { MCPServer } from 'mcp-use/server';\nimport { Effect } from 'effect';\n\nimport {\n startResearchParamsSchema,\n type StartResearchOutput,\n type StartResearchParams,\n} from '../schemas/start-research.js';\nimport { QUERY_REWRITE_PAIR_GUIDANCE_TEXT } from '../schemas/web-search.js';\nimport {\n createLLMProcessor,\n getLLMHealth,\n renderResearchBrief,\n type LLMHealthSnapshot,\n} from '../services/llm-processor.js';\nimport { runExternalEffect } from '../effect/runtime.js';\nimport { LlmService, LlmServiceLive } from '../effect/services.js';\nimport { classifyError } from '../utils/errors.js';\nimport { mcpLog } from '../utils/logger.js';\nimport { toolFailure, toolSuccess, toToolResponse, type ToolExecutionResult } from './mcp-helpers.js';\nimport { formatError } from './utils.js';\n\nconst SKILL_INSTALL_HINT = [\n '> \uD83D\uDCA1 **Pair this server with the `run-research` skill** for the full agentic playbook',\n '> (single-agent loop, multi-agent orchestrator, mission-prompt templates, output discipline).',\n '> Install once per machine \u2014 the skill is what teaches the agent how to spend these tools well:',\n '>',\n '> ```bash',\n '> npx -y skills add -y -g https://github.com/yigitkonur/skills-by-yigitkonur --skill /run-research',\n '> ```',\n '>',\n '> Already installed? Skip this \u2014 the skill auto-loads on relevant prompts. The full pack',\n '> ships ~50 sibling skills: `npx -y skills add -y -g https://github.com/yigitkonur/skills-by-yigitkonur`.',\n].join('\\n');\n\n/**\n * Full research-loop playbook. Teaches the 5-tool mental model\n * (start-research, raw/smart search, raw/smart scrape), the aggressive multi-call\n * discipline, parallel-callability, and the cite-from-scrape rule.\n *\n * Emitted when the LLM planner is healthy OR `include_playbook: true`.\n */\nexport function buildStaticScaffolding(goal?: string, opts: { plannerAvailable?: boolean } = {}): string {\n const plannerAvailable = opts.plannerAvailable ?? true;\n const focusLine = goal\n ? `> Focus for this session: ${goal}`\n : '> Focus for this session: not yet specified \u2014 set one on the next pass';\n\n const classifierLoopStep = plannerAvailable\n ? '3. If you used `smart-web-search`, treat its output as a prioritization layer over your keyword fan-out: read `Start here`, HIGHLY/MAYBE tiers, `gaps[]`, and `refine_queries[]`, then decide what to scrape. Do not treat smart search as evidence; it only sees titles/snippets. If you used `raw-web-search`, use the ranked URLs/snippets as the candidate pool and do your own prioritization.'\n : '3. Classifier output is NOT available (LLM planner offline). Use `raw-web-search` and synthesize the terrain yourself from titles + snippets; then scrape before making claims.';\n\n return [\n SKILL_INSTALL_HINT,\n '',\n '# Research session started',\n '',\n focusLine,\n '',\n 'You are running a research LOOP, not answering from memory. Training data is stale; the web is authoritative for anything dated, versioned, priced, or contested. Every non-trivial claim in your final answer must be traceable to a raw or smart scrape excerpt you read. Never cite a URL from a search snippet alone.',\n '',\n '## The 5 tools',\n '',\n '**1. `start-research`** \u2014 you just called me. I plan this session and return the brief below. Call me again only if the goal materially shifts.',\n '',\n '**2. `raw-web-search`** \u2014 fan out search keywords in parallel and return the ranked markdown list directly. Serper is primary when configured; Jina Search is fallback when Serper is missing, fails, or yields empty query results. One call carries **up to 50 keywords** in a flat `keywords` array. Use raw search when recall matters more than interpretation: broad discovery, audit trails, exact candidate URLs, cheap second/third passes, and Reddit permalink discovery.',\n '',\n '**3. `smart-web-search`** \u2014 fan out search keywords, then always run LLM classification/synthesis with required `extract`. Its value is **prioritization, not evidence**: it ranks title/snippet results into HIGHLY_RELEVANT / MAYBE / OTHER, surfaces `## Gaps`, and suggests sharper follow-up keywords. Use smart search after you have a clear goal and a deliberately diverse keyword set; use raw search when you mainly need all candidate URLs or maximum distinct keyword coverage without classifier cost. Call search **aggressively** \u2014 2\u20134 rounds per session is normal, not 1. **Parallel-safe**: run multiple search calls in the same turn for orthogonal subtopics. `scope` values:',\n '- `\"reddit\"` \u2192 server appends `site:reddit.com` and filters to post permalinks. Use for sentiment / migration / lived experience.',\n '- `\"web\"` (default) \u2192 open web. Use for spec / bug / pricing / CVE / API / primary-source hunts.',\n '- `\"both\"` \u2192 fans each query across both. Use when the topic is opinion-heavy AND needs official sources.',\n '',\n `**Query rewrite discipline** \u2014 ${QUERY_REWRITE_PAIR_GUIDANCE_TEXT}`,\n '',\n '## Raw vs smart choice',\n '',\n '- Prefer **raw search** for exploration and breadth: maximize distinct keywords, source classes, domains, and exact-result capture. Prefer **smart search** for triage: give it the same diverse keyword fan-out plus a precise `extract` goal so it can prioritize which URLs deserve scraping.',\n '- Prefer **raw scrape** when the page/comments are the research object, when you need complete context, when extraction shape is unclear, or when preserving Reddit threads matters. Prefer **smart scrape** when you know the fields/facets you need and want compact evidence with `## Matches`, `## Not found`, and `## Follow-up signals`.',\n '- Smart search is allowed to steer attention, but final claims must come from raw or smart scrape content. Smart scrape is usually the highest-value smart tool for answer construction because it reads the page body, not just snippets.',\n '',\n '**4. `raw-scrape-links`** \u2014 fetch URLs in parallel and return full markdown directly. **Auto-detects** `reddit.com/r/.../comments/` permalinks and routes them through the Reddit API (threaded post + comments). Non-Reddit URLs use Jina Reader first, then Jina Reader through Scrape.do proxy mode when configured, then optional Kernel browser rendering for web pages. Use raw scrape when you need complete markdown/comments, when the extraction shape is not yet known, or before narrowing a long/important source.',\n '',\n '**5. `smart-scrape-links`** \u2014 fetch URLs with the same provider stack, then always run per-URL LLM extraction with required `extract`. Use smart scrape for focused extraction, final evidence packs, long pages where only specific facts matter, and cross-source comparison. Describe extraction SHAPE in `extract`, facets separated by `|`: `root cause | affected versions | fix | workarounds | timeline`. It should preserve numbers/versions/errors/quotes and explicitly say what the source did not answer.',\n '',\n '## The loop',\n '',\n '1. Read the brief below (if present). Note `primary_branch`, `keyword_seeds`, `gaps_to_watch`, `stop_criteria`.',\n '2. Fire `first_call_sequence` in order. For `primary_branch: reddit`, lead with `raw-web-search` using Reddit permalink probes, then `raw-scrape-links` on the best post permalinks for full comments. For `web`, use `raw-web-search` for maximum candidate breadth or `smart-web-search scope:\"web\"` to prioritize a strong keyword fan-out, then `smart-scrape-links` on selected URLs when the extraction shape is known. For `both`, issue parallel search calls split by source need.',\n classifierLoopStep,\n '4. Scrape every HIGHLY_RELEVANT plus the 2\u20133 best MAYBE_RELEVANT. Use `raw-scrape-links` first for full documents/comments and ambiguous sources; use `smart-scrape-links` when you can name the extraction facets precisely.',\n '5. Harvest from smart scrape `## Follow-up signals` and from raw scrape full markdown/comments \u2014 new terms, version numbers, vendor names, failure modes, referenced URLs. These seed your next search round.',\n '6. Fire the next search round with harvested terms plus any `refine_queries[]` the classifier suggested. Do NOT paraphrase queries already run \u2014 the classifier tracks them.',\n '7. **Stop** when every `gaps_to_watch` item is closed AND the last search pass surfaced no new terms, OR when you have completed 4 full passes. State remaining gaps explicitly if you hit the cap.',\n '',\n '## Output discipline',\n '',\n '- Cite URL (or Reddit permalink) for every non-trivial claim \u2014 only from a raw or smart scrape excerpt you read.',\n '- Quote verbatim: numbers, versions, API names, prices, error messages, stacktraces, people\\'s words.',\n '- Separate documented facts from inferred conclusions explicitly.',\n '- Include the scrape date for time-sensitive claims.',\n '- If you could not verify something, say so \u2014 do not paper over gaps.',\n '- Never cite a URL from a search snippet alone.',\n '',\n '## Post-cutoff discipline',\n '',\n 'For anything released / changed after your training cutoff \u2014 new products, versions, prices, benchmarks \u2014 treat your own query suggestions as hypotheses until a scraped first-party page confirms them. Include `site:<vendor-domain>` keywords in your first search call when the goal names a vendor or product.',\n ].join('\\n');\n}\n\n/**\n * Compact stub emitted when the LLM planner is offline AND the caller did\n * not opt into the full playbook. Names the 5 tools, the loop, parallel-safety,\n * Reddit branch, and cite-from-scrape \u2014 enough to keep an agent moving.\n */\nexport function buildDegradedStub(goal?: string): string {\n const focusLine = goal\n ? `> Focus for this session: ${goal}`\n : '> Focus for this session: not specified \u2014 set one on the next pass.';\n return [\n SKILL_INSTALL_HINT,\n '',\n '# Research session started (LLM planner offline \u2014 compact stub)',\n '',\n focusLine,\n '',\n '**5 tools**: `start-research` (plans), `raw-web-search` (keywords-only raw search), `smart-web-search` (search + required LLM prioritization over snippets, `scope: web|reddit|both`), `raw-scrape-links` (urls-only full markdown/comments), `smart-scrape-links` (scrape + required LLM extraction over page bodies). All are **parallel-callable** \u2014 fire multiple in the same turn when subtopics are orthogonal.',\n '',\n '**Loop**: search \u2192 scrape \u2192 harvest terms/signals \u2192 next search round \u2192 stop when gaps close OR after 4 passes. Use raw search for breadth, smart search for prioritization, raw scrape for full context, and smart scrape for focused extraction.',\n '',\n '**Reddit branch**: use `raw-web-search` with Reddit permalink probes for sentiment / migration / lived experience, then `raw-scrape-links` to capture full threaded comments. Use smart search only to prioritize many candidate posts; use smart scrape only after you know the extraction facets.',\n '',\n '**Cite**: every non-trivial claim must trace to a raw or smart scrape excerpt, never a search snippet. Quote verbatim for numbers, versions, stacktraces, people\\'s words.',\n '',\n 'Pass `include_playbook: true` to `start-research` for the full tactic reference.',\n ].join('\\n');\n}\n\n/**\n * Backward-compat alias \u2014 older call sites import `buildOrientation` directly.\n */\nexport const buildOrientation = buildStaticScaffolding;\n\n// ============================================================================\n// Planner-offline gate.\n//\n// The problem we are guarding against: a single transient LLM failure (one bad\n// 429, one malformed JSON response from the classifier) used to poison the\n// gate forever and force every subsequent `start-research` call into the\n// compact stub \u2014 even when env was fine and the next call would have\n// succeeded. That created a deadlock where the very tool that could reset\n// the health flag was the tool being blocked.\n//\n// The safer semantics implemented here:\n// 1. If env is not configured, we are offline. Hard stop.\n// 2. Otherwise, require **two consecutive failures** before gating (one\n// blip is tolerated).\n// 3. Even then, the gate only holds for PLANNER_FAILURE_TTL_MS after the\n// most recent failure. After that window we give the planner another\n// chance regardless of the counter \u2014 if it is still broken the next\n// call's failure will re-arm the gate.\n// 4. Any success resets the counter to 0, so the gate opens immediately\n// on recovery.\n// ============================================================================\n\n/** Minimum consecutive failures before the gate closes. */\nexport const PLANNER_FAILURE_THRESHOLD = 2;\n\n/** How long a recent failure burst keeps the gate closed, in ms. */\nexport const PLANNER_FAILURE_TTL_MS = 60_000;\n\ntype PlannerGateHealth = Pick<\n LLMHealthSnapshot,\n 'plannerConfigured' | 'consecutivePlannerFailures' | 'lastPlannerCheckedAt'\n>;\n\n/**\n * Pure predicate \u2014 returns true when the planner should be treated as\n * offline for the purposes of `start-research`. Kept exported and\n * dependency-free so tests can drive it without touching the LLM.\n */\nexport function isPlannerKnownOffline(\n health: PlannerGateHealth,\n nowMs: number = Date.now(),\n): boolean {\n if (!health.plannerConfigured) {\n return true;\n }\n if (health.consecutivePlannerFailures < PLANNER_FAILURE_THRESHOLD) {\n return false;\n }\n if (health.lastPlannerCheckedAt === null) {\n return false;\n }\n const lastMs = Date.parse(health.lastPlannerCheckedAt);\n if (Number.isNaN(lastMs)) {\n return false;\n }\n return nowMs - lastMs < PLANNER_FAILURE_TTL_MS;\n}\n\nasync function buildGoalAwareBrief(\n goal: string,\n signal?: AbortSignal,\n): Promise<string> {\n const processor = createLLMProcessor();\n if (!processor) {\n mcpLog('info', 'start-research: LLM unavailable, returning static orientation only', 'start-research');\n return '';\n }\n\n void signal;\n const brief = await runExternalEffect(\n Effect.gen(function* () {\n const llm = yield* LlmService;\n return yield* llm.generateResearchBrief(goal, processor);\n }),\n LlmServiceLive,\n );\n if (!brief) {\n mcpLog('warning', 'start-research: brief generation failed, returning static orientation only', 'start-research');\n return '';\n }\n\n return renderResearchBrief(brief);\n}\n\nasync function handleStartResearch(\n params: StartResearchParams,\n signal?: AbortSignal,\n): Promise<ToolExecutionResult<StartResearchOutput>> {\n try {\n const llmHealth = getLLMHealth();\n const plannerKnownOffline = isPlannerKnownOffline(llmHealth);\n\n if (plannerKnownOffline && !params.include_playbook) {\n const stub = buildDegradedStub(params.goal);\n return toolSuccess(stub);\n }\n\n const scaffolding = buildStaticScaffolding(params.goal, {\n plannerAvailable: !plannerKnownOffline,\n });\n\n let brief = '';\n if (params.goal) {\n brief = await buildGoalAwareBrief(params.goal, signal);\n }\n\n const briefFallbackNote = params.goal && !brief\n ? '\\n\\n---\\n\\n> _Goal-tailored brief unavailable: LLM planner is not configured or failed this call. The static playbook above still applies; you can proceed with it, or retry `start-research` after verifying `LLM_API_KEY`._'\n : '';\n\n const content = brief\n ? `${scaffolding}\\n\\n---\\n\\n${brief}`\n : `${scaffolding}${briefFallbackNote}`;\n\n return toolSuccess(content);\n } catch (err: unknown) {\n const structuredError = classifyError(err);\n mcpLog('error', `start-research: ${structuredError.message}`, 'start-research');\n return toolFailure(\n formatError({\n code: structuredError.code,\n message: structuredError.message,\n retryable: structuredError.retryable,\n toolName: 'start-research',\n howToFix: ['Retry start-research. If the failure persists, verify LLM_API_KEY / LLM_BASE_URL / LLM_MODEL.'],\n }),\n );\n }\n}\n\nexport function registerStartResearchTool(server: MCPServer): void {\n server.tool(\n {\n name: 'start-research',\n title: 'Start Research Session',\n description:\n `Call this FIRST every research session. Provide a \\`goal\\`; I return a goal-tailored brief naming (a) \\`primary_branch\\` (reddit for sentiment/migration, web for spec/bug/pricing, both when opinion-heavy AND needs official sources), (b) the exact \\`first_call_sequence\\` of raw/smart search and scrape calls to fire, (c) 25\u201350 keyword seeds for your first search call, (d) iteration hints, (e) gaps to watch, (f) stop criteria. ${QUERY_REWRITE_PAIR_GUIDANCE_TEXT} No goal? You still get the generic 5-tool playbook. Other tools work without calling this, but you will use them worse.`,\n schema: startResearchParamsSchema,\n annotations: {\n readOnlyHint: true,\n idempotentHint: true,\n destructiveHint: false,\n openWorldHint: false,\n },\n },\n async (args) => toToolResponse(await handleStartResearch(args)),\n );\n}\n", "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 is configured (LLM_API_KEY + LLM_BASE_URL + LLM_MODEL all set), the server returns a goal-tailored brief: classified goal type (spec | bug | migration | sentiment | pricing | security | synthesis | product_launch), a `primary_branch` recommendation (reddit for sentiment/migration; web for spec/bug/pricing; both when opinion-heavy AND needs official sources), the exact `first_call_sequence` of raw/smart search and scrape calls to fire, 25\u201350 keyword seeds for the first search call, iteration hints, gaps to watch, and stop criteria. The goal also sets the post-sort relevance target, so state the evidence you need and what \"done\" means. No goal \u2192 the generic 5-tool playbook (no tailored brief). 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 5-tool research playbook (toolbelt overview, the loop, output discipline). Default false \u2014 when the LLM planner is offline the server emits a compact stub that already names the tools and 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 type StartResearchParams = z.infer<typeof startResearchParamsSchema>;\n\n// `start-research` is text-only: the tool registration deliberately omits\n// `outputSchema`, and successful calls omit `structuredContent`.\nexport type StartResearchOutput = Record<string, never>;\n", "import { z } from 'zod';\n\nexport const QUERY_REWRITE_PAIR_EXAMPLES = [\n 'Bad: `<feature> support` \u2192 Better: `site:<official-docs-domain> \"<feature>\" \"<platform-or-version>\"`',\n 'Bad: `<product> pricing` \u2192 Better: `site:<vendor-domain> \"<product>\" pricing \"enterprise\" OR \"free tier\"`',\n 'Bad: `<library> bug fix` \u2192 Better: `\"<exact error text>\" \"<library-or-package>\" \"<version>\" site:github.com`',\n 'Bad: `<tool> reviews` \u2192 Better: `site:reddit.com/r/<community>/comments \"<tool>\" \"migration\" OR \"regression\"`',\n] as const;\n\nexport const QUERY_REWRITE_PAIR_GUIDANCE = [\n 'Write Google retrieval probes, not topic labels.',\n 'For each broad idea, rewrite it into a query that names the evidence source class, discriminating anchor terms, and one useful operator when possible.',\n 'Use rewrite-pair thinking before searching:',\n ...QUERY_REWRITE_PAIR_EXAMPLES,\n 'Do not repeat the same noun phrase with adjectives changed; fan out by source type and evidence need.',\n] as const;\n\nexport const QUERY_REWRITE_PAIR_GUIDANCE_TEXT = QUERY_REWRITE_PAIR_GUIDANCE.join(' ');\n\nconst keywordSchema = z\n .string()\n .min(1, { message: 'search: Keyword cannot be empty' })\n .describe(\n `A single search keyword/query. Each item runs as a separate parallel search. ${QUERY_REWRITE_PAIR_GUIDANCE_TEXT}`,\n );\n\nconst keywordsSchema = z\n .array(keywordSchema)\n .min(1, { message: 'search: At least 1 keyword required' })\n .max(50, { message: 'search: At most 50 keywords allowed per call' })\n .describe(\n `Search keywords to run in parallel. Serper is primary when configured; Jina Search is fallback when Serper is missing, fails, or yields empty query results. ${QUERY_REWRITE_PAIR_GUIDANCE_TEXT} Think of keywords as retrieval probes, not topic labels. Pack distinct facets in one call: official docs, implementation, failures, comparisons, sentiment, changelog, CVE, pricing, or other source classes.`,\n );\n\nexport const rawWebSearchParamsSchema = z.object({\n keywords: keywordsSchema,\n extract: z.never().optional(),\n scope: z.never().optional(),\n verbose: z.never().optional(),\n}).strict();\n\nexport const smartWebSearchParamsSchema = z.object({\n keywords: keywordsSchema,\n extract: z\n .string()\n .min(1, { message: 'smart-web-search: extract cannot be empty' })\n .describe(\n 'Semantic instruction for the relevance classifier \u2014 what \"relevant\" means for THIS goal. This is the post-sort target, so name the evidence you need and the source-of-truth expectation: e.g. official docs/release notes for specs, issue/PR/error text for bugs, Reddit/HN/blogs for lived experience, vendor pricing pages for pricing, CVE databases for security. 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\".',\n ),\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 keyword and filters results to post permalinks (`/r/.+/comments/[a-z0-9]+/`); subreddit homepages are dropped. \"both\" = runs every keyword 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 opinion-heavy AND official sources also matter.',\n ),\n verbose: z\n .boolean()\n .default(false)\n .describe(\n 'Include per-row scoring/coverage metadata, the trailing Signals block, and CONSENSUS labels even when they carry little signal. Default false.',\n ),\n}).strict();\n\nexport type RawWebSearchParams = z.infer<typeof rawWebSearchParamsSchema>;\nexport type SmartWebSearchParams = z.infer<typeof smartWebSearchParamsSchema>;\n\n// Internal alias retained for shared tests/helpers. Public tools register the\n// raw/smart schemas above, not this alias.\nexport const webSearchParamsSchema = smartWebSearchParamsSchema;\nexport type WebSearchParams = SmartWebSearchParams;\n\n// Search tools are markdown-only at the MCP boundary.\nexport type WebSearchOutput = Record<string, never>;\n", "/**\n * LLM Processor for content extraction\n * Uses any OpenAI-compatible endpoint. Reasoning effort is always 'low'.\n * Primary model exhausts its retries first; fallback model (LLM_FALLBACK_MODEL) then\n * gets up to FALLBACK_RETRY_COUNT additional attempts before the call fails.\n * NEVER throws \u2014 always returns a valid result.\n */\n\nimport OpenAI from 'openai';\nimport { LLM_EXTRACTION, getCapabilities } from '../config/index.js';\nimport { QUERY_REWRITE_PAIR_GUIDANCE_TEXT } from '../schemas/web-search.js';\nimport {\n classifyError,\n sleep,\n ErrorCode,\n withStallProtection,\n type StructuredError,\n} from '../utils/errors.js';\nimport { mcpLog } from '../utils/logger.js';\n\n/** Maximum input characters for LLM processing (~125k tokens, sized for the larger fallback model) */\nconst MAX_LLM_INPUT_CHARS = 500_000 as const;\n\n/**\n * Maximum input characters for the primary model when it has a smaller context window.\n * Used when an input would exceed the mini model's limits so the call goes straight to fallback\n * instead of burning retries on guaranteed context_length_exceeded errors.\n */\nconst MAX_PRIMARY_MODEL_INPUT_CHARS = 100_000 as const;\n\n/** LLM client timeout in milliseconds */\nconst LLM_CLIENT_TIMEOUT_MS = 600_000 as const;\n\n/** Jitter factor for exponential backoff */\nconst BACKOFF_JITTER_FACTOR = 0.3 as const;\n\n/** Stall detection timeout \u2014 abort if no response in this time */\nconst LLM_STALL_TIMEOUT_MS = 75_000 as const;\n\n/** Hard request deadline for LLM calls */\nconst LLM_REQUEST_DEADLINE_MS = 150_000 as const;\n\n// ============================================================================\n// LLM health tracking \u2014 surfaced via health://status so capability-aware\n// clients can branch on degraded mode without parsing per-call footers.\n// ============================================================================\n\ntype LLMHealthKind = 'planner' | 'extractor';\n\nexport interface LLMHealthSnapshot {\n readonly lastPlannerOk: boolean;\n readonly lastExtractorOk: boolean;\n readonly lastPlannerCheckedAt: string | null;\n readonly lastExtractorCheckedAt: string | null;\n readonly lastPlannerError: string | null;\n readonly lastExtractorError: string | null;\n readonly plannerConfigured: boolean;\n readonly extractorConfigured: boolean;\n /** Failures since the last success. Reset to 0 on `markLLMSuccess`. */\n readonly consecutivePlannerFailures: number;\n readonly consecutiveExtractorFailures: number;\n}\n\nconst llmHealth = {\n lastPlannerOk: false,\n lastExtractorOk: false,\n lastPlannerCheckedAt: null as string | null,\n lastExtractorCheckedAt: null as string | null,\n lastPlannerError: null as string | null,\n lastExtractorError: null as string | null,\n consecutivePlannerFailures: 0,\n consecutiveExtractorFailures: 0,\n};\n\nexport function markLLMSuccess(kind: LLMHealthKind): void {\n const ts = new Date().toISOString();\n if (kind === 'planner') {\n llmHealth.lastPlannerOk = true;\n llmHealth.lastPlannerCheckedAt = ts;\n llmHealth.lastPlannerError = null;\n llmHealth.consecutivePlannerFailures = 0;\n } else {\n llmHealth.lastExtractorOk = true;\n llmHealth.lastExtractorCheckedAt = ts;\n llmHealth.lastExtractorError = null;\n llmHealth.consecutiveExtractorFailures = 0;\n }\n}\n\nexport function markLLMFailure(kind: LLMHealthKind, err: unknown): void {\n const ts = new Date().toISOString();\n const message = err instanceof Error ? err.message : String(err ?? 'unknown error');\n if (kind === 'planner') {\n llmHealth.lastPlannerOk = false;\n llmHealth.lastPlannerCheckedAt = ts;\n llmHealth.lastPlannerError = message;\n llmHealth.consecutivePlannerFailures += 1;\n } else {\n llmHealth.lastExtractorOk = false;\n llmHealth.lastExtractorCheckedAt = ts;\n llmHealth.lastExtractorError = message;\n llmHealth.consecutiveExtractorFailures += 1;\n }\n}\n\nexport function getLLMHealth(): LLMHealthSnapshot {\n const cap = getCapabilities();\n return {\n lastPlannerOk: llmHealth.lastPlannerOk,\n lastExtractorOk: llmHealth.lastExtractorOk,\n lastPlannerCheckedAt: llmHealth.lastPlannerCheckedAt,\n lastExtractorCheckedAt: llmHealth.lastExtractorCheckedAt,\n lastPlannerError: llmHealth.lastPlannerError,\n lastExtractorError: llmHealth.lastExtractorError,\n // Static capability \u2014 based on env presence at boot. Runtime health (above)\n // tells whether the last attempt actually succeeded.\n plannerConfigured: cap.llmExtraction,\n extractorConfigured: cap.llmExtraction,\n consecutivePlannerFailures: llmHealth.consecutivePlannerFailures,\n consecutiveExtractorFailures: llmHealth.consecutiveExtractorFailures,\n };\n}\n\n/** Test-only \u2014 reset state between tests. Not exported from index. */\nexport function _resetLLMHealthForTests(): void {\n llmHealth.lastPlannerOk = false;\n llmHealth.lastExtractorOk = false;\n llmHealth.lastPlannerCheckedAt = null;\n llmHealth.lastExtractorCheckedAt = null;\n llmHealth.lastPlannerError = null;\n llmHealth.lastExtractorError = null;\n llmHealth.consecutivePlannerFailures = 0;\n llmHealth.consecutiveExtractorFailures = 0;\n}\n\ninterface ProcessingConfig {\n readonly enabled: boolean;\n readonly extract: string | undefined;\n readonly url?: string;\n}\n\ninterface LLMResult {\n readonly content: string;\n readonly processed: boolean;\n readonly error?: string;\n readonly errorDetails?: StructuredError;\n}\n\n// LLM-specific retry configuration\nconst LLM_RETRY_CONFIG = {\n maxRetries: 2,\n baseDelayMs: 1000,\n maxDelayMs: 5000,\n} as const;\n\n/** Number of additional attempts using the fallback model after primary exhausts. */\nconst FALLBACK_RETRY_COUNT = 3 as const;\n\n// OpenAI-compatible retryable error codes (using Set for type-safe lookup)\nconst RETRYABLE_LLM_ERROR_CODES = new Set([\n 'rate_limit_exceeded',\n 'server_error',\n 'timeout',\n 'service_unavailable',\n]);\n\n/** Type guard for errors with an HTTP status code */\nfunction hasStatus(error: unknown): error is { status: number } {\n return (\n typeof error === 'object' &&\n error !== null &&\n 'status' in error &&\n typeof (error as Record<string, unknown>).status === 'number'\n );\n}\n\nlet llmClient: OpenAI | null = null;\n\ninterface ChatCompletionTextResponse {\n readonly choices?: ReadonlyArray<{\n readonly message?: {\n readonly content?: string | null;\n } | null;\n } | null>;\n}\n\nexport interface OpenAITextGenerator {\n readonly chat: {\n readonly completions: {\n readonly create: (\n body: OpenAI.ChatCompletionCreateParamsNonStreaming,\n options: { readonly signal?: AbortSignal; readonly timeout: number },\n ) => Promise<ChatCompletionTextResponse>;\n };\n };\n}\n\ninterface LLMTextSuccess {\n readonly content: string;\n readonly model: string;\n}\n\ninterface LLMTextEmptyFailure {\n readonly content: null;\n readonly model: string;\n readonly error: string;\n readonly failureKind: 'empty';\n}\n\ninterface LLMTextProviderFailure {\n readonly content: null;\n readonly model: string;\n readonly error: string;\n readonly failureKind: 'provider';\n readonly errorCause: unknown;\n}\n\ntype LLMTextFailure = LLMTextEmptyFailure | LLMTextProviderFailure;\n\nexport type LLMTextResponse = LLMTextSuccess | LLMTextFailure;\n\nexport function createLLMProcessor(): OpenAI | null {\n if (!getCapabilities().llmExtraction) return null;\n\n if (!llmClient) {\n llmClient = new OpenAI({\n baseURL: LLM_EXTRACTION.BASE_URL,\n apiKey: LLM_EXTRACTION.API_KEY,\n timeout: LLM_CLIENT_TIMEOUT_MS,\n maxRetries: 0,\n defaultHeaders: { 'X-Title': 'mcp-research-powerpack' },\n });\n mcpLog('info', `LLM extraction configured (model: ${LLM_EXTRACTION.MODEL}, baseURL: ${LLM_EXTRACTION.BASE_URL})`, 'llm');\n }\n return llmClient;\n}\n\nfunction buildChatRequestBody(model: string, prompt: string): Record<string, unknown> {\n return {\n model,\n messages: [{ role: 'user', content: prompt }],\n reasoning_effort: 'low',\n };\n}\n\nfunction normalizeProviderError(err: unknown, message: string): unknown {\n if (typeof err === 'object' && err !== null) return err;\n return new Error(message);\n}\n\nfunction getProviderFailure(response: LLMTextResponse): unknown | null {\n if (response.content !== null || response.failureKind !== 'provider') return null;\n return response.errorCause;\n}\n\nfunction emptyLLMExtractionResult(content: string): LLMResult {\n return {\n content,\n processed: false,\n error: 'LLM returned empty response',\n errorDetails: {\n code: ErrorCode.INTERNAL_ERROR,\n message: 'LLM returned empty response',\n retryable: false,\n },\n };\n}\n\nexport async function requestText(\n processor: OpenAITextGenerator,\n prompt: string,\n operationLabel: string,\n signal?: AbortSignal,\n modelOverride?: string,\n): Promise<LLMTextResponse> {\n const model = modelOverride || LLM_EXTRACTION.MODEL;\n\n try {\n const response = await withStallProtection(\n (stallSignal) => processor.chat.completions.create(\n buildChatRequestBody(model, prompt) as unknown as OpenAI.ChatCompletionCreateParamsNonStreaming,\n {\n signal: signal ? AbortSignal.any([stallSignal, signal]) : stallSignal,\n timeout: LLM_REQUEST_DEADLINE_MS,\n },\n ),\n LLM_STALL_TIMEOUT_MS,\n 3,\n `${operationLabel} (${model})`,\n );\n\n const content = response.choices?.[0]?.message?.content?.trim();\n if (content) {\n return { content, model };\n }\n\n const err = `Empty response from model ${model}`;\n mcpLog('warning', `${operationLabel} returned empty content for model ${model}`, 'llm');\n return { content: null, model, error: err, failureKind: 'empty' };\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n mcpLog('warning', `${operationLabel} failed for model ${model}: ${message}`, 'llm');\n return {\n content: null,\n model,\n error: message,\n failureKind: 'provider',\n errorCause: normalizeProviderError(err, message),\n };\n }\n}\n\n/**\n * Single LLM call with automatic fallback model.\n * Tries the primary model once; if it fails and LLM_FALLBACK_MODEL is set,\n * retries up to FALLBACK_RETRY_COUNT times on the fallback model.\n * Used for single-shot calls (classify, brief, refine queries).\n */\nexport async function requestTextWithFallback(\n processor: OpenAITextGenerator,\n prompt: string,\n operationLabel: string,\n signal?: AbortSignal,\n): Promise<LLMTextResponse> {\n const primary = await requestText(processor, prompt, operationLabel, signal);\n if (primary.content !== null) return primary;\n\n const fallbackModel = LLM_EXTRACTION.FALLBACK_MODEL;\n if (!fallbackModel) return primary;\n\n mcpLog('warning', `Primary model failed, switching to fallback ${fallbackModel}`, 'llm');\n\n let lastFailure: LLMTextFailure = primary;\n for (let attempt = 0; attempt < FALLBACK_RETRY_COUNT; attempt++) {\n if (attempt > 0) {\n const delayMs = calculateLLMBackoff(attempt - 1);\n mcpLog('warning', `Fallback retry ${attempt}/${FALLBACK_RETRY_COUNT - 1} in ${delayMs}ms`, 'llm');\n try { await sleep(delayMs, signal); } catch { break; }\n }\n const result = await requestText(processor, prompt, `${operationLabel} [fallback]`, signal, fallbackModel);\n if (result.content !== null) return result;\n lastFailure = result;\n }\n\n return lastFailure;\n}\n\n/**\n * Check if an LLM error is retryable\n */\nfunction isRetryableLLMError(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false;\n\n // Stall/timeout protection errors - always retry these\n const stallCode = (error as { code?: string })?.code;\n if (stallCode === 'ESTALLED' || stallCode === 'ETIMEDOUT') {\n return true;\n }\n\n // Check HTTP status codes\n if (hasStatus(error)) {\n if (error.status === 429 || error.status === 500 || error.status === 502 || error.status === 503 || error.status === 504) {\n return true;\n }\n }\n\n // Check error codes from the OpenAI-compatible endpoint\n const record = error as Record<string, unknown>;\n const code = typeof record.code === 'string' ? record.code : undefined;\n const nested =\n typeof record.error === 'object' && record.error !== null\n ? (record.error as Record<string, unknown>)\n : null;\n const errorCode =\n code ??\n (nested && typeof nested.code === 'string' ? nested.code : undefined) ??\n (nested && typeof nested.type === 'string' ? nested.type : undefined);\n if (errorCode && RETRYABLE_LLM_ERROR_CODES.has(errorCode)) {\n return true;\n }\n\n // Check message for common patterns\n const message = typeof record.message === 'string' ? record.message.toLowerCase() : '';\n if (\n message.includes('rate limit') ||\n message.includes('timeout') ||\n message.includes('timed out') ||\n message.includes('service unavailable') ||\n message.includes('server error') ||\n message.includes('connection') ||\n message.includes('econnreset')\n ) {\n return true;\n }\n\n return false;\n}\n\n/**\n * Detect \"the prompt is too long for this model\" errors.\n * These are NOT retryable on the same model \u2014 we should skip remaining primary retries\n * and go straight to the fallback model (which has a larger context window).\n */\nfunction isContextWindowError(error: unknown): boolean {\n if (!error || typeof error !== 'object') return false;\n\n const record = error as Record<string, unknown>;\n const nested =\n typeof record.error === 'object' && record.error !== null\n ? (record.error as Record<string, unknown>)\n : null;\n\n const code = typeof record.code === 'string' ? record.code : undefined;\n const nestedCode = nested && typeof nested.code === 'string' ? nested.code : undefined;\n if (code === 'context_length_exceeded' || nestedCode === 'context_length_exceeded') {\n return true;\n }\n\n const messages: string[] = [];\n if (typeof record.message === 'string') messages.push(record.message);\n if (nested && typeof nested.message === 'string') messages.push(nested.message);\n const combined = messages.join(' ').toLowerCase();\n return (\n combined.includes('context length') ||\n combined.includes('context window') ||\n combined.includes('maximum context') ||\n combined.includes('maximum tokens') ||\n combined.includes('token limit') ||\n combined.includes('too many tokens') ||\n combined.includes('prompt is too long') ||\n combined.includes('reduce the length')\n );\n}\n\n/**\n * Calculate backoff delay with jitter for LLM retries\n */\nfunction calculateLLMBackoff(attempt: number): number {\n const exponentialDelay = LLM_RETRY_CONFIG.baseDelayMs * Math.pow(2, attempt);\n const jitter = Math.random() * BACKOFF_JITTER_FACTOR * exponentialDelay;\n return Math.min(exponentialDelay + jitter, LLM_RETRY_CONFIG.maxDelayMs);\n}\n\n/**\n * Process content with LLM extraction\n * NEVER throws - always returns a valid LLMResult\n * Implements retry logic with exponential backoff for transient failures\n */\nexport async function processContentWithLLM(\n content: string,\n config: ProcessingConfig,\n processor?: OpenAITextGenerator | null,\n signal?: AbortSignal\n): Promise<LLMResult> {\n // Early returns for invalid/skip conditions\n if (!config.enabled) {\n return { content, processed: false };\n }\n\n if (!processor) {\n return {\n content,\n processed: false,\n error: 'LLM processor not available (LLM_API_KEY, LLM_BASE_URL, and LLM_MODEL must all be set)',\n errorDetails: {\n code: ErrorCode.AUTH_ERROR,\n message: 'LLM processor not available',\n retryable: false,\n },\n };\n }\n\n if (!content?.trim()) {\n return { content: content || '', processed: false, error: 'Empty content provided' };\n }\n\n // Truncate extremely long content to avoid blowing past even the fallback model's context.\n const truncatedContent = content.length > MAX_LLM_INPUT_CHARS\n ? content.substring(0, MAX_LLM_INPUT_CHARS) + '\\n\\n[Content truncated due to length]'\n : content;\n\n // If the prompt would exceed the primary (mini) model's smaller context window,\n // skip it entirely and go straight to the fallback model. Saves burning retries\n // on guaranteed context_length_exceeded errors.\n const skipPrimaryForSize =\n truncatedContent.length > MAX_PRIMARY_MODEL_INPUT_CHARS && !!LLM_EXTRACTION.FALLBACK_MODEL;\n\n // Sanitize URL before sending to LLM: drop query string and fragment\n // so signed URLs, session tokens, auth params, or tracking hashes never\n // land in a third-party LLM prompt. Keep origin + path for page-type classification.\n const safeUrl = (() => {\n if (!config.url) return undefined;\n try {\n const u = new URL(config.url);\n return `${u.origin}${u.pathname}`;\n } catch {\n return undefined;\n }\n })();\n const urlLine = safeUrl ? `PAGE URL: ${safeUrl}\\n\\n` : '';\n\n const prompt = config.extract\n ? `You are a factual extractor for a research agent. Extract ONLY the information that matches the instruction below. Do not summarize, interpret, or editorialize.\n\n${urlLine}EXTRACTION INSTRUCTION: ${config.extract}\n\nSTEP 1 \u2014 Classify this page. Look at the URL if present, plus structural cues (code blocks, table patterns, comment threads, marketing copy). Pick ONE:\n\\`docs | changelog | github-readme | github-thread | reddit | hackernews | forum | blog | marketing | announcement | qa | cve | paper | release-notes | other\\`\n\nSTEP 2 \u2014 Adjust emphasis by page type:\n- docs / changelog / github-readme / release-notes \u2192 API signatures, version numbers, flags, exact config keys, code blocks. Copy verbatim. Preserve tables as tables.\n- github-thread \u2192 weight MAINTAINER comments (label \"[maintainer]\") over drive-by commenters. Preserve stacktraces verbatim. Capture chronological resolution \u2014 what was decided and when. Link the accepted-fix commit/PR if referenced.\n- reddit / hackernews / forum \u2192 lived experience. Quote verbatim with attribution (\"u/foo wrote: \u2026\" or \"user <name>\"). Prioritize replies with stack details, specific failure stories, or replies that contradict the OP. Record overall sentiment distribution as one bullet if clear skew (\"~70% agree / ~20% dissent / rest off-topic\"). Drop context-free opinions (\"this sucks\") from Matches.\n- blog \u2192 prioritize concrete reproductions, code, measurements. If the author makes a claim without evidence, mark \"[unsourced claim]\".\n- marketing / announcement \u2192 pricing tiers, feature matrices verbatim, free-tier quotas, enterprise contact. Preserve tables as tables. Treat roadmap/future-tense claims skeptically \u2014 note them as \"[announced, not shipped]\" when framing is future-tense.\n- qa (stackoverflow) \u2192 accepted answer's code + high-voted disagreements. Always note the answer date \u2014 SO rots.\n- cve \u2192 CVSS vector verbatim, CWE, CPE ranges, affected versions, fix version, references. Each with its label.\n- paper \u2192 claim, method, dataset, benchmark numbers, comparison baseline. Preserve numeric deltas verbatim.\n\nSTEP 3 \u2014 Emit markdown with these sections, in order:\n\n## Source\n- URL: <verbatim if visible, else \"unknown\">\n- Page type: <the type you picked>\n- Page date: <verbatim if visible, else \"not visible\">\n- Author / maintainer (if identifiable): <verbatim>\n\n## Matches\nOne bullet per distinct piece of matching info:\n- **<short label>** \u2014 the information. Quote VERBATIM for: numbers, versions, dates, API names, prices, error messages, stacktraces, CVSS vectors, benchmark scores, command flags, proper nouns, and people's words. Backticks for code/identifiers. Preserve tables.\n\n## Not found\nEvery part of the extraction instruction this page did NOT answer. Be explicit. Example: \"Enterprise pricing contact \u2014 not present on this page.\"\n\n## Follow-up signals\nShort bullets \u2014 NEW angles this page surfaced that the agent should investigate. Include: new terms, unexpected vendor names, contradicting claims, referenced-but-unscraped URLs. Copy URLs VERBATIM from the source; if only anchor text is visible, write \"anchor: <text> (URL not in scraped content)\". Skip this section if nothing new surfaced. Do NOT invent.\n\n## Contradictions\n(Include this section only if the page contains internally contradictory claims.) Bullet each contradiction with both sides quoted verbatim.\n\n## Truncation\n(Include only if content appears cut mid-element.) \"Content cut mid-<table row / code block / comment / paragraph>; extraction may be incomplete for <section>.\"\n\nRULES:\n- Never paraphrase numbers, versions, code, or quoted text.\n- If an instruction item is not answered, it goes in \"Not found\" \u2014 do NOT invent an answer to please the caller.\n- Preserve code blocks, command examples, tables exactly.\n- Do NOT add commentary or recommendations outside \"Follow-up signals\".\n- Page language \u2260 English: quote verbatim in the original language AND provide a parenthetical gloss in English.\n- Page appears gated (login wall, paywall, JS-render-empty shell) or near-empty: BEFORE dismissing the page, look for ANY visible text \u2014 og:title, og:description, meta description, headline, author name, nav labels, teaser/preview sentences, visible comment snippets. If ANY such text exists, extract it as usual under \\`## Source\\` + \\`## Matches\\`, and list the blocked facets under \\`## Not found\\`. Prefix the first \\`## Matches\\` bullet with \\`**[partial \u2014 <reason>]**\\` so the caller knows the body is gated (reasons: \\`login-wall | paywall | JS-render-empty | truncated-before-relevant-section\\`). ONLY when there is NO visible extractable text at all (< 50 words AND no og:* AND no headline AND no preview), return exactly one line:\n \\`## Matches\\\\n_Page did not load: <reason>_\\`\n Valid reasons: \\`404 | login-wall | paywall | JS-render-empty | non-text-asset | truncated-before-relevant-section\\`.\n\nContent:\n${truncatedContent}`\n : `Clean the following page content: drop navigation, ads, cookie banners, footers, author bios, related-article lists. Preserve headings, paragraphs, code blocks, tables, and inline links as \\`[text](url)\\`. Do NOT summarize \u2014 preserve the full body.\n\n${urlLine}Content:\n${truncatedContent}`;\n\n let lastError: StructuredError | undefined;\n\n // Phase 1: primary model with up to LLM_RETRY_CONFIG.maxRetries retries.\n // Skip entirely when the input is too big for the primary's context window.\n if (skipPrimaryForSize) {\n mcpLog(\n 'info',\n `Input ${truncatedContent.length} chars exceeds primary model cap (${MAX_PRIMARY_MODEL_INPUT_CHARS}); routing directly to fallback`,\n 'llm',\n );\n } else {\n for (let attempt = 0; attempt <= LLM_RETRY_CONFIG.maxRetries; attempt++) {\n try {\n if (attempt === 0) {\n mcpLog('info', `Starting extraction with ${LLM_EXTRACTION.MODEL}`, 'llm');\n } else {\n mcpLog('warning', `Retry attempt ${attempt}/${LLM_RETRY_CONFIG.maxRetries}`, 'llm');\n }\n\n const response = await requestText(processor, prompt, 'LLM extraction', signal);\n\n if (response.content !== null) {\n mcpLog('info', `Successfully extracted ${response.content.length} characters`, 'llm');\n markLLMSuccess('extractor');\n return { content: response.content, processed: true };\n }\n\n const providerFailure = getProviderFailure(response);\n if (providerFailure) {\n throw providerFailure;\n }\n\n // Empty response \u2014 not retryable\n mcpLog('warning', 'Received empty response from LLM', 'llm');\n markLLMFailure('extractor', 'LLM returned empty response');\n return emptyLLMExtractionResult(content);\n\n } catch (err: unknown) {\n lastError = classifyError(err);\n const status = hasStatus(err) ? err.status : undefined;\n const code = typeof err === 'object' && err !== null && 'code' in err\n ? String((err as Record<string, unknown>).code)\n : undefined;\n const ctxErr = isContextWindowError(err);\n mcpLog('error', `Error (attempt ${attempt + 1}): ${lastError.message} [status=${status}, code=${code}, retryable=${isRetryableLLMError(err)}, context_window=${ctxErr}]`, 'llm');\n\n // Context window errors are not retryable on the same model \u2014 jump to fallback.\n if (ctxErr) {\n mcpLog('warning', 'Context window exceeded on primary \u2014 skipping remaining retries, routing to fallback', 'llm');\n break;\n }\n\n if (isRetryableLLMError(err) && attempt < LLM_RETRY_CONFIG.maxRetries) {\n const delayMs = calculateLLMBackoff(attempt);\n mcpLog('warning', `Retrying in ${delayMs}ms...`, 'llm');\n try { await sleep(delayMs, signal); } catch { break; }\n continue;\n }\n break;\n }\n }\n }\n\n // Phase 2: fallback model \u2014 FALLBACK_RETRY_COUNT attempts before giving up\n const fallbackModel = LLM_EXTRACTION.FALLBACK_MODEL;\n if (fallbackModel) {\n mcpLog('warning', `Primary exhausted, switching to fallback ${fallbackModel}`, 'llm');\n for (let attempt = 0; attempt < FALLBACK_RETRY_COUNT; attempt++) {\n if (attempt > 0) {\n const delayMs = calculateLLMBackoff(attempt - 1);\n mcpLog('warning', `Fallback retry ${attempt}/${FALLBACK_RETRY_COUNT - 1} in ${delayMs}ms`, 'llm');\n try { await sleep(delayMs, signal); } catch { break; }\n }\n try {\n const response = await requestText(processor, prompt, 'LLM extraction [fallback]', signal, fallbackModel);\n if (response.content !== null) {\n mcpLog('info', `Fallback extracted ${response.content.length} characters`, 'llm');\n markLLMSuccess('extractor');\n return { content: response.content, processed: true };\n }\n\n const providerFailure = getProviderFailure(response);\n if (providerFailure) {\n throw providerFailure;\n }\n\n mcpLog('warning', 'Fallback returned empty response', 'llm');\n markLLMFailure('extractor', 'LLM returned empty response');\n return emptyLLMExtractionResult(content);\n } catch (err: unknown) {\n lastError = classifyError(err);\n mcpLog('error', `Fallback error (attempt ${attempt + 1}): ${lastError.message}`, 'llm');\n // Stop burning attempts on errors that won't change with another try.\n // Context-window errors are deterministic on the same model (the prompt\n // is identical every retry) \u2014 see isContextWindowError docstring and\n // the matching primary-loop short-circuit at line 608. Only keep\n // retrying on transient failures (rate-limit / 5xx / stall).\n if (isContextWindowError(err) || !isRetryableLLMError(err)) break;\n }\n }\n }\n\n const errorMessage = lastError?.message || 'Unknown LLM error';\n mcpLog('error', `All attempts failed: ${errorMessage}. Returning original content.`, 'llm');\n markLLMFailure('extractor', errorMessage);\n\n return {\n content,\n processed: false,\n error: `LLM extraction failed: ${errorMessage}`,\n errorDetails: lastError || {\n code: ErrorCode.UNKNOWN_ERROR,\n message: errorMessage,\n retryable: false,\n },\n };\n}\n\n// ============================================================================\n// Web-Search Result Classification\n// ============================================================================\n\n/** Maximum URLs to send to the LLM for classification */\nconst MAX_CLASSIFICATION_URLS = 50 as const;\n\n/** Classification tiers */\ntype ClassificationTier = 'HIGHLY_RELEVANT' | 'MAYBE_RELEVANT' | 'OTHER';\n\nexport interface ClassificationEntry {\n readonly rank: number;\n readonly tier: ClassificationTier;\n readonly source_type?: string;\n readonly reason?: string;\n}\n\nexport interface ClassificationGap {\n readonly id: number;\n readonly description: string;\n}\n\nexport interface ClassificationResult {\n readonly title: string;\n readonly synthesis: string;\n readonly results: ClassificationEntry[];\n readonly refine_queries?: Array<{\n readonly query: string;\n readonly rationale: string;\n readonly gap_id?: number;\n }>;\n readonly confidence?: 'high' | 'medium' | 'low';\n readonly confidence_reason?: string;\n readonly gaps?: ClassificationGap[];\n}\n\nexport interface RefineQuerySuggestion {\n readonly query: string;\n readonly rationale: string;\n readonly gap_id?: number;\n readonly gap_description?: string;\n}\n\n/**\n * Classify smart search results by relevance to an objective using the LLM.\n * Sends only titles, snippets, and domain names \u2014 does NOT fetch URLs.\n * Returns null on failure (caller should fall back to raw output).\n */\nexport async function classifySearchResults(\n rankedUrls: ReadonlyArray<{\n readonly rank: number;\n readonly url: string;\n readonly title: string;\n readonly snippet: string;\n readonly frequency: number;\n readonly queries: string[];\n }>,\n objective: string,\n totalQueries: number,\n processor: OpenAI,\n previousQueries: readonly string[] = [],\n): Promise<{ result: ClassificationResult | null; error?: string }> {\n const urlsToClassify = rankedUrls.slice(0, MAX_CLASSIFICATION_URLS);\n\n // Descending static weights fed to the LLM. Higher-ranked URLs get a bigger\n // weight so the classifier biases HIGHLY_RELEVANT toward them. The weights\n // here are a shown-to-LLM summary, not the internal CTR ranking (which\n // still runs in url-aggregator.ts). Rank 11+ all bucket to w=1.\n const STATIC_WEIGHTS = [30, 20, 15, 10, 8, 6, 5, 4, 3, 2] as const;\n const weightForRank = (rank: number): number => STATIC_WEIGHTS[rank - 1] ?? 1;\n\n // Build compressed result list \u2014 weight + title + domain + snippet (truncated)\n const lines: string[] = [];\n for (const url of urlsToClassify) {\n let domain: string;\n try {\n domain = new URL(url.url).hostname.replace(/^www\\./, '');\n } catch {\n domain = url.url;\n }\n const snippet = url.snippet.length > 120\n ? url.snippet.slice(0, 117) + '...'\n : url.snippet;\n lines.push(`[${url.rank}] w=${weightForRank(url.rank)} ${url.title} \u2014 ${domain} \u2014 ${snippet}`);\n }\n\n const prevQueriesBlock = previousQueries.length > 0\n ? previousQueries.map((q) => `- ${q}`).join('\\n')\n : '- (none provided)';\n const today = new Date().toISOString().slice(0, 10);\n\n const prompt = `You are the relevance filter for a research agent. Classify each search result below against the objective and produce a structured analysis.\n\nOBJECTIVE: ${objective}\nTODAY: ${today}\n\nPREVIOUS QUERIES (already run \u2014 do NOT paraphrase in refine_queries):\n${prevQueriesBlock}\n\nReturn ONLY a JSON object (no markdown, no code fences):\n\n{\n \"title\": \"2\u20138 word label for this RESULT CLUSTER (not the objective)\",\n \"synthesis\": \"3\u20135 sentences grounded in the results. Every non-trivial claim cites a rank in [brackets], e.g. '[3] documents the flag; [7][12] report it is broken on macOS.' A synthesis with zero citations is invalid.\",\n \"confidence\": \"high | medium | low\",\n \"confidence_reason\": \"one sentence \u2014 why\",\n \"gaps\": [\n { \"id\": 0, \"description\": \"specific, actionable thing the current results do NOT answer \u2014 not 'more info needed'\" }\n ],\n \"refine_queries\": [\n { \"query\": \"concrete next search\", \"gap_id\": 0, \"rationale\": \"\u226412 words\" }\n ],\n \"results\": [\n {\n \"rank\": 1,\n \"tier\": \"HIGHLY_RELEVANT | MAYBE_RELEVANT | OTHER\",\n \"source_type\": \"vendor_doc | github | reddit | hackernews | blog | news | marketing | stackoverflow | cve | paper | release_notes | aggregator | other\",\n \"reason\": \"\u226412 words citing the snippet cue that drove the tier\"\n }\n ]\n}\n\nWEIGHT SCHEME: each row is prefixed with a weight (w=N). Higher weight means the URL ranked better across input queries \u2014 prefer HIGHLY_RELEVANT for high-weight rows when content matches the objective. Weight alone never justifies HIGHLY_RELEVANT; snippet cues still drive the decision.\n\nSOURCE-OF-TRUTH RUBRIC (the \"primary source\" is goal-dependent \u2014 infer goal type from the objective):\n- spec / API / config questions \u2192 vendor_doc, github (README, RFC), release_notes are primary\n- bug / failure-mode questions \u2192 github (issue/PR), stackoverflow are primary\n- migration / sentiment / lived-experience \u2192 reddit, hackernews, blog are primary; docs are secondary\n- pricing / commercial \u2192 marketing (the vendor's own pricing page IS the primary source, but treat feature lists skeptically)\n- security / CVE \u2192 cve databases, distro security trackers (nvd.nist.gov, security-tracker.debian.org, ubuntu.com/security) are primary\n- synthesis / open-ended \u2192 blend; no single type is primary\n- product launch \u2192 vendor_doc + news + marketing for the launch itself; blogs + reddit for independent verification\n\nFRESHNESS: proportional to topic velocity. For a week-old release, demote anything older than 30 days. For general tech questions, demote older than 18 months. For stable protocols (HTTP, TCP, POSIX), don't demote by age.\n\nCONFIDENCE:\n- high = \u22653 HIGHLY_RELEVANT results from INDEPENDENT domains agree on the core answer\n- medium = \u22652 HIGHLY_RELEVANT exist but disagree or share a domain; OR a single authoritative primary source answers it\n- low = otherwise; snippet-only judgments cap at medium\n\nREFINE QUERIES \u2014 each MUST differ from every previousQuery by:\n- a new operator (site:, quotes, verbatim version number), OR\n- a domain-specific noun ABSENT from every prior query\nAdding a year alone does NOT count as differentiation.\nEach refine_query MUST reference a specific gap_id from the gaps array above.\nProduce 4\u20138 refine_queries total. Cover: (a) a primary-source probe, (b) a temporal sharpener, (c) a failure-mode or comparison probe, (d) at least one new-term probe seeded by a specific result's snippet.\n\nRULES:\n- Classify ALL ${urlsToClassify.length} results. Do not skip or collapse any.\n- Use only the three tier values.\n- Judge from title + domain + snippet only. Do NOT invent facts not present in the snippet.\n- If ALL results are OTHER: synthesis = \"\", confidence = \"low\", and \\`gaps\\` must explicitly state why the current queries missed the target.\n- Casing: tier = UPPERCASE_WITH_UNDERSCORES, confidence = lowercase.\n\nSEARCH RESULTS (${urlsToClassify.length} URLs from ${totalQueries} queries):\n${lines.join('\\n')}`;\n\n try {\n mcpLog('info', `Classifying ${urlsToClassify.length} URLs against objective`, 'llm');\n\n const response = await requestTextWithFallback(\n processor,\n prompt,\n 'Search classification',\n );\n\n if (response.content === null) {\n const errMsg = response.error ?? 'LLM returned empty classification response';\n markLLMFailure('planner', errMsg);\n return { result: null, error: errMsg };\n }\n\n // Strip markdown code fences if present\n const cleaned = response.content.replace(/^```(?:json)?\\s*\\n?/m, '').replace(/\\n?```\\s*$/m, '').trim();\n const parsed = JSON.parse(cleaned) as ClassificationResult;\n\n // Validate the response shape.\n // Note: synthesis is typed not truthy \u2014 the prompt explicitly instructs an empty string\n // for the all-OTHER case, and we must not reject that.\n if (!parsed.title || typeof parsed.synthesis !== 'string' || !Array.isArray(parsed.results)) {\n const errMsg = 'LLM response missing required fields (title, synthesis, results)';\n markLLMFailure('planner', errMsg);\n return { result: null, error: errMsg };\n }\n\n mcpLog('info', `Classification complete: ${parsed.results.filter(r => r.tier === 'HIGHLY_RELEVANT').length} highly relevant`, 'llm');\n markLLMSuccess('planner');\n return { result: parsed };\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n mcpLog('error', `Classification failed: ${message}`, 'llm');\n markLLMFailure('planner', message);\n return { result: null, error: `Classification failed: ${message}` };\n }\n}\n\nexport async function suggestRefineQueriesForRawMode(\n rankedUrls: ReadonlyArray<{\n readonly rank: number;\n readonly url: string;\n readonly title: string;\n }>,\n objective: string,\n originalQueries: readonly string[],\n processor: OpenAI,\n): Promise<{ result: RefineQuerySuggestion[]; error?: string }> {\n const urlsToSummarize = rankedUrls.slice(0, 12);\n const lines = urlsToSummarize.map((url) => {\n let domain: string;\n try {\n domain = new URL(url.url).hostname.replace(/^www\\./, '');\n } catch {\n domain = url.url;\n }\n return `[${url.rank}] ${url.title} \u2014 ${domain}`;\n });\n\n const prompt = `You are generating follow-up search queries for an agent using raw search results.\n\nReturn ONLY a JSON object (no markdown, no code fences):\n{\n \"refine_queries\": [\n { \"query\": \"next search query\", \"gap_description\": \"what gap this closes\", \"rationale\": \"\u226412 words on why\" }\n ]\n}\n\nOBJECTIVE: ${objective}\n\nPREVIOUS QUERIES (already run \u2014 do NOT paraphrase):\n${originalQueries.map((query) => `- ${query}`).join('\\n')}\n\nTOP RESULT TITLES (to seed new-term probes):\n${lines.join('\\n')}\n\nRULES:\n- Produce 4\u20136 diverse follow-ups. Cover: (a) a primary-source probe (site:, RFC, vendor docs); (b) a temporal sharpener (changelog, version number); (c) a failure-mode or comparison probe; (d) at least one new-term probe seeded by a specific result title.\n- Each query MUST differ from every previousQuery by either a new operator (site:, quotes, a verbatim version number) OR a domain-specific noun absent from every prior query. Adding a year alone does NOT count.\n- Each refine_query MUST include a \\`gap_description\\` naming what the current results don't answer.\n- Do not include URLs.\n- Keep rationales \u226412 words.`;\n\n try {\n const response = await requestTextWithFallback(\n processor,\n prompt,\n 'Raw-mode refine query generation',\n );\n\n if (response.content === null) {\n const errMsg = response.error ?? 'LLM returned empty raw-mode refine query response';\n markLLMFailure('planner', errMsg);\n return { result: [], error: errMsg };\n }\n\n const cleaned = response.content.replace(/^```(?:json)?\\s*\\n?/m, '').replace(/\\n?```\\s*$/m, '').trim();\n const parsed = JSON.parse(cleaned) as { refine_queries?: RefineQuerySuggestion[] };\n\n markLLMSuccess('planner');\n return { result: Array.isArray(parsed.refine_queries) ? parsed.refine_queries : [] };\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n mcpLog('error', `Raw-mode refine query generation failed: ${message}`, 'llm');\n markLLMFailure('planner', message);\n return { result: [], error: message };\n }\n}\n\n// ============================================================================\n// Research Brief \u2014 goal-aware orientation (called by start-research)\n// ============================================================================\n\nexport type PrimaryBranch = 'reddit' | 'web' | 'both';\n\nexport interface ResearchBriefStep {\n readonly tool: 'raw-web-search' | 'smart-web-search' | 'raw-scrape-links' | 'smart-scrape-links';\n readonly reason: string;\n}\n\nexport interface ResearchBrief {\n readonly goal_class: string;\n readonly goal_class_reason: string;\n readonly primary_branch: PrimaryBranch;\n readonly primary_branch_reason: string;\n readonly freshness_window: string;\n readonly first_call_sequence: readonly ResearchBriefStep[];\n readonly keyword_seeds: readonly string[];\n readonly iteration_hints: readonly string[];\n readonly gaps_to_watch: readonly string[];\n readonly stop_criteria: readonly string[];\n}\n\nconst VALID_GOAL_CLASSES = new Set([\n 'spec', 'bug', 'migration', 'sentiment', 'pricing', 'security',\n 'synthesis', 'product_launch', 'other',\n]);\n\nconst VALID_FRESHNESS = new Set(['days', 'weeks', 'months', 'years']);\nconst VALID_BRANCHES = new Set<PrimaryBranch>(['reddit', 'web', 'both']);\nconst VALID_STEP_TOOLS = new Set(['raw-web-search', 'smart-web-search', 'raw-scrape-links', 'smart-scrape-links']);\n\nfunction isStringArray(value: unknown): value is string[] {\n return Array.isArray(value) && value.every((v) => typeof v === 'string');\n}\n\nfunction isStepArray(value: unknown): value is ResearchBriefStep[] {\n return Array.isArray(value) && value.every((s) => {\n if (typeof s !== 'object' || s === null) return false;\n const tool = (s as Record<string, unknown>).tool;\n const reason = (s as Record<string, unknown>).reason;\n return typeof tool === 'string'\n && VALID_STEP_TOOLS.has(tool)\n && typeof reason === 'string'\n && reason.trim().length > 0;\n });\n}\n\nexport function parseResearchBrief(raw: string): ResearchBrief | null {\n try {\n const cleaned = raw.replace(/^```(?:json)?\\s*\\n?/m, '').replace(/\\n?```\\s*$/m, '').trim();\n const parsed = JSON.parse(cleaned) as Record<string, unknown>;\n\n const goal_class = typeof parsed.goal_class === 'string' ? parsed.goal_class : null;\n if (!goal_class || !VALID_GOAL_CLASSES.has(goal_class)) return null;\n\n const freshness_window = typeof parsed.freshness_window === 'string' ? parsed.freshness_window : null;\n if (!freshness_window || !VALID_FRESHNESS.has(freshness_window)) return null;\n\n const primary_branch = parsed.primary_branch;\n if (typeof primary_branch !== 'string' || !VALID_BRANCHES.has(primary_branch as PrimaryBranch)) return null;\n\n if (!isStepArray(parsed.first_call_sequence) || parsed.first_call_sequence.length === 0) return null;\n if (!isStringArray(parsed.keyword_seeds) || parsed.keyword_seeds.length === 0) return null;\n\n return {\n goal_class,\n goal_class_reason: typeof parsed.goal_class_reason === 'string' ? parsed.goal_class_reason : '',\n primary_branch: primary_branch as PrimaryBranch,\n primary_branch_reason: typeof parsed.primary_branch_reason === 'string' ? parsed.primary_branch_reason : '',\n freshness_window,\n first_call_sequence: parsed.first_call_sequence,\n keyword_seeds: parsed.keyword_seeds.filter((s) => s.trim().length > 0),\n iteration_hints: isStringArray(parsed.iteration_hints) ? parsed.iteration_hints : [],\n gaps_to_watch: isStringArray(parsed.gaps_to_watch) ? parsed.gaps_to_watch : [],\n stop_criteria: isStringArray(parsed.stop_criteria) ? parsed.stop_criteria : [],\n };\n } catch {\n return null;\n }\n}\n\nexport async function generateResearchBrief(\n goal: string,\n processor: OpenAI,\n signal?: AbortSignal,\n): Promise<ResearchBrief | null> {\n const today = new Date().toISOString().slice(0, 10);\n\n const prompt = `You are a research planner. An agent is about to run a multi-pass research loop on the goal below using 5 tools:\n\n - start-research: orientation and this brief\n - raw-web-search: raw search fan-out, keywords only, up to 50 keywords per call, no LLM; best for breadth, audit trails, and candidate URL capture\n - smart-web-search: search fan-out + required LLM prioritization/classification over titles/snippets, scope: web|reddit|both, up to 50 keywords per call; best for triage after a strong diverse keyword set\n - raw-scrape-links: fetch URLs as full markdown, urls only, no LLM; Reddit permalinks return threaded comments; best for complete context and ambiguous sources\n - smart-scrape-links: fetch URLs then required LLM extraction over page bodies; Reddit permalinks return threaded comments before extraction; best for focused evidence extraction once facets are known\n\nProduce a tailored JSON brief.\n\nGOAL: ${goal}\nTODAY: ${today}\n\nReturn ONLY a JSON object (no markdown, no code fences):\n\n{\n \"goal_class\": \"spec | bug | migration | sentiment | pricing | security | synthesis | product_launch | other\",\n \"goal_class_reason\": \"one sentence \u2014 why this class\",\n \"primary_branch\": \"reddit | web | both\",\n \"primary_branch_reason\": \"one sentence \u2014 why this branch leads\",\n \"freshness_window\": \"days | weeks | months | years\",\n \"first_call_sequence\": [\n { \"tool\": \"raw-web-search | smart-web-search | raw-scrape-links | smart-scrape-links\", \"reason\": \"what this call establishes for the agent\" }\n ],\n \"keyword_seeds\": [\"25\u201350 concrete search keywords \u2014 flat list, to be fired in the first search call as keywords\"],\n \"iteration_hints\": [\"2\u20135 pointers on which harvested terms / follow-up signals to watch for after pass 1\"],\n \"gaps_to_watch\": [\"2\u20135 concrete questions the agent MUST verify or the answer is incomplete\"],\n \"stop_criteria\": [\"2\u20134 checkable conditions \u2014 all must hold before the agent declares done\"]\n}\n\nRULES:\n\nprimary_branch:\n- \"reddit\" \u2192 sentiment / migration / lived-experience / community-consensus goals. Usually leads with raw-web-search using Reddit-focused keywords, then raw-scrape-links on post permalinks to preserve full comments.\n- \"web\" \u2192 spec / bug / pricing / CVE / API / primary-source goals. Usually leads with raw-web-search for maximum candidate breadth OR smart-web-search scope:\"web\" when the initial keyword set is already diverse and needs prioritization.\n- \"both\" \u2192 opinion-heavy AND needs official sources (e.g. product launch + practitioner reception).\n\nfirst_call_sequence:\n- 1\u20133 steps.\n- Use raw-web-search when the first need is recall: many distinct source classes, exact candidate URLs, Reddit permalink discovery, or cheap follow-up passes.\n- Use smart-web-search when the first need is prioritization: the agent has 10\u201350 distinct keyword probes and needs HIGHLY/MAYBE tiers, gaps, and refine queries. Smart search reads snippets only; never plan it as final evidence.\n- Use raw-scrape-links when complete page/thread context is valuable, the extraction shape is unclear, or Reddit comments are the source of truth.\n- Use smart-scrape-links when the extraction shape is known (facets separated by |) and the agent needs compact evidence from page bodies; this is usually the highest-value smart tool for answer construction.\n- reddit-first: step 1 = raw-web-search with Reddit permalink probes; step 2 = raw-scrape-links on best post permalinks for full comments. Add smart-web-search only when there are many candidate posts to triage.\n- web-first: step 1 = raw-web-search for broad URL capture OR smart-web-search scope:\"web\" for prioritizing a diverse keyword fan-out; step 2 = smart-scrape-links on selected URLs when extraction facets are known, otherwise raw-scrape-links first.\n- both: step 1 = parallel search calls split by source need; step 2 = raw-scrape-links for full evidence and smart-scrape-links for final extraction.\n\nkeyword_seeds:\n- 25\u201350 total. Narrow bug \u2192 fewer. Open synthesis \u2192 more.\n- Write Google retrieval probes, not topic labels.\n- For each broad idea, first do a bad \u2192 better rewrite in your head: replace a vague phrase with a query that names the evidence source class, discriminating anchor terms, and one useful operator when possible.\n- ${QUERY_REWRITE_PAIR_GUIDANCE_TEXT}\n- Use operators where helpful (site:, quotes, verbatim version numbers, exact error text, package names, release/version strings).\n- DIVERSE facets \u2014 same noun-phrase cannot repeat across seeds with adjectives-only variation.\n- Optimize keyword_seeds for distinct coverage first. Smart-web-search can prioritize a broad result set, but it cannot compensate for a narrow or repetitive keyword set.\n- Do NOT invent vendor names you are uncertain exist.\n- For \\`site:<domain>\\` filters, ONLY use domains you are highly confident are real. Safe choices: \\`github.com\\`, \\`stackoverflow.com\\`, \\`reddit.com\\`, \\`news.ycombinator.com\\`, \\`arxiv.org\\`, \\`nvd.nist.gov\\`, \\`pypi.org\\`, \\`npmjs.com\\`, plus any canonical homepage/docs domain explicitly spelled out in the goal itself (e.g. goal names \"Cursor\" \u2192 \\`cursor.com\\`/\\`docs.cursor.com\\` is acceptable). If you don't know the product's real docs domain, leave the query open (no \\`site:\\`) instead of guessing.\n\nfreshness_window:\n- If the goal mentions a recent release / date / version, use \"days\" or \"weeks\".\n- Stable protocols / APIs \u2192 \"months\" or \"years\".`;\n\n try {\n const response = await requestTextWithFallback(\n processor,\n prompt,\n 'Research brief generation',\n signal,\n );\n\n if (response.content === null) {\n mcpLog('warning', `Research brief generation returned no content: ${response.error ?? 'unknown'}`, 'llm');\n markLLMFailure('planner', response.error ?? 'empty response');\n return null;\n }\n\n const brief = parseResearchBrief(response.content);\n if (!brief) {\n mcpLog('warning', 'Research brief JSON parse or shape validation failed', 'llm');\n markLLMFailure('planner', 'brief parse/validation failed');\n return null;\n }\n\n markLLMSuccess('planner');\n return brief;\n } catch (err: unknown) {\n const message = err instanceof Error ? err.message : String(err);\n mcpLog('warning', `Research brief generation failed: ${message}`, 'llm');\n markLLMFailure('planner', message);\n return null;\n }\n}\n\nexport function renderResearchBrief(brief: ResearchBrief): string {\n const lines: string[] = [];\n\n lines.push('## Your research brief (goal-tailored)');\n lines.push('');\n lines.push(`**Goal class**: \\`${brief.goal_class}\\` \u2014 ${brief.goal_class_reason}`);\n lines.push(`**Primary branch**: \\`${brief.primary_branch}\\` \u2014 ${brief.primary_branch_reason}`);\n lines.push(`**Freshness**: \\`${brief.freshness_window}\\``);\n lines.push('');\n\n if (brief.first_call_sequence.length > 0) {\n lines.push('### First-call sequence');\n brief.first_call_sequence.forEach((step, i) => {\n lines.push(`${i + 1}. \\`${step.tool}\\` \u2014 ${step.reason}`);\n });\n lines.push('');\n }\n\n if (brief.keyword_seeds.length > 0) {\n lines.push(`### Keyword seeds (${brief.keyword_seeds.length}) \u2014 fire these in your first search call as a flat \\`keywords\\` array`);\n for (const seed of brief.keyword_seeds) {\n lines.push(`- ${seed}`);\n }\n lines.push('');\n }\n\n if (brief.iteration_hints.length > 0) {\n lines.push('### Iteration hints (harvest new terms from scrape extracts\\' `## Follow-up signals`)');\n for (const hint of brief.iteration_hints) lines.push(`- ${hint}`);\n lines.push('');\n }\n\n if (brief.gaps_to_watch.length > 0) {\n lines.push('### Gaps to watch');\n for (const gap of brief.gaps_to_watch) lines.push(`- ${gap}`);\n lines.push('');\n }\n\n if (brief.stop_criteria.length > 0) {\n lines.push('### Stop criteria');\n for (const c of brief.stop_criteria) lines.push(`- ${c}`);\n lines.push('');\n }\n\n lines.push('---');\n lines.push('');\n lines.push('Fire `first_call_sequence` now. After each smart scrape, harvest new terms from `## Follow-up signals`; after each raw scrape, inspect the full markdown/comments and seed the next search round. Stop when every gap is closed.');\n\n return lines.join('\\n');\n}\n", "/**\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// ============================================================================\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 JINA_API_KEY: string | undefined;\n KERNEL_API_KEY: string | undefined;\n KERNEL_PROJECT: 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 JINA_API_KEY: process.env.JINA_API_KEY || undefined,\n KERNEL_API_KEY: process.env.KERNEL_API_KEY || undefined,\n KERNEL_PROJECT: process.env.KERNEL_PROJECT || 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 or JINA_API_KEY\n serperSearch: boolean; // SERPER_API_KEY\n jina: boolean; // JINA_API_KEY (Reader auth + Search)\n scraping: boolean; // SCRAPEDO_API_KEY\n kernel: boolean; // KERNEL_API_KEY\n llmExtraction: boolean; // LLM_API_KEY + LLM_BASE_URL + LLM_MODEL\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 || env.JINA_API_KEY),\n serperSearch: !!env.SEARCH_API_KEY,\n jina: !!env.JINA_API_KEY,\n scraping: !!env.SCRAPER_API_KEY,\n kernel: !!env.KERNEL_API_KEY,\n llmExtraction: getLLMConfigStatus().configured,\n };\n}\n\nexport function getMissingEnvMessage(capability: keyof Capabilities): string {\n const messages: Record<keyof Capabilities, string> = {\n reddit: '\u274C **Reddit scraping unavailable.** Set `REDDIT_CLIENT_ID` and `REDDIT_CLIENT_SECRET` to enable threaded Reddit post fetching in `raw-scrape-links` and `smart-scrape-links`.\\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` or `JINA_API_KEY` to enable `raw-web-search` and `smart-web-search`.\\n\\n\uD83D\uDC49 Serper provides Google SERPs; Jina Search is used as a fallback/search-only provider.',\n serperSearch: '\u274C **Serper search unavailable.** Set `SERPER_API_KEY` to enable Google-backed primary search.',\n jina: '\u274C **Jina unavailable.** Set `JINA_API_KEY` to enable Jina Search and authenticated Jina Reader requests.',\n scraping: '\u26A0\uFE0F **Scrape.do proxy fallback unavailable.** Set `SCRAPEDO_API_KEY` to enable Jina Reader retries through Scrape.do proxy mode.\\n\\n\uD83D\uDC49 Sign up at: https://scrape.do (1,000 free credits)',\n kernel: '\u274C **Kernel browser rendering unavailable.** Set `KERNEL_API_KEY` to enable optional browser-render fallback for raw/smart scraping.',\n llmExtraction: '\u26A0\uFE0F **AI extraction disabled.** Set `LLM_API_KEY`, `LLM_BASE_URL`, and `LLM_MODEL` to enable `smart-web-search` and `smart-scrape-links`.\\n\\nUse the raw tools when you need markdown without LLM processing.',\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 JINA_READER: safeParseInt(process.env.CONCURRENCY_JINA_READER, 50, 1, 200),\n REDDIT: safeParseInt(process.env.CONCURRENCY_REDDIT, 50, 1, 200),\n LLM_EXTRACTION: safeParseInt(process.env.LLM_CONCURRENCY, 50, 1, 200),\n KERNEL: safeParseInt(process.env.CONCURRENCY_KERNEL, 3, 1, 20),\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// Required vars (all must be set together when LLM is enabled):\n// LLM_API_KEY \u2014 API key for the OpenAI-compatible endpoint\n// LLM_BASE_URL \u2014 endpoint base URL (e.g. https://server.up.railway.app/v1)\n// LLM_MODEL \u2014 primary model (e.g. gpt-5.4-mini)\n//\n// Optional:\n// LLM_FALLBACK_MODEL \u2014 model to use after primary exhausts all retries (e.g. gpt-5.4)\n// LLM_CONCURRENCY \u2014 parallel LLM calls (default: 50)\n//\n// Reasoning effort is always 'low' \u2014 not configurable.\n// ============================================================================\n\ninterface LlmExtractionConfig {\n readonly MODEL: string;\n readonly FALLBACK_MODEL: string;\n readonly BASE_URL: string;\n readonly API_KEY: string;\n}\n\nexport type LLMRequiredEnvVar = 'LLM_API_KEY' | 'LLM_BASE_URL' | 'LLM_MODEL';\n\nexport interface LLMConfigStatus {\n readonly configured: boolean;\n readonly apiKeyPresent: boolean;\n readonly baseUrlPresent: boolean;\n readonly modelPresent: boolean;\n readonly missingVars: readonly LLMRequiredEnvVar[];\n readonly error: string | null;\n}\n\nlet cachedLlmConfigStatus: LLMConfigStatus | null = null;\n\nexport function getLLMConfigStatus(): LLMConfigStatus {\n if (cachedLlmConfigStatus) return cachedLlmConfigStatus;\n\n const apiKeyPresent = !!process.env.LLM_API_KEY?.trim();\n const baseUrlPresent = !!process.env.LLM_BASE_URL?.trim();\n const modelPresent = !!process.env.LLM_MODEL?.trim();\n const missingVars: LLMRequiredEnvVar[] = [];\n\n if (!apiKeyPresent) missingVars.push('LLM_API_KEY');\n if (!baseUrlPresent) missingVars.push('LLM_BASE_URL');\n if (!modelPresent) missingVars.push('LLM_MODEL');\n\n const configured = missingVars.length === 0;\n cachedLlmConfigStatus = {\n configured,\n apiKeyPresent,\n baseUrlPresent,\n modelPresent,\n missingVars,\n error: configured\n ? null\n : `LLM disabled: missing ${missingVars.join(', ')}`,\n };\n return cachedLlmConfigStatus;\n}\n\n/**\n * Test-only \u2014 drop every env-derived cache so a test can mutate process.env\n * and re-read fresh values. Covers the LLM config caches AND the parseEnv()\n * cache (which holds SCRAPEDO_API_KEY, SERPER_API_KEY, REDDIT_CLIENT_*,\n * JINA_API_KEY, KERNEL_API_KEY, KERNEL_PROJECT). Tests that scrub non-LLM env vars must also see a clean\n * env on the next parseEnv() call, otherwise getCapabilities() returns\n * stale flags.\n */\nexport function _resetLLMConfigStatusForTests(): void {\n cachedLlmConfigStatus = null;\n cachedLlmExtraction = null;\n cachedEnv = null;\n}\n\nlet cachedLlmExtraction: LlmExtractionConfig | null = null;\n\nfunction getLlmExtraction(): LlmExtractionConfig {\n if (cachedLlmExtraction) return cachedLlmExtraction;\n\n const apiKey = process.env.LLM_API_KEY?.trim() || '';\n const baseUrl = process.env.LLM_BASE_URL?.trim();\n const model = process.env.LLM_MODEL?.trim();\n const fallbackModel = process.env.LLM_FALLBACK_MODEL?.trim() || '';\n\n if (apiKey && !baseUrl) {\n throw new Error(\n 'LLM_BASE_URL is required when LLM_API_KEY is set. ' +\n 'Set LLM_BASE_URL to your OpenAI-compatible endpoint.',\n );\n }\n if (apiKey && !model) {\n throw new Error(\n 'LLM_MODEL is required when LLM_API_KEY is set.',\n );\n }\n\n cachedLlmExtraction = {\n API_KEY: apiKey,\n BASE_URL: baseUrl || '',\n MODEL: model || '',\n FALLBACK_MODEL: fallbackModel,\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", "/**\n * Version Module - Single Source of Truth\n * \n * This module reads package metadata from package.json at runtime and keeps a\n * fallback copy for environments where package.json cannot be resolved.\n * \n * Usage:\n * import { VERSION, PACKAGE_NAME } from './version.js';\n */\n\nimport { createRequire } from 'module';\nimport { fileURLToPath } from 'url';\nimport { dirname, join } from 'path';\n\n// Defaults used if package.json cannot be loaded at runtime.\nconst DEFAULT_PACKAGE_INFO = {\n version: '3.9.5',\n name: 'mcp-researchpowerpack-http',\n description: 'Research Powerpack MCP Server',\n} as const;\n\nlet packageJson: { version: string; name: string; description: string } = { ...DEFAULT_PACKAGE_INFO };\n\ntry {\n if (typeof import.meta.url === 'string' && import.meta.url.startsWith('file:')) {\n const _require = createRequire(import.meta.url);\n const _dirname = dirname(fileURLToPath(import.meta.url));\n try {\n packageJson = _require(join(_dirname, '..', 'package.json'));\n } catch {\n packageJson = _require(join(_dirname, '..', '..', 'package.json'));\n }\n }\n} catch {\n // Keep hardcoded defaults when package.json is unavailable\n}\n\n/**\n * Package version from package.json\n * This is the single source of truth for versioning\n */\nexport const VERSION: string = packageJson.version;\n\n/**\n * Package name from package.json\n */\nexport const PACKAGE_NAME: string = packageJson.name;\n\n/**\n * Package description from package.json\n */\nexport const PACKAGE_DESCRIPTION: string = packageJson.description;\n\n/**\n * Formatted version string for user agents and logging\n * Example: \"mcp-researchpowerpack-http/3.2.0\"\n */\nexport const USER_AGENT_VERSION: string = `${PACKAGE_NAME}/${VERSION}`;\n\n// VERSION_INFO removed - unused, individual exports sufficient\n", "/**\n * Server logging utility.\n *\n * This server is HTTP-only, so logging must never depend on a transport-bound\n * MCP server instance. All logs flow through mcp-use's Logger, which writes to\n * stderr in Node and console in the browser.\n */\n\nimport { Logger } from 'mcp-use';\n\nexport type LogLevel = 'debug' | 'info' | 'warning' | 'error';\n\nfunction getLogger(name: string) {\n return Logger.get(name);\n}\n\n/**\n * Structured log helper backed by mcp-use's Logger.\n *\n * @param level - Log level.\n * @param message - Message to emit.\n * @param loggerName - Tool/component name for context (falls back to \"research-powerpack\").\n */\nexport function mcpLog(level: LogLevel, message: string, loggerName?: string): void {\n const logger = getLogger(loggerName ?? 'research-powerpack');\n\n switch (level) {\n case 'debug':\n logger.debug(message);\n break;\n case 'info':\n logger.info(message);\n break;\n case 'warning':\n logger.warn(message);\n break;\n case 'error':\n logger.error(message);\n break;\n }\n}\n", "/**\n * Robust error handling utilities for MCP server\n * Ensures the server NEVER crashes and always returns structured responses\n */\n\nimport { mcpLog } from './logger.js';\n\n// ============================================================================\n// Error Codes (MCP-compliant)\n// ============================================================================\n\nexport const ErrorCode = {\n // Retryable errors\n RATE_LIMITED: 'RATE_LIMITED',\n TIMEOUT: 'TIMEOUT',\n NETWORK_ERROR: 'NETWORK_ERROR',\n SERVICE_UNAVAILABLE: 'SERVICE_UNAVAILABLE',\n \n // Non-retryable errors\n AUTH_ERROR: 'AUTH_ERROR',\n INVALID_INPUT: 'INVALID_INPUT',\n NOT_FOUND: 'NOT_FOUND',\n QUOTA_EXCEEDED: 'QUOTA_EXCEEDED',\n UNSUPPORTED_BINARY_CONTENT: 'UNSUPPORTED_BINARY_CONTENT',\n\n // Internal errors\n INTERNAL_ERROR: 'INTERNAL_ERROR',\n PARSE_ERROR: 'PARSE_ERROR',\n UNKNOWN_ERROR: 'UNKNOWN_ERROR',\n} as const;\n\ntype ErrorCodeType = typeof ErrorCode[keyof typeof ErrorCode];\n\n// ============================================================================\n// Structured Error Types\n// ============================================================================\n\nexport interface StructuredError {\n code: ErrorCodeType;\n message: string;\n retryable: boolean;\n statusCode?: number;\n cause?: string;\n}\n\ninterface RetryOptions {\n readonly maxRetries: number;\n readonly baseDelayMs: number;\n readonly maxDelayMs: number;\n readonly retryableStatuses: readonly number[];\n readonly onRetry?: (attempt: number, error: StructuredError, delayMs: number) => void;\n}\n\nconst DEFAULT_RETRY_OPTIONS: RetryOptions = {\n maxRetries: 3,\n baseDelayMs: 1000,\n maxDelayMs: 30000,\n retryableStatuses: [408, 429, 500, 502, 503, 504, 510],\n};\n\n// ============================================================================\n// Error Classification \u2014 Atomic Classifiers\n// ============================================================================\n\n/**\n * Classify DOMException (AbortError from AbortController timeouts)\n */\nfunction classifyDomException(error: DOMException): StructuredError {\n if (error.name === 'AbortError') {\n return { code: ErrorCode.TIMEOUT, message: 'Request timed out', retryable: true };\n }\n return { code: ErrorCode.UNKNOWN_ERROR, message: error.message, retryable: false };\n}\n\n/**\n * Classify by Node.js error codes (ECONNREFUSED, ENOTFOUND, etc.)\n * Returns null if no matching code is found.\n */\nfunction classifyByErrorCode(error: { code?: string; message?: string }): StructuredError | null {\n const errCode = error.code;\n if (!errCode) return null;\n\n const networkErrorMessages: Record<string, string> = {\n ECONNREFUSED: 'Connection refused \u2014 service may be down',\n ECONNRESET: 'Connection was reset \u2014 please retry',\n ECONNABORTED: 'Connection aborted \u2014 please retry',\n ENOTFOUND: 'Service not reachable \u2014 check your network',\n EPIPE: 'Connection lost \u2014 please retry',\n EAI_AGAIN: 'DNS lookup failed \u2014 check your network',\n };\n\n if (errCode === 'ECONNREFUSED' || errCode === 'ENOTFOUND' || errCode === 'ECONNRESET') {\n return { code: ErrorCode.NETWORK_ERROR, message: networkErrorMessages[errCode] || 'Network connection failed', retryable: true, cause: error.message };\n }\n\n if (errCode === 'ECONNABORTED' || errCode === 'ETIMEDOUT') {\n return { code: ErrorCode.TIMEOUT, message: networkErrorMessages[errCode] || 'Request timed out', retryable: true, cause: error.message };\n }\n\n return null;\n}\n\n/**\n * Classify by HTTP status code extracted from error objects (axios-style, fetch-style, etc.)\n * Returns null if no status code is found.\n */\nfunction classifyByStatusCode(error: { status?: number; statusCode?: number; response?: { status?: number }; message?: string }): StructuredError | null {\n const status = error.response?.status || error.status || error.statusCode;\n if (!status) return null;\n return classifyHttpError(status, error.message || String(error));\n}\n\n/**\n * Classify by error message patterns (timeout, rate-limit, auth, parse errors)\n * Returns null if no pattern matches.\n */\nfunction classifyByMessage(message: string): StructuredError | null {\n const lower = message.toLowerCase();\n\n // Timeout patterns\n if (lower.includes('timeout') || lower.includes('timed out') || lower.includes('aborterror')) {\n return { code: ErrorCode.TIMEOUT, message: 'Request timed out', retryable: true, cause: message };\n }\n\n // Rate-limit patterns\n if (lower.includes('rate limit') || lower.includes('too many requests')) {\n return { code: ErrorCode.RATE_LIMITED, message: 'Rate limit exceeded', retryable: true, cause: message };\n }\n\n // API key errors\n if (message.includes('API_KEY') || message.includes('api_key') || message.includes('Invalid API')) {\n return { code: ErrorCode.AUTH_ERROR, message: 'API key missing or invalid', retryable: false, cause: message };\n }\n\n // Parse errors\n if (message.includes('JSON') || message.includes('parse') || message.includes('Unexpected token')) {\n return { code: ErrorCode.PARSE_ERROR, message: 'Failed to parse response', retryable: false, cause: message };\n }\n\n return null;\n}\n\n/**\n * Catch-all fallback classification when no other classifier matches.\n */\nfunction classifyFallback(message: string, cause?: unknown): StructuredError {\n return {\n code: ErrorCode.UNKNOWN_ERROR,\n message,\n retryable: false,\n cause: cause ? String(cause) : undefined,\n };\n}\n\n// ============================================================================\n// Main Error Classification Pipeline\n// ============================================================================\n\n/**\n * Classify any error into a structured format.\n * NEVER throws \u2014 always returns a valid StructuredError.\n */\nexport function classifyError(error: unknown): StructuredError {\n if (error == null) {\n return { code: ErrorCode.UNKNOWN_ERROR, message: 'An unknown error occurred', retryable: false };\n }\n\n if (error instanceof DOMException) return classifyDomException(error);\n\n if (!isErrorLike(error)) {\n return { code: ErrorCode.UNKNOWN_ERROR, message: String(error), retryable: false };\n }\n\n return classifyByErrorCode(error)\n ?? classifyByStatusCode(error)\n ?? classifyByMessage(error.message ?? String(error))\n ?? classifyFallback(error.message ?? String(error), error.cause);\n}\n\n/**\n * Type guard for error-like objects with common error properties\n */\nfunction isErrorLike(value: unknown): value is {\n message?: string;\n response?: { status?: number; data?: unknown };\n status?: number;\n statusCode?: number;\n code?: string;\n name?: string;\n cause?: unknown;\n} {\n return typeof value === 'object' && value !== null;\n}\n\n/**\n * Classify HTTP status codes into structured errors.\n * Exhaustive switch with grouped default handling for unknown ranges.\n */\nfunction classifyHttpError(status: number, message: string): StructuredError {\n switch (status) {\n case 400:\n return { code: ErrorCode.INVALID_INPUT, message: 'Bad request', retryable: false, statusCode: status };\n case 401:\n return { code: ErrorCode.AUTH_ERROR, message: 'Invalid API key', retryable: false, statusCode: status };\n case 403:\n return { code: ErrorCode.QUOTA_EXCEEDED, message: 'Access forbidden or quota exceeded', retryable: false, statusCode: status };\n case 404:\n return { code: ErrorCode.NOT_FOUND, message: 'Resource not found', retryable: false, statusCode: status };\n case 408:\n return { code: ErrorCode.TIMEOUT, message: 'Request timeout', retryable: true, statusCode: status };\n case 429:\n return { code: ErrorCode.RATE_LIMITED, message: 'Rate limit exceeded', retryable: true, statusCode: status };\n case 500:\n return { code: ErrorCode.INTERNAL_ERROR, message: 'Server error', retryable: true, statusCode: status };\n case 502:\n return { code: ErrorCode.SERVICE_UNAVAILABLE, message: 'Bad gateway', retryable: true, statusCode: status };\n case 503:\n return { code: ErrorCode.SERVICE_UNAVAILABLE, message: 'Service unavailable', retryable: true, statusCode: status };\n case 504:\n return { code: ErrorCode.TIMEOUT, message: 'Gateway timeout', retryable: true, statusCode: status };\n case 510:\n return { code: ErrorCode.SERVICE_UNAVAILABLE, message: 'Request canceled', retryable: true, statusCode: status };\n default:\n if (status >= 500) {\n return { code: ErrorCode.SERVICE_UNAVAILABLE, message: `Server error: ${status}`, retryable: true, statusCode: status };\n }\n if (status >= 400) {\n return { code: ErrorCode.INVALID_INPUT, message: `Client error: ${status}`, retryable: false, statusCode: status };\n }\n return { code: ErrorCode.UNKNOWN_ERROR, message: `HTTP ${status}: ${message}`, retryable: false, statusCode: status };\n }\n}\n\n// ============================================================================\n// Retry Logic with Exponential Backoff\n// ============================================================================\n\n/**\n * Calculate delay with exponential backoff and jitter\n */\nfunction calculateBackoff(attempt: number, options: RetryOptions): number {\n const exponentialDelay = options.baseDelayMs * Math.pow(2, attempt);\n const jitter = Math.random() * 0.3 * exponentialDelay; // 0-30% jitter\n return Math.min(exponentialDelay + jitter, options.maxDelayMs);\n}\n\n/**\n * Sleep utility that respects abort signals\n */\nexport function sleep(ms: number, signal?: AbortSignal): Promise<void> {\n return new Promise((resolve, reject) => {\n if (signal?.aborted) {\n reject(new DOMException('Aborted', 'AbortError'));\n return;\n }\n\n function onAbort() {\n clearTimeout(timeout);\n reject(new DOMException('Aborted', 'AbortError'));\n }\n\n const timeout = setTimeout(() => {\n if (signal) signal.removeEventListener('abort', onAbort);\n resolve();\n }, ms);\n\n signal?.addEventListener('abort', onAbort, { once: true });\n // Re-check: signal may have aborted between initial check and listener registration\n if (signal?.aborted) {\n onAbort();\n }\n });\n}\n\n/**\n * Wrap a fetch call with timeout via AbortController\n */\nexport function fetchWithTimeout(\n url: string,\n options: RequestInit & { timeoutMs?: number } = {}\n): Promise<Response> {\n const { timeoutMs = 30000, signal: externalSignal, ...fetchOptions } = options;\n\n const controller = new AbortController();\n const timeoutId = setTimeout(() => controller.abort(), timeoutMs);\n\n let onExternalAbort: (() => void) | undefined;\n if (externalSignal) {\n onExternalAbort = () => controller.abort();\n externalSignal.addEventListener('abort', onExternalAbort, { once: true });\n if (externalSignal.aborted) {\n controller.abort();\n }\n }\n\n return fetch(url, { ...fetchOptions, signal: controller.signal }).finally(() => {\n clearTimeout(timeoutId);\n if (externalSignal && onExternalAbort) {\n externalSignal.removeEventListener('abort', onExternalAbort);\n }\n });\n}\n\n// ============================================================================\n// Stability Wrappers \u2014 Network resilience for LLM API calls\n// ============================================================================\n\n/**\n * Wrap a non-streaming API call with activity-based timeout detection.\n * If the call hasn't completed within `stallMs`, abort and retry.\n * This catches \"stuck\" connections where TCP stays open but no data flows.\n *\n * @param fn - Async function that accepts an AbortSignal\n * @param stallMs - Max milliseconds to wait for the call to complete before considering it stuck\n * @param maxAttempts - Max retry attempts for stalled requests\n * @param label - Label for log messages\n * @returns The result of the function\n */\nexport async function withStallProtection<T>(\n fn: (signal: AbortSignal) => Promise<T>,\n stallMs: number,\n maxAttempts: number = 2,\n label: string = 'request',\n): Promise<T> {\n for (let attempt = 0; attempt < maxAttempts; attempt++) {\n const controller = new AbortController();\n let stallTimer: ReturnType<typeof setTimeout> | undefined;\n\n const stallPromise = new Promise<never>((_, reject) => {\n stallTimer = setTimeout(() => {\n controller.abort();\n reject(Object.assign(new Error(`Service temporarily unavailable \u2014 no response received (attempt ${attempt + 1}/${maxAttempts})`), {\n code: 'ESTALLED',\n retryable: attempt < maxAttempts - 1,\n }));\n }, stallMs);\n });\n\n let fnPromise: Promise<T> | undefined;\n try {\n fnPromise = fn(controller.signal);\n const result = await Promise.race([fnPromise, stallPromise]);\n clearTimeout(stallTimer);\n return result;\n } catch (err) {\n // Suppress unhandled rejection from the losing promise\n // (e.g. fnPromise rejects after stallPromise wins the race)\n fnPromise?.catch(() => {});\n clearTimeout(stallTimer);\n const isStall = err instanceof Error && (err as NodeJS.ErrnoException).code === 'ESTALLED';\n if (isStall && attempt < maxAttempts - 1) {\n const backoff = calculateBackoff(attempt, DEFAULT_RETRY_OPTIONS);\n mcpLog('warning', `${label} stalled, retrying in ${backoff}ms (attempt ${attempt + 1})`, 'stability');\n await sleep(backoff);\n continue;\n }\n throw err;\n }\n }\n // Should never reach here, but TypeScript needs it\n throw new Error(`${label} failed after ${maxAttempts} stall-protection attempts`);\n}\n", "import { Effect, Layer } from 'effect';\n\nimport { JinaServiceLive, LlmServiceLive, SearchServiceLive } from './services.js';\n\nexport const BaseExternalLive = Layer.mergeAll(\n JinaServiceLive,\n SearchServiceLive,\n LlmServiceLive,\n);\n\nexport function runExternalEffect<A>(\n effect: Effect.Effect<A, unknown, unknown>,\n layer: Layer.Layer<any>,\n): Promise<A> {\n return Effect.runPromise(effect.pipe(Effect.provide(layer)));\n}\n", "import { Context, Effect, Layer } from 'effect';\n\nimport { SearchClient, type MultipleSearchResponse } from '../clients/search.js';\nimport { JinaClient, type JinaConvertRequest, type JinaConvertResponse } from '../clients/jina.js';\nimport { KernelClient, type KernelRenderRequest, type KernelRenderResponse } from '../clients/kernel.js';\nimport { RedditClient, type BatchPostResult } from '../clients/reddit.js';\nimport {\n createLLMProcessor,\n processContentWithLLM,\n classifySearchResults,\n suggestRefineQueriesForRawMode,\n generateResearchBrief,\n type ClassificationResult,\n type RefineQuerySuggestion,\n type ResearchBrief,\n} from '../services/llm-processor.js';\nimport { parseEnv } from '../config/index.js';\nimport { type StructuredError } from '../utils/errors.js';\nimport { providerError, type ProviderRequestError } from './errors.js';\n\nexport interface SearchServiceShape {\n readonly serperSearchMultiple: (queries: readonly string[]) => Effect.Effect<MultipleSearchResponse, ProviderRequestError>;\n readonly jinaSearchMultiple: (queries: readonly string[]) => Effect.Effect<MultipleSearchResponse, ProviderRequestError>;\n}\n\nexport interface JinaServiceShape {\n readonly convert: (request: JinaConvertRequest) => Effect.Effect<JinaConvertResponse, ProviderRequestError>;\n}\n\nexport interface KernelServiceShape {\n readonly render: (request: KernelRenderRequest) => Effect.Effect<KernelRenderResponse, ProviderRequestError>;\n}\n\nexport interface RedditServiceShape {\n readonly batchGetPosts: (urls: readonly string[], includeComments: boolean) => Effect.Effect<BatchPostResult, ProviderRequestError>;\n}\n\nexport interface LlmServiceShape {\n readonly extractContent: (\n content: string,\n config: { readonly enabled: boolean; readonly extract: string | undefined; readonly url?: string },\n ) => Effect.Effect<Awaited<ReturnType<typeof processContentWithLLM>>, ProviderRequestError>;\n readonly classifySearchResults: (\n rankedUrls: Parameters<typeof classifySearchResults>[0],\n objective: string,\n totalQueries: number,\n processor: Parameters<typeof classifySearchResults>[3],\n previousQueries?: readonly string[],\n ) => Effect.Effect<{ result: ClassificationResult | null; error?: string }, ProviderRequestError>;\n readonly suggestRefineQueriesForRawMode: (\n rankedUrls: Parameters<typeof suggestRefineQueriesForRawMode>[0],\n objective: string,\n originalQueries: readonly string[],\n processor: Parameters<typeof suggestRefineQueriesForRawMode>[3],\n ) => Effect.Effect<{ result: RefineQuerySuggestion[]; error?: string }, ProviderRequestError>;\n readonly generateResearchBrief: (\n goal: string,\n processor: Parameters<typeof generateResearchBrief>[1],\n ) => Effect.Effect<ResearchBrief | null, ProviderRequestError>;\n}\n\nexport const SearchService = Context.GenericTag<SearchServiceShape>('SearchService');\nexport const JinaService = Context.GenericTag<JinaServiceShape>('JinaService');\nexport const KernelService = Context.GenericTag<KernelServiceShape>('KernelService');\nexport const RedditService = Context.GenericTag<RedditServiceShape>('RedditService');\nexport const LlmService = Context.GenericTag<LlmServiceShape>('LlmService');\n\nexport const SearchServiceLive = Layer.sync(SearchService, () => {\n const env = parseEnv();\n return {\n serperSearchMultiple: (queries) =>\n Effect.tryPromise({\n try: () => new SearchClient(env.SEARCH_API_KEY).searchMultiple([...queries]),\n catch: (error) => providerError('serper', 'searchMultiple', error),\n }),\n jinaSearchMultiple: (queries) =>\n Effect.tryPromise({\n try: () => new JinaClient(env.JINA_API_KEY).searchMultiple([...queries]),\n catch: (error) => providerError('jina', 'searchMultiple', error),\n }),\n };\n});\n\nexport const JinaServiceLive = Layer.sync(JinaService, () => {\n const env = parseEnv();\n const client = new JinaClient(env.JINA_API_KEY);\n return {\n convert: (request) =>\n Effect.tryPromise({\n try: () => client.convert(request),\n catch: (error) => providerError('jina', 'convert', error),\n }),\n };\n});\n\nexport const KernelServiceLive = Layer.sync(KernelService, () => {\n const client = new KernelClient();\n return {\n render: (request) =>\n Effect.tryPromise({\n try: () => client.render(request),\n catch: (error) => providerError('kernel', 'render', error),\n }),\n };\n});\n\nexport function RedditServiceLive(clientId: string, clientSecret: string) {\n return Layer.sync(RedditService, () => {\n const client = new RedditClient(clientId, clientSecret);\n return {\n batchGetPosts: (urls, includeComments) =>\n Effect.tryPromise({\n try: () => client.batchGetPosts([...urls], includeComments),\n catch: (error) => providerError('reddit', 'batchGetPosts', error),\n }),\n };\n });\n}\n\nexport const LlmServiceLive = Layer.succeed(LlmService, {\n extractContent: (content, config) =>\n Effect.tryPromise({\n try: () => processContentWithLLM(content, config, createLLMProcessor()),\n catch: (error) => providerError('llm', 'extractContent', error),\n }),\n classifySearchResults: (rankedUrls, objective, totalQueries, processor, previousQueries) =>\n Effect.tryPromise({\n try: () => classifySearchResults(rankedUrls, objective, totalQueries, processor, previousQueries),\n catch: (error) => providerError('llm', 'classifySearchResults', error),\n }),\n suggestRefineQueriesForRawMode: (rankedUrls, objective, originalQueries, processor) =>\n Effect.tryPromise({\n try: () => suggestRefineQueriesForRawMode(rankedUrls, objective, originalQueries, processor),\n catch: (error) => providerError('llm', 'suggestRefineQueriesForRawMode', error),\n }),\n generateResearchBrief: (goal, processor) =>\n Effect.tryPromise({\n try: () => generateResearchBrief(goal, processor),\n catch: (error) => providerError('llm', 'generateResearchBrief', error),\n }),\n});\n\nexport function structuredFromProviderError(error: ProviderRequestError): StructuredError {\n return error.error;\n}\n", "/**\n * Shared retry and backoff utilities\n */\n\n/** Jitter factor to prevent thundering herd */\nconst JITTER_FACTOR = 0.3 as const;\n\n/** Exponential base for backoff calculation */\nconst EXPONENTIAL_BASE = 2 as const;\n\n/** Default base delay for exponential backoff (ms) */\nconst DEFAULT_BASE_DELAY_MS = 1_000 as const;\n\n/** Default maximum backoff delay cap (ms) */\nconst DEFAULT_MAX_DELAY_MS = 30_000 as const;\n\n/**\n * Calculate exponential backoff delay with jitter.\n * Formula: min(base * 2^attempt + random_jitter, maxDelay)\n *\n * @param attempt - Zero-based retry attempt number\n * @param baseDelayMs - Base delay in milliseconds (default: 1000)\n * @param maxDelayMs - Maximum delay cap in milliseconds (default: 30000)\n * @returns Delay in milliseconds with jitter applied\n */\nexport function calculateBackoff(\n attempt: number,\n baseDelayMs: number = DEFAULT_BASE_DELAY_MS,\n maxDelayMs: number = DEFAULT_MAX_DELAY_MS,\n): number {\n const exponentialDelay = baseDelayMs * Math.pow(EXPONENTIAL_BASE, attempt);\n const jitter = JITTER_FACTOR * exponentialDelay * Math.random();\n return Math.min(exponentialDelay + jitter, maxDelayMs);\n}\n", "/**\n * Thin wrappers around the `p-map` library for bounded parallel execution.\n * Keeps the internal API stable (`pMap` / `pMapSettled`) while delegating\n * scheduling and AbortSignal handling to `p-map@7`.\n */\n\nimport pMapLib from 'p-map';\n\n/**\n * Run `mapper` over `items` with at most `concurrency` in flight.\n * Rejects with the first error the mapper throws \u2014 matches the prior\n * hand-rolled semantics. Results preserve input order.\n */\nexport async function pMap<T, R>(\n items: readonly T[],\n mapper: (item: T, index: number) => Promise<R>,\n concurrency: number = 6,\n signal?: AbortSignal,\n): Promise<R[]> {\n if (items.length === 0) return [];\n const limit = Math.max(1, Math.min(concurrency, items.length));\n return pMapLib(items, mapper, { concurrency: limit, signal });\n}\n\n/**\n * Like `pMap` but never rejects \u2014 mapper errors surface as\n * `{ status: 'rejected', reason }` entries. Useful when one per-item\n * failure must not cancel the others (e.g. per-URL scraping).\n */\nexport async function pMapSettled<T, R>(\n items: readonly T[],\n mapper: (item: T, index: number) => Promise<R>,\n concurrency: number = 6,\n signal?: AbortSignal,\n): Promise<PromiseSettledResult<R>[]> {\n if (items.length === 0) return [];\n const limit = Math.max(1, Math.min(concurrency, items.length));\n return pMapLib(\n items,\n async (item, index): Promise<PromiseSettledResult<R>> => {\n try {\n const value = await mapper(item, index);\n return { status: 'fulfilled', value };\n } catch (reason) {\n return { status: 'rejected', reason };\n }\n },\n { concurrency: limit, signal, stopOnError: false },\n );\n}\n", "/**\n * Web Search Client\n * Generic interface for web search via Google (Serper implementation)\n * Implements robust error handling that NEVER crashes\n */\n\nimport { parseEnv, CONCURRENCY } from '../config/index.js';\nimport {\n classifyError,\n fetchWithTimeout,\n sleep,\n ErrorCode,\n type StructuredError,\n} from '../utils/errors.js';\nimport { calculateBackoff } from '../utils/retry.js';\nimport { pMap } from '../utils/concurrency.js';\nimport { mcpLog } from '../utils/logger.js';\n\n// \u2500\u2500 Constants \u2500\u2500\n\nconst SERPER_API_URL = 'https://google.serper.dev/search' as const;\nconst DEFAULT_RESULTS_PER_QUERY = 10 as const;\nconst MAX_RETRIES = 3 as const;\n\n// \u2500\u2500 Data Interfaces \u2500\u2500\n\ninterface SearchResult {\n readonly title: string;\n readonly link: string;\n readonly snippet: string;\n readonly date?: string;\n readonly position: number;\n}\n\nexport interface QuerySearchResult {\n readonly query: string;\n readonly results: SearchResult[];\n readonly totalResults: number;\n readonly related: string[];\n readonly error?: StructuredError;\n}\n\nexport interface MultipleSearchResponse {\n readonly searches: QuerySearchResult[];\n readonly totalQueries: number;\n readonly executionTime: number;\n readonly error?: StructuredError;\n}\n\nexport interface RedditSearchResult {\n readonly title: string;\n readonly url: string;\n readonly snippet: string;\n readonly date?: string;\n}\n\n// \u2500\u2500 Retry Configuration \u2500\u2500\n\nconst SEARCH_RETRY_CONFIG = {\n maxRetries: MAX_RETRIES,\n baseDelayMs: 1000,\n maxDelayMs: 10000,\n timeoutMs: 30000,\n} as const;\n\nconst RETRYABLE_SEARCH_CODES = new Set([429, 500, 502, 503, 504]);\n\n// Pre-compiled regex patterns for Reddit search\nconst REDDIT_SITE_REGEX = /site:\\s*reddit\\.com/i;\nconst REDDIT_SUBREDDIT_SUFFIX_REGEX = / : r\\/\\w+$/;\nconst REDDIT_SUFFIX_REGEX = / - Reddit$/;\n\n// \u2500\u2500 Helper: Parse Serper search responses into structured results \u2500\u2500\n\nfunction parseSearchResponses(\n responses: Array<Record<string, unknown>>,\n queries: string[],\n): QuerySearchResult[] {\n return responses.map((resp, index) => {\n try {\n const organic = (resp.organic || []) as Array<Record<string, unknown>>;\n const results: SearchResult[] = organic.map((item, idx) => ({\n title: (item.title as string) || 'No title',\n link: (item.link as string) || '#',\n snippet: (item.snippet as string) || '',\n date: item.date as string | undefined,\n position: (item.position as number) || idx + 1,\n }));\n\n const searchInfo = resp.searchInformation as Record<string, unknown> | undefined;\n const totalResults = searchInfo?.totalResults\n ? parseInt(String(searchInfo.totalResults).replace(/,/g, ''), 10)\n : results.length;\n\n const relatedSearches = (resp.relatedSearches || []) as Array<Record<string, unknown>>;\n const related = relatedSearches.map((r) => (r.query as string) || '');\n\n return { query: queries[index] || '', results, totalResults, related };\n } catch {\n return { query: queries[index] || '', results: [], totalResults: 0, related: [] };\n }\n });\n}\n\n// \u2500\u2500 Helper: Execute search API call with retry \u2500\u2500\n\nasync function executeSearchWithRetry(\n apiKey: string,\n body: unknown,\n isRetryable: (status?: number, error?: unknown) => boolean,\n): Promise<{ data: unknown; error?: StructuredError }> {\n let lastError: StructuredError | undefined;\n\n for (let attempt = 0; attempt <= SEARCH_RETRY_CONFIG.maxRetries; attempt++) {\n try {\n if (attempt > 0) {\n mcpLog('warning', `Retry attempt ${attempt}/${SEARCH_RETRY_CONFIG.maxRetries}`, 'search');\n }\n\n const response = await fetchWithTimeout(SERPER_API_URL, {\n method: 'POST',\n headers: {\n 'X-API-KEY': apiKey,\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify(body),\n timeoutMs: SEARCH_RETRY_CONFIG.timeoutMs,\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => '');\n lastError = classifyError({ status: response.status, message: errorText });\n\n if (isRetryable(response.status) && attempt < SEARCH_RETRY_CONFIG.maxRetries) {\n const delayMs = calculateBackoff(attempt, SEARCH_RETRY_CONFIG.baseDelayMs, SEARCH_RETRY_CONFIG.maxDelayMs);\n mcpLog('warning', `API returned ${response.status}, retrying in ${delayMs}ms...`, 'search');\n await sleep(delayMs);\n continue;\n }\n\n return { data: undefined, error: lastError };\n }\n\n try {\n const data = await response.json();\n return { data };\n } catch {\n return {\n data: undefined,\n error: { code: ErrorCode.PARSE_ERROR, message: 'Failed to parse search response', retryable: false },\n };\n }\n } catch (error) {\n lastError = classifyError(error);\n\n if (isRetryable(undefined, error) && attempt < SEARCH_RETRY_CONFIG.maxRetries) {\n const delayMs = calculateBackoff(attempt, SEARCH_RETRY_CONFIG.baseDelayMs, SEARCH_RETRY_CONFIG.maxDelayMs);\n mcpLog('warning', `${lastError.code}: ${lastError.message}, retrying in ${delayMs}ms...`, 'search');\n await sleep(delayMs);\n continue;\n }\n\n return { data: undefined, error: lastError };\n }\n }\n\n return {\n data: undefined,\n error: lastError || { code: ErrorCode.UNKNOWN_ERROR, message: 'Search failed', retryable: false },\n };\n}\n\n// \u2500\u2500 SearchClient \u2500\u2500\n\nexport class SearchClient {\n private apiKey: string;\n\n constructor(apiKey?: string) {\n const env = parseEnv();\n this.apiKey = apiKey || env.SEARCH_API_KEY || '';\n\n if (!this.apiKey) {\n throw new Error('Web search capability is not configured. Please set up the required API credentials.');\n }\n }\n\n /**\n * Check if error is retryable\n */\n private isRetryable(status?: number, error?: unknown): boolean {\n if (status && RETRYABLE_SEARCH_CODES.has(status)) return true;\n\n if (error == null) return false;\n const message = (typeof error === 'object' && 'message' in error && typeof (error as { message?: string }).message === 'string')\n ? (error as { message: string }).message.toLowerCase()\n : '';\n return message.includes('timeout') || message.includes('rate limit') || message.includes('connection');\n }\n\n /**\n * Search multiple queries in parallel\n * NEVER throws - always returns a valid response\n */\n async searchMultiple(queries: string[]): Promise<MultipleSearchResponse> {\n const startTime = Date.now();\n\n if (queries.length === 0) {\n return {\n searches: [],\n totalQueries: 0,\n executionTime: 0,\n error: { code: ErrorCode.INVALID_INPUT, message: 'No queries provided', retryable: false },\n };\n }\n\n const searchQueries = queries.map(query => ({ q: query }));\n const { data, error } = await executeSearchWithRetry(\n this.apiKey,\n searchQueries,\n (status, err) => this.isRetryable(status, err),\n );\n\n if (error || data === undefined) {\n return {\n searches: [],\n totalQueries: queries.length,\n executionTime: Date.now() - startTime,\n error: error ?? { code: ErrorCode.UNKNOWN_ERROR, message: 'Search provider returned no data', retryable: false },\n };\n }\n\n const responses = Array.isArray(data) ? data : [data];\n const searches = parseSearchResponses(responses as Array<Record<string, unknown>>, queries);\n\n return { searches, totalQueries: queries.length, executionTime: Date.now() - startTime };\n }\n\n /**\n * Search Reddit via Google (adds site:reddit.com automatically)\n * NEVER throws - returns empty array on failure\n */\n async searchReddit(query: string, dateAfter?: string): Promise<RedditSearchResult[]> {\n if (!query?.trim()) {\n return [];\n }\n\n let q = query.replace(REDDIT_SITE_REGEX, '').trim() + ' site:reddit.com';\n\n if (dateAfter) {\n q += ` after:${dateAfter}`;\n }\n\n for (let attempt = 0; attempt <= SEARCH_RETRY_CONFIG.maxRetries; attempt++) {\n try {\n const res = await fetchWithTimeout(SERPER_API_URL, {\n method: 'POST',\n headers: { 'X-API-KEY': this.apiKey, 'Content-Type': 'application/json' },\n body: JSON.stringify({ q, num: DEFAULT_RESULTS_PER_QUERY }),\n timeoutMs: SEARCH_RETRY_CONFIG.timeoutMs,\n });\n\n if (!res.ok) {\n if (this.isRetryable(res.status) && attempt < SEARCH_RETRY_CONFIG.maxRetries) {\n const delayMs = calculateBackoff(attempt, SEARCH_RETRY_CONFIG.baseDelayMs, SEARCH_RETRY_CONFIG.maxDelayMs);\n mcpLog('warning', `Reddit search ${res.status}, retrying in ${delayMs}ms...`, 'search');\n await sleep(delayMs);\n continue;\n }\n mcpLog('error', `Reddit search failed with status ${res.status}`, 'search');\n return [];\n }\n\n const data = await res.json() as { organic?: Array<{ title: string; link: string; snippet: string; date?: string }> };\n return (data.organic || []).map((r) => ({\n title: (r.title || '').replace(REDDIT_SUBREDDIT_SUFFIX_REGEX, '').replace(REDDIT_SUFFIX_REGEX, ''),\n url: r.link || '',\n snippet: r.snippet || '',\n date: r.date,\n }));\n\n } catch (error) {\n const err = classifyError(error);\n if (this.isRetryable(undefined, error) && attempt < SEARCH_RETRY_CONFIG.maxRetries) {\n const delayMs = calculateBackoff(attempt, SEARCH_RETRY_CONFIG.baseDelayMs, SEARCH_RETRY_CONFIG.maxDelayMs);\n mcpLog('warning', `Reddit search ${err.code}, retrying in ${delayMs}ms...`, 'search');\n await sleep(delayMs);\n continue;\n }\n mcpLog('error', `Reddit search failed: ${err.message}`, 'search');\n return [];\n }\n }\n\n return [];\n }\n\n /**\n * Search Reddit with multiple queries (bounded concurrency)\n * NEVER throws - searchReddit never throws, pMap preserves order\n */\n async searchRedditMultiple(queries: string[], dateAfter?: string): Promise<Map<string, RedditSearchResult[]>> {\n if (queries.length === 0) {\n return new Map();\n }\n\n const results = await pMap(\n queries,\n q => this.searchReddit(q, dateAfter),\n CONCURRENCY.SEARCH\n );\n\n return new Map(queries.map((q, i) => [q, results[i] || []]));\n }\n}\n", "/**\n * Jina Reader Client\n *\n * Converts any URL (including PDFs, DOCX, PPTX, HTML) into clean markdown via\n * the public Jina Reader endpoint. Used by raw/smart scrape tools for full\n * markdown capture before optional LLM extraction.\n *\n * NEVER throws \u2014 every failure surfaces as a classified `StructuredError`\n * in the returned response, matching the shape of `ScraperClient.scrape`.\n *\n * Auth: optional `JINA_API_KEY` raises the rate limit from 20 RPM to 200+ RPM.\n * Without a key the endpoint still works; we just retry more aggressively on\n * 429 responses.\n */\n\nimport {\n classifyError,\n fetchWithTimeout,\n sleep,\n ErrorCode,\n type StructuredError,\n} from '../utils/errors.js';\nimport { calculateBackoff } from '../utils/retry.js';\nimport { mcpLog } from '../utils/logger.js';\nimport type { MultipleSearchResponse, QuerySearchResult } from './search.js';\n\n// \u2500\u2500 Constants \u2500\u2500\n\nconst JINA_READER_BASE = 'https://r.jina.ai/' as const;\nconst JINA_SEARCH_BASE = 'https://s.jina.ai/' as const;\nconst DEFAULT_TIMEOUT_SECONDS = 15 as const;\nconst DEFAULT_TIMEOUT_MS = DEFAULT_TIMEOUT_SECONDS * 1000;\nconst MAX_RETRIES = 2 as const;\nconst SEARCH_RESULTS_PER_QUERY = 10 as const;\n\nexport function buildJinaSearchUrl(query: string): string {\n const params = new URLSearchParams({ q: query });\n return `${JINA_SEARCH_BASE}?${params.toString()}`;\n}\n\nexport function buildScrapeDoProxyUrl(token: string, parameters = 'render=false'): string {\n const trimmed = token.trim();\n if (!trimmed) return '';\n return `http://${encodeURIComponent(trimmed)}:${parameters}@proxy.scrape.do:8080`;\n}\n\n// \u2500\u2500 Interfaces \u2500\u2500\n\nexport interface JinaConvertRequest {\n readonly url: string;\n readonly timeoutSeconds?: number;\n readonly proxyUrl?: string;\n readonly noCache?: boolean;\n readonly allowProxyRetry?: boolean;\n}\n\nexport interface JinaConvertResponse {\n readonly content: string;\n readonly statusCode: number;\n /** Always 0 \u2014 Jina is a separate service from Scrape.do's credit pool. */\n readonly credits: 0;\n readonly usageTokens?: number;\n readonly error?: StructuredError;\n}\n\n// \u2500\u2500 Client \u2500\u2500\n\nexport class JinaClient {\n private readonly apiKey: string | undefined;\n\n constructor(apiKey?: string) {\n const fromEnv = process.env.JINA_API_KEY?.trim();\n this.apiKey = apiKey?.trim() || fromEnv || undefined;\n }\n\n /**\n * Convert a URL to markdown via Jina Reader.\n * NEVER throws \u2014 always returns a JinaConvertResponse (possibly with error).\n */\n async convert(request: JinaConvertRequest): Promise<JinaConvertResponse> {\n const {\n url,\n timeoutSeconds = DEFAULT_TIMEOUT_SECONDS,\n proxyUrl,\n noCache = false,\n allowProxyRetry = false,\n } = request;\n\n try {\n new URL(url);\n } catch {\n return {\n content: `Invalid URL: ${url}`,\n statusCode: 400,\n credits: 0,\n error: { code: ErrorCode.INVALID_INPUT, message: `Invalid URL: ${url}`, retryable: false },\n };\n }\n\n const first = await this.convertOnce({\n url,\n timeoutSeconds,\n proxyUrl,\n noCache,\n });\n\n if (!first.error || !allowProxyRetry || proxyUrl || isTerminalReaderError(first.error)) {\n return first;\n }\n\n mcpLog('warning', `Jina Reader failed for ${url}; retrying with Jina proxy`, 'jina');\n return this.convertOnce({\n url,\n timeoutSeconds,\n proxyUrl: 'auto',\n noCache: true,\n });\n }\n\n async searchMultiple(queries: string[]): Promise<MultipleSearchResponse> {\n const startTime = Date.now();\n if (queries.length === 0) {\n return {\n searches: [],\n totalQueries: 0,\n executionTime: 0,\n error: { code: ErrorCode.INVALID_INPUT, message: 'No queries provided', retryable: false },\n };\n }\n if (!this.apiKey) {\n return {\n searches: [],\n totalQueries: queries.length,\n executionTime: Date.now() - startTime,\n error: { code: ErrorCode.AUTH_ERROR, message: 'Jina Search requires JINA_API_KEY', retryable: false },\n };\n }\n\n const searches = await Promise.all(queries.map((query) => this.searchOne(query)));\n const firstError = searches.find((search) => search.error)?.error;\n const allFailed = searches.every((search) => search.error);\n return {\n searches,\n totalQueries: queries.length,\n executionTime: Date.now() - startTime,\n ...(allFailed && firstError ? { error: firstError } : {}),\n };\n }\n\n private async convertOnce(request: Required<Pick<JinaConvertRequest, 'url' | 'timeoutSeconds' | 'noCache'>> & Pick<JinaConvertRequest, 'proxyUrl'>): Promise<JinaConvertResponse> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n 'Content-Type': 'application/json',\n };\n if (this.apiKey) headers['Authorization'] = `Bearer ${this.apiKey}`;\n if (request.proxyUrl && request.proxyUrl !== 'auto') {\n headers['X-Proxy-Url'] = request.proxyUrl;\n }\n\n const body: Record<string, unknown> = {\n url: request.url,\n respondWith: 'markdown',\n timeout: request.timeoutSeconds,\n base: 'final',\n removeOverlay: true,\n };\n if (request.proxyUrl === 'auto') body['proxy'] = 'auto';\n if (request.noCache) body['noCache'] = true;\n\n return this.fetchReader(body, headers, request.timeoutSeconds);\n }\n\n private async fetchReader(\n body: Record<string, unknown>,\n headers: Record<string, string>,\n timeoutSeconds: number,\n ): Promise<JinaConvertResponse> {\n let lastError: StructuredError | undefined;\n\n for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {\n try {\n const response = await fetchWithTimeout(JINA_READER_BASE, {\n method: 'POST',\n headers,\n body: JSON.stringify(body),\n timeoutMs: (timeoutSeconds + 5) * 1000,\n });\n\n const raw = await response.text().catch((readError: unknown) =>\n `Failed to read Jina response: ${readError instanceof Error ? readError.message : String(readError)}`,\n );\n const usageHeader = response.headers.get('x-usage-tokens');\n const usageTokens = usageHeader ? Number(usageHeader) : undefined;\n const parsed = parseReaderContent(raw);\n\n if (response.ok) {\n if (!parsed.content.trim()) {\n return emptyReaderResponse(response.status, usageTokens);\n }\n return {\n content: parsed.content,\n statusCode: response.status,\n credits: 0,\n usageTokens: Number.isFinite(usageTokens) ? usageTokens : undefined,\n };\n }\n\n const terminal = terminalReaderResponse(response.status, parsed.content || raw);\n if (terminal) return terminal;\n\n lastError = classifyError({ status: response.status, message: raw.slice(0, 200) });\n if (lastError.retryable && attempt < MAX_RETRIES) {\n const delayMs = calculateBackoff(attempt);\n mcpLog(\n 'warning',\n `Jina ${response.status} on attempt ${attempt + 1}/${MAX_RETRIES + 1}. Retrying in ${delayMs}ms`,\n 'jina',\n );\n await sleep(delayMs);\n continue;\n }\n return {\n content: `Jina Reader error (${response.status}): ${raw.slice(0, 200)}`,\n statusCode: response.status,\n credits: 0,\n error: lastError,\n };\n } catch (error) {\n lastError = classifyError(error);\n if (lastError.retryable && attempt < MAX_RETRIES) {\n const delayMs = calculateBackoff(attempt);\n mcpLog(\n 'warning',\n `Jina ${lastError.code}: ${lastError.message}. Retry ${attempt + 1}/${MAX_RETRIES + 1} in ${delayMs}ms`,\n 'jina',\n );\n await sleep(delayMs);\n continue;\n }\n return {\n content: `Jina Reader failed: ${lastError.message}`,\n statusCode: lastError.statusCode ?? 500,\n credits: 0,\n error: lastError,\n };\n }\n }\n\n return {\n content: `Jina Reader failed after ${MAX_RETRIES + 1} attempts: ${lastError?.message ?? 'Unknown error'}`,\n statusCode: lastError?.statusCode ?? 500,\n credits: 0,\n error: lastError ?? { code: ErrorCode.UNKNOWN_ERROR, message: 'All retries exhausted', retryable: false },\n };\n }\n\n private async searchOne(query: string): Promise<QuerySearchResult> {\n const headers: Record<string, string> = {\n Accept: 'application/json',\n Authorization: `Bearer ${this.apiKey ?? ''}`,\n };\n\n try {\n const response = await fetchWithTimeout(buildJinaSearchUrl(query), {\n method: 'GET',\n headers,\n timeoutMs: DEFAULT_TIMEOUT_MS,\n });\n const raw = await response.text().catch((readError: unknown) =>\n `Failed to read Jina Search response: ${readError instanceof Error ? readError.message : String(readError)}`,\n );\n\n if (!response.ok) {\n return {\n query,\n results: [],\n totalResults: 0,\n related: [],\n error: classifyError({ status: response.status, message: raw.slice(0, 200) }),\n };\n }\n\n const results = parseSearchResults(raw);\n return { query, results, totalResults: results.length, related: [] };\n } catch (error) {\n return {\n query,\n results: [],\n totalResults: 0,\n related: [],\n error: classifyError(error),\n };\n }\n }\n}\n\nfunction parseReaderContent(raw: string): { content: string } {\n try {\n const parsed: unknown = JSON.parse(raw);\n const data = readRecord(parsed, 'data');\n const content = readString(data, 'content');\n if (content) return { content };\n } catch {\n // Jina may return text/plain on some degraded paths; use it as markdown.\n }\n return { content: raw };\n}\n\nfunction emptyReaderResponse(statusCode: number, usageTokens: number | undefined): JinaConvertResponse {\n return {\n content: 'Jina returned an empty body',\n statusCode,\n credits: 0,\n usageTokens: Number.isFinite(usageTokens) ? usageTokens : undefined,\n error: {\n code: ErrorCode.UNSUPPORTED_BINARY_CONTENT,\n message: 'Jina Reader returned empty content for this URL',\n retryable: false,\n },\n };\n}\n\nfunction terminalReaderResponse(statusCode: number, content: string): JinaConvertResponse | null {\n if (statusCode === 401 || statusCode === 403) {\n return {\n content: `Jina auth/quota error (${statusCode}): ${content.slice(0, 200)}`,\n statusCode,\n credits: 0,\n error: {\n code: statusCode === 401 ? ErrorCode.AUTH_ERROR : ErrorCode.QUOTA_EXCEEDED,\n message: statusCode === 401\n ? 'Jina Reader auth failed \u2014 check JINA_API_KEY'\n : 'Jina Reader quota exceeded',\n retryable: false,\n statusCode,\n },\n };\n }\n\n if (statusCode === 404) {\n return {\n content: 'Jina could not fetch the target URL (404)',\n statusCode: 404,\n credits: 0,\n error: {\n code: ErrorCode.NOT_FOUND,\n message: 'Target URL not reachable by Jina Reader',\n retryable: false,\n statusCode: 404,\n },\n };\n }\n\n if (statusCode >= 400 && statusCode < 500 && statusCode !== 429) {\n return {\n content: `Jina Reader error (${statusCode}): ${content.slice(0, 200)}`,\n statusCode,\n credits: 0,\n error: {\n code: ErrorCode.INVALID_INPUT,\n message: `Jina Reader returned ${statusCode}`,\n retryable: false,\n statusCode,\n },\n };\n }\n\n return null;\n}\n\nexport function isTerminalReaderError(error: StructuredError): boolean {\n return !error.retryable\n && (\n error.code === ErrorCode.AUTH_ERROR\n || error.code === ErrorCode.QUOTA_EXCEEDED\n || error.code === ErrorCode.NOT_FOUND\n || error.code === ErrorCode.INVALID_INPUT\n );\n}\n\nfunction parseSearchResults(raw: string): QuerySearchResult['results'] {\n let data: unknown;\n try {\n const parsed: unknown = JSON.parse(raw);\n data = readUnknown(parsed, 'data');\n } catch {\n data = parseMarkdownSearchResults(raw);\n }\n\n const items = Array.isArray(data) ? data : [];\n return items\n .map((item, index) => normalizeSearchItem(item, index))\n .filter((item): item is QuerySearchResult['results'][number] => item !== null)\n .slice(0, SEARCH_RESULTS_PER_QUERY);\n}\n\nfunction normalizeSearchItem(item: unknown, index: number): QuerySearchResult['results'][number] | null {\n const link = readString(item, 'url') ?? readString(item, 'link');\n if (!link) return null;\n return {\n title: readString(item, 'title') || link,\n link,\n snippet: (\n readString(item, 'snippet')\n || readString(item, 'description')\n || readString(item, 'content')\n || ''\n ).slice(0, 500),\n date: readString(item, 'date') ?? readString(item, 'publishedTime'),\n position: index + 1,\n };\n}\n\nfunction parseMarkdownSearchResults(raw: string): Array<Record<string, string>> {\n const items: Array<Record<string, string>> = [];\n const markdownLink = /\\[([^\\]]+)\\]\\((https?:\\/\\/[^)]+)\\)/g;\n let match: RegExpExecArray | null;\n while ((match = markdownLink.exec(raw)) !== null && items.length < SEARCH_RESULTS_PER_QUERY) {\n const title = match[1];\n const url = match[2];\n if (title && url) items.push({ title, url });\n }\n return items;\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction readUnknown(value: unknown, key: string): unknown {\n return isRecord(value) ? value[key] : undefined;\n}\n\nfunction readRecord(value: unknown, key: string): Record<string, unknown> | undefined {\n const child = readUnknown(value, key);\n return isRecord(child) ? child : undefined;\n}\n\nfunction readString(value: unknown, key: string): string | undefined {\n const child = readUnknown(value, key);\n return typeof child === 'string' ? child : undefined;\n}\n", "import Kernel from '@onkernel/sdk';\n\nimport { parseEnv } from '../config/index.js';\nimport { classifyError, ErrorCode, type StructuredError } from '../utils/errors.js';\nimport { mcpLog } from '../utils/logger.js';\n\nconst DEFAULT_RENDER_TIMEOUT_SECONDS = 15 as const;\nconst BROWSER_IDLE_TIMEOUT_SECONDS = 300 as const;\n\nexport interface KernelRenderRequest {\n readonly url: string;\n readonly timeoutSeconds?: number;\n}\n\nexport interface KernelRenderResponse {\n readonly content: string;\n readonly statusCode: number;\n readonly credits: 0;\n readonly finalUrl?: string;\n readonly title?: string;\n readonly text?: string;\n readonly error?: StructuredError;\n}\n\ninterface KernelRenderedPage {\n readonly html: string;\n readonly finalUrl: string;\n readonly title: string;\n readonly text: string;\n}\n\nexport class KernelClient {\n private readonly kernel: Kernel;\n\n constructor(apiKey?: string) {\n const env = parseEnv();\n const resolvedKey = apiKey?.trim() || env.KERNEL_API_KEY;\n if (!resolvedKey) {\n throw new Error('Kernel browser rendering is not configured. Set KERNEL_API_KEY.');\n }\n\n this.kernel = new Kernel({\n apiKey: resolvedKey,\n timeout: 30_000,\n maxRetries: 1,\n ...(env.KERNEL_PROJECT\n ? { defaultHeaders: { 'X-Kernel-Project-Id': env.KERNEL_PROJECT } }\n : {}),\n });\n }\n\n async render(request: KernelRenderRequest): Promise<KernelRenderResponse> {\n const { url, timeoutSeconds = DEFAULT_RENDER_TIMEOUT_SECONDS } = request;\n try {\n new URL(url);\n } catch {\n return {\n content: `Invalid URL: ${url}`,\n statusCode: 400,\n credits: 0,\n error: { code: ErrorCode.INVALID_INPUT, message: `Invalid URL: ${url}`, retryable: false },\n };\n }\n\n let sessionId: string | undefined;\n try {\n const session = await this.kernel.browsers.create({\n headless: true,\n stealth: true,\n timeout_seconds: BROWSER_IDLE_TIMEOUT_SECONDS,\n viewport: { width: 1280, height: 800 },\n });\n sessionId = session.session_id;\n\n const response = await this.kernel.browsers.playwright.execute(session.session_id, {\n code: buildRenderScript(url, timeoutSeconds),\n timeout_sec: Math.min(timeoutSeconds + 5, 300),\n });\n\n if (!response.success) {\n const message = response.error || response.stderr || 'Kernel Playwright execution failed';\n return {\n content: `Kernel render failed: ${message}`,\n statusCode: 500,\n credits: 0,\n error: { code: ErrorCode.SERVICE_UNAVAILABLE, message, retryable: true },\n };\n }\n\n const rendered = parseRenderedPage(response.result);\n if (!rendered) {\n return {\n content: 'Kernel render returned an invalid payload',\n statusCode: 500,\n credits: 0,\n error: {\n code: ErrorCode.PARSE_ERROR,\n message: 'Kernel render returned an invalid payload',\n retryable: false,\n },\n };\n }\n\n return {\n content: rendered.html,\n statusCode: 200,\n credits: 0,\n finalUrl: rendered.finalUrl,\n title: rendered.title,\n text: rendered.text,\n };\n } catch (error) {\n const err = formatKernelError(error);\n return {\n content: `Kernel render failed: ${err.message}`,\n statusCode: err.statusCode ?? 500,\n credits: 0,\n error: err,\n };\n } finally {\n if (sessionId) {\n try {\n await this.kernel.browsers.deleteByID(sessionId);\n } catch (deleteError) {\n const err = formatKernelError(deleteError);\n mcpLog('warning', `Kernel browser cleanup failed for ${sessionId}: ${err.message}`, 'kernel');\n }\n }\n }\n }\n}\n\nfunction buildRenderScript(url: string, timeoutSeconds: number): string {\n const timeoutMs = timeoutSeconds * 1000;\n return `\nconst targetUrl = ${JSON.stringify(url)};\nconst timeoutMs = ${timeoutMs};\nawait page.goto(targetUrl, { waitUntil: 'domcontentloaded', timeout: timeoutMs });\nawait page.waitForLoadState('networkidle', { timeout: Math.min(5000, timeoutMs) }).catch(() => {});\nconst html = await page.content();\nconst text = await page.locator('body').innerText({ timeout: 2000 }).catch(() => '');\nreturn {\n html,\n text,\n title: await page.title(),\n finalUrl: page.url(),\n};\n`;\n}\n\nfunction parseRenderedPage(value: unknown): KernelRenderedPage | null {\n if (!isRecord(value)) return null;\n const html = readString(value, 'html');\n const finalUrl = readString(value, 'finalUrl');\n const title = readString(value, 'title');\n const text = readString(value, 'text');\n if (!html || !finalUrl || title === undefined || text === undefined) return null;\n return { html, finalUrl, title, text };\n}\n\nfunction formatKernelError(error: unknown): StructuredError {\n if (error instanceof Kernel.APIError) {\n return {\n ...classifyError({ status: error.status, message: error.message }),\n cause: error.message,\n };\n }\n return classifyError(error);\n}\n\nfunction isRecord(value: unknown): value is Record<string, unknown> {\n return typeof value === 'object' && value !== null;\n}\n\nfunction readString(value: Record<string, unknown>, key: string): string | undefined {\n const child = value[key];\n return typeof child === 'string' ? child : undefined;\n}\n", "/**\n * Reddit OAuth API Client\n * Fetches posts and comments sorted by score (most upvoted first)\n * Implements robust error handling that NEVER crashes\n */\n\nimport { Logger } from 'mcp-use';\n\nimport { REDDIT, CONCURRENCY } from '../config/index.js';\nimport { USER_AGENT_VERSION } from '../version.js';\nimport { calculateBackoff } from '../utils/retry.js';\nimport {\n classifyError,\n fetchWithTimeout,\n sleep,\n ErrorCode,\n type StructuredError,\n} from '../utils/errors.js';\nimport { pMap, pMapSettled } from '../utils/concurrency.js';\nimport { mcpLog } from '../utils/logger.js';\n\n// \u2500\u2500 Constants \u2500\u2500\n\nconst REDDIT_TOKEN_URL = 'https://www.reddit.com/api/v1/access_token' as const;\nconst REDDIT_API_BASE = 'https://oauth.reddit.com' as const;\nconst TOKEN_EXPIRY_MS = 55_000 as const; // 55 second expiry (conservative)\n\n// \u2500\u2500 Data Interfaces \u2500\u2500\n\ninterface Post {\n readonly title: string;\n readonly author: string;\n readonly subreddit: string;\n readonly body: string;\n readonly score: number;\n readonly commentCount: number;\n readonly url: string;\n readonly created: Date;\n readonly flair?: string;\n readonly isNsfw: boolean;\n readonly isPinned: boolean;\n}\n\nexport interface Comment {\n readonly author: string;\n readonly body: string;\n readonly score: number;\n readonly depth: number;\n readonly isOP: boolean;\n}\n\nexport interface PostResult {\n readonly post: Post;\n readonly comments: Comment[];\n readonly actualComments: number;\n}\n\nexport interface BatchPostResult {\n readonly results: Map<string, PostResult | Error>;\n readonly batchesProcessed: number;\n readonly totalPosts: number;\n readonly rateLimitHits: number;\n}\n\n/** Reddit API \"Listing\" wrapper */\ninterface RedditListing<T> {\n readonly kind: string;\n readonly data: {\n readonly children: ReadonlyArray<{ readonly kind: string; readonly data: T }>;\n readonly after?: string;\n readonly before?: string;\n };\n}\n\n/** Reddit post data from API */\ninterface RedditPostData {\n readonly title: string;\n readonly selftext: string;\n readonly selftext_html?: string;\n readonly author: string;\n readonly subreddit: string;\n readonly score: number;\n readonly upvote_ratio: number;\n readonly num_comments: number;\n readonly created_utc: number;\n readonly url: string;\n readonly permalink: string;\n readonly is_self: boolean;\n readonly over_18: boolean;\n readonly stickied: boolean;\n readonly link_flair_text?: string;\n readonly [key: string]: unknown;\n}\n\n/** Reddit comment data from API */\ninterface RedditCommentData {\n readonly body?: string;\n readonly author?: string;\n readonly score?: number;\n readonly created_utc?: number;\n readonly replies?: RedditListing<RedditCommentData> | string;\n readonly [key: string]: unknown;\n}\n\ntype RedditPostResponse = [RedditListing<RedditPostData>, RedditListing<RedditCommentData>];\n\n/** Max comments to fetch per post from Reddit API */\n/** Reddit API caps at 500 comments per request */\nconst FETCH_LIMIT = 500;\n\n// ============================================================================\n// Module-Level Token Cache (shared across all RedditClient instances)\n// ============================================================================\nlet cachedToken: string | null = null;\nlet cachedTokenExpiry = 0;\n\n// Token cache logging only when DEBUG env is set\nconst DEBUG_TOKEN_CACHE = process.env.DEBUG_REDDIT === 'true';\nconst clientLogger = Logger.get('reddit-client');\n\n// Pending auth promise for deduplicating concurrent auth calls\nlet pendingAuthPromise: Promise<string | null> | null = null;\n\n// \u2500\u2500 Decomposed Helpers \u2500\u2500\n\n/**\n * Fetch a Reddit post's JSON from the API\n */\nasync function fetchRedditJson(\n sub: string,\n id: string,\n token: string,\n userAgent: string,\n): Promise<RedditPostResponse> {\n const limit = Math.min(FETCH_LIMIT, 500);\n const apiUrl = `${REDDIT_API_BASE}/r/${sub}/comments/${id}?sort=top&limit=${limit}&depth=10&raw_json=1`;\n\n const res = await fetchWithTimeout(apiUrl, {\n headers: {\n 'Authorization': `Bearer ${token}`,\n 'User-Agent': userAgent,\n },\n timeoutMs: 30000,\n });\n\n if (res.status === 429) {\n const err = new Error('Rate limited by Reddit API');\n (err as Error & { status: number }).status = 429;\n throw err;\n }\n\n if (res.status === 404) {\n throw new Error(`Post not found: /r/${sub}/comments/${id}`);\n }\n\n if (!res.ok) {\n const err = new Error(`Reddit API error: ${res.status}`);\n (err as Error & { status: number }).status = res.status;\n throw err;\n }\n\n try {\n return await res.json() as RedditPostResponse;\n } catch {\n throw new Error('Failed to parse Reddit API response');\n }\n}\n\n/**\n * Extract structured post data from a Reddit listing\n */\nfunction parsePostData(\n postListing: RedditListing<RedditPostData>,\n sub: string,\n): Post {\n const p = postListing?.data?.children?.[0]?.data;\n if (!p) {\n throw new Error(`Post data not found in response for /r/${sub}`);\n }\n\n return {\n title: p.title || 'Untitled',\n author: p.author || '[deleted]',\n subreddit: p.subreddit || sub,\n body: formatBody(p),\n score: p.score || 0,\n commentCount: p.num_comments || 0,\n url: `https://reddit.com${p.permalink || ''}`,\n created: new Date((p.created_utc || 0) * 1000),\n flair: p.link_flair_text || undefined,\n isNsfw: p.over_18 || false,\n isPinned: p.stickied || false,\n };\n}\n\nfunction formatBody(p: RedditPostData): string {\n if (p.selftext?.trim()) return p.selftext;\n if (p.is_self) return '';\n if (p.url) return `**Link:** ${p.url}`;\n return '';\n}\n\n/** Safety cap on comment tree recursion depth */\nconst MAX_COMMENT_DEPTH = 15 as const;\n\n/**\n * Extract and sort comments from a Reddit comment listing\n */\nfunction parseCommentTree(\n commentListing: RedditListing<RedditCommentData>,\n opAuthor: string,\n): Comment[] {\n const result: Comment[] = [];\n\n const extract = (items: ReadonlyArray<{ readonly kind: string; readonly data: RedditCommentData }>, depth = 0): void => {\n if (depth > MAX_COMMENT_DEPTH) return;\n const sorted = [...items].sort((a, b) => (b.data?.score || 0) - (a.data?.score || 0));\n\n for (const c of sorted) {\n if (c.kind !== 't1' || !c.data?.author || c.data.author === '[deleted]') continue;\n\n result.push({\n author: c.data.author,\n body: c.data.body || '',\n score: c.data.score || 0,\n depth,\n isOP: c.data.author === opAuthor,\n });\n\n if (typeof c.data.replies === 'object' && c.data.replies?.data?.children) {\n extract(c.data.replies.data.children, depth + 1);\n }\n }\n };\n\n extract(commentListing?.data?.children || []);\n return result;\n}\n\n// \u2500\u2500 Batch Helpers \u2500\u2500\n\n/**\n * Process a single batch of Reddit URLs, returning results keyed by URL\n */\nasync function processBatch(\n client: RedditClient,\n batchUrls: string[],\n): Promise<{ results: Map<string, PostResult | Error>; rateLimitHits: number }> {\n const results = new Map<string, PostResult | Error>();\n let rateLimitHits = 0;\n\n const batchResults = await pMapSettled(\n batchUrls,\n url => client.getPost(url),\n CONCURRENCY.REDDIT,\n );\n\n for (let i = 0; i < batchResults.length; i++) {\n const result = batchResults[i];\n if (!result) continue;\n const url = batchUrls[i] ?? '';\n\n if (result.status === 'fulfilled') {\n results.set(url, result.value);\n } else {\n const errorMsg = result.reason?.message || String(result.reason);\n if (errorMsg.includes('429') || errorMsg.includes('rate')) rateLimitHits++;\n results.set(url, new Error(errorMsg));\n }\n }\n\n return { results, rateLimitHits };\n}\n\n// \u2500\u2500 RedditClient Class \u2500\u2500\n\nexport class RedditClient {\n private userAgent = `script:${USER_AGENT_VERSION} (by /u/research-powerpack)`;\n\n constructor(private clientId: string, private clientSecret: string) {}\n\n /**\n * Authenticate with Reddit API with retry logic\n * Uses module-level token cache and promise deduplication to prevent\n * concurrent auth calls from firing multiple token requests\n * Returns null on failure instead of throwing\n */\n private async auth(): Promise<string | null> {\n if (cachedToken && Date.now() < cachedTokenExpiry - TOKEN_EXPIRY_MS) {\n if (DEBUG_TOKEN_CACHE) clientLogger.debug('Token cache HIT');\n return cachedToken;\n }\n\n if (pendingAuthPromise) {\n if (DEBUG_TOKEN_CACHE) clientLogger.debug('Auth already in flight, awaiting...');\n return pendingAuthPromise;\n }\n\n pendingAuthPromise = this.performAuth();\n try {\n return await pendingAuthPromise;\n } finally {\n pendingAuthPromise = null;\n }\n }\n\n private async performAuth(): Promise<string | null> {\n if (DEBUG_TOKEN_CACHE) clientLogger.debug('Token cache MISS - authenticating');\n\n const credentials = Buffer.from(`${this.clientId}:${this.clientSecret}`).toString('base64');\n\n for (let attempt = 0; attempt < 3; attempt++) {\n try {\n const res = await fetchWithTimeout(REDDIT_TOKEN_URL, {\n method: 'POST',\n headers: {\n 'Authorization': `Basic ${credentials}`,\n 'Content-Type': 'application/x-www-form-urlencoded',\n 'User-Agent': this.userAgent,\n },\n body: 'grant_type=client_credentials',\n timeoutMs: 15000,\n });\n\n if (!res.ok) {\n const text = await res.text().catch(() => '');\n mcpLog('error', `Auth failed (${res.status}): ${text}`, 'reddit');\n\n if (res.status === 401 || res.status === 403) {\n cachedToken = null;\n cachedTokenExpiry = 0;\n return null;\n }\n\n if (res.status >= 500 && attempt < 2) {\n await sleep(calculateBackoff(attempt));\n continue;\n }\n\n return null;\n }\n\n const data = await res.json() as { access_token?: string; expires_in?: number };\n if (!data.access_token) {\n mcpLog('error', 'Auth response missing access_token', 'reddit');\n return null;\n }\n\n cachedToken = data.access_token;\n cachedTokenExpiry = Date.now() + (data.expires_in || 3600) * 1000;\n return cachedToken;\n\n } catch (error) {\n const err = classifyError(error);\n mcpLog('error', `Auth error (attempt ${attempt + 1}): ${err.message}`, 'reddit');\n\n if (err.code === ErrorCode.AUTH_ERROR) {\n cachedToken = null;\n cachedTokenExpiry = 0;\n }\n\n if (attempt < 2 && err.retryable) {\n await sleep(calculateBackoff(attempt));\n continue;\n }\n\n return null;\n }\n }\n\n return null;\n }\n\n private parseUrl(url: string): { sub: string; id: string } | null {\n const m = url.match(/reddit\\.com\\/r\\/([^\\/]+)\\/comments\\/([a-z0-9]+)/i);\n return m ? { sub: m[1]!, id: m[2]! } : null;\n }\n\n /**\n * Get a single Reddit post with comments\n * Returns PostResult or throws Error (for use with Promise.allSettled)\n */\n async getPost(url: string): Promise<PostResult> {\n const parsed = this.parseUrl(url);\n if (!parsed) {\n throw new Error(`Invalid Reddit URL format: ${url}`);\n }\n\n const token = await this.auth();\n if (!token) {\n throw new Error('Reddit authentication failed - check credentials');\n }\n\n let lastError: StructuredError | null = null;\n\n for (let attempt = 0; attempt < REDDIT.RETRY_COUNT; attempt++) {\n try {\n const data = await fetchRedditJson(parsed.sub, parsed.id, token, this.userAgent);\n const [postListing, commentListing] = data;\n\n const post = parsePostData(postListing, parsed.sub);\n const comments = parseCommentTree(commentListing, post.author);\n\n return { post, comments, actualComments: post.commentCount };\n\n } catch (error) {\n lastError = classifyError(error);\n\n // Rate limited \u2014 always retry with backoff\n const status = (error as Error & { status?: number }).status;\n if (status === 429) {\n const delay = REDDIT.RETRY_DELAYS[attempt] || 32000;\n mcpLog('warning', `Rate limited. Retry ${attempt + 1}/${REDDIT.RETRY_COUNT} after ${delay}ms`, 'reddit');\n await sleep(delay);\n continue;\n }\n\n if (!lastError.retryable) {\n throw error instanceof Error ? error : new Error(lastError.message);\n }\n\n if (attempt < REDDIT.RETRY_COUNT - 1) {\n const delay = REDDIT.RETRY_DELAYS[attempt] || 2000;\n mcpLog('warning', `${lastError.code}: ${lastError.message}. Retry ${attempt + 1}/${REDDIT.RETRY_COUNT}`, 'reddit');\n await sleep(delay);\n }\n }\n }\n\n throw new Error(lastError?.message || 'Failed to fetch Reddit post after retries');\n }\n\n async batchGetPosts(\n urls: string[],\n fetchComments = true,\n onBatchComplete?: (batchNum: number, totalBatches: number, processed: number) => void,\n ): Promise<BatchPostResult> {\n const allResults = new Map<string, PostResult | Error>();\n let rateLimitHits = 0;\n\n const totalBatches = Math.ceil(urls.length / REDDIT.BATCH_SIZE);\n mcpLog('info', `Fetching ${urls.length} posts in ${totalBatches} batch(es), up to ${FETCH_LIMIT} comments/post`, 'reddit');\n\n for (let batchNum = 0; batchNum < totalBatches; batchNum++) {\n const startIdx = batchNum * REDDIT.BATCH_SIZE;\n const batchUrls = urls.slice(startIdx, startIdx + REDDIT.BATCH_SIZE);\n\n mcpLog('info', `Batch ${batchNum + 1}/${totalBatches} (${batchUrls.length} posts)`, 'reddit');\n\n const batchResult = await processBatch(this, batchUrls);\n for (const [url, result] of batchResult.results) {\n allResults.set(url, result);\n }\n rateLimitHits += batchResult.rateLimitHits;\n\n try {\n onBatchComplete?.(batchNum + 1, totalBatches, allResults.size);\n } catch (callbackError) {\n mcpLog('error', `onBatchComplete callback error: ${callbackError}`, 'reddit');\n }\n\n mcpLog('info', `Batch ${batchNum + 1} complete (${allResults.size}/${urls.length})`, 'reddit');\n\n if (batchNum < totalBatches - 1) {\n await sleep(500);\n }\n }\n\n return { results: allResults, batchesProcessed: totalBatches, totalPosts: urls.length, rateLimitHits };\n }\n}\n", "import { Data } from 'effect';\n\nimport { classifyError, ErrorCode, type StructuredError } from '../utils/errors.js';\n\nexport type ExternalProvider =\n | 'serper'\n | 'jina'\n | 'reddit'\n | 'kernel'\n | 'llm';\n\nexport class ProviderRequestError extends Data.TaggedError('ProviderRequestError')<{\n readonly provider: ExternalProvider;\n readonly operation: string;\n readonly error: StructuredError;\n}> {}\n\nexport class ProviderTimeoutError extends Data.TaggedError('ProviderTimeoutError')<{\n readonly provider: ExternalProvider;\n readonly operation: string;\n readonly durationMs: number;\n}> {}\n\nexport class WeakContentError extends Data.TaggedError('WeakContentError')<{\n readonly provider: ExternalProvider;\n readonly operation: string;\n readonly reason: string;\n}> {}\n\nexport class AllStrategiesExhaustedError extends Data.TaggedError('AllStrategiesExhaustedError')<{\n readonly url: string;\n readonly attempts: readonly ProviderAttemptFailure[];\n}> {}\n\nexport interface ProviderAttemptFailure {\n readonly provider: ExternalProvider;\n readonly strategy: string;\n readonly message: string;\n readonly code?: string;\n}\n\nexport type ExternalRequestError =\n | ProviderRequestError\n | ProviderTimeoutError\n | WeakContentError\n | AllStrategiesExhaustedError;\n\nexport function providerError(\n provider: ExternalProvider,\n operation: string,\n error: unknown,\n): ProviderRequestError {\n return new ProviderRequestError({\n provider,\n operation,\n error: classifyError(error),\n });\n}\n\nexport function providerResponseError(\n provider: ExternalProvider,\n operation: string,\n message: string,\n code: StructuredError['code'] = ErrorCode.UNKNOWN_ERROR,\n retryable = false,\n): ProviderRequestError {\n return new ProviderRequestError({\n provider,\n operation,\n error: { code, message, retryable },\n });\n}\n\nexport function attemptFromError(\n provider: ExternalProvider,\n strategy: string,\n error: ProviderRequestError | ProviderTimeoutError | WeakContentError,\n): ProviderAttemptFailure {\n if (error._tag === 'ProviderTimeoutError') {\n return {\n provider,\n strategy,\n code: ErrorCode.TIMEOUT,\n message: `${error.operation} timed out after ${error.durationMs}ms`,\n };\n }\n if (error._tag === 'WeakContentError') {\n return {\n provider,\n strategy,\n code: 'WEAK_CONTENT',\n message: error.reason,\n };\n }\n return {\n provider,\n strategy,\n code: error.error.code,\n message: error.error.message,\n };\n}\n", "import { error, markdown, type TypedCallToolResult } from 'mcp-use/server';\n\ntype ClientLogLevel =\n | 'debug'\n | 'info'\n | 'notice'\n | 'warning'\n | 'error'\n | 'critical'\n | 'alert'\n | 'emergency';\n\ninterface ReporterContext {\n log(level: ClientLogLevel, message: string, loggerName?: string): Promise<void>;\n reportProgress?: (loaded: number, total?: number, message?: string) => Promise<void>;\n}\n\nexport interface ToolExecutionSuccess<T extends Record<string, unknown>> {\n readonly isError: false;\n readonly content: string;\n readonly structuredContent?: T;\n}\n\nexport interface ToolExecutionFailure {\n readonly isError: true;\n readonly content: string;\n}\n\nexport type ToolExecutionResult<T extends Record<string, unknown>> =\n | ToolExecutionSuccess<T>\n | ToolExecutionFailure;\n\nexport interface ToolReporter {\n log(level: ClientLogLevel, message: string): Promise<void>;\n progress(loaded: number, total?: number, message?: string): Promise<void>;\n}\n\nexport const NOOP_REPORTER: ToolReporter = {\n async log() {},\n async progress() {},\n};\n\nexport function toolSuccess<T extends Record<string, unknown>>(\n content: string,\n structuredContent?: T,\n): ToolExecutionSuccess<T> {\n return {\n isError: false,\n content,\n structuredContent,\n };\n}\n\nexport function toolFailure(content: string): ToolExecutionFailure {\n return {\n isError: true,\n content,\n };\n}\n\nexport function createToolReporter(\n ctx: ReporterContext,\n loggerName: string,\n): ToolReporter {\n return {\n log(level, message) {\n return ctx.log(level, message, loggerName);\n },\n progress(loaded, total, message) {\n return ctx.reportProgress?.(loaded, total, message) ?? Promise.resolve();\n },\n };\n}\n\nexport function toToolResponse<T extends Record<string, unknown>>(\n result: ToolExecutionResult<T>,\n): TypedCallToolResult<T> | TypedCallToolResult<never> {\n if (result.isError) {\n return error(result.content);\n }\n\n if (result.structuredContent) {\n return {\n ...markdown(result.content),\n structuredContent: result.structuredContent,\n } as TypedCallToolResult<T>;\n }\n\n return markdown(result.content) as TypedCallToolResult<never>;\n}\n", "/**\n * MCP Response Formatters\n */\n\n/** Duration thresholds in milliseconds */\nconst SECONDS_MS = 1_000 as const;\nconst MINUTES_MS = 60_000 as const;\n\n// ============================================================================\n// Success Response Formatter\n// ============================================================================\n\nexport interface SuccessOptions {\n /** Title/header for the response */\n readonly title: string;\n /** Summary section (70% of content) */\n readonly summary: string;\n /** Optional data section (20% of content) */\n readonly data?: string;\n /** Optional next steps (10% of content) */\n readonly nextSteps?: string[];\n /** Optional metadata footer */\n readonly metadata?: Record<string, string | number>;\n}\n\n/**\n * Format a successful response using 70/20/10 pattern\n */\nexport function formatSuccess(opts: SuccessOptions): string {\n const parts: string[] = [];\n\n // Title\n parts.push(`\u2713 ${opts.title}`);\n parts.push('');\n\n // Summary (70%)\n parts.push(opts.summary);\n\n // Data section (20%)\n if (opts.data) {\n parts.push('');\n parts.push('---');\n parts.push(opts.data);\n }\n\n // Next steps (10%)\n if (opts.nextSteps?.length) {\n parts.push('');\n parts.push('---');\n parts.push('**Next Steps:**');\n opts.nextSteps.forEach(step => parts.push(`\u2192 ${step}`));\n }\n\n // Metadata footer\n if (opts.metadata && Object.keys(opts.metadata).length > 0) {\n parts.push('');\n parts.push('---');\n const metaStr = Object.entries(opts.metadata)\n .map(([k, v]) => `${k}: ${v}`)\n .join(' | ');\n parts.push(`*${metaStr}*`);\n }\n\n return parts.join('\\n');\n}\n\n// ============================================================================\n// Error Response Formatter\n// ============================================================================\n\nexport interface ErrorOptions {\n /** Error code (e.g., RATE_LIMITED, TIMEOUT) */\n readonly code: string;\n /** Human-readable error message */\n readonly message: string;\n /** Is this error retryable? */\n readonly retryable?: boolean;\n /** How to fix the error */\n readonly howToFix?: string[];\n /** Alternative actions */\n readonly alternatives?: string[];\n /** Tool name for context */\n readonly toolName?: string;\n}\n\n/**\n * Format an error response with recovery guidance\n * Designed to keep agents moving \u2014 every error includes actionable alternatives\n */\nexport function formatError(opts: ErrorOptions): string {\n const parts: string[] = [];\n\n // Error header\n const prefix = opts.toolName ? `[${opts.toolName}] ` : '';\n parts.push(`\u274C ${prefix}${opts.code}: ${opts.message}`);\n\n // Retryable hint\n if (opts.retryable) {\n parts.push('*Retryable.*');\n }\n\n // How to fix\n if (opts.howToFix?.length) {\n parts.push('');\n parts.push('**How to Fix:**');\n opts.howToFix.forEach((step, i) => parts.push(`${i + 1}. ${step}`));\n }\n\n // Alternatives\n if (opts.alternatives?.length) {\n parts.push('');\n parts.push('**Alternatives:**');\n opts.alternatives.forEach((alt, i) => parts.push(`${i + 1}. ${alt}`));\n }\n\n return parts.join('\\n');\n}\n\n// ============================================================================\n// Batch Header Formatter\n// ============================================================================\n\nexport interface BatchHeaderOptions {\n /** Batch operation title */\n readonly title: string;\n /** Total items attempted */\n readonly totalItems: number;\n /** Successfully processed count */\n readonly successful: number;\n /** Failed count */\n readonly failed: number;\n /** Optional tokens per item */\n readonly tokensPerItem?: number;\n /** Optional batch count */\n readonly batches?: number;\n /** Extra stats to include */\n readonly extras?: Record<string, string | number>;\n}\n\n/**\n * Format a batch operation header with stats\n */\nexport function formatBatchHeader(opts: BatchHeaderOptions): string {\n const parts: string[] = [];\n\n // Title with emoji based on success rate\n const successRate = opts.totalItems > 0 ? opts.successful / opts.totalItems : 0;\n const emoji = successRate === 1 ? '\u2713' : successRate >= 0.5 ? '\u26A0\uFE0F' : '\u274C';\n parts.push(`${emoji} ${opts.title}`);\n parts.push('');\n\n // Stats\n parts.push(`\u2022 Total: ${opts.totalItems}`);\n parts.push(`\u2022 Successful: ${opts.successful}`);\n if (opts.failed > 0) {\n parts.push(`\u2022 Failed: ${opts.failed}`);\n }\n if (opts.tokensPerItem) {\n parts.push(`\u2022 Tokens/item: ~${opts.tokensPerItem.toLocaleString()}`);\n }\n if (opts.batches) {\n parts.push(`\u2022 Batches: ${opts.batches}`);\n }\n\n // Extra stats\n if (opts.extras) {\n Object.entries(opts.extras).forEach(([key, val]) => {\n parts.push(`\u2022 ${key}: ${val}`);\n });\n }\n\n return parts.join('\\n');\n}\n\n// ============================================================================\n// Duration Formatter\n// ============================================================================\n\n/**\n * Format duration in human-readable form\n */\nexport function formatDuration(ms: number): string {\n if (ms < SECONDS_MS) return `${ms}ms`;\n if (ms < MINUTES_MS) return `${(ms / SECONDS_MS).toFixed(1)}s`;\n return `${(ms / MINUTES_MS).toFixed(1)}m`;\n}\n\n"],
5
+ "mappings": "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",
6
+ "names": ["Effect", "z", "Logger", "error", "error", "Effect", "Layer", "calculateBackoff", "calculateBackoff", "error", "MAX_RETRIES", "MAX_RETRIES", "calculateBackoff", "error", "error", "isRecord", "readString", "Logger", "Logger", "error", "error", "error", "Layer", "Effect", "Effect", "llmHealth"]
7
7
  }