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,3 +1,4 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import { createReadStream, existsSync } from "node:fs";
2
3
  import { createServer, type IncomingMessage, type ServerResponse } from "node:http";
3
4
  import { extname, join, resolve } from "node:path";
@@ -6,45 +7,52 @@ import {
6
7
  AuthStorage,
7
8
  createAgentSessionRuntime,
8
9
  createAgentSessionServices,
9
- type AgentSession,
10
10
  getAgentDir,
11
11
  ModelRegistry,
12
12
  SessionManager,
13
13
  SettingsManager,
14
14
  } from "@earendil-works/pi-coding-agent";
15
15
  import { createOpenCandleSession } from "../../src/index.js";
16
- import { getAllTools } from "../../src/tools/index.js";
17
- import { persistProviderCredential } from "../../src/onboarding/connect.js";
16
+ import { probeProviderStatus } from "../../src/onboarding/provider-status.js";
17
+ import type { ChatEvent } from "../shared/chat-events.js";
18
+ import { createAskUserBridge } from "./ask-user-bridge.js";
18
19
  import {
19
- getCredentialSource,
20
- PROVIDERS,
21
- type ProviderId,
22
- } from "../../src/onboarding/providers.js";
23
- import { validateCredential } from "../../src/onboarding/validation.js";
24
- import { buildModelSetupState, findPreferredModel, modelSetupProviders } from "./model-setup.js";
25
- import { projectDashboard } from "./projector.js";
26
- import { acceptWebSocket, type WsClient } from "./websocket.js";
27
- import { invokeToolFromUi } from "./invoke-tool.js";
28
- import { buildCatalog, setToolEnabled } from "./tool-metadata.js";
29
- import { deleteSessionFile, renameSessionFile } from "./session-actions.js";
30
- import { acquireWriterLock, refreshWriterLock, releaseWriterLock } from "./writer-lock.js";
20
+ createLocalAutomationHeartbeat,
21
+ normalizeAutomationHeartbeatMs,
22
+ } from "./automation-heartbeat.js";
23
+ import {
24
+ type BackgroundQuotePoller,
25
+ BackgroundQuoteRefreshes,
26
+ createBackgroundQuotePoller,
27
+ } from "./background-quotes.js";
31
28
  import { sessionEntriesToChatEvents } from "./chat-event-adapter.js";
29
+ import { chatRunSessionConflict } from "./chat-run-session.js";
30
+ import { createInitialGuiSessionManager } from "./gui-session-manager.js";
31
+ import { createToolInvokeController } from "./invoke-tool.js";
32
32
  import { createLiveChatEventAdapter } from "./live-chat-event-adapter.js";
33
- import { waitForNewEntryId, waitForSessionTurnSettlement } from "./session-entry-wait.js";
34
33
  import {
35
- createPromptObservation,
36
- observePromptEvent,
37
- selectReplayPrompt,
38
- type PromptObservation,
39
- } from "./prompt-observation.js";
40
- import { BackgroundQuoteRefreshes } from "./background-quotes.js";
41
- import { createAskUserBridge } from "./ask-user-bridge.js";
42
- import { createInitialGuiSessionManager } from "./gui-session-manager.js";
43
- import type { ChatEvent } from "../shared/chat-events.js";
34
+ buildMarketStateQuoteSnapshot,
35
+ buildMarketStateSnapshot,
36
+ searchInstrumentCandidates,
37
+ } from "./market-state-api.js";
38
+ import { buildModelSetupState, createModelSetupController } from "./model-setup.js";
39
+ import { isTrustedPrivateApiRequest, privateApiCookieHeader } from "./private-api-access.js";
40
+ import { createPromptObservation, observePromptEvent } from "./prompt-observation.js";
41
+ import { QuoteSnapshotStore } from "./quote-snapshot-store.js";
42
+ import { createSessionActionsController, promptAndSettle } from "./session-actions.js";
43
+ import { waitForNewEntryId } from "./session-entry-wait.js";
44
+ import { createGracefulShutdown } from "./shutdown.js";
45
+ import { acquireWriterLock, refreshWriterLock, releaseWriterLock } from "./writer-lock.js";
46
+ import { createWsHub, type WsHub } from "./ws-hub.js";
44
47
 
45
48
  const cwd = process.cwd();
46
49
  const host = process.env.OPENCANDLE_GUI_HOST ?? "127.0.0.1";
47
50
  const port = Number(process.env.OPENCANDLE_GUI_PORT ?? 14567);
51
+ const automationHeartbeatMs = normalizeAutomationHeartbeatMs(
52
+ process.env.OPENCANDLE_AUTOMATION_HEARTBEAT_MS,
53
+ );
54
+ const allowRemotePrivateApi = process.env.OPENCANDLE_GUI_ALLOW_REMOTE_PRIVATE_API === "1";
55
+ const privateApiSessionToken = randomBytes(32).toString("base64url");
48
56
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
49
57
  const webDist = resolve(__dirname, "../web/dist");
50
58
 
@@ -56,8 +64,10 @@ const initialSessionManager = createInitialGuiSessionManager(cwd);
56
64
  let sessionManager = initialSessionManager;
57
65
  const sessionDir = sessionManager.getSessionDir();
58
66
  const lockResult = await acquireWriterLock(sessionDir, "gui");
67
+ let wsHub: WsHub;
68
+ let quotePoller: BackgroundQuotePoller;
59
69
  const askUserBridge = createAskUserBridge({
60
- broadcast,
70
+ broadcast: (message) => wsHub.broadcast(message),
61
71
  getSessionId: () => sessionManager.getSessionId(),
62
72
  });
63
73
  const runtime = await createAgentSessionRuntime(
@@ -83,18 +93,71 @@ const runtime = await createAgentSessionRuntime(
83
93
  { cwd, agentDir, sessionManager },
84
94
  );
85
95
  let session = runtime.session;
86
- const clients = new Set<WsClient>();
87
96
  const heartbeat = setInterval(() => refreshWriterLock(sessionDir), 5000);
88
97
  const backgroundQuoteRefreshes = new BackgroundQuoteRefreshes();
89
- let poller: NodeJS.Timeout | null = null;
90
- let quotePollInFlight = false;
98
+ const quoteSnapshotStore = new QuoteSnapshotStore(() => buildMarketStateQuoteSnapshot());
99
+ quotePoller = createBackgroundQuotePoller({
100
+ getClientCount: () => wsHub.getClientCount(),
101
+ getSessionManager: () => sessionManager,
102
+ refreshes: backgroundQuoteRefreshes,
103
+ broadcastState: () => wsHub.broadcastState(),
104
+ });
105
+ const localAutomationHeartbeat = createLocalAutomationHeartbeat({
106
+ role: lockResult.role,
107
+ getSessionId: () => sessionManager.getSessionId(),
108
+ intervalMs: automationHeartbeatMs,
109
+ });
110
+ const modelSetupController = createModelSetupController({
111
+ role: lockResult.role,
112
+ getSession: () => session,
113
+ getSessionManager: () => sessionManager,
114
+ broadcastState: () => wsHub.broadcastState(),
115
+ });
116
+ const toolInvokeController = createToolInvokeController({
117
+ role: lockResult.role,
118
+ getSessionManager: () => sessionManager,
119
+ broadcastState: () => wsHub.broadcastState(),
120
+ onMarketStateChanged: () => quoteSnapshotStore.invalidate(),
121
+ askUserHandler: askUserBridge.ask,
122
+ });
123
+ const sessionActionsController = createSessionActionsController({
124
+ role: lockResult.role,
125
+ cwd,
126
+ sessionDir,
127
+ getSession: () => session,
128
+ getSessionManager: () => sessionManager,
129
+ getModelSetupState: () => modelSetupController.buildCurrentModelSetupState(),
130
+ askUserBridge,
131
+ runtime,
132
+ sendBoot: (client) => wsHub.sendBoot(client),
133
+ broadcastState: () => wsHub.broadcastState(),
134
+ broadcastSessions: () => wsHub.broadcastSessions(),
135
+ });
136
+ wsHub = createWsHub({
137
+ role: lockResult.role,
138
+ lock: lockResult.lock,
139
+ cwd,
140
+ sessionDir,
141
+ getSession: () => session,
142
+ getSessionManager: () => sessionManager,
143
+ backgroundQuoteRefreshes,
144
+ askUserBridge,
145
+ modelSetupController,
146
+ toolInvokeController,
147
+ sessionActionsController,
148
+ onClientCountChanged: () => quotePoller.updatePoller(),
149
+ isTrustedRequest: (req) =>
150
+ isTrustedPrivateApiRequest(req.headers, privateApiSessionToken, req.socket.remoteAddress, {
151
+ allowRemote: allowRemotePrivateApi,
152
+ }),
153
+ });
91
154
 
92
- let unsubscribeSession = subscribeToSessionEvents();
155
+ let unsubscribeSession = wsHub.subscribeToSessionEvents();
93
156
  runtime.setRebindSession(async (nextSession) => {
94
157
  unsubscribeSession();
95
158
  session = nextSession;
96
159
  sessionManager = nextSession.sessionManager;
97
- unsubscribeSession = subscribeToSessionEvents();
160
+ unsubscribeSession = wsHub.subscribeToSessionEvents();
98
161
  });
99
162
 
100
163
  const server = createServer((req, res) => {
@@ -109,19 +172,26 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
109
172
  }
110
173
 
111
174
  if (url.pathname === "/api/bootstrap" && req.method === "GET") {
112
- writeJson(res, {
113
- role: lockResult.role,
114
- sessionId: sessionManager.getSessionId(),
115
- catalog: buildCatalog(),
116
- modelSetup: buildCurrentModelSetupState(),
117
- askUserPrompts: askUserBridge.getPrompts(),
118
- sessions: await SessionManager.list(cwd, sessionDir),
119
- snapshot: buildStateSnapshot(),
120
- });
175
+ if (!allowTrustedGuiRequest(req, res, "Bootstrap API")) return;
176
+ writeJson(res, await wsHub.buildBootstrapPayload());
177
+ return;
178
+ }
179
+
180
+ if (url.pathname === "/api/session/new" && req.method === "POST") {
181
+ if (!allowTrustedGuiRequest(req, res, "Session API")) return;
182
+ if (lockResult.role !== "writer") {
183
+ writeJson(res, { error: "Read-only follower mode" }, 409);
184
+ return;
185
+ }
186
+ await sessionActionsController.handleNewSession();
187
+ wsHub.broadcastState();
188
+ wsHub.broadcastSessions();
189
+ writeJson(res, await wsHub.buildBootstrapPayload());
121
190
  return;
122
191
  }
123
192
 
124
193
  if (url.pathname === "/api/sessions" && req.method === "GET") {
194
+ if (!allowTrustedGuiRequest(req, res, "Session API")) return;
125
195
  writeJson(res, {
126
196
  currentSessionId: sessionManager.getSessionId(),
127
197
  role: lockResult.role,
@@ -131,15 +201,51 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
131
201
  }
132
202
 
133
203
  if (url.pathname === "/api/session/events" && req.method === "GET") {
204
+ if (!allowTrustedGuiRequest(req, res, "Session API")) return;
134
205
  writeJson(res, {
135
206
  sessionId: sessionManager.getSessionId(),
136
207
  role: lockResult.role,
137
- events: currentChatEvents(),
208
+ events: wsHub.currentChatEvents(),
138
209
  });
139
210
  return;
140
211
  }
141
212
 
213
+ if (url.pathname === "/api/market-state" && req.method === "GET") {
214
+ if (!allowTrustedGuiRequest(req, res, "Market-state API")) return;
215
+ writeJson(res, buildMarketStateSnapshot());
216
+ return;
217
+ }
218
+
219
+ if (url.pathname === "/api/market-state/quotes" && req.method === "GET") {
220
+ if (!allowTrustedGuiRequest(req, res, "Market-state API")) return;
221
+ writeJson(res, await quoteSnapshotStore.get());
222
+ return;
223
+ }
224
+
225
+ if (url.pathname === "/api/instruments/search" && req.method === "GET") {
226
+ if (!allowTrustedGuiRequest(req, res, "Market-state API")) return;
227
+ writeJson(res, await searchInstrumentCandidates(url.searchParams.get("q") ?? ""));
228
+ return;
229
+ }
230
+
231
+ if (url.pathname === "/api/diagnostics/twitter-cli" && req.method === "GET") {
232
+ if (!allowTrustedGuiRequest(req, res, "Diagnostics API")) return;
233
+ const mode = url.searchParams.get("mode") === "session" ? "session" : "install";
234
+ const force = url.searchParams.get("force") === "1";
235
+ writeJson(res, await probeProviderStatus("twitter", { mode, force }));
236
+ return;
237
+ }
238
+
239
+ if (url.pathname === "/api/diagnostics/reddit-cli" && req.method === "GET") {
240
+ if (!allowTrustedGuiRequest(req, res, "Diagnostics API")) return;
241
+ const mode = url.searchParams.get("mode") === "session" ? "session" : "install";
242
+ const force = url.searchParams.get("force") === "1";
243
+ writeJson(res, await probeProviderStatus("reddit", { mode, force }));
244
+ return;
245
+ }
246
+
142
247
  if (url.pathname === "/api/chat/run" && req.method === "POST") {
248
+ if (!allowTrustedGuiRequest(req, res, "Chat run API")) return;
143
249
  await handleSseChatRun(req, res);
144
250
  return;
145
251
  }
@@ -149,7 +255,7 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
149
255
  if (!path.startsWith(webDist) || !existsSync(path)) {
150
256
  const fallback = resolve(join(webDist, "index.html"));
151
257
  if (!extname(requested) && fallback.startsWith(webDist) && existsSync(fallback)) {
152
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
258
+ res.writeHead(200, privateGuiHeaders("text/html; charset=utf-8"));
153
259
  createReadStream(fallback).pipe(res);
154
260
  return;
155
261
  }
@@ -157,298 +263,43 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
157
263
  return;
158
264
  }
159
265
 
160
- res.writeHead(200, { "content-type": contentType(path) });
266
+ const type = contentType(path);
267
+ res.writeHead(200, privateGuiHeaders(type));
161
268
  createReadStream(path).pipe(res);
162
269
  }
163
270
 
164
- server.on("upgrade", (req, socket) => {
165
- if (req.url !== "/ws") {
166
- socket.destroy();
167
- return;
168
- }
169
-
170
- const client = acceptWebSocket(req, socket);
171
- clients.add(client);
172
- client.onClose(() => {
173
- clients.delete(client);
174
- updatePoller();
175
- });
176
- client.onMessage((message) => void handleClientMessage(client, message));
177
- sendBoot(client);
178
- updatePoller();
179
- });
271
+ server.on("upgrade", (req, socket) => wsHub.handleUpgrade(req, socket));
180
272
 
181
273
  server.listen(port, host, () => {
182
274
  console.log(`OpenCandle GUI listening on http://${host}:${port}`);
183
275
  if (host === "0.0.0.0") {
184
276
  console.log(`OpenCandle GUI is accepting LAN/Tailscale connections on port ${port}`);
185
277
  }
186
- console.log(`Writer role: ${lockResult.role}`);
187
- });
188
-
189
- process.on("SIGINT", shutdown);
190
- process.on("SIGTERM", shutdown);
191
-
192
- async function handleClientMessage(client: WsClient, message: unknown): Promise<void> {
193
- const data = asRecord(message);
194
- try {
195
- switch (data.type) {
196
- case "chat.prompt":
197
- await handlePrompt(String(data.prompt ?? ""));
198
- break;
199
- case "ask_user.answer":
200
- await handleAskUserAnswer(String(data.id ?? ""), data.answer);
201
- break;
202
- case "ask_user.cancel":
203
- await handleAskUserCancel(String(data.id ?? ""));
204
- break;
205
- case "tool.invoke":
206
- await handleToolInvoke(String(data.toolName ?? ""), asRecord(data.args));
207
- break;
208
- case "tool.enabled":
209
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
210
- setToolEnabled(String(data.toolName), Boolean(data.enabled));
211
- broadcast({ type: "catalog", catalog: buildCatalog(), restartRequired: true });
212
- break;
213
- case "catalog.refresh":
214
- client.send({ type: "catalog", catalog: buildCatalog() });
215
- break;
216
- case "model.setup.refresh":
217
- session.modelRegistry.refresh();
218
- broadcastModelSetup();
219
- break;
220
- case "model.setup.save_api_key":
221
- await handleSaveModelApiKey(String(data.provider ?? ""), String(data.apiKey ?? ""));
222
- broadcastModelSetup();
223
- break;
224
- case "model.setup.select_model":
225
- await handleSelectModel(String(data.provider ?? ""), String(data.modelId ?? ""));
226
- broadcastModelSetup();
227
- break;
228
- case "provider.save_api_key":
229
- await handleSaveProviderApiKey(String(data.providerId ?? ""), String(data.apiKey ?? ""));
230
- broadcast({ type: "catalog", catalog: buildCatalog() });
231
- break;
232
- case "session.new":
233
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
234
- await handleNewSession();
235
- sendBoot(client);
236
- broadcastState();
237
- broadcastSessions();
238
- break;
239
- case "session.open":
240
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
241
- await handleOpenSession(String(data.path ?? ""));
242
- sendBoot(client);
243
- broadcastState();
244
- broadcastSessions();
245
- break;
246
- case "session.rename":
247
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
248
- await handleRenameSession(String(data.path ?? ""), String(data.name ?? ""));
249
- broadcastState();
250
- broadcastSessions();
251
- break;
252
- case "session.delete":
253
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
254
- await handleDeleteSession(client, String(data.path ?? ""));
255
- break;
256
- }
257
- } catch (error) {
258
- const message = error instanceof Error ? error.message : String(error);
259
- client.send({
260
- type: "error",
261
- message,
262
- });
263
- }
264
- }
265
-
266
- async function handlePrompt(prompt: string): Promise<void> {
267
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
268
-
269
- const modelSetup = buildCurrentModelSetupState();
270
- const trimmedPrompt = prompt.trim();
271
- if (!trimmedPrompt.startsWith("/") && modelSetup.requirement !== "ready") {
272
- sessionManager.appendMessage({ role: "user", content: prompt, timestamp: Date.now() });
273
- broadcastState();
274
- const message =
275
- modelSetup.requirement === "select_model"
276
- ? "Choose an available model before chat can run. OpenCandle found configured credentials but no active model."
277
- : "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
278
- sessionManager.appendCustomMessageEntry(
279
- "opencandle-model-setup",
280
- message,
281
- true,
282
- { source: "gui", requirement: modelSetup.requirement },
283
- );
284
- broadcastState();
285
- return;
286
- }
287
-
288
- const beforeIds = new Set(sessionManager.getEntries().map((entry) => entry.id));
289
- await promptAndSettle(session, prompt, beforeIds);
290
- broadcastState();
291
- }
292
-
293
- async function handleAskUserAnswer(id: string, value: unknown): Promise<void> {
294
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
295
- const answer = String(value ?? "").trim();
296
- if (!answer) throw new Error("Answer cannot be empty");
297
- if (!askUserBridge.answer(id, answer)) throw new Error("Unknown or resolved question");
298
- }
299
-
300
- async function handleAskUserCancel(id: string): Promise<void> {
301
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
302
- if (!askUserBridge.cancel(id)) throw new Error("Unknown or resolved question");
303
- }
304
-
305
- async function handleNewSession(): Promise<void> {
306
- const result = await runtime.newSession();
307
- if (result.cancelled) throw new Error("Session switch cancelled");
308
- }
309
-
310
- async function handleOpenSession(path: string): Promise<void> {
311
- const sessions = await SessionManager.list(cwd, sessionDir);
312
- const match = sessions.find((candidate) => candidate.path === path);
313
- if (!match) throw new Error("Unknown saved session");
314
- const result = await runtime.switchSession(match.path);
315
- if (result.cancelled) throw new Error("Session switch cancelled");
316
- }
317
-
318
- async function handleRenameSession(path: string, name: string): Promise<void> {
319
- const nextName = name.trim();
320
- if (!nextName) throw new Error("Session name cannot be empty");
321
- if (sessionManager.getSessionFile() === path) {
322
- sessionManager.appendSessionInfo(nextName);
323
- return;
324
- }
325
- await renameSessionFile(cwd, sessionDir, path, nextName);
326
- }
327
-
328
- async function handleDeleteSession(client: WsClient, path: string): Promise<void> {
329
- const deletingCurrent = sessionManager.getSessionFile() === path;
330
- await deleteSessionFile(cwd, sessionDir, path);
331
- if (deletingCurrent) {
332
- await handleNewSession();
333
- sendBoot(client);
334
- broadcastState();
335
- }
336
- broadcastSessions();
337
- }
338
-
339
- async function handleSaveModelApiKey(providerId: string, apiKey: string): Promise<void> {
340
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
341
-
342
- const provider = modelSetupProviders.find((candidate) => candidate.id === providerId);
343
- if (!provider) throw new Error(`Unknown model provider: ${providerId}`);
344
-
345
- const trimmed = apiKey.trim();
346
- if (!trimmed) throw new Error(`Paste a ${provider.label} API key first.`);
347
-
348
- session.modelRegistry.authStorage.set(provider.id, { type: "api_key", key: trimmed });
349
- session.modelRegistry.refresh();
350
-
351
- const model = findPreferredModel(session.modelRegistry, provider);
352
- if (!model) {
353
- throw new Error(`Saved the ${provider.label} key, but no ${provider.label} models are available yet.`);
354
- }
355
-
356
- await session.setModel(model);
357
- await session.settingsManager.flush();
358
- sessionManager.appendCustomMessageEntry(
359
- "opencandle-model-setup",
360
- `Connected ${provider.label} and selected ${model.provider}/${model.id}.`,
361
- true,
362
- { source: "gui", provider: provider.id, model: `${model.provider}/${model.id}` },
363
- );
364
- broadcastState();
365
- }
366
-
367
- async function handleSaveProviderApiKey(providerId: string, apiKey: string): Promise<void> {
368
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
369
-
370
- const descriptor = PROVIDERS.find((candidate) => candidate.id === providerId);
371
- if (!descriptor) throw new Error(`Unknown provider: ${providerId}`);
372
-
373
- if (getCredentialSource(descriptor.id) === "env") {
374
- throw new Error(
375
- `${descriptor.displayName} is set via the ${descriptor.envVar} environment variable. Unset it to override here.`,
376
- );
377
- }
378
-
379
- const trimmed = apiKey.trim();
380
- if (!trimmed) throw new Error(`Paste a ${descriptor.displayName} API key first.`);
381
-
382
- const validation = await validateCredential(descriptor.id as ProviderId, trimmed);
383
- if (validation.status === "invalid") {
384
- const statusHint = validation.httpStatus !== undefined ? ` (HTTP ${validation.httpStatus})` : "";
385
- const messageHint = validation.message ? ` — ${validation.message}` : "";
386
- throw new Error(
387
- `${descriptor.displayName} rejected the key${statusHint}${messageHint}. The existing configuration was not changed.`,
278
+ if (allowRemotePrivateApi) {
279
+ console.log(
280
+ "OpenCandle GUI private market-state API accepts cookie-authenticated remote requests.",
388
281
  );
389
282
  }
283
+ console.log(`Writer role: ${lockResult.role}`);
284
+ localAutomationHeartbeat.start();
285
+ });
390
286
 
391
- persistProviderCredential(descriptor.id as ProviderId, trimmed);
392
-
393
- const verifiedNote =
394
- validation.status === "transient"
395
- ? `Saved ${descriptor.displayName} key but couldn't verify it (${validation.reason}). The next request will surface any issue.`
396
- : `Connected ${descriptor.displayName}. Key saved to ~/.opencandle/config.json.`;
397
-
398
- sessionManager.appendCustomMessageEntry(
399
- "opencandle-provider-setup",
400
- verifiedNote,
401
- true,
402
- { source: "gui", provider: descriptor.id, status: validation.status },
403
- );
404
- broadcastState();
405
- }
406
-
407
- async function handleSelectModel(provider: string, modelId: string): Promise<void> {
408
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
409
- session.modelRegistry.refresh();
410
- const model = session.modelRegistry.find(provider, modelId);
411
- if (!model) throw new Error(`Unknown model: ${provider}/${modelId}`);
412
- await session.setModel(model);
413
- await session.settingsManager.flush();
414
- }
415
-
416
- async function handleToolInvoke(toolName: string, args: Record<string, unknown>): Promise<void> {
417
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
418
- const tool = getAllTools().find((candidate) => candidate.name === toolName);
419
- if (!tool) throw new Error(`Unknown tool: ${toolName}`);
420
- await invokeToolFromUi(sessionManager, tool, args, "ui");
421
- broadcastState();
422
- }
423
-
424
- function sendBoot(client: WsClient): void {
425
- const snapshot = buildStateSnapshot();
426
- client.send({
427
- type: "boot",
428
- role: lockResult.role,
429
- lock: lockResult.lock,
430
- sessionId: sessionManager.getSessionId(),
431
- catalog: buildCatalog(),
432
- modelSetup: buildCurrentModelSetupState(),
433
- askUserPrompts: askUserBridge.getPrompts(),
434
- });
435
- client.send({
436
- type: "state.snapshot",
437
- ...snapshot,
438
- });
439
- void SessionManager.list(cwd, sessionDir).then((sessions) => client.send({ type: "sessions", sessions }));
440
- }
441
-
442
- function broadcastModelSetup(): void {
443
- broadcast({ type: "model.setup", modelSetup: buildCurrentModelSetupState() });
444
- }
287
+ const shutdown = createGracefulShutdown({
288
+ server,
289
+ cleanup: async () => {
290
+ clearInterval(heartbeat);
291
+ quotePoller.stop();
292
+ localAutomationHeartbeat.stop();
293
+ wsHub.closeClients();
294
+ unsubscribeSession();
295
+ releaseWriterLock(sessionDir);
296
+ await runtime.dispose();
297
+ },
298
+ exit: (code) => process.exit(code),
299
+ });
445
300
 
446
- function broadcastState(): void {
447
- broadcast({
448
- type: "state.snapshot",
449
- ...buildStateSnapshot(),
450
- });
451
- }
301
+ process.once("SIGINT", shutdown);
302
+ process.once("SIGTERM", shutdown);
452
303
 
453
304
  async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Promise<void> {
454
305
  if (lockResult.role !== "writer") {
@@ -463,6 +314,15 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
463
314
  return;
464
315
  }
465
316
 
317
+ const sessionConflict = chatRunSessionConflict(
318
+ asRecord(body).sessionId,
319
+ sessionManager.getSessionId(),
320
+ );
321
+ if (sessionConflict) {
322
+ writeJson(res, sessionConflict, 409);
323
+ return;
324
+ }
325
+
466
326
  res.writeHead(200, {
467
327
  "content-type": "text/event-stream; charset=utf-8",
468
328
  "cache-control": "no-cache, no-transform",
@@ -474,6 +334,11 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
474
334
  const runSession = session;
475
335
  const runSessionManager = sessionManager;
476
336
  const sessionId = runSessionManager.getSessionId();
337
+ // Name new sessions by the user's words before any workflow transform
338
+ // replaces the turn text, so the sidebar shows what the user actually asked.
339
+ if (!prompt.startsWith("/") && !runSessionManager.getSessionName()) {
340
+ runSessionManager.appendSessionInfo(prompt.length > 80 ? `${prompt.slice(0, 77)}...` : prompt);
341
+ }
477
342
  const beforeEntries = runSessionManager.getEntries();
478
343
  const beforeCount = beforeEntries.length;
479
344
  const beforeIds = new Set(beforeEntries.map((entry) => entry.id));
@@ -485,6 +350,7 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
485
350
  sessionId,
486
351
  startSeq: seq,
487
352
  emit: (event) => writeSse(res, event),
353
+ originalPrompt: prompt,
488
354
  });
489
355
  const observation = createPromptObservation();
490
356
  const unsubscribeLive = runSession.subscribe((event) => {
@@ -500,16 +366,14 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
500
366
  modelSetup.requirement === "select_model"
501
367
  ? "Choose an available model before chat can run. OpenCandle found configured credentials but no active model."
502
368
  : "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
503
- runSessionManager.appendCustomMessageEntry(
504
- "opencandle-model-setup",
505
- message,
506
- true,
507
- { source: "gui", requirement: modelSetup.requirement },
508
- );
509
- broadcastState();
369
+ runSessionManager.appendCustomMessageEntry("opencandle-model-setup", message, true, {
370
+ source: "gui",
371
+ requirement: modelSetup.requirement,
372
+ });
373
+ wsHub.broadcastState();
510
374
  } else {
511
375
  await promptAndSettle(runSession, prompt, beforeIds, observation);
512
- broadcastState();
376
+ wsHub.broadcastState();
513
377
  }
514
378
  seq = liveAdapter.nextSeq();
515
379
  if (seq === liveStartSeq) {
@@ -517,7 +381,8 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
517
381
  () => runSessionManager.getEntries().map((entry) => entry.id),
518
382
  beforeIds,
519
383
  );
520
- const newEntries = runSessionManager.getEntries()
384
+ const newEntries = runSessionManager
385
+ .getEntries()
521
386
  .slice(beforeCount)
522
387
  .filter((entry) => !beforeIds.has(entry.id));
523
388
  const events = sessionEntriesToChatEvents(newEntries, {
@@ -541,122 +406,33 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
541
406
  }
542
407
  }
543
408
 
544
- async function promptAndSettle(
545
- runSession: AgentSession,
546
- prompt: string,
547
- beforeIds: Set<string>,
548
- observation?: PromptObservation,
549
- ): Promise<void> {
550
- await runSession.prompt(prompt);
551
- await waitForSessionTurnSettlement(() => ({
552
- isStreaming: runSession.isStreaming,
553
- pendingMessageCount: runSession.pendingMessageCount,
554
- }));
555
- await waitForNewEntryId(() => runSession.sessionManager.getEntries().map((entry) => entry.id), beforeIds);
556
- await replayObservedWorkflowPromptIfNeeded(runSession, prompt, observation);
557
- }
558
-
559
- async function replayObservedWorkflowPromptIfNeeded(
560
- runSession: AgentSession,
561
- originalPrompt: string,
562
- observation?: PromptObservation,
563
- ): Promise<void> {
564
- if (!observation) return;
565
- const replayPrompt = selectReplayPrompt(observation, originalPrompt);
566
- if (!replayPrompt) return;
567
-
568
- await runSession.prompt(replayPrompt, {
569
- expandPromptTemplates: false,
570
- source: "extension",
571
- });
572
- await waitForSessionTurnSettlement(() => ({
573
- isStreaming: runSession.isStreaming,
574
- pendingMessageCount: runSession.pendingMessageCount,
575
- }));
576
- }
577
-
578
- function buildStateSnapshot() {
579
- const sessionId = sessionManager.getSessionId();
580
- const entries = sessionManager.getEntries();
581
- return {
582
- sessionId,
583
- state: projectDashboard(backgroundQuoteRefreshes.withEntries(entries), sessionId),
584
- entries,
585
- events: currentChatEvents(entries),
586
- };
587
- }
588
-
589
- function currentChatEvents(entries = sessionManager.getEntries()): ChatEvent[] {
590
- return sessionEntriesToChatEvents(entries, {
591
- sessionId: sessionManager.getSessionId(),
592
- title: sessionManager.getSessionName(),
593
- });
594
- }
595
-
596
- function broadcastSessions(): void {
597
- void SessionManager.list(cwd, sessionDir).then((sessions) => broadcast({ type: "sessions", sessions }));
598
- }
599
-
600
- function buildCurrentModelSetupState() {
601
- return buildModelSetupState(session.modelRegistry, session.model);
602
- }
603
-
604
- function broadcast(message: unknown): void {
605
- for (const client of clients) client.send(message);
409
+ function writeJson(res: ServerResponse, value: unknown, status = 200): void {
410
+ res.writeHead(status, { "content-type": "application/json" });
411
+ res.end(JSON.stringify(value));
606
412
  }
607
413
 
608
- function subscribeToSessionEvents(): () => void {
609
- return session.subscribe((event) => {
610
- broadcast({ type: "session.event", event });
611
- broadcastState();
612
- });
414
+ function allowTrustedGuiRequest(req: IncomingMessage, res: ServerResponse, label: string): boolean {
415
+ if (
416
+ isTrustedPrivateApiRequest(req.headers, privateApiSessionToken, req.socket.remoteAddress, {
417
+ allowRemote: allowRemotePrivateApi,
418
+ })
419
+ )
420
+ return true;
421
+ res.writeHead(403, { "content-type": "application/json" });
422
+ res.end(
423
+ JSON.stringify({
424
+ error: `${label} is only available to trusted GUI browser sessions.`,
425
+ }),
426
+ );
427
+ return false;
613
428
  }
614
429
 
615
- function updatePoller(): void {
616
- if (clients.size > 0 && !poller) {
617
- poller = setInterval(() => void pollVisibleQuotes(), 30000);
618
- }
619
- if (clients.size === 0 && poller) {
620
- clearInterval(poller);
621
- poller = null;
430
+ function privateGuiHeaders(contentTypeValue: string): Record<string, string> {
431
+ const headers: Record<string, string> = { "content-type": contentTypeValue };
432
+ if (contentTypeValue.startsWith("text/html")) {
433
+ headers["set-cookie"] = privateApiCookieHeader(privateApiSessionToken);
622
434
  }
623
- }
624
-
625
- async function pollVisibleQuotes(): Promise<void> {
626
- if (quotePollInFlight) return;
627
- quotePollInFlight = true;
628
- try {
629
- const state = projectDashboard(sessionManager.getEntries(), sessionManager.getSessionId());
630
- const tool = getAllTools().find((candidate) => candidate.name === "get_stock_quote");
631
- if (!tool) return;
632
- for (const row of state.watchlist.filter((item) => item.pinned || item.quote)) {
633
- const result = await invokeToolFromUi(
634
- sessionManager,
635
- tool,
636
- { symbol: row.symbol },
637
- "background",
638
- { recordTranscript: false },
639
- );
640
- backgroundQuoteRefreshes.upsert({
641
- symbol: row.symbol,
642
- toolName: tool.name,
643
- args: { symbol: row.symbol },
644
- value: result.result.details,
645
- content: result.result.content,
646
- isError: result.isError,
647
- });
648
- }
649
- broadcastState();
650
- } catch (error) {
651
- console.warn(`Background quote refresh failed: ${error instanceof Error ? error.message : String(error)}`);
652
- } finally {
653
- quotePollInFlight = false;
654
- }
655
- }
656
-
657
- function writeJson(res: ServerResponse, value: unknown): void {
658
- res.writeHead(200, { "content-type": "application/json" });
659
- res.end(JSON.stringify(value));
435
+ return headers;
660
436
  }
661
437
 
662
438
  function writeSse(res: ServerResponse, event: ChatEvent): void {
@@ -689,15 +465,6 @@ function contentType(path: string): string {
689
465
 
690
466
  function asRecord(value: unknown): Record<string, unknown> {
691
467
  return typeof value === "object" && value !== null && !Array.isArray(value)
692
- ? value as Record<string, unknown>
468
+ ? (value as Record<string, unknown>)
693
469
  : {};
694
470
  }
695
-
696
- function shutdown(): void {
697
- clearInterval(heartbeat);
698
- if (poller) clearInterval(poller);
699
- releaseWriterLock(sessionDir);
700
- void runtime.dispose().finally(() => {
701
- server.close(() => process.exit(0));
702
- });
703
- }