opencandle 0.1.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (278) hide show
  1. package/README.md +111 -79
  2. package/dist/analysts/contracts.d.ts +31 -0
  3. package/dist/analysts/contracts.js +158 -0
  4. package/dist/analysts/contracts.js.map +1 -0
  5. package/dist/analysts/orchestrator.d.ts +11 -2
  6. package/dist/analysts/orchestrator.js +156 -9
  7. package/dist/analysts/orchestrator.js.map +1 -1
  8. package/dist/cli.js +26 -10
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +29 -5
  11. package/dist/config.js +18 -8
  12. package/dist/config.js.map +1 -1
  13. package/dist/index.d.ts +1 -1
  14. package/dist/index.js +1 -1
  15. package/dist/index.js.map +1 -1
  16. package/dist/infra/cache.d.ts +34 -0
  17. package/dist/infra/cache.js +44 -3
  18. package/dist/infra/cache.js.map +1 -1
  19. package/dist/infra/index.d.ts +1 -1
  20. package/dist/infra/index.js +1 -1
  21. package/dist/infra/index.js.map +1 -1
  22. package/dist/infra/opencandle-paths.d.ts +1 -0
  23. package/dist/infra/opencandle-paths.js +3 -0
  24. package/dist/infra/opencandle-paths.js.map +1 -1
  25. package/dist/infra/rate-limiter.js +7 -0
  26. package/dist/infra/rate-limiter.js.map +1 -1
  27. package/dist/memory/index.d.ts +3 -0
  28. package/dist/memory/index.js +2 -0
  29. package/dist/memory/index.js.map +1 -1
  30. package/dist/memory/manager.d.ts +19 -0
  31. package/dist/memory/manager.js +132 -0
  32. package/dist/memory/manager.js.map +1 -0
  33. package/dist/memory/sqlite.js +12 -1
  34. package/dist/memory/sqlite.js.map +1 -1
  35. package/dist/memory/types.d.ts +21 -0
  36. package/dist/memory/types.js +47 -0
  37. package/dist/memory/types.js.map +1 -0
  38. package/dist/onboarding/connect.d.ts +23 -0
  39. package/dist/onboarding/connect.js +107 -0
  40. package/dist/onboarding/connect.js.map +1 -0
  41. package/dist/onboarding/credential-interceptor.d.ts +44 -0
  42. package/dist/onboarding/credential-interceptor.js +72 -0
  43. package/dist/onboarding/credential-interceptor.js.map +1 -0
  44. package/dist/onboarding/degradation-accumulator.d.ts +21 -0
  45. package/dist/onboarding/degradation-accumulator.js +55 -0
  46. package/dist/onboarding/degradation-accumulator.js.map +1 -0
  47. package/dist/onboarding/prompt-user.d.ts +23 -0
  48. package/dist/onboarding/prompt-user.js +61 -0
  49. package/dist/onboarding/prompt-user.js.map +1 -0
  50. package/dist/onboarding/providers.d.ts +109 -0
  51. package/dist/onboarding/providers.js +236 -0
  52. package/dist/onboarding/providers.js.map +1 -0
  53. package/dist/onboarding/state.d.ts +31 -2
  54. package/dist/onboarding/state.js +141 -13
  55. package/dist/onboarding/state.js.map +1 -1
  56. package/dist/onboarding/tool-helpers.d.ts +34 -0
  57. package/dist/onboarding/tool-helpers.js +80 -0
  58. package/dist/onboarding/tool-helpers.js.map +1 -0
  59. package/dist/onboarding/tool-tags.d.ts +37 -0
  60. package/dist/onboarding/tool-tags.js +149 -0
  61. package/dist/onboarding/tool-tags.js.map +1 -0
  62. package/dist/onboarding/validation.d.ts +19 -0
  63. package/dist/onboarding/validation.js +117 -0
  64. package/dist/onboarding/validation.js.map +1 -0
  65. package/dist/pi/opencandle-extension.d.ts +5 -1
  66. package/dist/pi/opencandle-extension.js +345 -143
  67. package/dist/pi/opencandle-extension.js.map +1 -1
  68. package/dist/pi/session.d.ts +2 -0
  69. package/dist/pi/session.js +1 -1
  70. package/dist/pi/session.js.map +1 -1
  71. package/dist/pi/setup.d.ts +0 -1
  72. package/dist/pi/setup.js +66 -119
  73. package/dist/pi/setup.js.map +1 -1
  74. package/dist/prompts/context-builder.d.ts +26 -0
  75. package/dist/prompts/context-builder.js +127 -0
  76. package/dist/prompts/context-builder.js.map +1 -0
  77. package/dist/prompts/sections.d.ts +13 -0
  78. package/dist/prompts/sections.js +35 -0
  79. package/dist/prompts/sections.js.map +1 -0
  80. package/dist/providers/alpha-vantage.d.ts +3 -0
  81. package/dist/providers/alpha-vantage.js +204 -77
  82. package/dist/providers/alpha-vantage.js.map +1 -1
  83. package/dist/providers/coingecko.js +53 -37
  84. package/dist/providers/coingecko.js.map +1 -1
  85. package/dist/providers/exa-search.d.ts +39 -0
  86. package/dist/providers/exa-search.js +276 -0
  87. package/dist/providers/exa-search.js.map +1 -0
  88. package/dist/providers/fear-greed.js +23 -15
  89. package/dist/providers/fear-greed.js.map +1 -1
  90. package/dist/providers/finnhub.d.ts +17 -0
  91. package/dist/providers/finnhub.js +94 -0
  92. package/dist/providers/finnhub.js.map +1 -0
  93. package/dist/providers/fred.js +48 -28
  94. package/dist/providers/fred.js.map +1 -1
  95. package/dist/providers/index.d.ts +2 -0
  96. package/dist/providers/index.js +1 -0
  97. package/dist/providers/index.js.map +1 -1
  98. package/dist/providers/provider-credential-error.d.ts +8 -0
  99. package/dist/providers/provider-credential-error.js +22 -0
  100. package/dist/providers/provider-credential-error.js.map +1 -0
  101. package/dist/providers/reddit.d.ts +8 -0
  102. package/dist/providers/reddit.js +78 -43
  103. package/dist/providers/reddit.js.map +1 -1
  104. package/dist/providers/twitter.d.ts +20 -0
  105. package/dist/providers/twitter.js +137 -0
  106. package/dist/providers/twitter.js.map +1 -0
  107. package/dist/providers/web-search.d.ts +17 -0
  108. package/dist/providers/web-search.js +224 -0
  109. package/dist/providers/web-search.js.map +1 -0
  110. package/dist/providers/with-fallback.d.ts +15 -0
  111. package/dist/providers/with-fallback.js +32 -0
  112. package/dist/providers/with-fallback.js.map +1 -0
  113. package/dist/providers/wrap-provider.d.ts +20 -0
  114. package/dist/providers/wrap-provider.js +58 -0
  115. package/dist/providers/wrap-provider.js.map +1 -0
  116. package/dist/providers/yahoo-finance.js +77 -57
  117. package/dist/providers/yahoo-finance.js.map +1 -1
  118. package/dist/routing/classify-intent.js +22 -0
  119. package/dist/routing/classify-intent.js.map +1 -1
  120. package/dist/runtime/evidence.d.ts +35 -0
  121. package/dist/runtime/evidence.js +29 -0
  122. package/dist/runtime/evidence.js.map +1 -0
  123. package/dist/runtime/index.d.ts +16 -0
  124. package/dist/runtime/index.js +10 -0
  125. package/dist/runtime/index.js.map +1 -0
  126. package/dist/runtime/prompt-step.d.ts +41 -0
  127. package/dist/runtime/prompt-step.js +42 -0
  128. package/dist/runtime/prompt-step.js.map +1 -0
  129. package/dist/runtime/provider-ids.d.ts +14 -0
  130. package/dist/runtime/provider-ids.js +14 -0
  131. package/dist/runtime/provider-ids.js.map +1 -0
  132. package/dist/runtime/provider-tracker.d.ts +20 -0
  133. package/dist/runtime/provider-tracker.js +36 -0
  134. package/dist/runtime/provider-tracker.js.map +1 -0
  135. package/dist/runtime/run-context.d.ts +11 -0
  136. package/dist/runtime/run-context.js +14 -0
  137. package/dist/runtime/run-context.js.map +1 -0
  138. package/dist/runtime/session-coordinator.d.ts +47 -0
  139. package/dist/runtime/session-coordinator.js +171 -0
  140. package/dist/runtime/session-coordinator.js.map +1 -0
  141. package/dist/runtime/validation.d.ts +44 -0
  142. package/dist/runtime/validation.js +157 -0
  143. package/dist/runtime/validation.js.map +1 -0
  144. package/dist/runtime/workflow-events.d.ts +21 -0
  145. package/dist/runtime/workflow-events.js +31 -0
  146. package/dist/runtime/workflow-events.js.map +1 -0
  147. package/dist/runtime/workflow-runner.d.ts +36 -0
  148. package/dist/runtime/workflow-runner.js +129 -0
  149. package/dist/runtime/workflow-runner.js.map +1 -0
  150. package/dist/runtime/workflow-types.d.ts +60 -0
  151. package/dist/runtime/workflow-types.js +32 -0
  152. package/dist/runtime/workflow-types.js.map +1 -0
  153. package/dist/sentiment/adapters/finnhub.d.ts +7 -0
  154. package/dist/sentiment/adapters/finnhub.js +39 -0
  155. package/dist/sentiment/adapters/finnhub.js.map +1 -0
  156. package/dist/sentiment/adapters/reddit.d.ts +11 -0
  157. package/dist/sentiment/adapters/reddit.js +54 -0
  158. package/dist/sentiment/adapters/reddit.js.map +1 -0
  159. package/dist/sentiment/adapters/twitter.d.ts +9 -0
  160. package/dist/sentiment/adapters/twitter.js +32 -0
  161. package/dist/sentiment/adapters/twitter.js.map +1 -0
  162. package/dist/sentiment/adapters/web.d.ts +9 -0
  163. package/dist/sentiment/adapters/web.js +40 -0
  164. package/dist/sentiment/adapters/web.js.map +1 -0
  165. package/dist/sentiment/index.d.ts +16 -0
  166. package/dist/sentiment/index.js +44 -0
  167. package/dist/sentiment/index.js.map +1 -0
  168. package/dist/sentiment/keywords.d.ts +2 -0
  169. package/dist/sentiment/keywords.js +9 -0
  170. package/dist/sentiment/keywords.js.map +1 -0
  171. package/dist/sentiment/pipeline.d.ts +9 -0
  172. package/dist/sentiment/pipeline.js +57 -0
  173. package/dist/sentiment/pipeline.js.map +1 -0
  174. package/dist/sentiment/scorer.d.ts +9 -0
  175. package/dist/sentiment/scorer.js +64 -0
  176. package/dist/sentiment/scorer.js.map +1 -0
  177. package/dist/sentiment/store.d.ts +24 -0
  178. package/dist/sentiment/store.js +177 -0
  179. package/dist/sentiment/store.js.map +1 -0
  180. package/dist/sentiment/trends.d.ts +13 -0
  181. package/dist/sentiment/trends.js +73 -0
  182. package/dist/sentiment/trends.js.map +1 -0
  183. package/dist/sentiment/types.d.ts +66 -0
  184. package/dist/sentiment/types.js +54 -0
  185. package/dist/sentiment/types.js.map +1 -0
  186. package/dist/system-prompt.js +60 -2
  187. package/dist/system-prompt.js.map +1 -1
  188. package/dist/tool-kit.d.ts +9 -5
  189. package/dist/tool-kit.js +29 -6
  190. package/dist/tool-kit.js.map +1 -1
  191. package/dist/tools/fundamentals/company-overview.d.ts +3 -1
  192. package/dist/tools/fundamentals/company-overview.js +28 -17
  193. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  194. package/dist/tools/fundamentals/comps.js +47 -39
  195. package/dist/tools/fundamentals/comps.js.map +1 -1
  196. package/dist/tools/fundamentals/dcf.js +83 -65
  197. package/dist/tools/fundamentals/dcf.js.map +1 -1
  198. package/dist/tools/fundamentals/earnings.d.ts +3 -1
  199. package/dist/tools/fundamentals/earnings.js +26 -18
  200. package/dist/tools/fundamentals/earnings.js.map +1 -1
  201. package/dist/tools/fundamentals/financials.d.ts +3 -1
  202. package/dist/tools/fundamentals/financials.js +24 -16
  203. package/dist/tools/fundamentals/financials.js.map +1 -1
  204. package/dist/tools/fundamentals/sec-filings.js +9 -1
  205. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  206. package/dist/tools/index.js +10 -2
  207. package/dist/tools/index.js.map +1 -1
  208. package/dist/tools/interaction/ask-user.d.ts +3 -0
  209. package/dist/tools/interaction/ask-user.js +51 -0
  210. package/dist/tools/interaction/ask-user.js.map +1 -0
  211. package/dist/tools/interaction/twitter-login.d.ts +8 -0
  212. package/dist/tools/interaction/twitter-login.js +77 -0
  213. package/dist/tools/interaction/twitter-login.js.map +1 -0
  214. package/dist/tools/macro/fear-greed.js +9 -1
  215. package/dist/tools/macro/fear-greed.js.map +1 -1
  216. package/dist/tools/macro/fred-data.d.ts +3 -1
  217. package/dist/tools/macro/fred-data.js +27 -16
  218. package/dist/tools/macro/fred-data.js.map +1 -1
  219. package/dist/tools/market/crypto-history.js +9 -1
  220. package/dist/tools/market/crypto-history.js.map +1 -1
  221. package/dist/tools/market/crypto-price.js +9 -1
  222. package/dist/tools/market/crypto-price.js.map +1 -1
  223. package/dist/tools/market/stock-history.js +28 -1
  224. package/dist/tools/market/stock-history.js.map +1 -1
  225. package/dist/tools/market/stock-quote.js +29 -4
  226. package/dist/tools/market/stock-quote.js.map +1 -1
  227. package/dist/tools/options/option-chain.js +9 -1
  228. package/dist/tools/options/option-chain.js.map +1 -1
  229. package/dist/tools/portfolio/correlation.js +15 -3
  230. package/dist/tools/portfolio/correlation.js.map +1 -1
  231. package/dist/tools/portfolio/predictions.js +6 -5
  232. package/dist/tools/portfolio/predictions.js.map +1 -1
  233. package/dist/tools/portfolio/risk-analysis.js +9 -1
  234. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  235. package/dist/tools/portfolio/tracker.js +6 -3
  236. package/dist/tools/portfolio/tracker.js.map +1 -1
  237. package/dist/tools/portfolio/watchlist.js +6 -1
  238. package/dist/tools/portfolio/watchlist.js.map +1 -1
  239. package/dist/tools/sentiment/reddit-sentiment.d.ts +3 -1
  240. package/dist/tools/sentiment/reddit-sentiment.js +112 -19
  241. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  242. package/dist/tools/sentiment/sentiment-summary.d.ts +7 -0
  243. package/dist/tools/sentiment/sentiment-summary.js +230 -0
  244. package/dist/tools/sentiment/sentiment-summary.js.map +1 -0
  245. package/dist/tools/sentiment/sentiment-trend.d.ts +22 -0
  246. package/dist/tools/sentiment/sentiment-trend.js +39 -0
  247. package/dist/tools/sentiment/sentiment-trend.js.map +1 -0
  248. package/dist/tools/sentiment/twitter-sentiment.d.ts +9 -0
  249. package/dist/tools/sentiment/twitter-sentiment.js +75 -0
  250. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -0
  251. package/dist/tools/sentiment/web-search.d.ts +11 -0
  252. package/dist/tools/sentiment/web-search.js +115 -0
  253. package/dist/tools/sentiment/web-search.js.map +1 -0
  254. package/dist/tools/sentiment/web-sentiment.d.ts +8 -0
  255. package/dist/tools/sentiment/web-sentiment.js +66 -0
  256. package/dist/tools/sentiment/web-sentiment.js.map +1 -0
  257. package/dist/tools/technical/backtest.js +9 -1
  258. package/dist/tools/technical/backtest.js.map +1 -1
  259. package/dist/tools/technical/indicators.js +9 -1
  260. package/dist/tools/technical/indicators.js.map +1 -1
  261. package/dist/types/index.d.ts +16 -1
  262. package/dist/types/sentiment.d.ts +41 -0
  263. package/dist/workflows/compare-assets.d.ts +3 -0
  264. package/dist/workflows/compare-assets.js +21 -5
  265. package/dist/workflows/compare-assets.js.map +1 -1
  266. package/dist/workflows/index.d.ts +3 -3
  267. package/dist/workflows/index.js +3 -3
  268. package/dist/workflows/index.js.map +1 -1
  269. package/dist/workflows/options-screener.d.ts +3 -0
  270. package/dist/workflows/options-screener.js +24 -7
  271. package/dist/workflows/options-screener.js.map +1 -1
  272. package/dist/workflows/portfolio-builder.d.ts +3 -0
  273. package/dist/workflows/portfolio-builder.js +30 -9
  274. package/dist/workflows/portfolio-builder.js.map +1 -1
  275. package/package.json +27 -7
  276. package/dist/tools/sentiment/news-sentiment.d.ts +0 -7
  277. package/dist/tools/sentiment/news-sentiment.js +0 -57
  278. package/dist/tools/sentiment/news-sentiment.js.map +0 -1
@@ -0,0 +1,276 @@
1
+ import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
2
+ import { rateLimiter } from "../infra/rate-limiter.js";
3
+ import { getConfig } from "../config.js";
4
+ import { ProviderCredentialError } from "./provider-credential-error.js";
5
+ const EXA_MCP_URL = "https://mcp.exa.ai/mcp";
6
+ const EXA_API_URL = "https://api.exa.ai/search";
7
+ const TIMEOUT_MS = 5_000;
8
+ const SNIPPET_MAX_CHARS = 300;
9
+ const CONTEXT_MAX_CHARS = 1_000;
10
+ let requestIdCounter = 0;
11
+ // ---------------------------------------------------------------------------
12
+ // Freshness helpers
13
+ // ---------------------------------------------------------------------------
14
+ function freshnessToMs(freshness) {
15
+ switch (freshness) {
16
+ case "hours": return 60 * 60 * 1000;
17
+ case "day": return 24 * 60 * 60 * 1000;
18
+ case "week": return 7 * 24 * 60 * 60 * 1000;
19
+ case "month": return 30 * 24 * 60 * 60 * 1000;
20
+ }
21
+ }
22
+ function enrichQueryForMcp(query, freshness) {
23
+ switch (freshness) {
24
+ case "hours": return `${query} past hour`;
25
+ case "day": return `${query} past 24 hours`;
26
+ case "week": return `${query} past week`;
27
+ case "month": return `${query} past month`;
28
+ }
29
+ }
30
+ function startPublishedDate(freshness) {
31
+ return new Date(Date.now() - freshnessToMs(freshness)).toISOString();
32
+ }
33
+ function extractJsonRpcPayload(body, contentType) {
34
+ // Path 1: plain JSON response
35
+ if (contentType.includes("application/json")) {
36
+ return JSON.parse(body);
37
+ }
38
+ // Path 2: SSE — scan all data: lines for valid JSON-RPC
39
+ const dataLines = body.split("\n").filter((line) => line.startsWith("data:"));
40
+ for (const line of dataLines) {
41
+ const payload = line.slice(5).trim();
42
+ if (!payload)
43
+ continue;
44
+ try {
45
+ const candidate = JSON.parse(payload);
46
+ if (candidate?.result || candidate?.error)
47
+ return candidate;
48
+ }
49
+ catch {
50
+ // not valid JSON, try next line
51
+ }
52
+ }
53
+ // Path 3: fallback — try parsing entire body
54
+ try {
55
+ const candidate = JSON.parse(body);
56
+ if (candidate?.result || candidate?.error)
57
+ return candidate;
58
+ }
59
+ catch {
60
+ // not valid JSON
61
+ }
62
+ throw new Error("Exa MCP returned empty content");
63
+ }
64
+ function parseMcpResultBlocks(text) {
65
+ const blocks = text.split(/\n---\n/).filter((b) => b.trim().length > 0);
66
+ const results = [];
67
+ for (const block of blocks) {
68
+ const title = block.match(/^Title: (.+)/m)?.[1]?.trim() ?? "";
69
+ const url = block.match(/^URL: (.+)/m)?.[1]?.trim() ?? "";
70
+ if (!url)
71
+ continue;
72
+ const published = block.match(/^Published: (.+)/m)?.[1]?.trim() ?? null;
73
+ // Extract highlights/text content after the header fields
74
+ let content = "";
75
+ const hlMatch = block.match(/^Highlights:\n?/m);
76
+ if (hlMatch?.index != null) {
77
+ content = block.slice(hlMatch.index + hlMatch[0].length).trim();
78
+ }
79
+ const snippet = content.slice(0, SNIPPET_MAX_CHARS);
80
+ results.push({ title, url, published, snippet });
81
+ }
82
+ return results;
83
+ }
84
+ function mapApiResults(results) {
85
+ return results
86
+ .filter((r) => r.url)
87
+ .map((r) => ({
88
+ title: r.title ?? "",
89
+ url: r.url,
90
+ published: r.publishedDate ?? null,
91
+ snippet: (r.highlights?.join(" ") || r.text || "").slice(0, SNIPPET_MAX_CHARS),
92
+ }));
93
+ }
94
+ // ---------------------------------------------------------------------------
95
+ // Freshness post-filter
96
+ // ---------------------------------------------------------------------------
97
+ function filterByFreshness(results, freshness) {
98
+ const cutoff = Date.now() - freshnessToMs(freshness);
99
+ return results.filter((r) => {
100
+ if (!r.published)
101
+ return true; // benefit of the doubt
102
+ const pubTime = new Date(r.published).getTime();
103
+ return !isNaN(pubTime) && pubTime >= cutoff;
104
+ });
105
+ }
106
+ // ---------------------------------------------------------------------------
107
+ // Common helpers
108
+ // ---------------------------------------------------------------------------
109
+ function extractDomain(url) {
110
+ try {
111
+ return new URL(url).hostname.replace(/^www\./, "");
112
+ }
113
+ catch {
114
+ return url;
115
+ }
116
+ }
117
+ function toWebSearchResults(parsed, category) {
118
+ return parsed.map((r) => ({
119
+ title: r.title,
120
+ url: r.url,
121
+ snippet: r.snippet,
122
+ source: extractDomain(r.url),
123
+ published: r.published,
124
+ category,
125
+ }));
126
+ }
127
+ function exaCacheKey(query, opts) {
128
+ return `web:exa:${query}:${opts.category}:${opts.freshness}:${opts.limit}`;
129
+ }
130
+ // ---------------------------------------------------------------------------
131
+ // MCP path
132
+ // ---------------------------------------------------------------------------
133
+ async function exaMcpSearch(query, opts) {
134
+ const enrichedQuery = enrichQueryForMcp(query, opts.freshness);
135
+ const response = await fetch(EXA_MCP_URL, {
136
+ method: "POST",
137
+ headers: {
138
+ "Content-Type": "application/json",
139
+ Accept: "application/json, text/event-stream",
140
+ },
141
+ body: JSON.stringify({
142
+ jsonrpc: "2.0",
143
+ id: ++requestIdCounter,
144
+ method: "tools/call",
145
+ params: {
146
+ name: "web_search_exa",
147
+ arguments: {
148
+ query: enrichedQuery,
149
+ numResults: opts.limit,
150
+ livecrawl: "fallback",
151
+ type: "auto",
152
+ contextMaxCharacters: CONTEXT_MAX_CHARS,
153
+ },
154
+ },
155
+ }),
156
+ signal: AbortSignal.timeout(TIMEOUT_MS),
157
+ });
158
+ // Anti-abuse handling
159
+ const contentType = response.headers.get("content-type") ?? "";
160
+ if (!response.ok) {
161
+ if (response.status === 429) {
162
+ const retryAfter = response.headers.get("retry-after");
163
+ throw new Error(`Exa MCP rate limited (429)${retryAfter ? ` — retry after ${retryAfter}s` : ""}`);
164
+ }
165
+ if (response.status === 403) {
166
+ throw new Error("Exa MCP blocked (403) — possible IP-based blocking");
167
+ }
168
+ throw new Error(`Exa MCP HTTP ${response.status} ${response.statusText}`);
169
+ }
170
+ if (contentType.includes("text/html")) {
171
+ throw new Error("Exa MCP returned HTML instead of JSON-RPC (possible challenge page)");
172
+ }
173
+ const body = await response.text();
174
+ const payload = extractJsonRpcPayload(body, contentType);
175
+ if (payload.error) {
176
+ throw new Error(payload.error.message ?? `Exa MCP error: code ${payload.error.code}`);
177
+ }
178
+ const text = payload.result?.content?.find((item) => item.type === "text" && typeof item.text === "string")?.text;
179
+ if (!text || text.trim().length === 0) {
180
+ // Legitimate zero results
181
+ return {
182
+ query,
183
+ results: [],
184
+ resultCount: 0,
185
+ fetchedAt: new Date().toISOString(),
186
+ provider: "exa",
187
+ };
188
+ }
189
+ const parsed = parseMcpResultBlocks(text);
190
+ const filtered = filterByFreshness(parsed, opts.freshness);
191
+ const results = toWebSearchResults(filtered, opts.category);
192
+ return {
193
+ query,
194
+ results: results.slice(0, opts.limit),
195
+ resultCount: Math.min(results.length, opts.limit),
196
+ fetchedAt: new Date().toISOString(),
197
+ provider: "exa",
198
+ };
199
+ }
200
+ // ---------------------------------------------------------------------------
201
+ // Direct API path
202
+ // ---------------------------------------------------------------------------
203
+ async function exaApiSearch(query, opts, apiKey) {
204
+ const response = await fetch(EXA_API_URL, {
205
+ method: "POST",
206
+ headers: {
207
+ "Content-Type": "application/json",
208
+ Authorization: `Bearer ${apiKey}`,
209
+ },
210
+ body: JSON.stringify({
211
+ query,
212
+ type: "auto",
213
+ numResults: opts.limit,
214
+ startPublishedDate: startPublishedDate(opts.freshness),
215
+ contents: {
216
+ text: { maxCharacters: CONTEXT_MAX_CHARS },
217
+ highlights: true,
218
+ },
219
+ }),
220
+ signal: AbortSignal.timeout(TIMEOUT_MS),
221
+ });
222
+ if (!response.ok) {
223
+ if (response.status === 401 || response.status === 403) {
224
+ throw new ProviderCredentialError("exa", "stale", response.status);
225
+ }
226
+ const body = await response.text().catch(() => "");
227
+ throw new Error(`Exa API HTTP ${response.status}: ${body.slice(0, 300)}`);
228
+ }
229
+ const data = (await response.json());
230
+ const parsed = mapApiResults(data.results ?? []);
231
+ const filtered = filterByFreshness(parsed, opts.freshness);
232
+ const results = toWebSearchResults(filtered, opts.category);
233
+ return {
234
+ query,
235
+ results: results.slice(0, opts.limit),
236
+ resultCount: Math.min(results.length, opts.limit),
237
+ fetchedAt: new Date().toISOString(),
238
+ provider: "exa",
239
+ };
240
+ }
241
+ // ---------------------------------------------------------------------------
242
+ // Public entry point
243
+ // ---------------------------------------------------------------------------
244
+ export async function exaSearch(query, opts) {
245
+ const key = exaCacheKey(query, opts);
246
+ const cached = cache.get(key);
247
+ if (cached)
248
+ return cached;
249
+ try {
250
+ await rateLimiter.acquire("exa");
251
+ const config = getConfig();
252
+ const envelope = config.exaApiKey
253
+ ? await exaApiSearch(query, opts, config.exaApiKey)
254
+ : await exaMcpSearch(query, opts);
255
+ cache.set(key, envelope, TTL.WEB_SEARCH);
256
+ return envelope;
257
+ }
258
+ catch (error) {
259
+ // Re-throw abort errors immediately
260
+ if (error instanceof Error && error.name === "AbortError")
261
+ throw error;
262
+ if (error instanceof DOMException && error.name === "TimeoutError")
263
+ throw error;
264
+ // Re-throw credential errors so the tool layer can convert them to the
265
+ // tagged content block. Stale cache must not mask a real auth failure.
266
+ if (error instanceof ProviderCredentialError)
267
+ throw error;
268
+ const stale = cache.getStale(key, STALE_LIMIT.WEB_SEARCH);
269
+ if (stale)
270
+ return stale.value;
271
+ throw error;
272
+ }
273
+ }
274
+ // Exported for testing
275
+ export { parseMcpResultBlocks, extractJsonRpcPayload, enrichQueryForMcp, filterByFreshness, mapApiResults, };
276
+ //# sourceMappingURL=exa-search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exa-search.js","sourceRoot":"","sources":["../../src/providers/exa-search.ts"],"names":[],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { httpGet } from "../infra/http-client.js";
2
- import { cache, TTL } from "../infra/cache.js";
2
+ import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
3
3
  // alternative.me provides a free crypto Fear & Greed index
4
4
  // CNN endpoint (production.dataviz.cnn.io) blocks automated requests (HTTP 418)
5
5
  const ENDPOINT = "https://api.alternative.me/fng/?limit=3";
@@ -8,19 +8,27 @@ export async function getFearGreedIndex() {
8
8
  const cached = cache.get(cacheKey);
9
9
  if (cached)
10
10
  return cached;
11
- const data = await httpGet(ENDPOINT);
12
- const entries = data.data;
13
- const current = entries[0];
14
- const value = parseInt(current.value, 10);
15
- const result = {
16
- value,
17
- label: current.value_classification,
18
- timestamp: Date.now(),
19
- previousClose: entries[1] ? parseInt(entries[1].value, 10) : value,
20
- weekAgo: null,
21
- monthAgo: null,
22
- };
23
- cache.set(cacheKey, result, TTL.SENTIMENT);
24
- return result;
11
+ try {
12
+ const data = await httpGet(ENDPOINT);
13
+ const entries = data.data;
14
+ const current = entries[0];
15
+ const value = parseInt(current.value, 10);
16
+ const result = {
17
+ value,
18
+ label: current.value_classification,
19
+ timestamp: Date.now(),
20
+ previousClose: entries[1] ? parseInt(entries[1].value, 10) : value,
21
+ weekAgo: null,
22
+ monthAgo: null,
23
+ };
24
+ cache.set(cacheKey, result, TTL.SENTIMENT);
25
+ return result;
26
+ }
27
+ catch (error) {
28
+ const stale = cache.getStale(cacheKey, STALE_LIMIT.SENTIMENT);
29
+ if (stale)
30
+ return stale.value;
31
+ throw error;
32
+ }
25
33
  }
26
34
  //# sourceMappingURL=fear-greed.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fear-greed.js","sourceRoot":"","sources":["../../src/providers/fear-greed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAG/C,2DAA2D;AAC3D,gFAAgF;AAChF,MAAM,QAAQ,GAAG,yCAAyC,CAAC;AAU3D,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,MAAM,QAAQ,GAAG,iBAAiB,CAAC;IACnC,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAgB,QAAQ,CAAC,CAAC;IAClD,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,IAAI,GAAG,MAAM,OAAO,CAA2B,QAAQ,CAAC,CAAC;IAE/D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC;IAC1B,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;IAC3B,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IAE1C,MAAM,MAAM,GAAkB;QAC5B,KAAK;QACL,KAAK,EAAE,OAAO,CAAC,oBAAoB;QACnC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;QACrB,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK;QAClE,OAAO,EAAE,IAAI;QACb,QAAQ,EAAE,IAAI;KACf,CAAC;IAEF,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;IAC3C,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"fear-greed.js","sourceRoot":"","sources":["../../src/providers/fear-greed.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAG5D,2DAA2D;AAC3D,gFAAgF;AAChF,MAAM,QAAQ,GAAG,yCAAyC,CAAC;AAU3D,MAAM,CAAC,KAAK,UAAU,iBAAiB;IACrC,MAAM,QAAQ,GAAG,iBAAiB,CAAC;IACnC,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAgB,QAAQ,CAAC,CAAC;IAClD,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,IAAI,CAAC;QACH,MAAM,IAAI,GAAG,MAAM,OAAO,CAA2B,QAAQ,CAAC,CAAC;QAE/D,MAAM,OAAO,GAAG,IAAI,CAAC,IAAI,CAAC;QAC1B,MAAM,OAAO,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;QAC3B,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;QAE1C,MAAM,MAAM,GAAkB;YAC5B,KAAK;YACL,KAAK,EAAE,OAAO,CAAC,oBAAoB;YACnC,SAAS,EAAE,IAAI,CAAC,GAAG,EAAE;YACrB,aAAa,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,KAAK;YAClE,OAAO,EAAE,IAAI;YACb,QAAQ,EAAE,IAAI;SACf,CAAC;QAEF,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,SAAS,CAAC,CAAC;QAC3C,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAgB,QAAQ,EAAE,WAAW,CAAC,SAAS,CAAC,CAAC;QAC7E,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC,KAAK,CAAC;QAC9B,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -0,0 +1,17 @@
1
+ export interface FinnhubArticle {
2
+ headline: string;
3
+ summary: string;
4
+ source: string;
5
+ datetime: number;
6
+ url: string;
7
+ related: string;
8
+ id: number;
9
+ category: string;
10
+ image: string;
11
+ }
12
+ export declare function finnhubDateRange(freshness: "hours" | "day" | "week" | "month"): {
13
+ from: string;
14
+ to: string;
15
+ };
16
+ export declare function filterByRelevance(articles: FinnhubArticle[], symbol: string, limit?: number): FinnhubArticle[];
17
+ export declare function getCompanyNews(symbol: string, from: string, to: string, apiKey: string): Promise<FinnhubArticle[]>;
@@ -0,0 +1,94 @@
1
+ import { httpGet, HttpError } from "../infra/http-client.js";
2
+ import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
3
+ import { rateLimiter } from "../infra/rate-limiter.js";
4
+ import { ProviderCredentialError } from "./provider-credential-error.js";
5
+ const FINNHUB_BASE = "https://finnhub.io/api/v1";
6
+ // Ticker → company name mapping for relevance filtering
7
+ const TICKER_NAMES = {
8
+ AAPL: "apple",
9
+ MSFT: "microsoft",
10
+ GOOGL: "google",
11
+ GOOG: "google",
12
+ AMZN: "amazon",
13
+ META: "meta",
14
+ TSLA: "tesla",
15
+ NVDA: "nvidia",
16
+ TSM: "tsmc",
17
+ BABA: "alibaba",
18
+ NFLX: "netflix",
19
+ AMD: "amd",
20
+ INTC: "intel",
21
+ CRM: "salesforce",
22
+ ORCL: "oracle",
23
+ };
24
+ export function finnhubDateRange(freshness) {
25
+ const now = new Date();
26
+ const to = formatDate(now);
27
+ switch (freshness) {
28
+ case "hours":
29
+ return { from: to, to };
30
+ case "day": {
31
+ const d = new Date(now);
32
+ d.setDate(d.getDate() - 1);
33
+ return { from: formatDate(d), to };
34
+ }
35
+ case "week": {
36
+ const d = new Date(now);
37
+ d.setDate(d.getDate() - 7);
38
+ return { from: formatDate(d), to };
39
+ }
40
+ case "month": {
41
+ const d = new Date(now);
42
+ d.setDate(d.getDate() - 30);
43
+ return { from: formatDate(d), to };
44
+ }
45
+ }
46
+ }
47
+ function formatDate(d) {
48
+ return d.toISOString().slice(0, 10);
49
+ }
50
+ function cacheKey(symbol, from, to) {
51
+ return `finnhub:news:${symbol}:${from}:${to}`;
52
+ }
53
+ export function filterByRelevance(articles, symbol, limit = 20) {
54
+ const symLower = symbol.toLowerCase();
55
+ const companyName = TICKER_NAMES[symbol.toUpperCase()];
56
+ const filtered = articles.filter((a) => {
57
+ const hl = a.headline.toLowerCase();
58
+ const sm = a.summary.toLowerCase();
59
+ if (hl.includes(symLower) || sm.includes(symLower))
60
+ return true;
61
+ if (companyName && (hl.includes(companyName) || sm.includes(companyName)))
62
+ return true;
63
+ return false;
64
+ });
65
+ // Most recent first, capped
66
+ return filtered
67
+ .sort((a, b) => b.datetime - a.datetime)
68
+ .slice(0, limit);
69
+ }
70
+ export async function getCompanyNews(symbol, from, to, apiKey) {
71
+ const key = cacheKey(symbol, from, to);
72
+ const cached = cache.get(key);
73
+ if (cached)
74
+ return cached;
75
+ try {
76
+ await rateLimiter.acquire("finnhub");
77
+ const url = `${FINNHUB_BASE}/company-news?symbol=${encodeURIComponent(symbol)}&from=${from}&to=${to}&token=${apiKey}`;
78
+ const data = await httpGet(url);
79
+ const articles = Array.isArray(data) ? data : [];
80
+ const filtered = filterByRelevance(articles, symbol);
81
+ cache.set(key, filtered, TTL.FINNHUB_NEWS);
82
+ return filtered;
83
+ }
84
+ catch (error) {
85
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
86
+ throw new ProviderCredentialError("finnhub", "stale", error.status);
87
+ }
88
+ const stale = cache.getStale(key, STALE_LIMIT.FINNHUB_NEWS);
89
+ if (stale)
90
+ return stale.value;
91
+ throw error;
92
+ }
93
+ }
94
+ //# sourceMappingURL=finnhub.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"finnhub.js","sourceRoot":"","sources":["../../src/providers/finnhub.ts"],"names":[],"mappings":"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"}
@@ -1,37 +1,57 @@
1
- import { httpGet } from "../infra/http-client.js";
2
- import { cache, TTL } from "../infra/cache.js";
1
+ import { httpGet, HttpError } from "../infra/http-client.js";
2
+ import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
3
3
  import { rateLimiter } from "../infra/rate-limiter.js";
4
+ import { ProviderCredentialError } from "./provider-credential-error.js";
4
5
  const BASE_URL = "https://api.stlouisfed.org/fred";
5
6
  export async function getSeries(seriesId, apiKey, limit = 60) {
6
7
  const cacheKey = `fred:series:${seriesId}:${limit}`;
7
8
  const cached = cache.get(cacheKey);
8
9
  if (cached)
9
10
  return cached;
10
- await rateLimiter.acquire("fred");
11
- // Fetch series metadata and observations in parallel
12
- const metaUrl = `${BASE_URL}/series?series_id=${seriesId}&api_key=${apiKey}&file_type=json`;
13
- const obsUrl = `${BASE_URL}/series/observations?series_id=${seriesId}&api_key=${apiKey}&file_type=json&sort_order=desc&limit=${limit}`;
14
- const [metaData, obsData] = await Promise.all([
15
- httpGet(metaUrl),
16
- httpGet(obsUrl),
17
- ]);
18
- const meta = metaData.seriess[0];
19
- const observations = obsData.observations
20
- .filter((o) => o.value !== ".")
21
- .map((o) => ({
22
- date: o.date,
23
- value: parseFloat(o.value),
24
- }))
25
- .reverse(); // chronological order
26
- const result = {
27
- id: meta.id,
28
- title: meta.title,
29
- observations,
30
- units: meta.units,
31
- frequency: meta.frequency,
32
- lastUpdated: meta.last_updated,
33
- };
34
- cache.set(cacheKey, result, TTL.MACRO);
35
- return result;
11
+ try {
12
+ await rateLimiter.acquire("fred");
13
+ // Fetch series metadata and observations in parallel
14
+ const metaUrl = `${BASE_URL}/series?series_id=${seriesId}&api_key=${apiKey}&file_type=json`;
15
+ const obsUrl = `${BASE_URL}/series/observations?series_id=${seriesId}&api_key=${apiKey}&file_type=json&sort_order=desc&limit=${limit}`;
16
+ const [metaData, obsData] = await Promise.all([
17
+ httpGet(metaUrl),
18
+ httpGet(obsUrl),
19
+ ]);
20
+ const meta = metaData.seriess[0];
21
+ const observations = obsData.observations
22
+ .filter((o) => o.value !== ".")
23
+ .map((o) => ({
24
+ date: o.date,
25
+ value: parseFloat(o.value),
26
+ }))
27
+ .reverse(); // chronological order
28
+ const result = {
29
+ id: meta.id,
30
+ title: meta.title,
31
+ observations,
32
+ units: meta.units,
33
+ frequency: meta.frequency,
34
+ lastUpdated: meta.last_updated,
35
+ };
36
+ cache.set(cacheKey, result, TTL.MACRO);
37
+ return result;
38
+ }
39
+ catch (error) {
40
+ // FRED historically returns 400 with a body like "Bad Request. The value
41
+ // for variable api_key is not registered..." when the key is invalid. A
42
+ // separate 400 can also indicate a bad series_id, which is NOT a credential
43
+ // problem. We only reclassify as credential-related on 401/403 here to
44
+ // avoid surfacing a `/connect` prompt when the user's real mistake is a
45
+ // typo in a series id. Bad-FRED-key users will still see the raw error
46
+ // message in v1; a body-string check can be added later if this becomes a
47
+ // reported friction.
48
+ if (error instanceof HttpError && (error.status === 401 || error.status === 403)) {
49
+ throw new ProviderCredentialError("fred", "stale", error.status);
50
+ }
51
+ const stale = cache.getStale(cacheKey, STALE_LIMIT.MACRO);
52
+ if (stale)
53
+ return stale.value;
54
+ throw error;
55
+ }
36
56
  }
37
57
  //# sourceMappingURL=fred.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"fred.js","sourceRoot":"","sources":["../../src/providers/fred.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAClD,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,MAAM,mBAAmB,CAAC;AAC/C,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AAGvD,MAAM,QAAQ,GAAG,iCAAiC,CAAC;AAmBnD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,QAAgB,EAChB,MAAc,EACd,QAAgB,EAAE;IAElB,MAAM,QAAQ,GAAG,eAAe,QAAQ,IAAI,KAAK,EAAE,CAAC;IACpD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAa,QAAQ,CAAC,CAAC;IAC/C,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,MAAM,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;IAElC,qDAAqD;IACrD,MAAM,OAAO,GAAG,GAAG,QAAQ,qBAAqB,QAAQ,YAAY,MAAM,iBAAiB,CAAC;IAC5F,MAAM,MAAM,GAAG,GAAG,QAAQ,kCAAkC,QAAQ,YAAY,MAAM,yCAAyC,KAAK,EAAE,CAAC;IAEvI,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;QAC5C,OAAO,CAAqB,OAAO,CAAC;QACpC,OAAO,CAA2B,MAAM,CAAC;KAC1C,CAAC,CAAC;IAEH,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IACjC,MAAM,YAAY,GAAsB,OAAO,CAAC,YAAY;SACzD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,GAAG,CAAC;SAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;QACX,IAAI,EAAE,CAAC,CAAC,IAAI;QACZ,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC;KAC3B,CAAC,CAAC;SACF,OAAO,EAAE,CAAC,CAAC,sBAAsB;IAEpC,MAAM,MAAM,GAAe;QACzB,EAAE,EAAE,IAAI,CAAC,EAAE;QACX,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,YAAY;QACZ,KAAK,EAAE,IAAI,CAAC,KAAK;QACjB,SAAS,EAAE,IAAI,CAAC,SAAS;QACzB,WAAW,EAAE,IAAI,CAAC,YAAY;KAC/B,CAAC;IAEF,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;IACvC,OAAO,MAAM,CAAC;AAChB,CAAC"}
1
+ {"version":3,"file":"fred.js","sourceRoot":"","sources":["../../src/providers/fred.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,OAAO,EAAE,SAAS,EAAE,MAAM,yBAAyB,CAAC;AAC7D,OAAO,EAAE,KAAK,EAAE,GAAG,EAAE,WAAW,EAAE,MAAM,mBAAmB,CAAC;AAC5D,OAAO,EAAE,WAAW,EAAE,MAAM,0BAA0B,CAAC;AACvD,OAAO,EAAE,uBAAuB,EAAE,MAAM,gCAAgC,CAAC;AAGzE,MAAM,QAAQ,GAAG,iCAAiC,CAAC;AAmBnD,MAAM,CAAC,KAAK,UAAU,SAAS,CAC7B,QAAgB,EAChB,MAAc,EACd,QAAgB,EAAE;IAElB,MAAM,QAAQ,GAAG,eAAe,QAAQ,IAAI,KAAK,EAAE,CAAC;IACpD,MAAM,MAAM,GAAG,KAAK,CAAC,GAAG,CAAa,QAAQ,CAAC,CAAC;IAC/C,IAAI,MAAM;QAAE,OAAO,MAAM,CAAC;IAE1B,IAAI,CAAC;QACH,MAAM,WAAW,CAAC,OAAO,CAAC,MAAM,CAAC,CAAC;QAElC,qDAAqD;QACrD,MAAM,OAAO,GAAG,GAAG,QAAQ,qBAAqB,QAAQ,YAAY,MAAM,iBAAiB,CAAC;QAC5F,MAAM,MAAM,GAAG,GAAG,QAAQ,kCAAkC,QAAQ,YAAY,MAAM,yCAAyC,KAAK,EAAE,CAAC;QAEvI,MAAM,CAAC,QAAQ,EAAE,OAAO,CAAC,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC;YAC5C,OAAO,CAAqB,OAAO,CAAC;YACpC,OAAO,CAA2B,MAAM,CAAC;SAC1C,CAAC,CAAC;QAEH,MAAM,IAAI,GAAG,QAAQ,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACjC,MAAM,YAAY,GAAsB,OAAO,CAAC,YAAY;aACzD,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,KAAK,GAAG,CAAC;aAC9B,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC;YACX,IAAI,EAAE,CAAC,CAAC,IAAI;YACZ,KAAK,EAAE,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC;SAC3B,CAAC,CAAC;aACF,OAAO,EAAE,CAAC,CAAC,sBAAsB;QAEpC,MAAM,MAAM,GAAe;YACzB,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,YAAY;YACZ,KAAK,EAAE,IAAI,CAAC,KAAK;YACjB,SAAS,EAAE,IAAI,CAAC,SAAS;YACzB,WAAW,EAAE,IAAI,CAAC,YAAY;SAC/B,CAAC;QAEF,KAAK,CAAC,GAAG,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,CAAC,KAAK,CAAC,CAAC;QACvC,OAAO,MAAM,CAAC;IAChB,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,yEAAyE;QACzE,wEAAwE;QACxE,4EAA4E;QAC5E,uEAAuE;QACvE,wEAAwE;QACxE,uEAAuE;QACvE,0EAA0E;QAC1E,qBAAqB;QACrB,IAAI,KAAK,YAAY,SAAS,IAAI,CAAC,KAAK,CAAC,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,MAAM,KAAK,GAAG,CAAC,EAAE,CAAC;YACjF,MAAM,IAAI,uBAAuB,CAAC,MAAM,EAAE,OAAO,EAAE,KAAK,CAAC,MAAM,CAAC,CAAC;QACnE,CAAC;QACD,MAAM,KAAK,GAAG,KAAK,CAAC,QAAQ,CAAa,QAAQ,EAAE,WAAW,CAAC,KAAK,CAAC,CAAC;QACtE,IAAI,KAAK;YAAE,OAAO,KAAK,CAAC,KAAK,CAAC;QAC9B,MAAM,KAAK,CAAC;IACd,CAAC;AACH,CAAC"}
@@ -5,3 +5,5 @@ export { getCryptoPrice, getCryptoHistory } from "./coingecko.js";
5
5
  export { getSubredditPosts, scoreSentiment } from "./reddit.js";
6
6
  export { searchFilings, type SECFiling } from "./sec-edgar.js";
7
7
  export { getFearGreedIndex } from "./fear-greed.js";
8
+ export { searchWeb, ddgSearch, braveSearch, normalizeFinancialQuery } from "./web-search.js";
9
+ export type { WebSearchOpts } from "./web-search.js";
@@ -5,4 +5,5 @@ export { getCryptoPrice, getCryptoHistory } from "./coingecko.js";
5
5
  export { getSubredditPosts, scoreSentiment } from "./reddit.js";
6
6
  export { searchFilings } from "./sec-edgar.js";
7
7
  export { getFearGreedIndex } from "./fear-greed.js";
8
+ export { searchWeb, ddgSearch, braveSearch, normalizeFinancialQuery } from "./web-search.js";
8
9
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAChI,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,aAAa,EAAkB,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/providers/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,UAAU,EAAE,eAAe,EAAE,eAAe,EAAE,aAAa,EAAE,mBAAmB,EAAE,MAAM,oBAAoB,CAAC;AAChI,OAAO,EAAE,WAAW,EAAE,WAAW,EAAE,aAAa,EAAE,MAAM,oBAAoB,CAAC;AAC7E,OAAO,EAAE,SAAS,EAAE,MAAM,WAAW,CAAC;AACtC,OAAO,EAAE,cAAc,EAAE,gBAAgB,EAAE,MAAM,gBAAgB,CAAC;AAClE,OAAO,EAAE,iBAAiB,EAAE,cAAc,EAAE,MAAM,aAAa,CAAC;AAChE,OAAO,EAAE,aAAa,EAAkB,MAAM,gBAAgB,CAAC;AAC/D,OAAO,EAAE,iBAAiB,EAAE,MAAM,iBAAiB,CAAC;AACpD,OAAO,EAAE,SAAS,EAAE,SAAS,EAAE,WAAW,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC"}
@@ -0,0 +1,8 @@
1
+ import type { ProviderId } from "../onboarding/providers.js";
2
+ export declare class ProviderCredentialError extends Error {
3
+ readonly provider: ProviderId;
4
+ readonly reason: "missing" | "stale";
5
+ readonly httpStatus?: number | undefined;
6
+ readonly name = "ProviderCredentialError";
7
+ constructor(provider: ProviderId, reason: "missing" | "stale", httpStatus?: number | undefined);
8
+ }
@@ -0,0 +1,22 @@
1
+ // Typed error thrown by providers when they detect a missing or stale credential.
2
+ //
3
+ // Providers remain pure fetchers that throw on failure — this class is the
4
+ // structured hand-off to the tool layer, where `withCredentialCheck` catches
5
+ // the error and emits a tagged tool-result content string.
6
+ //
7
+ // Keep this module dependency-free (aside from the ProviderId type) so it can
8
+ // be imported from anywhere without pulling in config loading or registry
9
+ // side effects.
10
+ export class ProviderCredentialError extends Error {
11
+ provider;
12
+ reason;
13
+ httpStatus;
14
+ name = "ProviderCredentialError";
15
+ constructor(provider, reason, httpStatus) {
16
+ super(`credential_required:${provider}:${reason}`);
17
+ this.provider = provider;
18
+ this.reason = reason;
19
+ this.httpStatus = httpStatus;
20
+ }
21
+ }
22
+ //# sourceMappingURL=provider-credential-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"provider-credential-error.js","sourceRoot":"","sources":["../../src/providers/provider-credential-error.ts"],"names":[],"mappings":"AAAA,kFAAkF;AAClF,EAAE;AACF,2EAA2E;AAC3E,6EAA6E;AAC7E,2DAA2D;AAC3D,EAAE;AACF,8EAA8E;AAC9E,0EAA0E;AAC1E,gBAAgB;AAIhB,MAAM,OAAO,uBAAwB,SAAQ,KAAK;IAIrC;IACA;IACA;IALF,IAAI,GAAG,yBAAyB,CAAC;IAE1C,YACW,QAAoB,EACpB,MAA2B,EAC3B,UAAmB;QAE5B,KAAK,CAAC,uBAAuB,QAAQ,IAAI,MAAM,EAAE,CAAC,CAAC;QAJ1C,aAAQ,GAAR,QAAQ,CAAY;QACpB,WAAM,GAAN,MAAM,CAAqB;QAC3B,eAAU,GAAV,UAAU,CAAS;IAG9B,CAAC;CACF"}
@@ -1,5 +1,13 @@
1
1
  import type { RedditSentimentResult } from "../types/sentiment.js";
2
2
  export declare function getSubredditPosts(subreddit: string, limit?: number): Promise<RedditSentimentResult>;
3
+ export interface RedditComment {
4
+ id: string;
5
+ body: string;
6
+ author: string;
7
+ score: number;
8
+ permalink: string;
9
+ }
10
+ export declare function getPostComments(subreddit: string, postId: string, limit?: number): Promise<RedditComment[]>;
3
11
  export declare function scoreSentiment(posts: Array<{
4
12
  title: string;
5
13
  }>): {