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,11 +1,24 @@
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";
2
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
3
+ import { Type } from "@sinclair/typebox";
4
+ import { getConfig } from "../../config.js";
5
+ import { promptUser } from "../../onboarding/prompt-user.js";
6
+ import { getProvider } from "../../onboarding/providers.js";
7
+ import {
8
+ loadOnboardingState,
9
+ markProviderNeverAsk,
10
+ saveOnboardingState,
11
+ } from "../../onboarding/state.js";
12
+ import { buildExternalToolRequiredTag, buildSkippedTag } from "../../onboarding/tool-tags.js";
13
+ import { getPostComments, getSubredditPosts } from "../../providers/reddit.js";
4
14
  import { wrapProvider } from "../../providers/wrap-provider.js";
5
- import type { RedditSentimentResult } from "../../types/sentiment.js";
6
15
  import { RedditAdapter } from "../../sentiment/adapters/reddit.js";
7
16
  import { getSentimentPipeline } from "../../sentiment/index.js";
8
- import { getConfig } from "../../config.js";
17
+ import type { AskUserHandler } from "../../types/index.js";
18
+ import type { RedditSentimentResult } from "../../types/sentiment.js";
19
+ import { formatInsightSection } from "./insight-format.js";
20
+ import { recordMatchesSentimentQuery, sentimentQueryTerms } from "./query-match.js";
21
+ import { renderUntrustedText, untrustedContentHeader } from "./untrusted-text.js";
9
22
 
10
23
  const params = Type.Object({
11
24
  subreddit: Type.Optional(
@@ -30,135 +43,361 @@ const params = Type.Object({
30
43
  ),
31
44
  });
32
45
 
33
- export const redditSentimentTool: AgentTool<typeof params, RedditSentimentResult> = {
34
- name: "get_reddit_sentiment",
35
- label: "Reddit Sentiment",
36
- description:
37
- "Analyze sentiment from financial Reddit communities. Supports single subreddit, multi-subreddit, and topic filtering. Returns scored posts with comment analysis and trend context.",
38
- parameters: params,
39
- async execute(_toolCallId, args) {
40
- const limit = Math.min(args.limit ?? 25, 100);
41
- const config = getConfig();
42
-
43
- // Determine subreddits to search
44
- let subreddits: string[];
45
- if (args.subreddits && args.subreddits.length > 0) {
46
- subreddits = args.subreddits;
47
- } else if (args.subreddit) {
48
- subreddits = [args.subreddit];
49
- } else {
50
- subreddits = config.sentiment?.defaultSubreddits ?? ["wallstreetbets", "stocks", "investing", "options"];
51
- }
46
+ const INSTALL_CONTINUE_LABEL = "Continue after installing Reddit";
47
+ const SESSION_CONTINUE_LABEL = "Continue after logging in to Reddit";
48
+ const SKIP_ONCE_LABEL = "Skip Reddit once";
49
+ const ALWAYS_SKIP_LABEL = "Always skip Reddit";
52
50
 
53
- // Fetch from all subreddits
54
- const allResults: RedditSentimentResult[] = [];
55
- const warnings: string[] = [];
56
- for (const sub of subreddits) {
57
- const providerResult = await wrapProvider("reddit", () => getSubredditPosts(sub, limit));
58
- if (providerResult.status === "unavailable") {
59
- warnings.push(`r/${sub}: ${providerResult.reason}`);
60
- continue;
51
+ interface ExtensionContextWithAskUserHandler extends Partial<ExtensionContext> {
52
+ askUserHandler?: AskUserHandler;
53
+ }
54
+
55
+ interface RedditSentimentToolOptions {
56
+ askUserHandler?: AskUserHandler;
57
+ }
58
+
59
+ type RedditToolDetails = RedditSentimentResult | null;
60
+
61
+ export function createRedditSentimentTool(
62
+ options: RedditSentimentToolOptions = {},
63
+ ): AgentTool<typeof params, RedditToolDetails> {
64
+ return {
65
+ name: "get_reddit_sentiment",
66
+ label: "Reddit Sentiment",
67
+ description:
68
+ "Analyze sentiment from financial Reddit communities. Supports single subreddit, multi-subreddit, and topic filtering. Returns scored posts with comment analysis and trend context.",
69
+ parameters: params,
70
+ async execute(_toolCallId, args, _signal, _onUpdate, ctx?: ExtensionContext) {
71
+ const limit = Math.min(args.limit ?? 25, 100);
72
+ const config = getConfig();
73
+ const state = loadOnboardingState();
74
+ const descriptor = getProvider("reddit");
75
+ if (state.providers.reddit?.status === "never_ask") {
76
+ return skippedRedditResult(
77
+ "You previously asked not to be reminded about Reddit.",
78
+ "run opencandle doctor --enable reddit to re-enable Reddit sentiment (silenced)",
79
+ true,
80
+ );
61
81
  }
62
- allResults.push(providerResult.data);
63
- }
64
82
 
65
- if (allResults.length === 0) {
66
- return {
67
- content: [{ type: "text", text: `⚠ Reddit sentiment unavailable (${warnings.join("; ")}).` }],
68
- details: null as any,
69
- };
70
- }
83
+ // Determine subreddits to search
84
+ let subreddits: string[];
85
+ if (args.subreddits && args.subreddits.length > 0) {
86
+ subreddits = args.subreddits;
87
+ } else if (args.subreddit) {
88
+ subreddits = [args.subreddit];
89
+ } else {
90
+ subreddits = config.sentiment?.defaultSubreddits ?? [
91
+ "wallstreetbets",
92
+ "stocks",
93
+ "investing",
94
+ "options",
95
+ ];
96
+ }
97
+
98
+ let { allResults, warnings } = await fetchRedditSubreddits(subreddits, limit, args.query);
99
+ let retriedAfterContinue = false;
100
+ let promptedSetupIssue: ReturnType<typeof classifyRedditSetupIssue> = null;
101
+
102
+ if (allResults.length === 0) {
103
+ const setupIssue = classifyRedditSetupIssue(warnings);
104
+ if (setupIssue) {
105
+ promptedSetupIssue = setupIssue;
106
+ const askUserHandler =
107
+ options.askUserHandler ??
108
+ (ctx as ExtensionContextWithAskUserHandler | undefined)?.askUserHandler;
109
+ const canAsk = askUserHandler != null || ctx?.hasUI === true;
110
+ if (canAsk) {
111
+ const promptResult = await promptUser(
112
+ (ctx ?? { hasUI: false }) as ExtensionContext,
113
+ {
114
+ question:
115
+ setupIssue === "not_installed"
116
+ ? "Reddit sentiment needs the local rdt-cli command before it can fetch discussions. Install command: uv tool install rdt-cli. How would you like to proceed?"
117
+ : "Reddit sentiment needs an active Reddit browser session for rdt-cli. Login command: rdt login. How would you like to proceed?",
118
+ questionType: "select",
119
+ options: [
120
+ setupIssue === "not_installed" ? INSTALL_CONTINUE_LABEL : SESSION_CONTINUE_LABEL,
121
+ SKIP_ONCE_LABEL,
122
+ ALWAYS_SKIP_LABEL,
123
+ ],
124
+ reason: "Reddit sentiment needs rdt-cli and a Reddit browser session.",
125
+ },
126
+ askUserHandler,
127
+ );
71
128
 
72
- // Merge and filter by query if provided
73
- const adapter = new RedditAdapter();
74
- let allRecords = allResults.flatMap((r) => adapter.mapPostsToRecords(r, args.query ?? subreddits.join("+")));
129
+ if (!promptResult.cancelled && promptResult.answer?.startsWith("Continue")) {
130
+ retriedAfterContinue = true;
131
+ ({ allResults, warnings } = await fetchRedditSubreddits(
132
+ subreddits,
133
+ limit,
134
+ args.query,
135
+ ));
136
+ } else if (!promptResult.cancelled && promptResult.answer === ALWAYS_SKIP_LABEL) {
137
+ saveOnboardingState(markProviderNeverAsk(state, "reddit"));
138
+ return skippedRedditResult(
139
+ `User chose to always skip ${descriptor.displayName} data. Do not ask about Reddit setup again unless the user explicitly re-enables it.`,
140
+ "run opencandle doctor --enable reddit to re-enable Reddit sentiment (silenced)",
141
+ true,
142
+ );
143
+ } else {
144
+ return skippedRedditResult(
145
+ `User chose to skip ${descriptor.displayName} data for this request. Do not ask about Reddit setup again in this turn.`,
146
+ "user chose to skip Reddit for this request",
147
+ false,
148
+ );
149
+ }
150
+ }
151
+ }
152
+ }
153
+
154
+ if (allResults.length === 0) {
155
+ const setupIssue = classifyRedditSetupIssue(warnings);
156
+ if (retriedAfterContinue && (!setupIssue || setupIssue === promptedSetupIssue)) {
157
+ return unavailableRedditResult(warnings);
158
+ }
159
+ if (setupIssue) {
160
+ const tag = buildExternalToolRequiredTag({
161
+ provider: "reddit",
162
+ reason: setupIssue,
163
+ installCmd: "uv tool install rdt-cli",
164
+ loginCmd: "rdt login",
165
+ fallback: "twitter-web-news",
166
+ });
167
+ const guidance =
168
+ setupIssue === "not_installed"
169
+ ? "Reddit sentiment requires the local rdt-cli tool. Install it with `uv tool install rdt-cli`, then run `rdt login` if needed."
170
+ : "Reddit sentiment requires an active Reddit browser session for rdt-cli. Run `rdt login` or refresh your Reddit browser login.";
171
+ return {
172
+ content: [{ type: "text", text: `${tag}\n\n${guidance}` }],
173
+ details: null,
174
+ };
175
+ }
176
+ return unavailableRedditResult(warnings);
177
+ }
75
178
 
76
- // Topic filtering
77
- if (args.query) {
78
- const queryLower = args.query.toLowerCase();
79
- allRecords = allRecords.filter((r) =>
80
- r.text.toLowerCase().includes(queryLower) ||
81
- (r.title?.toLowerCase().includes(queryLower) ?? false),
179
+ // Merge records returned by the provider. rdt-cli handles query-bearing
180
+ // requests server-side; the local filter remains a defensive post-filter.
181
+ const adapter = new RedditAdapter();
182
+ let allRecords = allResults.flatMap((r) =>
183
+ adapter.mapPostsToRecords(r, args.query ?? subreddits.join("+")),
82
184
  );
83
- }
185
+ const queryTerms = args.query ? sentimentQueryTerms(args.query) : null;
84
186
 
85
- // Deduplicate by sourceId (crossposts)
86
- const seen = new Set<string>();
87
- allRecords = allRecords.filter((r) => {
88
- if (seen.has(r.sourceId)) return false;
89
- seen.add(r.sourceId);
90
- return true;
91
- });
92
-
93
- // Fetch comments for top 10 posts by engagement
94
- const commentsPerPost = config.sentiment?.commentsPerPost ?? 5;
95
- const topPosts = [...allRecords]
96
- .sort((a, b) => b.engagement.score - a.engagement.score)
97
- .slice(0, 10);
98
-
99
- for (const post of topPosts) {
100
- const sub = (post.metadata.subreddit as string) ?? subreddits[0];
101
- if ((post.engagement.replies ?? 0) === 0) continue;
102
- try {
103
- const comments = await getPostComments(sub, post.sourceId, commentsPerPost);
104
- const commentRecords = adapter.mapCommentsToRecords(
105
- comments,
106
- post.sourceId,
107
- sub,
108
- args.query ?? subreddits.join("+"),
109
- );
110
- allRecords.push(...commentRecords);
111
- } catch {
112
- // Comment fetch failures are non-fatal
187
+ // Topic filtering
188
+ if (queryTerms) {
189
+ allRecords = allRecords.filter((record) => recordMatchesSentimentQuery(record, queryTerms));
113
190
  }
114
- }
115
191
 
116
- // Process through pipeline
117
- const pipeline = getSentimentPipeline();
118
- const pipelineResult = await pipeline.processRecords(allRecords, args.query ?? subreddits.join("+"));
119
-
120
- // Build output using first result as base for backward compatibility
121
- const firstResult = allResults[0];
122
- const postRecords = pipelineResult.fresh.filter((r) => !r.metadata.isComment);
123
- const commentRecords = pipelineResult.fresh.filter((r) => r.metadata.isComment);
124
- const avgScore = postRecords.length > 0
125
- ? postRecords.reduce((s, r) => s + r.sentiment.score, 0) / postRecords.length
126
- : 0;
127
-
128
- const sentimentLabel =
129
- avgScore > 0.3 ? "Bullish" :
130
- avgScore < -0.3 ? "Bearish" :
131
- avgScore > 0 ? "Leaning Bullish" :
132
- avgScore < 0 ? "Leaning Bearish" : "Neutral";
133
-
134
- const subLabel = subreddits.length === 1 ? `r/${subreddits[0]}` : `${subreddits.length} subreddits`;
135
- const lines = [
136
- `**Reddit: ${args.query ?? subLabel}** — ${postRecords.length} posts, ${commentRecords.length} comments`,
137
- `Sentiment: ${avgScore.toFixed(2)} (${sentimentLabel})`,
138
- ];
139
-
140
- if (firstResult.topMentions.length > 0) {
141
- lines.push(`Tickers: ${firstResult.topMentions.map((t) => `$${t}`).join(", ")}`);
142
- }
192
+ // Deduplicate by sourceId (crossposts)
193
+ const seen = new Set<string>();
194
+ allRecords = allRecords.filter((r) => {
195
+ if (seen.has(r.sourceId)) return false;
196
+ seen.add(r.sourceId);
197
+ return true;
198
+ });
143
199
 
144
- lines.push("");
145
- lines.push("Top posts:");
146
- for (const post of postRecords.slice(0, 10)) {
147
- const scoreIndicator = post.sentiment.score > 0 ? "🟢" : post.sentiment.score < 0 ? "🔴" : "⚪";
148
- lines.push(` ${scoreIndicator} ⬆${post.engagement.score} 💬${post.engagement.replies ?? 0} — ${(post.title ?? post.text).slice(0, 100)}`);
149
- }
200
+ // Fetch comments for top 10 posts by engagement
201
+ const commentsPerPost = config.sentiment?.commentsPerPost ?? 5;
202
+ const topPosts = [...allRecords]
203
+ .sort((a, b) => b.engagement.score - a.engagement.score)
204
+ .slice(0, 10);
150
205
 
151
- if (pipelineResult.trend && pipelineResult.trend.length > 0) {
152
- const t = pipelineResult.trend[0];
153
- lines.push("");
154
- lines.push(`Trend: ${t.sparkline} ${t.direction} (${t.delta >= 0 ? "+" : ""}${t.delta.toFixed(2)}, ${t.count} records)`);
155
- }
206
+ for (const post of topPosts) {
207
+ const sub = (post.metadata.subreddit as string) ?? subreddits[0];
208
+ if ((post.engagement.replies ?? 0) === 0) continue;
209
+ try {
210
+ const comments = await getPostComments(sub, post.sourceId, commentsPerPost);
211
+ const commentRecords = adapter.mapCommentsToRecords(
212
+ comments,
213
+ post.sourceId,
214
+ sub,
215
+ args.query ?? subreddits.join("+"),
216
+ );
217
+ allRecords.push(
218
+ ...(queryTerms
219
+ ? commentRecords.filter((record) => recordMatchesSentimentQuery(record, queryTerms))
220
+ : commentRecords),
221
+ );
222
+ } catch {
223
+ // Comment fetch failures are non-fatal
224
+ }
225
+ }
226
+
227
+ // Process through pipeline
228
+ const pipeline = getSentimentPipeline();
229
+ const pipelineResult = await pipeline.processRecords(
230
+ allRecords,
231
+ args.query ?? subreddits.join("+"),
232
+ );
233
+
234
+ // Build output using first result as base for backward compatibility
235
+ const firstResult = allResults[0];
236
+ const postRecords = pipelineResult.fresh.filter((r) => !r.metadata.isComment);
237
+ const commentRecords = pipelineResult.fresh.filter((r) => r.metadata.isComment);
238
+ const sentimentRecords = pipelineResult.fresh;
239
+ const rawPostsById = new Map(
240
+ allResults.flatMap((result) => result.posts.map((post) => [post.id, post] as const)),
241
+ );
242
+ const aggregatePosts: RedditSentimentResult["posts"] = postRecords.map((record) => {
243
+ const rawPost = rawPostsById.get(record.sourceId);
244
+ return {
245
+ id: record.sourceId,
246
+ title: rawPost?.title ?? record.title ?? "",
247
+ selftext: rawPost?.selftext ?? "",
248
+ author: rawPost?.author ?? record.author ?? "unknown",
249
+ score: rawPost?.score ?? record.engagement.score,
250
+ comments: rawPost?.comments ?? record.engagement.replies ?? 0,
251
+ url: rawPost?.url ?? record.url,
252
+ created: rawPost?.created ?? record.publishedAt ?? record.fetchedAt,
253
+ };
254
+ });
255
+ const aggregateTopMentions = [
256
+ ...new Set(postRecords.flatMap((record) => record.sentiment.tickers)),
257
+ ];
258
+ const avgScore =
259
+ sentimentRecords.length > 0
260
+ ? sentimentRecords.reduce((s, r) => s + r.sentiment.score, 0) / sentimentRecords.length
261
+ : 0;
262
+
263
+ const sentimentLabel =
264
+ avgScore > 0.3
265
+ ? "Bullish"
266
+ : avgScore < -0.3
267
+ ? "Bearish"
268
+ : avgScore > 0
269
+ ? "Leaning Bullish"
270
+ : avgScore < 0
271
+ ? "Leaning Bearish"
272
+ : "Neutral";
273
+
274
+ const subLabel =
275
+ subreddits.length === 1 ? `r/${subreddits[0]}` : `${subreddits.length} subreddits`;
276
+ const lines = [
277
+ `**Reddit: ${args.query ?? subLabel}** — ${postRecords.length} posts, ${commentRecords.length} comments`,
278
+ `Sentiment: ${avgScore.toFixed(2)} (${sentimentLabel})`,
279
+ ];
280
+
281
+ const details: RedditSentimentResult = {
282
+ ...firstResult,
283
+ subreddit: subreddits.length === 1 ? firstResult.subreddit : subreddits.join(","),
284
+ postCount: postRecords.length,
285
+ posts: aggregatePosts,
286
+ topMentions: aggregateTopMentions,
287
+ sentimentScore: avgScore,
288
+ bullishCount: sentimentRecords.filter((record) => record.sentiment.score > 0).length,
289
+ bearishCount: sentimentRecords.filter((record) => record.sentiment.score < 0).length,
290
+ insight: pipelineResult.insight,
291
+ records: pipelineResult.fresh,
292
+ };
293
+
294
+ if (pipelineResult.insight) {
295
+ lines.push(...formatInsightSection(pipelineResult.insight));
296
+ }
297
+
298
+ if (aggregateTopMentions.length > 0) {
299
+ lines.push(`Tickers: ${aggregateTopMentions.map((t) => `$${t}`).join(", ")}`);
300
+ }
156
301
 
157
- if (warnings.length > 0) {
158
302
  lines.push("");
159
- lines.push(`⚠ ${warnings.join("; ")}`);
303
+ lines.push(untrustedContentHeader("Reddit posts"));
304
+ for (const post of postRecords.slice(0, 10)) {
305
+ const scoreIndicator =
306
+ post.sentiment.score > 0 ? "🟢" : post.sentiment.score < 0 ? "🔴" : "⚪";
307
+ lines.push(
308
+ ` ${scoreIndicator} ⬆${post.engagement.score} 💬${post.engagement.replies ?? 0} — ${renderUntrustedText(post.title ?? post.text, 100)}`,
309
+ );
310
+ }
311
+
312
+ if (pipelineResult.trend && pipelineResult.trend.length > 0) {
313
+ const t = pipelineResult.trend[0];
314
+ lines.push("");
315
+ lines.push(
316
+ `Trend: ${t.sparkline} ${t.direction} (${t.delta >= 0 ? "+" : ""}${t.delta.toFixed(2)}, ${t.count} records)`,
317
+ );
318
+ }
319
+
320
+ if (warnings.length > 0) {
321
+ lines.push("");
322
+ lines.push(`⚠ ${warnings.join("; ")}`);
323
+ }
324
+
325
+ return { content: [{ type: "text", text: lines.join("\n") }], details };
326
+ },
327
+ };
328
+ }
329
+
330
+ export const redditSentimentTool = createRedditSentimentTool();
331
+
332
+ function unavailableRedditResult(warnings: readonly string[]): {
333
+ content: [{ type: "text"; text: string }];
334
+ details: null;
335
+ } {
336
+ return {
337
+ content: [{ type: "text", text: `⚠ Reddit sentiment unavailable (${warnings.join("; ")}).` }],
338
+ details: null,
339
+ };
340
+ }
341
+
342
+ async function fetchRedditSubreddits(
343
+ subreddits: readonly string[],
344
+ limit: number,
345
+ query: string | undefined,
346
+ ): Promise<{ allResults: RedditSentimentResult[]; warnings: string[] }> {
347
+ const allResults: RedditSentimentResult[] = [];
348
+ const warnings: string[] = [];
349
+ for (const sub of subreddits) {
350
+ const providerResult = await wrapProvider("reddit", () => getSubredditPosts(sub, limit, query));
351
+ if (providerResult.status === "unavailable") {
352
+ warnings.push(`r/${sub}: ${providerResult.reason}`);
353
+ continue;
160
354
  }
355
+ allResults.push(providerResult.data);
356
+ }
357
+ return { allResults, warnings };
358
+ }
359
+
360
+ function skippedRedditResult(
361
+ message: string,
362
+ remediation: string,
363
+ silenced: boolean,
364
+ ): { content: [{ type: "text"; text: string }]; details: null } {
365
+ return {
366
+ content: [
367
+ {
368
+ type: "text",
369
+ text: `${buildSkippedTag({
370
+ provider: "reddit",
371
+ reason: "credential_not_provided",
372
+ remediation,
373
+ silenced,
374
+ })}\n\n${message}`,
375
+ },
376
+ ],
377
+ details: null,
378
+ };
379
+ }
161
380
 
162
- return { content: [{ type: "text", text: lines.join("\n") }], details: firstResult };
163
- },
164
- };
381
+ function classifyRedditSetupIssue(
382
+ warnings: readonly string[],
383
+ ): "not_installed" | "session_missing" | "session_stale" | null {
384
+ const combined = warnings.join("\n").toLowerCase();
385
+ if (combined.includes("not installed") || combined.includes("enoent")) {
386
+ return "not_installed";
387
+ }
388
+ if (
389
+ combined.includes("no reddit cookies") ||
390
+ combined.includes("not authenticated") ||
391
+ combined.includes("rdt login")
392
+ ) {
393
+ return "session_missing";
394
+ }
395
+ if (
396
+ combined.includes("401") ||
397
+ combined.includes("unauthorized") ||
398
+ combined.includes("expired")
399
+ ) {
400
+ return "session_stale";
401
+ }
402
+ return null;
403
+ }