opencandle 0.2.0 → 0.4.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 (401) hide show
  1. package/README.md +110 -87
  2. package/assets/logo.svg +187 -0
  3. package/dist/analysts/orchestrator.js +1 -2
  4. package/dist/analysts/orchestrator.js.map +1 -1
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +38 -2
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts +34 -5
  9. package/dist/config.js +29 -8
  10. package/dist/config.js.map +1 -1
  11. package/dist/infra/browser.d.ts +10 -0
  12. package/dist/infra/browser.js +1 -0
  13. package/dist/infra/browser.js.map +1 -1
  14. package/dist/infra/cache.d.ts +4 -0
  15. package/dist/infra/cache.js +4 -0
  16. package/dist/infra/cache.js.map +1 -1
  17. package/dist/infra/native-dependencies.d.ts +1 -0
  18. package/dist/infra/native-dependencies.js +10 -0
  19. package/dist/infra/native-dependencies.js.map +1 -0
  20. package/dist/infra/node-version.d.ts +2 -0
  21. package/dist/infra/node-version.js +23 -0
  22. package/dist/infra/node-version.js.map +1 -0
  23. package/dist/infra/rate-limiter.js +6 -0
  24. package/dist/infra/rate-limiter.js.map +1 -1
  25. package/dist/memory/index.d.ts +2 -0
  26. package/dist/memory/index.js +1 -0
  27. package/dist/memory/index.js.map +1 -1
  28. package/dist/memory/sqlite.js +42 -4
  29. package/dist/memory/sqlite.js.map +1 -1
  30. package/dist/memory/storage.d.ts +6 -0
  31. package/dist/memory/storage.js +3 -3
  32. package/dist/memory/storage.js.map +1 -1
  33. package/dist/memory/tool-defaults.d.ts +8 -0
  34. package/dist/memory/tool-defaults.js +59 -0
  35. package/dist/memory/tool-defaults.js.map +1 -0
  36. package/dist/onboarding/connect.d.ts +35 -0
  37. package/dist/onboarding/connect.js +118 -0
  38. package/dist/onboarding/connect.js.map +1 -0
  39. package/dist/onboarding/credential-interceptor.d.ts +44 -0
  40. package/dist/onboarding/credential-interceptor.js +72 -0
  41. package/dist/onboarding/credential-interceptor.js.map +1 -0
  42. package/dist/onboarding/degradation-accumulator.d.ts +21 -0
  43. package/dist/onboarding/degradation-accumulator.js +55 -0
  44. package/dist/onboarding/degradation-accumulator.js.map +1 -0
  45. package/dist/onboarding/prompt-user.d.ts +23 -0
  46. package/dist/onboarding/prompt-user.js +61 -0
  47. package/dist/onboarding/prompt-user.js.map +1 -0
  48. package/dist/onboarding/providers.d.ts +116 -0
  49. package/dist/onboarding/providers.js +239 -0
  50. package/dist/onboarding/providers.js.map +1 -0
  51. package/dist/onboarding/state.d.ts +31 -2
  52. package/dist/onboarding/state.js +141 -13
  53. package/dist/onboarding/state.js.map +1 -1
  54. package/dist/onboarding/tool-helpers.d.ts +34 -0
  55. package/dist/onboarding/tool-helpers.js +80 -0
  56. package/dist/onboarding/tool-helpers.js.map +1 -0
  57. package/dist/onboarding/tool-tags.d.ts +37 -0
  58. package/dist/onboarding/tool-tags.js +149 -0
  59. package/dist/onboarding/tool-tags.js.map +1 -0
  60. package/dist/onboarding/validation.d.ts +19 -0
  61. package/dist/onboarding/validation.js +117 -0
  62. package/dist/onboarding/validation.js.map +1 -0
  63. package/dist/pi/opencandle-extension.d.ts +7 -1
  64. package/dist/pi/opencandle-extension.js +488 -13
  65. package/dist/pi/opencandle-extension.js.map +1 -1
  66. package/dist/pi/session-storage.d.ts +2 -0
  67. package/dist/pi/session-storage.js +5 -0
  68. package/dist/pi/session-storage.js.map +1 -0
  69. package/dist/pi/session.d.ts +4 -1
  70. package/dist/pi/session.js +25 -3
  71. package/dist/pi/session.js.map +1 -1
  72. package/dist/pi/setup.d.ts +1 -2
  73. package/dist/pi/setup.js +67 -120
  74. package/dist/pi/setup.js.map +1 -1
  75. package/dist/pi/tool-adapter.d.ts +2 -2
  76. package/dist/pi/tool-adapter.js +14 -1
  77. package/dist/pi/tool-adapter.js.map +1 -1
  78. package/dist/prompts/context-builder.d.ts +22 -0
  79. package/dist/prompts/context-builder.js +47 -11
  80. package/dist/prompts/context-builder.js.map +1 -1
  81. package/dist/prompts/disclaimer.d.ts +6 -0
  82. package/dist/prompts/disclaimer.js +9 -0
  83. package/dist/prompts/disclaimer.js.map +1 -0
  84. package/dist/prompts/workflow-prompts.d.ts +8 -0
  85. package/dist/prompts/workflow-prompts.js +39 -5
  86. package/dist/prompts/workflow-prompts.js.map +1 -1
  87. package/dist/providers/alpha-vantage.js +20 -1
  88. package/dist/providers/alpha-vantage.js.map +1 -1
  89. package/dist/providers/exa-search.d.ts +39 -0
  90. package/dist/providers/exa-search.js +276 -0
  91. package/dist/providers/exa-search.js.map +1 -0
  92. package/dist/providers/finnhub.d.ts +17 -0
  93. package/dist/providers/finnhub.js +94 -0
  94. package/dist/providers/finnhub.js.map +1 -0
  95. package/dist/providers/fred.js +13 -1
  96. package/dist/providers/fred.js.map +1 -1
  97. package/dist/providers/index.d.ts +2 -0
  98. package/dist/providers/index.js +1 -0
  99. package/dist/providers/index.js.map +1 -1
  100. package/dist/providers/provider-credential-error.d.ts +8 -0
  101. package/dist/providers/provider-credential-error.js +22 -0
  102. package/dist/providers/provider-credential-error.js.map +1 -0
  103. package/dist/providers/reddit.d.ts +8 -0
  104. package/dist/providers/reddit.js +36 -9
  105. package/dist/providers/reddit.js.map +1 -1
  106. package/dist/providers/twitter.js +2 -8
  107. package/dist/providers/twitter.js.map +1 -1
  108. package/dist/providers/web-search.d.ts +17 -0
  109. package/dist/providers/web-search.js +224 -0
  110. package/dist/providers/web-search.js.map +1 -0
  111. package/dist/providers/wrap-provider.d.ts +7 -0
  112. package/dist/providers/wrap-provider.js +15 -0
  113. package/dist/providers/wrap-provider.js.map +1 -1
  114. package/dist/providers/yahoo-finance.js +70 -33
  115. package/dist/providers/yahoo-finance.js.map +1 -1
  116. package/dist/routing/classify-intent.js +22 -0
  117. package/dist/routing/classify-intent.js.map +1 -1
  118. package/dist/routing/defaults.js +1 -1
  119. package/dist/routing/defaults.js.map +1 -1
  120. package/dist/routing/index.d.ts +4 -0
  121. package/dist/routing/index.js +3 -0
  122. package/dist/routing/index.js.map +1 -1
  123. package/dist/routing/router-llm-client.d.ts +11 -0
  124. package/dist/routing/router-llm-client.js +42 -0
  125. package/dist/routing/router-llm-client.js.map +1 -0
  126. package/dist/routing/router-prompt.d.ts +2 -0
  127. package/dist/routing/router-prompt.js +138 -0
  128. package/dist/routing/router-prompt.js.map +1 -0
  129. package/dist/routing/router-types.d.ts +62 -0
  130. package/dist/routing/router-types.js +2 -0
  131. package/dist/routing/router-types.js.map +1 -0
  132. package/dist/routing/router.d.ts +10 -0
  133. package/dist/routing/router.js +194 -0
  134. package/dist/routing/router.js.map +1 -0
  135. package/dist/runtime/session-coordinator.d.ts +63 -4
  136. package/dist/runtime/session-coordinator.js +155 -4
  137. package/dist/runtime/session-coordinator.js.map +1 -1
  138. package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
  139. package/dist/runtime/tool-defaults-wrapper.js +25 -0
  140. package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
  141. package/dist/sentiment/adapters/finnhub.d.ts +7 -0
  142. package/dist/sentiment/adapters/finnhub.js +39 -0
  143. package/dist/sentiment/adapters/finnhub.js.map +1 -0
  144. package/dist/sentiment/adapters/reddit.d.ts +11 -0
  145. package/dist/sentiment/adapters/reddit.js +54 -0
  146. package/dist/sentiment/adapters/reddit.js.map +1 -0
  147. package/dist/sentiment/adapters/twitter.d.ts +9 -0
  148. package/dist/sentiment/adapters/twitter.js +32 -0
  149. package/dist/sentiment/adapters/twitter.js.map +1 -0
  150. package/dist/sentiment/adapters/web.d.ts +9 -0
  151. package/dist/sentiment/adapters/web.js +40 -0
  152. package/dist/sentiment/adapters/web.js.map +1 -0
  153. package/dist/sentiment/index.d.ts +16 -0
  154. package/dist/sentiment/index.js +44 -0
  155. package/dist/sentiment/index.js.map +1 -0
  156. package/dist/sentiment/keywords.d.ts +2 -0
  157. package/dist/sentiment/keywords.js +9 -0
  158. package/dist/sentiment/keywords.js.map +1 -0
  159. package/dist/sentiment/pipeline.d.ts +9 -0
  160. package/dist/sentiment/pipeline.js +57 -0
  161. package/dist/sentiment/pipeline.js.map +1 -0
  162. package/dist/sentiment/scorer.d.ts +9 -0
  163. package/dist/sentiment/scorer.js +64 -0
  164. package/dist/sentiment/scorer.js.map +1 -0
  165. package/dist/sentiment/store.d.ts +24 -0
  166. package/dist/sentiment/store.js +182 -0
  167. package/dist/sentiment/store.js.map +1 -0
  168. package/dist/sentiment/trends.d.ts +13 -0
  169. package/dist/sentiment/trends.js +73 -0
  170. package/dist/sentiment/trends.js.map +1 -0
  171. package/dist/sentiment/types.d.ts +66 -0
  172. package/dist/sentiment/types.js +54 -0
  173. package/dist/sentiment/types.js.map +1 -0
  174. package/dist/system-prompt.js +29 -13
  175. package/dist/system-prompt.js.map +1 -1
  176. package/dist/tool-kit.d.ts +4 -4
  177. package/dist/tools/fundamentals/company-overview.d.ts +4 -2
  178. package/dist/tools/fundamentals/company-overview.js +27 -27
  179. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  180. package/dist/tools/fundamentals/comps.d.ts +1 -1
  181. package/dist/tools/fundamentals/comps.js +45 -45
  182. package/dist/tools/fundamentals/comps.js.map +1 -1
  183. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  184. package/dist/tools/fundamentals/dcf.js +82 -82
  185. package/dist/tools/fundamentals/dcf.js.map +1 -1
  186. package/dist/tools/fundamentals/earnings.d.ts +4 -2
  187. package/dist/tools/fundamentals/earnings.js +25 -25
  188. package/dist/tools/fundamentals/earnings.js.map +1 -1
  189. package/dist/tools/fundamentals/financials.d.ts +4 -2
  190. package/dist/tools/fundamentals/financials.js +23 -23
  191. package/dist/tools/fundamentals/financials.js.map +1 -1
  192. package/dist/tools/fundamentals/sec-filings.d.ts +1 -1
  193. package/dist/tools/index.d.ts +28 -1
  194. package/dist/tools/index.js +35 -2
  195. package/dist/tools/index.js.map +1 -1
  196. package/dist/tools/interaction/ask-user.d.ts +1 -1
  197. package/dist/tools/interaction/ask-user.js +28 -64
  198. package/dist/tools/interaction/ask-user.js.map +1 -1
  199. package/dist/tools/interaction/twitter-login.d.ts +1 -1
  200. package/dist/tools/macro/fear-greed.d.ts +1 -1
  201. package/dist/tools/macro/fred-data.d.ts +4 -2
  202. package/dist/tools/macro/fred-data.js +26 -26
  203. package/dist/tools/macro/fred-data.js.map +1 -1
  204. package/dist/tools/market/crypto-history.d.ts +1 -1
  205. package/dist/tools/market/crypto-price.d.ts +1 -1
  206. package/dist/tools/market/search-ticker.d.ts +1 -1
  207. package/dist/tools/market/stock-history.d.ts +1 -1
  208. package/dist/tools/market/stock-quote.d.ts +1 -1
  209. package/dist/tools/options/option-chain.d.ts +1 -1
  210. package/dist/tools/options/option-chain.js +4 -1
  211. package/dist/tools/options/option-chain.js.map +1 -1
  212. package/dist/tools/portfolio/correlation.d.ts +1 -1
  213. package/dist/tools/portfolio/predictions.d.ts +1 -1
  214. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  215. package/dist/tools/portfolio/tracker.d.ts +1 -1
  216. package/dist/tools/portfolio/watchlist.d.ts +1 -1
  217. package/dist/tools/sentiment/reddit-sentiment.d.ts +4 -2
  218. package/dist/tools/sentiment/reddit-sentiment.js +107 -22
  219. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  220. package/dist/tools/sentiment/sentiment-summary.d.ts +7 -0
  221. package/dist/tools/sentiment/sentiment-summary.js +230 -0
  222. package/dist/tools/sentiment/sentiment-summary.js.map +1 -0
  223. package/dist/tools/sentiment/sentiment-trend.d.ts +22 -0
  224. package/dist/tools/sentiment/sentiment-trend.js +39 -0
  225. package/dist/tools/sentiment/sentiment-trend.js.map +1 -0
  226. package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
  227. package/dist/tools/sentiment/twitter-sentiment.js +17 -0
  228. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  229. package/dist/tools/sentiment/web-search.d.ts +11 -0
  230. package/dist/tools/sentiment/web-search.js +115 -0
  231. package/dist/tools/sentiment/web-search.js.map +1 -0
  232. package/dist/tools/sentiment/web-sentiment.d.ts +8 -0
  233. package/dist/tools/sentiment/web-sentiment.js +66 -0
  234. package/dist/tools/sentiment/web-sentiment.js.map +1 -0
  235. package/dist/tools/technical/backtest.d.ts +1 -1
  236. package/dist/tools/technical/indicators.d.ts +1 -1
  237. package/dist/tools/technical/indicators.js +7 -1
  238. package/dist/tools/technical/indicators.js.map +1 -1
  239. package/dist/types/index.d.ts +1 -1
  240. package/dist/types/sentiment.d.ts +21 -0
  241. package/dist/workflows/options-screener.js +7 -2
  242. package/dist/workflows/options-screener.js.map +1 -1
  243. package/dist/workflows/portfolio-builder.js +3 -3
  244. package/dist/workflows/portfolio-builder.js.map +1 -1
  245. package/gui/server/background-quotes.ts +31 -0
  246. package/gui/server/chat-event-adapter.ts +142 -0
  247. package/gui/server/invoke-tool.ts +89 -0
  248. package/gui/server/live-chat-event-adapter.ts +181 -0
  249. package/gui/server/model-setup.ts +100 -0
  250. package/gui/server/package.json +5 -0
  251. package/gui/server/projector.ts +212 -0
  252. package/gui/server/server.ts +592 -0
  253. package/gui/server/session-actions.ts +31 -0
  254. package/gui/server/tool-metadata.ts +88 -0
  255. package/gui/server/websocket.ts +128 -0
  256. package/gui/server/writer-lock.ts +118 -0
  257. package/gui/shared/chat-events.ts +118 -0
  258. package/gui/shared/event-reducer.ts +186 -0
  259. package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +1 -0
  260. package/gui/web/dist/assets/index-DBrWq43L.css +1 -0
  261. package/gui/web/dist/assets/index-RflHaj0y.js +67 -0
  262. package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
  263. package/gui/web/dist/index.html +17 -0
  264. package/package.json +62 -20
  265. package/src/analysts/contracts.ts +189 -0
  266. package/src/analysts/orchestrator.ts +300 -0
  267. package/src/cli.ts +205 -0
  268. package/src/config.ts +161 -0
  269. package/src/index.ts +5 -0
  270. package/src/infra/browser.ts +111 -0
  271. package/src/infra/cache.ts +103 -0
  272. package/src/infra/http-client.ts +68 -0
  273. package/src/infra/index.ts +18 -0
  274. package/src/infra/native-dependencies.ts +12 -0
  275. package/src/infra/node-version.ts +24 -0
  276. package/src/infra/open-url.ts +28 -0
  277. package/src/infra/opencandle-paths.ts +64 -0
  278. package/src/infra/rate-limiter.ts +64 -0
  279. package/src/memory/index.ts +10 -0
  280. package/src/memory/manager.ts +159 -0
  281. package/src/memory/preference-extractor.ts +106 -0
  282. package/src/memory/retrieval.ts +70 -0
  283. package/src/memory/sqlite.ts +172 -0
  284. package/src/memory/storage.ts +204 -0
  285. package/src/memory/tool-defaults.ts +87 -0
  286. package/src/memory/types.ts +67 -0
  287. package/src/onboarding/connect.ts +184 -0
  288. package/src/onboarding/credential-interceptor.ts +134 -0
  289. package/src/onboarding/degradation-accumulator.ts +79 -0
  290. package/src/onboarding/prompt-user.ts +85 -0
  291. package/src/onboarding/providers.ts +315 -0
  292. package/src/onboarding/state.ts +218 -0
  293. package/src/onboarding/tool-helpers.ts +111 -0
  294. package/src/onboarding/tool-tags.ts +201 -0
  295. package/src/onboarding/validation.ts +158 -0
  296. package/src/pi/opencandle-extension.ts +724 -0
  297. package/src/pi/session-storage.ts +5 -0
  298. package/src/pi/session.ts +81 -0
  299. package/src/pi/setup.ts +371 -0
  300. package/src/pi/tool-adapter.ts +36 -0
  301. package/src/prompts/context-builder.ts +204 -0
  302. package/src/prompts/disclaimer.ts +9 -0
  303. package/src/prompts/sections.ts +46 -0
  304. package/src/prompts/workflow-prompts.ts +279 -0
  305. package/src/providers/alpha-vantage.ts +292 -0
  306. package/src/providers/coingecko.ts +96 -0
  307. package/src/providers/exa-search.ts +373 -0
  308. package/src/providers/fear-greed.ts +45 -0
  309. package/src/providers/finnhub.ts +124 -0
  310. package/src/providers/fred.ts +83 -0
  311. package/src/providers/index.ts +9 -0
  312. package/src/providers/provider-credential-error.ts +23 -0
  313. package/src/providers/reddit.ts +151 -0
  314. package/src/providers/sec-edgar.ts +96 -0
  315. package/src/providers/twitter.ts +173 -0
  316. package/src/providers/web-search.ts +293 -0
  317. package/src/providers/with-fallback.ts +41 -0
  318. package/src/providers/wrap-provider.ts +64 -0
  319. package/src/providers/yahoo-finance.ts +367 -0
  320. package/src/routing/classify-intent.ts +194 -0
  321. package/src/routing/defaults.ts +29 -0
  322. package/src/routing/entity-extractor.ts +140 -0
  323. package/src/routing/index.ts +26 -0
  324. package/src/routing/router-llm-client.ts +51 -0
  325. package/src/routing/router-prompt.ts +159 -0
  326. package/src/routing/router-types.ts +66 -0
  327. package/src/routing/router.ts +213 -0
  328. package/src/routing/slot-resolver.ts +152 -0
  329. package/src/routing/types.ts +63 -0
  330. package/src/runtime/evidence.ts +77 -0
  331. package/src/runtime/index.ts +55 -0
  332. package/src/runtime/prompt-step.ts +75 -0
  333. package/src/runtime/provider-ids.ts +15 -0
  334. package/src/runtime/provider-tracker.ts +40 -0
  335. package/src/runtime/run-context.ts +22 -0
  336. package/src/runtime/session-coordinator.ts +406 -0
  337. package/src/runtime/tool-defaults-wrapper.ts +35 -0
  338. package/src/runtime/validation.ts +214 -0
  339. package/src/runtime/workflow-events.ts +75 -0
  340. package/src/runtime/workflow-runner.ts +188 -0
  341. package/src/runtime/workflow-types.ts +102 -0
  342. package/src/sentiment/adapters/finnhub.ts +44 -0
  343. package/src/sentiment/adapters/reddit.ts +65 -0
  344. package/src/sentiment/adapters/twitter.ts +36 -0
  345. package/src/sentiment/adapters/web.ts +44 -0
  346. package/src/sentiment/index.ts +58 -0
  347. package/src/sentiment/keywords.ts +9 -0
  348. package/src/sentiment/pipeline.ts +68 -0
  349. package/src/sentiment/scorer.ts +78 -0
  350. package/src/sentiment/store.ts +260 -0
  351. package/src/sentiment/trends.ts +90 -0
  352. package/src/sentiment/types.ts +108 -0
  353. package/src/system-prompt.ts +115 -0
  354. package/src/tool-kit.ts +68 -0
  355. package/src/tools/AGENTS.md +36 -0
  356. package/src/tools/fundamentals/company-overview.ts +54 -0
  357. package/src/tools/fundamentals/comps.ts +156 -0
  358. package/src/tools/fundamentals/dcf.ts +267 -0
  359. package/src/tools/fundamentals/earnings.ts +47 -0
  360. package/src/tools/fundamentals/financials.ts +54 -0
  361. package/src/tools/fundamentals/sec-filings.ts +61 -0
  362. package/src/tools/index.ts +88 -0
  363. package/src/tools/interaction/ask-user.ts +81 -0
  364. package/src/tools/interaction/twitter-login.ts +93 -0
  365. package/src/tools/macro/fear-greed.ts +41 -0
  366. package/src/tools/macro/fred-data.ts +54 -0
  367. package/src/tools/market/crypto-history.ts +51 -0
  368. package/src/tools/market/crypto-price.ts +53 -0
  369. package/src/tools/market/search-ticker.ts +53 -0
  370. package/src/tools/market/stock-history.ts +79 -0
  371. package/src/tools/market/stock-quote.ts +64 -0
  372. package/src/tools/options/greeks.ts +82 -0
  373. package/src/tools/options/option-chain.ts +91 -0
  374. package/src/tools/portfolio/correlation.ts +162 -0
  375. package/src/tools/portfolio/predictions.ts +253 -0
  376. package/src/tools/portfolio/risk-analysis.ts +134 -0
  377. package/src/tools/portfolio/tracker.ts +147 -0
  378. package/src/tools/portfolio/watchlist.ts +153 -0
  379. package/src/tools/sentiment/reddit-sentiment.ts +164 -0
  380. package/src/tools/sentiment/sentiment-summary.ts +256 -0
  381. package/src/tools/sentiment/sentiment-trend.ts +58 -0
  382. package/src/tools/sentiment/twitter-sentiment.ts +96 -0
  383. package/src/tools/sentiment/web-search.ts +150 -0
  384. package/src/tools/sentiment/web-sentiment.ts +76 -0
  385. package/src/tools/technical/backtest.ts +246 -0
  386. package/src/tools/technical/indicators.ts +258 -0
  387. package/src/types/fundamentals.ts +46 -0
  388. package/src/types/index.ts +20 -0
  389. package/src/types/macro.ts +27 -0
  390. package/src/types/market.ts +43 -0
  391. package/src/types/options.ts +35 -0
  392. package/src/types/portfolio.ts +41 -0
  393. package/src/types/sentiment.ts +70 -0
  394. package/src/workflows/compare-assets.ts +39 -0
  395. package/src/workflows/index.ts +4 -0
  396. package/src/workflows/options-screener.ts +49 -0
  397. package/src/workflows/portfolio-builder.ts +52 -0
  398. package/src/workflows/types.ts +4 -0
  399. package/dist/tools/sentiment/news-sentiment.d.ts +0 -7
  400. package/dist/tools/sentiment/news-sentiment.js +0 -55
  401. package/dist/tools/sentiment/news-sentiment.js.map +0 -1
@@ -0,0 +1,293 @@
1
+ import { search, searchNews, SafeSearchType, SearchTimeType } from "duck-duck-scrape";
2
+ import type { SearchResult, SearchResults } from "duck-duck-scrape";
3
+ import type { NewsResult } from "duck-duck-scrape";
4
+ import { httpGet, HttpError } from "../infra/http-client.js";
5
+ import { cache, TTL, STALE_LIMIT } from "../infra/cache.js";
6
+ import { rateLimiter } from "../infra/rate-limiter.js";
7
+ import { getConfig } from "../config.js";
8
+ import { withFallback } from "./with-fallback.js";
9
+ import { exaSearch } from "./exa-search.js";
10
+ import { ProviderCredentialError } from "./provider-credential-error.js";
11
+ import type { ProviderResult } from "../runtime/evidence.js";
12
+ import type { WebSearchResult, WebSearchEnvelope } from "../types/sentiment.js";
13
+
14
+ export interface WebSearchOpts {
15
+ category: "news" | "general";
16
+ freshness: "hours" | "day" | "week" | "month";
17
+ limit: number;
18
+ /** Override provider: skip cascade, use this provider only. */
19
+ provider?: "exa" | "brave" | "ddg";
20
+ }
21
+
22
+ const BARE_TICKER = /^[A-Z]{1,5}$/;
23
+ const CASHTAG = /^\$[A-Z]{1,5}$/;
24
+
25
+ /**
26
+ * Normalize queries for financial context.
27
+ * Only applied when category is "news" — general queries pass through unchanged.
28
+ */
29
+ export function normalizeFinancialQuery(query: string, category: "news" | "general"): string {
30
+ if (category !== "news") return query;
31
+ if (CASHTAG.test(query)) return `${query.slice(1)} stock news`;
32
+ if (BARE_TICKER.test(query)) return `${query} stock news`;
33
+ return query;
34
+ }
35
+
36
+ function stripHtmlTags(text: string): string {
37
+ return text.replace(/<[^>]*>/g, "");
38
+ }
39
+
40
+ function mapFreshness(freshness: WebSearchOpts["freshness"]): SearchTimeType {
41
+ switch (freshness) {
42
+ case "hours":
43
+ return SearchTimeType.DAY; // closest available
44
+ case "day":
45
+ return SearchTimeType.DAY;
46
+ case "week":
47
+ return SearchTimeType.WEEK;
48
+ case "month":
49
+ return SearchTimeType.MONTH;
50
+ }
51
+ }
52
+
53
+ function extractDomain(url: string): string {
54
+ try {
55
+ const hostname = new URL(url).hostname;
56
+ return hostname.replace(/^www\./, "");
57
+ } catch {
58
+ return url;
59
+ }
60
+ }
61
+
62
+ function mapGeneralResult(r: SearchResult): WebSearchResult {
63
+ return {
64
+ title: r.title,
65
+ url: r.url,
66
+ snippet: stripHtmlTags(r.description),
67
+ source: r.hostname || extractDomain(r.url),
68
+ published: null,
69
+ category: "general",
70
+ };
71
+ }
72
+
73
+ function mapNewsResult(r: NewsResult): WebSearchResult {
74
+ return {
75
+ title: r.title,
76
+ url: r.url,
77
+ snippet: r.excerpt,
78
+ source: r.syndicate || extractDomain(r.url),
79
+ published: r.date ? new Date(r.date * 1000).toISOString() : null,
80
+ category: "news",
81
+ };
82
+ }
83
+
84
+ function ddgCacheKey(query: string, opts: WebSearchOpts): string {
85
+ return `web:ddg:${query}:${opts.category}:${opts.freshness}:${opts.limit}`;
86
+ }
87
+
88
+ export async function ddgSearch(
89
+ query: string,
90
+ opts: WebSearchOpts,
91
+ ): Promise<WebSearchEnvelope> {
92
+ const key = ddgCacheKey(query, opts);
93
+ const cached = cache.get<WebSearchEnvelope>(key);
94
+ if (cached) return cached;
95
+
96
+ try {
97
+ await rateLimiter.acquire("ddg");
98
+
99
+ let results: WebSearchResult[];
100
+
101
+ if (opts.category === "news") {
102
+ const response = await searchNews(
103
+ query,
104
+ { time: mapFreshness(opts.freshness), safeSearch: SafeSearchType.STRICT },
105
+ undefined,
106
+ );
107
+ results = (response.results || []).slice(0, opts.limit).map(mapNewsResult);
108
+ } else {
109
+ const response = await search(
110
+ query,
111
+ { time: mapFreshness(opts.freshness), safeSearch: SafeSearchType.STRICT },
112
+ undefined,
113
+ );
114
+ results = (response.results || []).slice(0, opts.limit).map(mapGeneralResult);
115
+ }
116
+
117
+ const envelope: WebSearchEnvelope = {
118
+ query,
119
+ results,
120
+ resultCount: results.length,
121
+ fetchedAt: new Date().toISOString(),
122
+ provider: "ddg",
123
+ };
124
+
125
+ cache.set(key, envelope, TTL.WEB_SEARCH);
126
+ return envelope;
127
+ } catch (error) {
128
+ const stale = cache.getStale<WebSearchEnvelope>(key, STALE_LIMIT.WEB_SEARCH);
129
+ if (stale) return stale.value;
130
+ throw error;
131
+ }
132
+ }
133
+
134
+ // ---------------------------------------------------------------------------
135
+ // Brave Search
136
+ // ---------------------------------------------------------------------------
137
+
138
+ const BRAVE_BASE = "https://api.search.brave.com/res/v1";
139
+
140
+ function mapBraveFreshness(freshness: WebSearchOpts["freshness"]): string {
141
+ switch (freshness) {
142
+ case "hours": return "ph";
143
+ case "day": return "pd";
144
+ case "week": return "pw";
145
+ case "month": return "pm";
146
+ }
147
+ }
148
+
149
+ interface BraveNewsResult {
150
+ title: string;
151
+ url: string;
152
+ description: string;
153
+ age?: string;
154
+ source?: string;
155
+ meta_url?: { hostname: string };
156
+ }
157
+
158
+ interface BraveWebResult {
159
+ title: string;
160
+ url: string;
161
+ description: string;
162
+ age?: string | null;
163
+ meta_url?: { hostname: string };
164
+ }
165
+
166
+ function mapBraveNewsResult(r: BraveNewsResult): WebSearchResult {
167
+ return {
168
+ title: r.title,
169
+ url: r.url,
170
+ snippet: r.description,
171
+ source: r.source || r.meta_url?.hostname || extractDomain(r.url),
172
+ published: null, // Brave news returns relative "age" not absolute timestamps
173
+ category: "news",
174
+ };
175
+ }
176
+
177
+ function mapBraveWebResult(r: BraveWebResult): WebSearchResult {
178
+ return {
179
+ title: r.title,
180
+ url: r.url,
181
+ snippet: r.description,
182
+ source: r.meta_url?.hostname || extractDomain(r.url),
183
+ published: null,
184
+ category: "general",
185
+ };
186
+ }
187
+
188
+ function braveCacheKey(query: string, opts: WebSearchOpts): string {
189
+ return `web:brave:${query}:${opts.category}:${opts.freshness}:${opts.limit}`;
190
+ }
191
+
192
+ export async function braveSearch(
193
+ query: string,
194
+ opts: WebSearchOpts,
195
+ apiKey: string,
196
+ ): Promise<WebSearchEnvelope> {
197
+ const key = braveCacheKey(query, opts);
198
+ const cached = cache.get<WebSearchEnvelope>(key);
199
+ if (cached) return cached;
200
+
201
+ try {
202
+ await rateLimiter.acquire("brave_search");
203
+
204
+ const endpoint = opts.category === "news" ? "news/search" : "web/search";
205
+ const freshness = mapBraveFreshness(opts.freshness);
206
+ const url = `${BRAVE_BASE}/${endpoint}?q=${encodeURIComponent(query)}&count=${opts.limit}&freshness=${freshness}`;
207
+
208
+ const data = await httpGet<Record<string, unknown>>(url, {
209
+ headers: {
210
+ "X-Subscription-Token": apiKey,
211
+ Accept: "application/json",
212
+ },
213
+ });
214
+
215
+ let results: WebSearchResult[];
216
+ if (opts.category === "news") {
217
+ const newsResults = ((data as any).results || []) as BraveNewsResult[];
218
+ results = newsResults.slice(0, opts.limit).map(mapBraveNewsResult);
219
+ } else {
220
+ const webResults = ((data as any).web?.results || []) as BraveWebResult[];
221
+ results = webResults.slice(0, opts.limit).map(mapBraveWebResult);
222
+ }
223
+
224
+ const envelope: WebSearchEnvelope = {
225
+ query,
226
+ results,
227
+ resultCount: results.length,
228
+ fetchedAt: new Date().toISOString(),
229
+ provider: "brave",
230
+ };
231
+
232
+ cache.set(key, envelope, TTL.WEB_SEARCH);
233
+ return envelope;
234
+ } catch (error: any) {
235
+ const status = error instanceof HttpError ? error.status : error?.statusCode;
236
+ if (status === 401 || status === 403) {
237
+ throw new ProviderCredentialError("brave", "stale", status);
238
+ }
239
+
240
+ const stale = cache.getStale<WebSearchEnvelope>(key, STALE_LIMIT.WEB_SEARCH);
241
+ if (stale) return stale.value;
242
+ throw error;
243
+ }
244
+ }
245
+
246
+ // ---------------------------------------------------------------------------
247
+ // Cascade orchestrator
248
+ // ---------------------------------------------------------------------------
249
+
250
+ const DEFAULT_OPTS: WebSearchOpts = {
251
+ category: "news",
252
+ freshness: "day",
253
+ limit: 10,
254
+ };
255
+
256
+ export async function searchWeb(
257
+ query: string,
258
+ opts?: Partial<WebSearchOpts>,
259
+ ): Promise<ProviderResult<WebSearchEnvelope>> {
260
+ const resolved: WebSearchOpts = { ...DEFAULT_OPTS, ...opts };
261
+ const normalized = normalizeFinancialQuery(query, resolved.category);
262
+ const config = getConfig();
263
+
264
+ const entries: Array<{ provider: string; fn: () => Promise<WebSearchEnvelope> }> = [];
265
+
266
+ // Provider override: skip cascade, use only the specified provider
267
+ if (resolved.provider) {
268
+ switch (resolved.provider) {
269
+ case "exa":
270
+ entries.push({ provider: "exa", fn: () => exaSearch(normalized, resolved) });
271
+ break;
272
+ case "brave":
273
+ if (!config.braveApiKey) {
274
+ return { status: "unavailable", reason: "BRAVE_API_KEY not configured", provider: "brave_search" };
275
+ }
276
+ entries.push({ provider: "brave_search", fn: () => braveSearch(normalized, resolved, config.braveApiKey!) });
277
+ break;
278
+ case "ddg":
279
+ entries.push({ provider: "ddg", fn: () => ddgSearch(normalized, resolved) });
280
+ break;
281
+ }
282
+ return withFallback<WebSearchEnvelope>(entries);
283
+ }
284
+
285
+ // Default cascade: Exa → Brave → DDG
286
+ entries.push({ provider: "exa", fn: () => exaSearch(normalized, resolved) });
287
+ if (config.braveApiKey) {
288
+ entries.push({ provider: "brave_search", fn: () => braveSearch(normalized, resolved, config.braveApiKey!) });
289
+ }
290
+ entries.push({ provider: "ddg", fn: () => ddgSearch(normalized, resolved) });
291
+
292
+ return withFallback<WebSearchEnvelope>(entries);
293
+ }
@@ -0,0 +1,41 @@
1
+ import type { ProviderResult } from "../runtime/evidence.js";
2
+ import { wrapProvider } from "./wrap-provider.js";
3
+ import { getProviderTracker } from "../runtime/run-context.js";
4
+
5
+ export interface FallbackEntry<T> {
6
+ provider: string;
7
+ fn: () => Promise<T>;
8
+ }
9
+
10
+ /**
11
+ * Try providers in order, stopping at the first success.
12
+ * Skips circuit-open providers. Each provider call goes through wrapProvider
13
+ * (circuit check + failure recording + stale flag propagation).
14
+ *
15
+ * Stale cache fallback is NOT managed here — it happens inside each
16
+ * provider function (Level B). By the time a provider throws to wrapProvider,
17
+ * it already tried its own stale cache.
18
+ */
19
+ export async function withFallback<T>(
20
+ entries: FallbackEntry<T>[],
21
+ ): Promise<ProviderResult<T>> {
22
+ const tracker = getProviderTracker();
23
+ const attempted: string[] = [];
24
+
25
+ for (const entry of entries) {
26
+ if (tracker?.isCircuitOpen(entry.provider)) continue;
27
+ attempted.push(entry.provider);
28
+
29
+ const result = await wrapProvider(entry.provider, entry.fn);
30
+ if (result.status === "ok") return result;
31
+ // wrapProvider already called recordFailure on the tracker
32
+ }
33
+
34
+ return {
35
+ status: "unavailable",
36
+ reason: attempted.length > 0
37
+ ? `all providers failed: ${attempted.join(", ")}`
38
+ : `all providers circuit-open: ${entries.map((e) => e.provider).join(", ")}`,
39
+ provider: entries[0]?.provider ?? "unknown",
40
+ };
41
+ }
@@ -0,0 +1,64 @@
1
+ import type { ProviderResult } from "../runtime/evidence.js";
2
+ import { getProviderTracker } from "../runtime/run-context.js";
3
+ import { cache } from "../infra/cache.js";
4
+ import { ProviderCredentialError } from "./provider-credential-error.js";
5
+
6
+ /**
7
+ * Wrap a provider function call so that thrown exceptions are caught
8
+ * and returned as a structured `ProviderResultUnavailable`.
9
+ *
10
+ * `ProviderCredentialError` is the one exception: it is re-thrown
11
+ * unchanged so the tool layer can catch it and emit a tagged
12
+ * `[OPENCANDLE_CREDENTIAL_REQUIRED ...]` tool-result content block
13
+ * (see `src/onboarding/tool-tags.ts`). Swallowing credential errors
14
+ * into `unavailable` would hide the just-in-time setup offer from
15
+ * the `tool_result` interception handler.
16
+ *
17
+ * When a run context is active, checks circuit breaker state before
18
+ * calling and records failures after.
19
+ *
20
+ * After a successful provider call, checks if the cache's stale flag
21
+ * was set (meaning the provider fell back to stale cached data internally)
22
+ * and propagates that metadata on the result.
23
+ */
24
+ export async function wrapProvider<T>(
25
+ providerId: string,
26
+ fn: () => Promise<T>,
27
+ ): Promise<ProviderResult<T>> {
28
+ const tracker = getProviderTracker();
29
+
30
+ if (tracker?.isCircuitOpen(providerId)) {
31
+ return {
32
+ status: "unavailable",
33
+ reason: "provider_circuit_open",
34
+ provider: providerId,
35
+ };
36
+ }
37
+
38
+ try {
39
+ const data = await fn();
40
+ const { stale, cachedAt } = cache.consumeStaleFlag();
41
+ return {
42
+ status: "ok",
43
+ data,
44
+ timestamp: stale ? new Date(cachedAt).toISOString() : new Date().toISOString(),
45
+ stale: stale || undefined,
46
+ };
47
+ } catch (error) {
48
+ // Credential errors are re-thrown so the tool-layer `withCredentialCheck`
49
+ // helper can convert them into LLM-visible tagged content. Do NOT record
50
+ // these as tracker failures — they are a user-config problem, not a
51
+ // provider reliability problem.
52
+ if (error instanceof ProviderCredentialError) {
53
+ throw error;
54
+ }
55
+ tracker?.recordFailure(providerId);
56
+ const reason =
57
+ error instanceof Error ? error.message : "unknown_error";
58
+ return {
59
+ status: "unavailable",
60
+ reason,
61
+ provider: providerId,
62
+ };
63
+ }
64
+ }