opencandle 0.5.0 → 0.7.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 (574) hide show
  1. package/README.md +170 -186
  2. package/dist/analysts/contracts.d.ts +1 -3
  3. package/dist/analysts/contracts.js +1 -11
  4. package/dist/analysts/contracts.js.map +1 -1
  5. package/dist/analysts/orchestrator.d.ts +1 -3
  6. package/dist/analysts/orchestrator.js +1 -26
  7. package/dist/analysts/orchestrator.js.map +1 -1
  8. package/dist/cli.js +66 -7
  9. package/dist/cli.js.map +1 -1
  10. package/dist/config.d.ts +13 -3
  11. package/dist/config.js +25 -5
  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 +8 -11
  17. package/dist/infra/cache.js +17 -15
  18. package/dist/infra/cache.js.map +1 -1
  19. package/dist/infra/http-client.d.ts +4 -1
  20. package/dist/infra/http-client.js +59 -6
  21. package/dist/infra/http-client.js.map +1 -1
  22. package/dist/infra/index.d.ts +2 -3
  23. package/dist/infra/index.js +2 -3
  24. package/dist/infra/index.js.map +1 -1
  25. package/dist/infra/native-dependencies.js +2 -2
  26. package/dist/infra/native-dependencies.js.map +1 -1
  27. package/dist/infra/node-version.js.map +1 -1
  28. package/dist/infra/opencandle-paths.d.ts +0 -3
  29. package/dist/infra/opencandle-paths.js +4 -11
  30. package/dist/infra/opencandle-paths.js.map +1 -1
  31. package/dist/infra/rate-limiter.js +12 -9
  32. package/dist/infra/rate-limiter.js.map +1 -1
  33. package/dist/market-state/alert-conditions.d.ts +34 -0
  34. package/dist/market-state/alert-conditions.js +23 -0
  35. package/dist/market-state/alert-conditions.js.map +1 -0
  36. package/dist/market-state/alert-runner.d.ts +55 -0
  37. package/dist/market-state/alert-runner.js +634 -0
  38. package/dist/market-state/alert-runner.js.map +1 -0
  39. package/dist/market-state/daily-report.d.ts +26 -0
  40. package/dist/market-state/daily-report.js +179 -0
  41. package/dist/market-state/daily-report.js.map +1 -0
  42. package/dist/market-state/local-automation-service.d.ts +25 -0
  43. package/dist/market-state/local-automation-service.js +119 -0
  44. package/dist/market-state/local-automation-service.js.map +1 -0
  45. package/dist/market-state/notification-delivery.d.ts +14 -0
  46. package/dist/market-state/notification-delivery.js +139 -0
  47. package/dist/market-state/notification-delivery.js.map +1 -0
  48. package/dist/market-state/resolve-for-mutation.d.ts +10 -0
  49. package/dist/market-state/resolve-for-mutation.js +15 -0
  50. package/dist/market-state/resolve-for-mutation.js.map +1 -0
  51. package/dist/market-state/resolve.d.ts +14 -0
  52. package/dist/market-state/resolve.js +89 -0
  53. package/dist/market-state/resolve.js.map +1 -0
  54. package/dist/market-state/service.d.ts +527 -0
  55. package/dist/market-state/service.js +1099 -0
  56. package/dist/market-state/service.js.map +1 -0
  57. package/dist/memory/index.d.ts +7 -7
  58. package/dist/memory/index.js +6 -6
  59. package/dist/memory/index.js.map +1 -1
  60. package/dist/memory/manager.js +11 -11
  61. package/dist/memory/manager.js.map +1 -1
  62. package/dist/memory/retrieval.js +7 -4
  63. package/dist/memory/retrieval.js.map +1 -1
  64. package/dist/memory/sqlite.js +385 -3
  65. package/dist/memory/sqlite.js.map +1 -1
  66. package/dist/memory/storage.js +1 -2
  67. package/dist/memory/storage.js.map +1 -1
  68. package/dist/memory/tool-defaults.js +64 -28
  69. package/dist/memory/tool-defaults.js.map +1 -1
  70. package/dist/memory/types.js.map +1 -1
  71. package/dist/monitor.d.ts +2 -0
  72. package/dist/monitor.js +104 -0
  73. package/dist/monitor.js.map +1 -0
  74. package/dist/onboarding/connect.d.ts +2 -2
  75. package/dist/onboarding/connect.js +13 -8
  76. package/dist/onboarding/connect.js.map +1 -1
  77. package/dist/onboarding/credential-interceptor.js +1 -1
  78. package/dist/onboarding/credential-interceptor.js.map +1 -1
  79. package/dist/onboarding/degradation-accumulator.js +1 -3
  80. package/dist/onboarding/degradation-accumulator.js.map +1 -1
  81. package/dist/onboarding/provider-status.d.ts +48 -0
  82. package/dist/onboarding/provider-status.js +285 -0
  83. package/dist/onboarding/provider-status.js.map +1 -0
  84. package/dist/onboarding/providers.d.ts +85 -8
  85. package/dist/onboarding/providers.js +83 -18
  86. package/dist/onboarding/providers.js.map +1 -1
  87. package/dist/onboarding/state.d.ts +1 -0
  88. package/dist/onboarding/state.js +5 -0
  89. package/dist/onboarding/state.js.map +1 -1
  90. package/dist/onboarding/tool-helpers.js +1 -1
  91. package/dist/onboarding/tool-helpers.js.map +1 -1
  92. package/dist/onboarding/tool-tags.d.ts +12 -1
  93. package/dist/onboarding/tool-tags.js +37 -5
  94. package/dist/onboarding/tool-tags.js.map +1 -1
  95. package/dist/onboarding/validation.d.ts +2 -2
  96. package/dist/onboarding/validation.js +1 -1
  97. package/dist/onboarding/validation.js.map +1 -1
  98. package/dist/pi/opencandle-extension.d.ts +8 -0
  99. package/dist/pi/opencandle-extension.js +502 -42
  100. package/dist/pi/opencandle-extension.js.map +1 -1
  101. package/dist/pi/session.d.ts +1 -1
  102. package/dist/pi/session.js +3 -1
  103. package/dist/pi/session.js.map +1 -1
  104. package/dist/pi/setup.js +8 -3
  105. package/dist/pi/setup.js.map +1 -1
  106. package/dist/pi/tool-adapter.d.ts +4 -1
  107. package/dist/pi/tool-adapter.js +10 -6
  108. package/dist/pi/tool-adapter.js.map +1 -1
  109. package/dist/prompts/context-builder.d.ts +1 -1
  110. package/dist/prompts/context-builder.js +20 -7
  111. package/dist/prompts/context-builder.js.map +1 -1
  112. package/dist/prompts/policy-cards.d.ts +1 -1
  113. package/dist/prompts/policy-cards.js +2 -2
  114. package/dist/prompts/policy-cards.js.map +1 -1
  115. package/dist/prompts/sections.d.ts +1 -1
  116. package/dist/prompts/symbol-preflight.d.ts +20 -0
  117. package/dist/prompts/symbol-preflight.js +49 -0
  118. package/dist/prompts/symbol-preflight.js.map +1 -0
  119. package/dist/prompts/workflow-prompts.d.ts +1 -1
  120. package/dist/prompts/workflow-prompts.js +54 -16
  121. package/dist/prompts/workflow-prompts.js.map +1 -1
  122. package/dist/providers/alpha-vantage.d.ts +1 -1
  123. package/dist/providers/alpha-vantage.js +26 -7
  124. package/dist/providers/alpha-vantage.js.map +1 -1
  125. package/dist/providers/coingecko.js +1 -1
  126. package/dist/providers/coingecko.js.map +1 -1
  127. package/dist/providers/errors.d.ts +5 -0
  128. package/dist/providers/errors.js +11 -0
  129. package/dist/providers/errors.js.map +1 -0
  130. package/dist/providers/exa-search.d.ts +2 -2
  131. package/dist/providers/exa-search.js +19 -11
  132. package/dist/providers/exa-search.js.map +1 -1
  133. package/dist/providers/external-tool-error.d.ts +10 -0
  134. package/dist/providers/external-tool-error.js +21 -0
  135. package/dist/providers/external-tool-error.js.map +1 -0
  136. package/dist/providers/fear-greed.js +1 -1
  137. package/dist/providers/fear-greed.js.map +1 -1
  138. package/dist/providers/finnhub.js +3 -5
  139. package/dist/providers/finnhub.js.map +1 -1
  140. package/dist/providers/fred.js +2 -2
  141. package/dist/providers/fred.js.map +1 -1
  142. package/dist/providers/index.d.ts +7 -6
  143. package/dist/providers/index.js +6 -5
  144. package/dist/providers/index.js.map +1 -1
  145. package/dist/providers/reddit-cli.d.ts +36 -0
  146. package/dist/providers/reddit-cli.js +201 -0
  147. package/dist/providers/reddit-cli.js.map +1 -0
  148. package/dist/providers/reddit.d.ts +1 -1
  149. package/dist/providers/reddit.js +9 -37
  150. package/dist/providers/reddit.js.map +1 -1
  151. package/dist/providers/sec-edgar.d.ts +1 -0
  152. package/dist/providers/sec-edgar.js +12 -4
  153. package/dist/providers/sec-edgar.js.map +1 -1
  154. package/dist/providers/tradingview.d.ts +47 -0
  155. package/dist/providers/tradingview.js +275 -0
  156. package/dist/providers/tradingview.js.map +1 -0
  157. package/dist/providers/twitter-cli.d.ts +40 -0
  158. package/dist/providers/twitter-cli.js +153 -0
  159. package/dist/providers/twitter-cli.js.map +1 -0
  160. package/dist/providers/twitter.d.ts +0 -8
  161. package/dist/providers/twitter.js +8 -60
  162. package/dist/providers/twitter.js.map +1 -1
  163. package/dist/providers/web-search.js +26 -12
  164. package/dist/providers/web-search.js.map +1 -1
  165. package/dist/providers/with-fallback.js +4 -2
  166. package/dist/providers/with-fallback.js.map +1 -1
  167. package/dist/providers/wrap-provider.d.ts +2 -3
  168. package/dist/providers/wrap-provider.js +44 -8
  169. package/dist/providers/wrap-provider.js.map +1 -1
  170. package/dist/providers/yahoo-finance.d.ts +1 -1
  171. package/dist/providers/yahoo-finance.js +153 -48
  172. package/dist/providers/yahoo-finance.js.map +1 -1
  173. package/dist/routing/classify-intent.d.ts +6 -0
  174. package/dist/routing/classify-intent.js +78 -7
  175. package/dist/routing/classify-intent.js.map +1 -1
  176. package/dist/routing/defaults.d.ts +1 -1
  177. package/dist/routing/entity-extractor.d.ts +1 -0
  178. package/dist/routing/entity-extractor.js +234 -29
  179. package/dist/routing/entity-extractor.js.map +1 -1
  180. package/dist/routing/fund-symbols.d.ts +2 -0
  181. package/dist/routing/fund-symbols.js +55 -0
  182. package/dist/routing/fund-symbols.js.map +1 -0
  183. package/dist/routing/horizon.d.ts +1 -0
  184. package/dist/routing/horizon.js +10 -0
  185. package/dist/routing/horizon.js.map +1 -0
  186. package/dist/routing/index.d.ts +10 -10
  187. package/dist/routing/index.js +6 -6
  188. package/dist/routing/index.js.map +1 -1
  189. package/dist/routing/planning.d.ts +2 -2
  190. package/dist/routing/planning.js +65 -34
  191. package/dist/routing/planning.js.map +1 -1
  192. package/dist/routing/route-manifest.d.ts +2 -2
  193. package/dist/routing/route-manifest.js +25 -4
  194. package/dist/routing/route-manifest.js.map +1 -1
  195. package/dist/routing/router-llm-client.js.map +1 -1
  196. package/dist/routing/router-prompt.js +7 -9
  197. package/dist/routing/router-prompt.js.map +1 -1
  198. package/dist/routing/router-types.d.ts +1 -0
  199. package/dist/routing/router.js +137 -22
  200. package/dist/routing/router.js.map +1 -1
  201. package/dist/routing/slot-resolver.d.ts +1 -1
  202. package/dist/routing/slot-resolver.js +2 -4
  203. package/dist/routing/slot-resolver.js.map +1 -1
  204. package/dist/routing/symbol-disambiguator.d.ts +11 -0
  205. package/dist/routing/symbol-disambiguator.js +52 -0
  206. package/dist/routing/symbol-disambiguator.js.map +1 -0
  207. package/dist/routing/turn-context.d.ts +1 -1
  208. package/dist/routing/turn-context.js +1 -1
  209. package/dist/routing/turn-context.js.map +1 -1
  210. package/dist/routing/types.d.ts +2 -0
  211. package/dist/runtime/answer-contracts.d.ts +1 -1
  212. package/dist/runtime/answer-contracts.js +48 -9
  213. package/dist/runtime/answer-contracts.js.map +1 -1
  214. package/dist/runtime/artifact-contracts.js.map +1 -1
  215. package/dist/runtime/planning-evidence.js +47 -26
  216. package/dist/runtime/planning-evidence.js.map +1 -1
  217. package/dist/runtime/prompt-step.d.ts +1 -9
  218. package/dist/runtime/prompt-step.js +0 -10
  219. package/dist/runtime/prompt-step.js.map +1 -1
  220. package/dist/runtime/run-context.d.ts +5 -2
  221. package/dist/runtime/run-context.js +8 -1
  222. package/dist/runtime/run-context.js.map +1 -1
  223. package/dist/runtime/session-coordinator.d.ts +13 -5
  224. package/dist/runtime/session-coordinator.js +160 -20
  225. package/dist/runtime/session-coordinator.js.map +1 -1
  226. package/dist/runtime/session-title.d.ts +14 -0
  227. package/dist/runtime/session-title.js +50 -0
  228. package/dist/runtime/session-title.js.map +1 -0
  229. package/dist/runtime/tool-defaults-wrapper.js +7 -5
  230. package/dist/runtime/tool-defaults-wrapper.js.map +1 -1
  231. package/dist/runtime/validation.js.map +1 -1
  232. package/dist/runtime/workflow-events.js.map +1 -1
  233. package/dist/runtime/workflow-runner.d.ts +3 -3
  234. package/dist/runtime/workflow-runner.js +1 -1
  235. package/dist/runtime/workflow-runner.js.map +1 -1
  236. package/dist/sentiment/adapters/finnhub.d.ts +1 -1
  237. package/dist/sentiment/adapters/finnhub.js +6 -1
  238. package/dist/sentiment/adapters/finnhub.js.map +1 -1
  239. package/dist/sentiment/adapters/reddit.d.ts +2 -2
  240. package/dist/sentiment/adapters/twitter.d.ts +1 -1
  241. package/dist/sentiment/adapters/web.d.ts +1 -1
  242. package/dist/sentiment/index.d.ts +10 -11
  243. package/dist/sentiment/index.js +10 -20
  244. package/dist/sentiment/index.js.map +1 -1
  245. package/dist/sentiment/insights.d.ts +17 -0
  246. package/dist/sentiment/insights.js +206 -0
  247. package/dist/sentiment/insights.js.map +1 -0
  248. package/dist/sentiment/keywords.js +26 -4
  249. package/dist/sentiment/keywords.js.map +1 -1
  250. package/dist/sentiment/pipeline.d.ts +2 -2
  251. package/dist/sentiment/pipeline.js +14 -2
  252. package/dist/sentiment/pipeline.js.map +1 -1
  253. package/dist/sentiment/scorer.d.ts +2 -0
  254. package/dist/sentiment/scorer.js +11 -2
  255. package/dist/sentiment/scorer.js.map +1 -1
  256. package/dist/sentiment/store.d.ts +1 -1
  257. package/dist/sentiment/store.js +1 -1
  258. package/dist/sentiment/store.js.map +1 -1
  259. package/dist/sentiment/trends.d.ts +1 -1
  260. package/dist/sentiment/trends.js.map +1 -1
  261. package/dist/sentiment/types.d.ts +2 -0
  262. package/dist/sentiment/types.js.map +1 -1
  263. package/dist/system-prompt.js +6 -9
  264. package/dist/system-prompt.js.map +1 -1
  265. package/dist/tool-kit.d.ts +7 -7
  266. package/dist/tool-kit.js +4 -4
  267. package/dist/tool-kit.js.map +1 -1
  268. package/dist/tools/fundamentals/company-overview.js +11 -6
  269. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  270. package/dist/tools/fundamentals/comps.js +18 -9
  271. package/dist/tools/fundamentals/comps.js.map +1 -1
  272. package/dist/tools/fundamentals/dcf.js +23 -11
  273. package/dist/tools/fundamentals/dcf.js.map +1 -1
  274. package/dist/tools/fundamentals/earnings.js +8 -3
  275. package/dist/tools/fundamentals/earnings.js.map +1 -1
  276. package/dist/tools/fundamentals/financials.js +8 -3
  277. package/dist/tools/fundamentals/financials.js.map +1 -1
  278. package/dist/tools/fundamentals/sec-filings.js +21 -6
  279. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  280. package/dist/tools/index.d.ts +27 -20
  281. package/dist/tools/index.js +55 -43
  282. package/dist/tools/index.js.map +1 -1
  283. package/dist/tools/interaction/ask-user.js +15 -3
  284. package/dist/tools/interaction/ask-user.js.map +1 -1
  285. package/dist/tools/macro/fear-greed.js.map +1 -1
  286. package/dist/tools/macro/fred-data.d.ts +1 -1
  287. package/dist/tools/macro/fred-data.js +17 -6
  288. package/dist/tools/macro/fred-data.js.map +1 -1
  289. package/dist/tools/market/crypto-history.js +3 -1
  290. package/dist/tools/market/crypto-history.js.map +1 -1
  291. package/dist/tools/market/crypto-price.js +3 -1
  292. package/dist/tools/market/crypto-price.js.map +1 -1
  293. package/dist/tools/market/screen-stocks.d.ts +18 -0
  294. package/dist/tools/market/screen-stocks.js +252 -0
  295. package/dist/tools/market/screen-stocks.js.map +1 -0
  296. package/dist/tools/market/search-ticker.js +160 -8
  297. package/dist/tools/market/search-ticker.js.map +1 -1
  298. package/dist/tools/market/stock-history.d.ts +2 -2
  299. package/dist/tools/market/stock-history.js +26 -7
  300. package/dist/tools/market/stock-history.js.map +1 -1
  301. package/dist/tools/market/stock-quote.js +5 -3
  302. package/dist/tools/market/stock-quote.js.map +1 -1
  303. package/dist/tools/options/greeks.js +1 -1
  304. package/dist/tools/options/greeks.js.map +1 -1
  305. package/dist/tools/options/option-chain.js +19 -6
  306. package/dist/tools/options/option-chain.js.map +1 -1
  307. package/dist/tools/portfolio/alerts.d.ts +15 -0
  308. package/dist/tools/portfolio/alerts.js +357 -0
  309. package/dist/tools/portfolio/alerts.js.map +1 -0
  310. package/dist/tools/portfolio/correlation.d.ts +1 -1
  311. package/dist/tools/portfolio/correlation.js +33 -13
  312. package/dist/tools/portfolio/correlation.js.map +1 -1
  313. package/dist/tools/portfolio/daily-report.d.ts +8 -0
  314. package/dist/tools/portfolio/daily-report.js +83 -0
  315. package/dist/tools/portfolio/daily-report.js.map +1 -0
  316. package/dist/tools/portfolio/holdings-overlap.js +10 -3
  317. package/dist/tools/portfolio/holdings-overlap.js.map +1 -1
  318. package/dist/tools/portfolio/notifications.d.ts +7 -0
  319. package/dist/tools/portfolio/notifications.js +43 -0
  320. package/dist/tools/portfolio/notifications.js.map +1 -0
  321. package/dist/tools/portfolio/predictions.d.ts +12 -6
  322. package/dist/tools/portfolio/predictions.js +337 -87
  323. package/dist/tools/portfolio/predictions.js.map +1 -1
  324. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  325. package/dist/tools/portfolio/risk-analysis.js +45 -6
  326. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  327. package/dist/tools/portfolio/tracker.d.ts +4 -3
  328. package/dist/tools/portfolio/tracker.js +246 -101
  329. package/dist/tools/portfolio/tracker.js.map +1 -1
  330. package/dist/tools/portfolio/watchlist.d.ts +6 -4
  331. package/dist/tools/portfolio/watchlist.js +208 -108
  332. package/dist/tools/portfolio/watchlist.js.map +1 -1
  333. package/dist/tools/sentiment/insight-format.d.ts +2 -0
  334. package/dist/tools/sentiment/insight-format.js +36 -0
  335. package/dist/tools/sentiment/insight-format.js.map +1 -0
  336. package/dist/tools/sentiment/query-match.d.ts +3 -0
  337. package/dist/tools/sentiment/query-match.js +113 -0
  338. package/dist/tools/sentiment/query-match.js.map +1 -0
  339. package/dist/tools/sentiment/reddit-sentiment.d.ts +12 -1
  340. package/dist/tools/sentiment/reddit-sentiment.js +266 -107
  341. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  342. package/dist/tools/sentiment/sentiment-summary.d.ts +9 -1
  343. package/dist/tools/sentiment/sentiment-summary.js +223 -205
  344. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  345. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  346. package/dist/tools/sentiment/sentiment-trend.js +12 -2
  347. package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
  348. package/dist/tools/sentiment/twitter-sentiment.d.ts +11 -1
  349. package/dist/tools/sentiment/twitter-sentiment.js +188 -58
  350. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  351. package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
  352. package/dist/tools/sentiment/untrusted-text.js +17 -0
  353. package/dist/tools/sentiment/untrusted-text.js.map +1 -0
  354. package/dist/tools/sentiment/web-search.js +9 -13
  355. package/dist/tools/sentiment/web-search.js.map +1 -1
  356. package/dist/tools/sentiment/web-sentiment.js +19 -3
  357. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  358. package/dist/tools/technical/backtest.d.ts +1 -1
  359. package/dist/tools/technical/backtest.js +27 -20
  360. package/dist/tools/technical/backtest.js.map +1 -1
  361. package/dist/tools/technical/indicators.js +23 -5
  362. package/dist/tools/technical/indicators.js.map +1 -1
  363. package/dist/types/index.d.ts +3 -3
  364. package/dist/types/index.js.map +1 -1
  365. package/dist/types/market.d.ts +1 -0
  366. package/dist/types/portfolio.d.ts +14 -4
  367. package/dist/types/sentiment.d.ts +52 -0
  368. package/dist/workflows/compare-assets.d.ts +0 -3
  369. package/dist/workflows/compare-assets.js +20 -11
  370. package/dist/workflows/compare-assets.js.map +1 -1
  371. package/dist/workflows/index.d.ts +3 -4
  372. package/dist/workflows/index.js +3 -3
  373. package/dist/workflows/index.js.map +1 -1
  374. package/dist/workflows/options-screener.d.ts +0 -3
  375. package/dist/workflows/options-screener.js +4 -11
  376. package/dist/workflows/options-screener.js.map +1 -1
  377. package/dist/workflows/portfolio-builder.d.ts +0 -3
  378. package/dist/workflows/portfolio-builder.js +0 -8
  379. package/dist/workflows/portfolio-builder.js.map +1 -1
  380. package/gui/server/ask-user-bridge.ts +1 -1
  381. package/gui/server/automation-heartbeat.ts +97 -0
  382. package/gui/server/background-quotes.ts +97 -1
  383. package/gui/server/chat-event-adapter.ts +32 -10
  384. package/gui/server/chat-run-session.ts +16 -0
  385. package/gui/server/invoke-tool.ts +160 -3
  386. package/gui/server/live-chat-event-adapter.ts +21 -6
  387. package/gui/server/market-state-api.ts +315 -0
  388. package/gui/server/model-setup.ts +156 -2
  389. package/gui/server/private-api-access.ts +62 -0
  390. package/gui/server/projector.ts +18 -9
  391. package/gui/server/prompt-observation.ts +4 -7
  392. package/gui/server/quote-snapshot-store.ts +50 -0
  393. package/gui/server/server.ts +218 -451
  394. package/gui/server/session-actions.ts +186 -1
  395. package/gui/server/shutdown.ts +47 -0
  396. package/gui/server/tool-invoke-ack.ts +49 -0
  397. package/gui/server/tool-metadata.ts +101 -24
  398. package/gui/server/websocket.ts +13 -3
  399. package/gui/server/writer-lock.ts +6 -2
  400. package/gui/server/ws-hub.ts +311 -0
  401. package/gui/shared/chat-events.ts +16 -1
  402. package/gui/shared/event-reducer.ts +24 -6
  403. package/gui/web/dist/assets/CatalogOverlay-CgeY5Pkp.js +1 -0
  404. package/gui/web/dist/assets/index-C6W_2eAn.js +69 -0
  405. package/gui/web/dist/assets/index-hwbx24a5.css +1 -0
  406. package/gui/web/dist/index.html +2 -2
  407. package/package.json +9 -6
  408. package/src/analysts/contracts.ts +10 -23
  409. package/src/analysts/orchestrator.ts +8 -43
  410. package/src/cli.ts +76 -12
  411. package/src/config.ts +44 -9
  412. package/src/index.ts +1 -1
  413. package/src/infra/cache.ts +41 -30
  414. package/src/infra/http-client.ts +72 -6
  415. package/src/infra/index.ts +6 -10
  416. package/src/infra/native-dependencies.ts +8 -3
  417. package/src/infra/node-version.ts +3 -1
  418. package/src/infra/opencandle-paths.ts +3 -14
  419. package/src/infra/rate-limiter.ts +22 -19
  420. package/src/market-state/alert-conditions.ts +82 -0
  421. package/src/market-state/alert-runner.ts +863 -0
  422. package/src/market-state/daily-report.ts +247 -0
  423. package/src/market-state/local-automation-service.ts +162 -0
  424. package/src/market-state/notification-delivery.ts +158 -0
  425. package/src/market-state/resolve-for-mutation.ts +24 -0
  426. package/src/market-state/resolve.ts +112 -0
  427. package/src/market-state/service.ts +2344 -0
  428. package/src/memory/index.ts +7 -7
  429. package/src/memory/manager.ts +14 -16
  430. package/src/memory/retrieval.ts +8 -7
  431. package/src/memory/sqlite.ts +407 -6
  432. package/src/memory/storage.ts +5 -15
  433. package/src/memory/tool-defaults.ts +60 -39
  434. package/src/memory/types.ts +3 -3
  435. package/src/monitor.ts +121 -0
  436. package/src/onboarding/connect.ts +24 -31
  437. package/src/onboarding/credential-interceptor.ts +3 -15
  438. package/src/onboarding/degradation-accumulator.ts +1 -3
  439. package/src/onboarding/provider-status.ts +410 -0
  440. package/src/onboarding/providers.ts +144 -45
  441. package/src/onboarding/state.ts +13 -15
  442. package/src/onboarding/tool-helpers.ts +2 -9
  443. package/src/onboarding/tool-tags.ts +51 -8
  444. package/src/onboarding/validation.ts +16 -22
  445. package/src/pi/opencandle-extension.ts +643 -101
  446. package/src/pi/session.ts +7 -5
  447. package/src/pi/setup.ts +61 -43
  448. package/src/pi/tool-adapter.ts +19 -6
  449. package/src/prompts/context-builder.ts +24 -13
  450. package/src/prompts/policy-cards.ts +3 -3
  451. package/src/prompts/sections.ts +1 -1
  452. package/src/prompts/symbol-preflight.ts +80 -0
  453. package/src/prompts/workflow-prompts.ts +77 -28
  454. package/src/providers/alpha-vantage.ts +58 -39
  455. package/src/providers/coingecko.ts +2 -5
  456. package/src/providers/errors.ts +9 -0
  457. package/src/providers/exa-search.ts +24 -22
  458. package/src/providers/external-tool-error.ts +20 -0
  459. package/src/providers/fear-greed.ts +1 -1
  460. package/src/providers/finnhub.ts +7 -6
  461. package/src/providers/fred.ts +3 -3
  462. package/src/providers/index.ts +14 -6
  463. package/src/providers/reddit-cli.ts +317 -0
  464. package/src/providers/reddit.ts +14 -59
  465. package/src/providers/sec-edgar.ts +20 -6
  466. package/src/providers/tradingview.ts +399 -0
  467. package/src/providers/twitter-cli.ts +233 -0
  468. package/src/providers/twitter.ts +8 -79
  469. package/src/providers/web-search.ts +30 -20
  470. package/src/providers/with-fallback.ts +8 -7
  471. package/src/providers/wrap-provider.ts +49 -10
  472. package/src/providers/yahoo-finance.ts +204 -66
  473. package/src/routing/classify-intent.ts +101 -10
  474. package/src/routing/defaults.ts +1 -1
  475. package/src/routing/entity-extractor.ts +287 -38
  476. package/src/routing/fund-symbols.ts +58 -0
  477. package/src/routing/horizon.ts +7 -0
  478. package/src/routing/index.ts +48 -48
  479. package/src/routing/planning.ts +145 -53
  480. package/src/routing/route-manifest.ts +37 -15
  481. package/src/routing/router-llm-client.ts +4 -4
  482. package/src/routing/router-prompt.ts +15 -19
  483. package/src/routing/router-types.ts +2 -5
  484. package/src/routing/router.ts +251 -53
  485. package/src/routing/slot-resolver.ts +34 -11
  486. package/src/routing/symbol-disambiguator.ts +72 -0
  487. package/src/routing/turn-context.ts +6 -9
  488. package/src/routing/types.ts +2 -0
  489. package/src/runtime/answer-contracts.ts +105 -45
  490. package/src/runtime/artifact-contracts.ts +2 -1
  491. package/src/runtime/planning-evidence.ts +157 -66
  492. package/src/runtime/prompt-step.ts +1 -16
  493. package/src/runtime/run-context.ts +12 -2
  494. package/src/runtime/session-coordinator.ts +238 -63
  495. package/src/runtime/session-title.ts +60 -0
  496. package/src/runtime/tool-defaults-wrapper.ts +13 -5
  497. package/src/runtime/validation.ts +1 -4
  498. package/src/runtime/workflow-events.ts +7 -7
  499. package/src/runtime/workflow-runner.ts +5 -11
  500. package/src/sentiment/adapters/finnhub.ts +7 -2
  501. package/src/sentiment/adapters/reddit.ts +2 -2
  502. package/src/sentiment/adapters/twitter.ts +1 -1
  503. package/src/sentiment/adapters/web.ts +1 -1
  504. package/src/sentiment/index.ts +17 -26
  505. package/src/sentiment/insights.ts +269 -0
  506. package/src/sentiment/keywords.ts +26 -4
  507. package/src/sentiment/pipeline.ts +28 -5
  508. package/src/sentiment/scorer.ts +13 -2
  509. package/src/sentiment/store.ts +2 -2
  510. package/src/sentiment/trends.ts +9 -3
  511. package/src/sentiment/types.ts +8 -4
  512. package/src/system-prompt.ts +6 -9
  513. package/src/tool-kit.ts +10 -9
  514. package/src/tools/fundamentals/company-overview.ts +19 -9
  515. package/src/tools/fundamentals/comps.ts +68 -55
  516. package/src/tools/fundamentals/dcf.ts +145 -95
  517. package/src/tools/fundamentals/earnings.ts +16 -6
  518. package/src/tools/fundamentals/financials.ts +16 -7
  519. package/src/tools/fundamentals/sec-filings.ts +37 -16
  520. package/src/tools/index.ts +56 -43
  521. package/src/tools/interaction/ask-user.ts +22 -10
  522. package/src/tools/macro/fear-greed.ts +1 -1
  523. package/src/tools/macro/fred-data.ts +58 -46
  524. package/src/tools/market/crypto-history.ts +8 -3
  525. package/src/tools/market/crypto-price.ts +6 -6
  526. package/src/tools/market/screen-stocks.ts +279 -0
  527. package/src/tools/market/search-ticker.ts +218 -17
  528. package/src/tools/market/stock-history.ts +37 -12
  529. package/src/tools/market/stock-quote.ts +10 -7
  530. package/src/tools/options/greeks.ts +5 -5
  531. package/src/tools/options/option-chain.ts +41 -17
  532. package/src/tools/portfolio/alerts.ts +457 -0
  533. package/src/tools/portfolio/correlation.ts +47 -20
  534. package/src/tools/portfolio/daily-report.ts +101 -0
  535. package/src/tools/portfolio/holdings-overlap.ts +31 -15
  536. package/src/tools/portfolio/notifications.ts +45 -0
  537. package/src/tools/portfolio/predictions.ts +406 -106
  538. package/src/tools/portfolio/risk-analysis.ts +46 -7
  539. package/src/tools/portfolio/tracker.ts +270 -109
  540. package/src/tools/portfolio/watchlist.ts +250 -121
  541. package/src/tools/sentiment/insight-format.ts +50 -0
  542. package/src/tools/sentiment/query-match.ts +117 -0
  543. package/src/tools/sentiment/reddit-sentiment.ts +360 -121
  544. package/src/tools/sentiment/sentiment-summary.ts +302 -235
  545. package/src/tools/sentiment/sentiment-trend.ts +24 -7
  546. package/src/tools/sentiment/twitter-sentiment.ts +264 -73
  547. package/src/tools/sentiment/untrusted-text.ts +21 -0
  548. package/src/tools/sentiment/web-search.ts +21 -18
  549. package/src/tools/sentiment/web-sentiment.ts +30 -10
  550. package/src/tools/technical/backtest.ts +32 -22
  551. package/src/tools/technical/indicators.ts +39 -14
  552. package/src/types/index.ts +8 -3
  553. package/src/types/market.ts +1 -0
  554. package/src/types/portfolio.ts +14 -4
  555. package/src/types/sentiment.ts +61 -2
  556. package/src/workflows/compare-assets.ts +33 -21
  557. package/src/workflows/index.ts +3 -4
  558. package/src/workflows/options-screener.ts +27 -29
  559. package/src/workflows/portfolio-builder.ts +34 -27
  560. package/dist/infra/browser.d.ts +0 -35
  561. package/dist/infra/browser.js +0 -103
  562. package/dist/infra/browser.js.map +0 -1
  563. package/dist/tools/interaction/twitter-login.d.ts +0 -8
  564. package/dist/tools/interaction/twitter-login.js +0 -77
  565. package/dist/tools/interaction/twitter-login.js.map +0 -1
  566. package/dist/workflows/types.d.ts +0 -4
  567. package/dist/workflows/types.js +0 -2
  568. package/dist/workflows/types.js.map +0 -1
  569. package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +0 -1
  570. package/gui/web/dist/assets/index-Bxt9QpLX.css +0 -1
  571. package/gui/web/dist/assets/index-CZ9DHZYy.js +0 -67
  572. package/src/infra/browser.ts +0 -111
  573. package/src/tools/interaction/twitter-login.ts +0 -93
  574. package/src/workflows/types.ts +0 -4
@@ -1,20 +1,22 @@
1
- import { Type } from "@sinclair/typebox";
2
1
  import type { AgentTool } from "@earendil-works/pi-agent-core";
3
- import { getSubredditPosts, getPostComments } from "../../providers/reddit.js";
4
- import { getTwitterSentiment } from "../../providers/twitter.js";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getConfig } from "../../config.js";
5
+ import { hasCredential } from "../../onboarding/providers.js";
6
+ import { buildSoftDegradedTag } from "../../onboarding/tool-tags.js";
7
+ import { finnhubDateRange, getCompanyNews } from "../../providers/finnhub.js";
5
8
  import { searchWeb } from "../../providers/web-search.js";
6
- import { getCompanyNews, finnhubDateRange } from "../../providers/finnhub.js";
7
9
  import { getQuote } from "../../providers/yahoo-finance.js";
8
- import { wrapProvider } from "../../providers/wrap-provider.js";
9
- import { getConfig } from "../../config.js";
10
- import { TwitterAdapter } from "../../sentiment/adapters/twitter.js";
10
+ import { extractTickersFromQuery, FinnhubAdapter } from "../../sentiment/adapters/finnhub.js";
11
11
  import { RedditAdapter } from "../../sentiment/adapters/reddit.js";
12
+ import { TwitterAdapter } from "../../sentiment/adapters/twitter.js";
12
13
  import { WebAdapter } from "../../sentiment/adapters/web.js";
13
- import { FinnhubAdapter, extractTickersFromQuery } from "../../sentiment/adapters/finnhub.js";
14
14
  import { getSentimentPipeline } from "../../sentiment/index.js";
15
15
  import type { SentinelRecord } from "../../sentiment/types.js";
16
- import { hasCredential } from "../../onboarding/providers.js";
17
- import { buildSoftDegradedTag } from "../../onboarding/tool-tags.js";
16
+ import type { AskUserHandler } from "../../types/index.js";
17
+ import { formatInsightSection } from "./insight-format.js";
18
+ import { createRedditSentimentTool } from "./reddit-sentiment.js";
19
+ import { createTwitterSentimentTool } from "./twitter-sentiment.js";
18
20
 
19
21
  const params = Type.Object({
20
22
  query: Type.String({ description: "Ticker or topic for cross-source sentiment summary" }),
@@ -23,199 +25,288 @@ const params = Type.Object({
23
25
  ),
24
26
  });
25
27
 
26
- export const sentimentSummaryTool: AgentTool<typeof params> = {
27
- name: "get_sentiment_summary",
28
- label: "Sentiment Summary",
29
- description:
30
- "Cross-source sentiment summary combining Twitter, Reddit, and web/news. Returns per-source scores, aggregate sentiment, and divergence detection.",
31
- parameters: params,
32
- async execute(_toolCallId, args) {
33
- const hours = args.hours ?? 24;
34
- const config = getConfig();
35
- const warnings: string[] = [];
36
- const allRecords: SentinelRecord[] = [];
37
-
38
- const twitterAdapter = new TwitterAdapter();
39
- const webAdapter = new WebAdapter();
40
- const finnhubAdapter = new FinnhubAdapter();
41
-
42
- // Determine if Finnhub should be included (key configured + ticker in
43
- // query). `candidateTickers` is extracted unconditionally so we can tell
44
- // a "no finnhub-mappable ticker in the query" case apart from a "query
45
- // has tickers but user has no Finnhub key" case — the latter warrants a
46
- // soft-degraded tag so the LLM surfaces it in the Data gaps section.
47
- const candidateTickers = extractTickersFromQuery(args.query);
48
- const finnhubTickers = config.finnhubApiKey ? candidateTickers : [];
49
- const includeFinnhub = finnhubTickers.length > 0 && Boolean(config.finnhubApiKey);
50
- const finnhubSoftDegraded =
51
- candidateTickers.length > 0 && !hasCredential("finnhub");
52
-
53
- // Finnhub fetch (built separately to avoid mixing promise types in allSettled)
54
- const finnhubFetch: Promise<import("../../providers/finnhub.js").FinnhubArticle[]> = includeFinnhub
55
- ? (async () => {
56
- const { from, to } = finnhubDateRange("day");
57
- const arrays = await Promise.all(
58
- finnhubTickers.map((sym) => getCompanyNews(sym, from, to, config.finnhubApiKey!)),
59
- );
60
- return arrays.flat();
61
- })()
62
- : Promise.resolve([]);
63
-
64
- // Fetch all sources in parallel
65
- const [twitterResult, redditResults, webResult, finnhubResult] = await Promise.allSettled([
66
- // Twitter
67
- wrapProvider("twitter", () => getTwitterSentiment(args.query, 50, hours)),
68
- // Reddit cross-subreddit
69
- fetchRedditCrossSubreddit(args.query, config.sentiment?.defaultSubreddits ?? ["wallstreetbets", "stocks", "investing", "options"]),
70
- // Web
71
- searchWeb(args.query, { freshness: "day", limit: 10, category: "news" }),
72
- // Finnhub only when includeFinnhub; otherwise resolves to []
73
- finnhubFetch,
74
- ]);
75
-
76
- // Process Twitter
77
- if (twitterResult.status === "fulfilled" && twitterResult.value.status === "ok") {
78
- const records = twitterAdapter.mapToRecords(twitterResult.value.data, args.query);
79
- allRecords.push(...records);
80
- } else {
81
- const reason = twitterResult.status === "rejected"
82
- ? twitterResult.reason?.message ?? "unknown error"
83
- : (twitterResult.value as any).reason ?? "unavailable";
84
- warnings.push(`Twitter: ${reason}`);
85
- }
86
-
87
- // Process Reddit
88
- if (redditResults.status === "fulfilled") {
89
- const { records: redditRecords, warnings: redditWarnings } = redditResults.value;
90
- allRecords.push(...redditRecords);
91
- warnings.push(...redditWarnings);
92
- } else {
93
- warnings.push(`Reddit: ${redditResults.reason?.message ?? "unknown error"}`);
94
- }
95
-
96
- // Process Web
97
- if (webResult.status === "fulfilled" && webResult.value.status === "ok") {
98
- const records = webAdapter.mapToRecords(webResult.value.data, args.query);
99
- allRecords.push(...records);
100
- } else {
101
- const reason = webResult.status === "rejected"
102
- ? webResult.reason?.message ?? "unknown error"
103
- : (webResult.value as any).reason ?? "unavailable";
104
- warnings.push(`Web: ${reason}`);
105
- }
106
-
107
- // Process Finnhub (only when included — otherwise resolves to empty array anyway)
108
- if (includeFinnhub) {
109
- if (finnhubResult.status === "fulfilled") {
110
- const articles = finnhubResult.value;
111
- if (articles.length > 0) {
112
- const records = finnhubAdapter.mapToRecords(articles, args.query);
113
- allRecords.push(...records);
28
+ type ToolTextContent = { type: string; text?: string };
29
+ type SetupAwareExecute<TParams, TDetails> = (
30
+ toolCallId: string,
31
+ params: TParams,
32
+ signal?: AbortSignal,
33
+ onUpdate?: undefined,
34
+ ctx?: ExtensionContext,
35
+ ) => Promise<{ content: ToolTextContent[]; details: TDetails }>;
36
+
37
+ interface SentimentSummaryToolOptions {
38
+ askUserHandler?: AskUserHandler;
39
+ }
40
+
41
+ export function createSentimentSummaryTool(
42
+ options: SentimentSummaryToolOptions = {},
43
+ ): AgentTool<typeof params> {
44
+ return {
45
+ name: "get_sentiment_summary",
46
+ label: "Sentiment Summary",
47
+ description:
48
+ "Cross-source sentiment summary combining Twitter, Reddit, and web/news. Returns per-source scores, aggregate sentiment, and divergence detection.",
49
+ parameters: params,
50
+ async execute(_toolCallId, args, _signal, _onUpdate, ctx?: ExtensionContext) {
51
+ const hours = args.hours ?? 24;
52
+ const config = getConfig();
53
+ const warnings: string[] = [];
54
+ const allRecords: SentinelRecord[] = [];
55
+
56
+ const twitterAdapter = new TwitterAdapter();
57
+ const webAdapter = new WebAdapter();
58
+ const finnhubAdapter = new FinnhubAdapter();
59
+
60
+ // Determine if Finnhub should be included (key configured + ticker in
61
+ // query). `candidateTickers` is extracted unconditionally so we can tell
62
+ // a "no finnhub-mappable ticker in the query" case apart from a "query
63
+ // has tickers but user has no Finnhub key" case — the latter warrants a
64
+ // soft-degraded tag so the LLM surfaces it in the Data gaps section.
65
+ const candidateTickers = extractTickersFromQuery(args.query);
66
+ const finnhubTickers = config.finnhubApiKey ? candidateTickers : [];
67
+ const includeFinnhub = finnhubTickers.length > 0 && Boolean(config.finnhubApiKey);
68
+ const finnhubSoftDegraded = candidateTickers.length > 0 && !hasCredential("finnhub");
69
+
70
+ const fetchFinnhub = async (): Promise<
71
+ import("../../providers/finnhub.js").FinnhubArticle[]
72
+ > => {
73
+ if (!includeFinnhub) return [];
74
+ const { from, to } = finnhubDateRange("day");
75
+ const arrays = await Promise.all(
76
+ finnhubTickers.map((sym) => getCompanyNews(sym, from, to, config.finnhubApiKey!)),
77
+ );
78
+ return arrays.flat();
79
+ };
80
+
81
+ // External-tool sources may need ask_user setup. Run them through the
82
+ // source-specific tools so install/login/skip preferences behave the same
83
+ // in cross-source summaries as they do in direct source requests.
84
+ try {
85
+ const twitterTool = createTwitterSentimentTool({
86
+ askUserHandler: options.askUserHandler,
87
+ });
88
+ const executeTwitter = twitterTool.execute as unknown as SetupAwareExecute<
89
+ { query: string; limit: number; hours: number },
90
+ NonNullable<Awaited<ReturnType<typeof twitterTool.execute>>["details"]> | null
91
+ >;
92
+ const twitterResult = await executeTwitter(
93
+ "summary-twitter",
94
+ { query: args.query, limit: 50, hours },
95
+ undefined,
96
+ undefined,
97
+ ctx,
98
+ );
99
+ if (twitterResult.details) {
100
+ allRecords.push(...twitterAdapter.mapToRecords(twitterResult.details, args.query));
101
+ } else {
102
+ warnings.push(sourceToolWarning("Twitter", toolText(twitterResult.content)));
114
103
  }
115
- } else {
116
- warnings.push(`Finnhub: ${finnhubResult.reason?.message ?? "unknown error"}`);
104
+ } catch (err) {
105
+ warnings.push(`Twitter: ${err instanceof Error ? err.message : String(err)}`);
117
106
  }
118
- }
119
-
120
- const softDegradedPrefix = finnhubSoftDegraded
121
- ? `${buildSoftDegradedTag({
122
- provider: "finnhub",
123
- fallback: "other-sentiment-sources",
124
- remediation: "run /connect news to enable Finnhub company news",
125
- })}\n\n`
126
- : "";
127
107
 
128
- if (allRecords.length === 0) {
129
- return {
130
- content: [
108
+ try {
109
+ const redditTool = createRedditSentimentTool({
110
+ askUserHandler: options.askUserHandler,
111
+ });
112
+ const executeReddit = redditTool.execute as unknown as SetupAwareExecute<
113
+ { query: string; subreddits: string[] },
114
+ NonNullable<Awaited<ReturnType<typeof redditTool.execute>>["details"]> | null
115
+ >;
116
+ const redditResult = await executeReddit(
117
+ "summary-reddit",
131
118
  {
132
- type: "text",
133
- text: `${softDegradedPrefix}⚠ Sentiment summary unavailable for "${args.query}" no sources returned data.\n${warnings.join("\n")}`,
119
+ query: args.query,
120
+ subreddits: config.sentiment?.defaultSubreddits ?? [
121
+ "wallstreetbets",
122
+ "stocks",
123
+ "investing",
124
+ "options",
125
+ ],
134
126
  },
135
- ],
136
- details: null as any,
137
- };
138
- }
139
-
140
- // Score and index through pipeline
141
- const pipeline = getSentimentPipeline();
142
- const result = await pipeline.processRecords(allRecords, args.query);
143
-
144
- // Group by source (exclude comments from per-source averages)
145
- const bySource: Record<string, { total: number; count: number }> = {};
146
- for (const rec of result.fresh) {
147
- if (rec.metadata.isComment) continue;
148
- if (!bySource[rec.source]) bySource[rec.source] = { total: 0, count: 0 };
149
- bySource[rec.source].total += rec.sentiment.score;
150
- bySource[rec.source].count++;
151
- }
152
-
153
- const lines: string[] = [];
154
- lines.push(`**Sentiment summary for "${args.query}"** (last ${hours}h):`);
155
- lines.push("");
156
- lines.push("| Source | Score | Count | Signal |");
157
- lines.push("|--------|-------|-------|--------|");
158
-
159
- let totalScore = 0;
160
- let totalCount = 0;
161
- for (const [source, stats] of Object.entries(bySource)) {
162
- const avg = stats.count > 0 ? stats.total / stats.count : 0;
163
- const label = sentimentLabel(avg);
164
- const sourceName = source === "web" ? "Web/News" : source.charAt(0).toUpperCase() + source.slice(1);
165
- lines.push(`| ${sourceName} | ${avg >= 0 ? "+" : ""}${avg.toFixed(2)} | ${stats.count} | ${label} |`);
166
- totalScore += stats.total;
167
- totalCount += stats.count;
168
- }
169
-
170
- const aggregate = totalCount > 0 ? totalScore / totalCount : 0;
171
- lines.push("");
172
- lines.push(`**Aggregate:** ${aggregate >= 0 ? "+" : ""}${aggregate.toFixed(2)} (${sentimentLabel(aggregate)})`);
173
-
174
- const priceContext = await buildPriceContext(candidateTickers[0], aggregate);
175
- if (priceContext) {
176
- lines.push("");
177
- lines.push(priceContext);
178
- }
127
+ undefined,
128
+ undefined,
129
+ ctx,
130
+ );
131
+ if (redditResult.details) {
132
+ allRecords.push(
133
+ ...(redditResult.details.records ??
134
+ new RedditAdapter().mapPostsToRecords(redditResult.details, args.query)),
135
+ );
136
+ } else {
137
+ warnings.push(sourceToolWarning("Reddit", toolText(redditResult.content)));
138
+ }
139
+ } catch (err) {
140
+ warnings.push(`Reddit: ${err instanceof Error ? err.message : String(err)}`);
141
+ }
179
142
 
180
- lines.push("");
181
- lines.push("Source-coverage risk: sentiment can be noisy and missing sources can skew the signal; treat this as supporting evidence, not a standalone buy/sell input.");
143
+ const [webResult, finnhubResult] = await Promise.allSettled([
144
+ searchWeb(args.query, { freshness: "day", limit: 10, category: "news" }),
145
+ // Finnhub — only when includeFinnhub; otherwise resolves to []
146
+ fetchFinnhub(),
147
+ ]);
182
148
 
183
- // Divergence
184
- if (result.divergence && result.divergence.detected) {
185
- lines.push("");
186
- lines.push(result.divergence.message);
187
- } else if (result.divergence && !result.divergence.detected) {
149
+ // Process Web
150
+ if (webResult.status === "fulfilled" && webResult.value.status === "ok") {
151
+ const records = webAdapter.mapToRecords(webResult.value.data, args.query);
152
+ allRecords.push(...records);
153
+ } else {
154
+ const reason =
155
+ webResult.status === "rejected"
156
+ ? (webResult.reason?.message ?? "unknown error")
157
+ : ((webResult.value as any).reason ?? "unavailable");
158
+ warnings.push(`Web: ${reason}`);
159
+ }
160
+
161
+ // Process Finnhub (only when included — otherwise resolves to empty array anyway)
162
+ if (includeFinnhub) {
163
+ if (finnhubResult.status === "fulfilled") {
164
+ const articles = finnhubResult.value;
165
+ if (articles.length > 0) {
166
+ const records = finnhubAdapter.mapToRecords(articles, args.query);
167
+ allRecords.push(...records);
168
+ }
169
+ } else {
170
+ warnings.push(`Finnhub: ${finnhubResult.reason?.message ?? "unknown error"}`);
171
+ }
172
+ }
173
+
174
+ const softDegradedPrefix = finnhubSoftDegraded
175
+ ? `${buildSoftDegradedTag({
176
+ provider: "finnhub",
177
+ fallback: "other-sentiment-sources",
178
+ remediation: "run /connect news to enable Finnhub company news",
179
+ })}\n\n`
180
+ : "";
181
+
182
+ if (allRecords.length === 0) {
183
+ return {
184
+ content: [
185
+ {
186
+ type: "text",
187
+ text: `${softDegradedPrefix}⚠ Sentiment summary unavailable for "${args.query}" — no sources returned data.\n${warnings.join("\n")}`,
188
+ },
189
+ ],
190
+ details: null as any,
191
+ };
192
+ }
193
+
194
+ const summaryWarnings = warnings.map(stripOpenCandleControlLines);
195
+
196
+ // Score and index through pipeline
197
+ const pipeline = getSentimentPipeline();
198
+ const result = await pipeline.processRecords(allRecords, args.query);
199
+ const insight =
200
+ result.insight && summaryWarnings.length > 0
201
+ ? {
202
+ ...result.insight,
203
+ caveats: [
204
+ ...result.insight.caveats,
205
+ ...summaryWarnings.map((warning) => `Source warning: ${warning}`),
206
+ ],
207
+ }
208
+ : result.insight;
209
+
210
+ // Group by source (exclude comments from per-source averages)
211
+ const bySource: Record<string, { total: number; count: number }> = {};
212
+ for (const rec of result.fresh) {
213
+ if (rec.metadata.isComment) continue;
214
+ if (!bySource[rec.source]) bySource[rec.source] = { total: 0, count: 0 };
215
+ bySource[rec.source].total += rec.sentiment.score;
216
+ bySource[rec.source].count++;
217
+ }
218
+
219
+ const lines: string[] = [];
220
+ lines.push(`**Sentiment summary for "${args.query}"** (last ${hours}h):`);
188
221
  lines.push("");
189
- lines.push(result.divergence.message);
190
- }
222
+ lines.push("| Source | Score | Count | Signal |");
223
+ lines.push("|--------|-------|-------|--------|");
224
+
225
+ let totalScore = 0;
226
+ let totalCount = 0;
227
+ for (const [source, stats] of Object.entries(bySource)) {
228
+ const avg = stats.count > 0 ? stats.total / stats.count : 0;
229
+ const label = sentimentLabel(avg);
230
+ const sourceName =
231
+ source === "web" ? "Web/News" : source.charAt(0).toUpperCase() + source.slice(1);
232
+ lines.push(
233
+ `| ${sourceName} | ${avg >= 0 ? "+" : ""}${avg.toFixed(2)} | ${stats.count} | ${label} |`,
234
+ );
235
+ totalScore += stats.total;
236
+ totalCount += stats.count;
237
+ }
191
238
 
192
- // Trend
193
- if (result.trend && result.trend.length > 0) {
194
- const t = result.trend[0];
239
+ const aggregate = totalCount > 0 ? totalScore / totalCount : 0;
195
240
  lines.push("");
196
- lines.push(`Trend: ${t.sparkline} ${t.direction} (${t.count} records)`);
197
- }
241
+ lines.push(
242
+ `**Aggregate:** ${aggregate >= 0 ? "+" : ""}${aggregate.toFixed(2)} (${sentimentLabel(aggregate)})`,
243
+ );
244
+
245
+ if (insight) {
246
+ lines.push(...formatInsightSection(insight));
247
+ }
248
+
249
+ const priceContext = await buildPriceContext(candidateTickers[0], aggregate);
250
+ if (priceContext) {
251
+ lines.push("");
252
+ lines.push(priceContext);
253
+ } else if (candidateTickers[0]) {
254
+ lines.push("");
255
+ lines.push(
256
+ `Price context: unavailable for ${candidateTickers[0]}; sentiment/price divergence could not be evaluated.`,
257
+ );
258
+ }
198
259
 
199
- if (warnings.length > 0) {
200
260
  lines.push("");
201
- lines.push(warnings.map((w) => `⚠ ${w}`).join("\n"));
202
- }
261
+ lines.push(
262
+ "Source-coverage risk: sentiment can be noisy and missing sources can skew the signal; treat this as supporting evidence, not a standalone buy/sell input.",
263
+ );
264
+
265
+ // Divergence
266
+ if (result.divergence && result.divergence.detected) {
267
+ lines.push("");
268
+ lines.push(result.divergence.message);
269
+ } else if (result.divergence && !result.divergence.detected) {
270
+ lines.push("");
271
+ lines.push(result.divergence.message);
272
+ }
273
+
274
+ // Trend
275
+ if (result.trend && result.trend.length > 0) {
276
+ const t = result.trend[0];
277
+ lines.push("");
278
+ lines.push(`Trend: ${t.sparkline} ${t.direction} (${t.count} records)`);
279
+ }
280
+
281
+ if (summaryWarnings.length > 0) {
282
+ lines.push("");
283
+ lines.push(summaryWarnings.map((w) => `⚠ ${w}`).join("\n"));
284
+ }
203
285
 
204
- const output = softDegradedPrefix + lines.join("\n");
205
- return { content: [{ type: "text", text: output }], details: result };
206
- },
207
- };
286
+ const output = softDegradedPrefix + lines.join("\n");
287
+ return { content: [{ type: "text", text: output }], details: { ...result, insight } };
288
+ },
289
+ };
290
+ }
291
+
292
+ export const sentimentSummaryTool = createSentimentSummaryTool();
208
293
 
209
- async function buildPriceContext(symbol: string | undefined, aggregateSentiment: number): Promise<string | null> {
294
+ async function buildPriceContext(
295
+ symbol: string | undefined,
296
+ aggregateSentiment: number,
297
+ ): Promise<string | null> {
210
298
  if (!symbol) return null;
211
299
  try {
212
300
  const quote = await getQuote(symbol);
213
301
  const sign = quote.changePercent >= 0 ? "+" : "";
214
- const direction = quote.changePercent > 0 ? "positive" : quote.changePercent < 0 ? "negative" : "flat";
215
- const sentimentDirection = aggregateSentiment > 0 ? "positive" : aggregateSentiment < 0 ? "negative" : "neutral";
216
- const relationship = sentimentDirection === "neutral" || direction === "flat" || sentimentDirection === direction
217
- ? "roughly aligns with price action"
218
- : "diverges from price action";
302
+ const direction =
303
+ quote.changePercent > 0 ? "positive" : quote.changePercent < 0 ? "negative" : "flat";
304
+ const sentimentDirection =
305
+ aggregateSentiment > 0 ? "positive" : aggregateSentiment < 0 ? "negative" : "neutral";
306
+ const relationship =
307
+ sentimentDirection === "neutral" || direction === "flat" || sentimentDirection === direction
308
+ ? "roughly aligns with price action"
309
+ : "diverges from price action";
219
310
  const freshnessNote = formatQuoteFreshnessNote(quote.timestamp);
220
311
  return `Price context: ${quote.symbol}: $${quote.price.toFixed(2)} (${sign}${quote.changePercent.toFixed(2)}%).${freshnessNote} The ${sentimentDirection} sentiment signal ${relationship}.`;
221
312
  } catch {
@@ -257,56 +348,6 @@ function formatQuoteFreshnessNote(timestamp: number | undefined): string {
257
348
  return ` Last available quote timestamp: ${quoteStamp} ET.${marketClosedNote}`;
258
349
  }
259
350
 
260
- async function fetchRedditCrossSubreddit(
261
- query: string,
262
- subreddits: string[],
263
- ): Promise<{ records: SentinelRecord[]; warnings: string[] }> {
264
- const adapter = new RedditAdapter();
265
- const records: SentinelRecord[] = [];
266
- const warnings: string[] = [];
267
- const config = getConfig();
268
- const commentsPerPost = config.sentiment?.commentsPerPost ?? 5;
269
-
270
- for (const sub of subreddits) {
271
- const result = await wrapProvider("reddit", () => getSubredditPosts(sub, 25));
272
- if (result.status === "unavailable") {
273
- warnings.push(`Reddit r/${sub}: ${result.reason}`);
274
- continue;
275
- }
276
- const postRecords = adapter.mapPostsToRecords(result.data, query);
277
-
278
- // Topic filter
279
- const queryLower = query.toLowerCase();
280
- const filtered = postRecords.filter((r) =>
281
- r.text.toLowerCase().includes(queryLower) ||
282
- (r.title?.toLowerCase().includes(queryLower) ?? false),
283
- );
284
- records.push(...filtered);
285
-
286
- // Fetch comments for top posts
287
- const topPosts = [...filtered]
288
- .sort((a, b) => b.engagement.score - a.engagement.score)
289
- .slice(0, 3); // fewer per sub since we're searching multiple
290
- for (const post of topPosts) {
291
- if ((post.engagement.replies ?? 0) === 0) continue;
292
- try {
293
- const comments = await getPostComments(sub, post.sourceId, commentsPerPost);
294
- records.push(...adapter.mapCommentsToRecords(comments, post.sourceId, sub, query));
295
- } catch { /* non-fatal */ }
296
- }
297
- }
298
-
299
- // Deduplicate
300
- const seen = new Set<string>();
301
- const deduped = records.filter((r) => {
302
- if (seen.has(r.sourceId)) return false;
303
- seen.add(r.sourceId);
304
- return true;
305
- });
306
-
307
- return { records: deduped, warnings };
308
- }
309
-
310
351
  function sentimentLabel(score: number): string {
311
352
  if (score > 0.3) return "Bullish";
312
353
  if (score < -0.3) return "Bearish";
@@ -314,3 +355,29 @@ function sentimentLabel(score: number): string {
314
355
  if (score < 0) return "Leaning Bearish";
315
356
  return "Neutral";
316
357
  }
358
+
359
+ function toolText(content: ToolTextContent[]): string {
360
+ return content
361
+ .map((part) => (part.type === "text" ? (part.text ?? "") : ""))
362
+ .filter(Boolean)
363
+ .join("\n");
364
+ }
365
+
366
+ function sourceToolWarning(source: string, text: string): string {
367
+ const lines = text
368
+ .split("\n")
369
+ .map((line) => line.trim())
370
+ .filter(Boolean);
371
+ const tag = lines.find((line) => line.includes("[OPENCANDLE_"));
372
+ const summary = lines.find((line) => !line.includes("[OPENCANDLE_")) ?? "unavailable";
373
+ return tag ? `${source}: ${summary}\n${tag}` : `${source}: ${summary}`;
374
+ }
375
+
376
+ function stripOpenCandleControlLines(text: string): string {
377
+ const stripped = text
378
+ .split("\n")
379
+ .map((line) => line.trim())
380
+ .filter((line) => line.length > 0 && !line.startsWith("[OPENCANDLE_"))
381
+ .join("\n");
382
+ return stripped || "unavailable";
383
+ }
@@ -1,7 +1,7 @@
1
- import { Type } from "@sinclair/typebox";
2
1
  import type { AgentTool } from "@earendil-works/pi-agent-core";
3
- import { SentimentStore } from "../../sentiment/store.js";
2
+ import { Type } from "@sinclair/typebox";
4
3
  import { getSentimentStore } from "../../sentiment/index.js";
4
+ import type { SentimentStore } from "../../sentiment/store.js";
5
5
  import { computeTrend } from "../../sentiment/trends.js";
6
6
 
7
7
  const params = Type.Object({
@@ -10,9 +10,17 @@ const params = Type.Object({
10
10
  Type.Number({ description: "Number of days of history. Default: 7, max: 30" }),
11
11
  ),
12
12
  source: Type.Optional(
13
- Type.Union([Type.Literal("twitter"), Type.Literal("reddit"), Type.Literal("web"), Type.Literal("finnhub")], {
14
- description: "Filter to a single source. Default: all sources.",
15
- }),
13
+ Type.Union(
14
+ [
15
+ Type.Literal("twitter"),
16
+ Type.Literal("reddit"),
17
+ Type.Literal("web"),
18
+ Type.Literal("finnhub"),
19
+ ],
20
+ {
21
+ description: "Filter to a single source. Default: all sources.",
22
+ },
23
+ ),
16
24
  ),
17
25
  });
18
26
 
@@ -22,7 +30,11 @@ interface TrendToolResult {
22
30
  }
23
31
 
24
32
  export const sentimentTrendTool: AgentTool<typeof params> & {
25
- executeWithStore: (toolCallId: string, args: { query: string; days?: number; source?: string }, store: SentimentStore) => Promise<TrendToolResult>;
33
+ executeWithStore: (
34
+ toolCallId: string,
35
+ args: { query: string; days?: number; source?: string },
36
+ store: SentimentStore,
37
+ ) => Promise<TrendToolResult>;
26
38
  } = {
27
39
  name: "get_sentiment_trend",
28
40
  label: "Sentiment Trend",
@@ -39,7 +51,12 @@ export const sentimentTrendTool: AgentTool<typeof params> & {
39
51
 
40
52
  if (series.length === 0) {
41
53
  return {
42
- content: [{ type: "text", text: `No historical sentiment data for "${args.query}". Run a sentiment query first to populate the store.` }],
54
+ content: [
55
+ {
56
+ type: "text",
57
+ text: `No historical sentiment data for "${args.query}". Run a sentiment query first to populate the store.`,
58
+ },
59
+ ],
43
60
  details: null,
44
61
  };
45
62
  }