opencandle 0.4.0 → 0.6.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 (564) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +186 -117
  3. package/dist/analysts/contracts.d.ts +1 -3
  4. package/dist/analysts/contracts.js +1 -11
  5. package/dist/analysts/contracts.js.map +1 -1
  6. package/dist/analysts/orchestrator.d.ts +1 -3
  7. package/dist/analysts/orchestrator.js +1 -26
  8. package/dist/analysts/orchestrator.js.map +1 -1
  9. package/dist/cli.js +32 -8
  10. package/dist/cli.js.map +1 -1
  11. package/dist/config.d.ts +19 -3
  12. package/dist/config.js +69 -3
  13. package/dist/config.js.map +1 -1
  14. package/dist/index.d.ts +1 -1
  15. package/dist/index.js +1 -1
  16. package/dist/index.js.map +1 -1
  17. package/dist/infra/browser.d.ts +1 -3
  18. package/dist/infra/browser.js +4 -2
  19. package/dist/infra/browser.js.map +1 -1
  20. package/dist/infra/cache.d.ts +8 -11
  21. package/dist/infra/cache.js +17 -15
  22. package/dist/infra/cache.js.map +1 -1
  23. package/dist/infra/http-client.d.ts +4 -1
  24. package/dist/infra/http-client.js +59 -6
  25. package/dist/infra/http-client.js.map +1 -1
  26. package/dist/infra/index.d.ts +3 -3
  27. package/dist/infra/index.js +3 -3
  28. package/dist/infra/index.js.map +1 -1
  29. package/dist/infra/native-dependencies.js +2 -2
  30. package/dist/infra/native-dependencies.js.map +1 -1
  31. package/dist/infra/node-version.js.map +1 -1
  32. package/dist/infra/opencandle-paths.d.ts +0 -3
  33. package/dist/infra/opencandle-paths.js +4 -11
  34. package/dist/infra/opencandle-paths.js.map +1 -1
  35. package/dist/infra/rate-limiter.d.ts +4 -0
  36. package/dist/infra/rate-limiter.js +17 -10
  37. package/dist/infra/rate-limiter.js.map +1 -1
  38. package/dist/market-state/alert-conditions.d.ts +34 -0
  39. package/dist/market-state/alert-conditions.js +23 -0
  40. package/dist/market-state/alert-conditions.js.map +1 -0
  41. package/dist/market-state/alert-runner.d.ts +55 -0
  42. package/dist/market-state/alert-runner.js +634 -0
  43. package/dist/market-state/alert-runner.js.map +1 -0
  44. package/dist/market-state/daily-report.d.ts +26 -0
  45. package/dist/market-state/daily-report.js +179 -0
  46. package/dist/market-state/daily-report.js.map +1 -0
  47. package/dist/market-state/local-automation-service.d.ts +25 -0
  48. package/dist/market-state/local-automation-service.js +119 -0
  49. package/dist/market-state/local-automation-service.js.map +1 -0
  50. package/dist/market-state/notification-delivery.d.ts +14 -0
  51. package/dist/market-state/notification-delivery.js +139 -0
  52. package/dist/market-state/notification-delivery.js.map +1 -0
  53. package/dist/market-state/resolve-for-mutation.d.ts +10 -0
  54. package/dist/market-state/resolve-for-mutation.js +15 -0
  55. package/dist/market-state/resolve-for-mutation.js.map +1 -0
  56. package/dist/market-state/resolve.d.ts +14 -0
  57. package/dist/market-state/resolve.js +89 -0
  58. package/dist/market-state/resolve.js.map +1 -0
  59. package/dist/market-state/service.d.ts +527 -0
  60. package/dist/market-state/service.js +1099 -0
  61. package/dist/market-state/service.js.map +1 -0
  62. package/dist/memory/index.d.ts +7 -7
  63. package/dist/memory/index.js +6 -6
  64. package/dist/memory/index.js.map +1 -1
  65. package/dist/memory/manager.d.ts +9 -0
  66. package/dist/memory/manager.js +39 -22
  67. package/dist/memory/manager.js.map +1 -1
  68. package/dist/memory/retrieval.js +7 -4
  69. package/dist/memory/retrieval.js.map +1 -1
  70. package/dist/memory/sqlite.js +385 -3
  71. package/dist/memory/sqlite.js.map +1 -1
  72. package/dist/memory/storage.d.ts +3 -2
  73. package/dist/memory/storage.js +1 -2
  74. package/dist/memory/storage.js.map +1 -1
  75. package/dist/memory/tool-defaults.js +64 -28
  76. package/dist/memory/tool-defaults.js.map +1 -1
  77. package/dist/memory/types.js +4 -0
  78. package/dist/memory/types.js.map +1 -1
  79. package/dist/monitor.d.ts +2 -0
  80. package/dist/monitor.js +104 -0
  81. package/dist/monitor.js.map +1 -0
  82. package/dist/onboarding/connect.js +4 -6
  83. package/dist/onboarding/connect.js.map +1 -1
  84. package/dist/onboarding/credential-interceptor.js +1 -1
  85. package/dist/onboarding/credential-interceptor.js.map +1 -1
  86. package/dist/onboarding/degradation-accumulator.js +1 -3
  87. package/dist/onboarding/degradation-accumulator.js.map +1 -1
  88. package/dist/onboarding/providers.js +3 -16
  89. package/dist/onboarding/providers.js.map +1 -1
  90. package/dist/onboarding/state.js.map +1 -1
  91. package/dist/onboarding/tool-helpers.js +1 -1
  92. package/dist/onboarding/tool-helpers.js.map +1 -1
  93. package/dist/onboarding/tool-tags.js +6 -4
  94. package/dist/onboarding/tool-tags.js.map +1 -1
  95. package/dist/onboarding/validation.js +1 -1
  96. package/dist/onboarding/validation.js.map +1 -1
  97. package/dist/pi/opencandle-extension.d.ts +8 -0
  98. package/dist/pi/opencandle-extension.js +637 -59
  99. package/dist/pi/opencandle-extension.js.map +1 -1
  100. package/dist/pi/session.d.ts +1 -1
  101. package/dist/pi/session.js +3 -1
  102. package/dist/pi/session.js.map +1 -1
  103. package/dist/pi/setup.js +17 -2
  104. package/dist/pi/setup.js.map +1 -1
  105. package/dist/pi/tool-adapter.js +5 -2
  106. package/dist/pi/tool-adapter.js.map +1 -1
  107. package/dist/prompts/context-builder.d.ts +18 -3
  108. package/dist/prompts/context-builder.js +117 -18
  109. package/dist/prompts/context-builder.js.map +1 -1
  110. package/dist/prompts/disclaimer.js +1 -1
  111. package/dist/prompts/disclaimer.js.map +1 -1
  112. package/dist/prompts/policy-cards.d.ts +13 -0
  113. package/dist/prompts/policy-cards.js +197 -0
  114. package/dist/prompts/policy-cards.js.map +1 -0
  115. package/dist/prompts/sections.d.ts +1 -1
  116. package/dist/prompts/sections.js +3 -3
  117. package/dist/prompts/sections.js.map +1 -1
  118. package/dist/prompts/symbol-preflight.d.ts +20 -0
  119. package/dist/prompts/symbol-preflight.js +49 -0
  120. package/dist/prompts/symbol-preflight.js.map +1 -0
  121. package/dist/prompts/workflow-prompts.d.ts +1 -1
  122. package/dist/prompts/workflow-prompts.js +209 -19
  123. package/dist/prompts/workflow-prompts.js.map +1 -1
  124. package/dist/providers/alpha-vantage.d.ts +1 -1
  125. package/dist/providers/alpha-vantage.js +49 -8
  126. package/dist/providers/alpha-vantage.js.map +1 -1
  127. package/dist/providers/coingecko.js +1 -1
  128. package/dist/providers/coingecko.js.map +1 -1
  129. package/dist/providers/errors.d.ts +5 -0
  130. package/dist/providers/errors.js +11 -0
  131. package/dist/providers/errors.js.map +1 -0
  132. package/dist/providers/exa-search.d.ts +2 -2
  133. package/dist/providers/exa-search.js +19 -11
  134. package/dist/providers/exa-search.js.map +1 -1
  135. package/dist/providers/fear-greed.js +1 -1
  136. package/dist/providers/fear-greed.js.map +1 -1
  137. package/dist/providers/finnhub.js +3 -5
  138. package/dist/providers/finnhub.js.map +1 -1
  139. package/dist/providers/fred.js +2 -2
  140. package/dist/providers/fred.js.map +1 -1
  141. package/dist/providers/index.d.ts +7 -6
  142. package/dist/providers/index.js +6 -5
  143. package/dist/providers/index.js.map +1 -1
  144. package/dist/providers/reddit.js +2 -2
  145. package/dist/providers/reddit.js.map +1 -1
  146. package/dist/providers/sec-edgar.d.ts +9 -1
  147. package/dist/providers/sec-edgar.js +181 -6
  148. package/dist/providers/sec-edgar.js.map +1 -1
  149. package/dist/providers/tradingview.d.ts +47 -0
  150. package/dist/providers/tradingview.js +275 -0
  151. package/dist/providers/tradingview.js.map +1 -0
  152. package/dist/providers/twitter.js +6 -8
  153. package/dist/providers/twitter.js.map +1 -1
  154. package/dist/providers/web-search.js +26 -12
  155. package/dist/providers/web-search.js.map +1 -1
  156. package/dist/providers/with-fallback.js +4 -2
  157. package/dist/providers/with-fallback.js.map +1 -1
  158. package/dist/providers/wrap-provider.d.ts +2 -3
  159. package/dist/providers/wrap-provider.js +14 -8
  160. package/dist/providers/wrap-provider.js.map +1 -1
  161. package/dist/providers/yahoo-finance.d.ts +3 -1
  162. package/dist/providers/yahoo-finance.js +226 -11
  163. package/dist/providers/yahoo-finance.js.map +1 -1
  164. package/dist/routing/classify-intent.d.ts +9 -0
  165. package/dist/routing/classify-intent.js +153 -3
  166. package/dist/routing/classify-intent.js.map +1 -1
  167. package/dist/routing/defaults.d.ts +1 -1
  168. package/dist/routing/defaults.js +3 -3
  169. package/dist/routing/defaults.js.map +1 -1
  170. package/dist/routing/entity-extractor.d.ts +2 -0
  171. package/dist/routing/entity-extractor.js +377 -26
  172. package/dist/routing/entity-extractor.js.map +1 -1
  173. package/dist/routing/fund-symbols.d.ts +2 -0
  174. package/dist/routing/fund-symbols.js +55 -0
  175. package/dist/routing/fund-symbols.js.map +1 -0
  176. package/dist/routing/horizon.d.ts +1 -0
  177. package/dist/routing/horizon.js +10 -0
  178. package/dist/routing/horizon.js.map +1 -0
  179. package/dist/routing/index.d.ts +12 -6
  180. package/dist/routing/index.js +8 -4
  181. package/dist/routing/index.js.map +1 -1
  182. package/dist/routing/legacy-rule-router.d.ts +9 -0
  183. package/dist/routing/legacy-rule-router.js +12 -0
  184. package/dist/routing/legacy-rule-router.js.map +1 -0
  185. package/dist/routing/planning.d.ts +54 -0
  186. package/dist/routing/planning.js +562 -0
  187. package/dist/routing/planning.js.map +1 -0
  188. package/dist/routing/route-manifest.d.ts +35 -0
  189. package/dist/routing/route-manifest.js +242 -0
  190. package/dist/routing/route-manifest.js.map +1 -0
  191. package/dist/routing/router-llm-client.js.map +1 -1
  192. package/dist/routing/router-prompt.js +46 -45
  193. package/dist/routing/router-prompt.js.map +1 -1
  194. package/dist/routing/router-types.d.ts +10 -0
  195. package/dist/routing/router.d.ts +1 -0
  196. package/dist/routing/router.js +572 -13
  197. package/dist/routing/router.js.map +1 -1
  198. package/dist/routing/slot-resolver.d.ts +1 -1
  199. package/dist/routing/slot-resolver.js +45 -7
  200. package/dist/routing/slot-resolver.js.map +1 -1
  201. package/dist/routing/symbol-disambiguator.d.ts +11 -0
  202. package/dist/routing/symbol-disambiguator.js +52 -0
  203. package/dist/routing/symbol-disambiguator.js.map +1 -0
  204. package/dist/routing/turn-context.d.ts +44 -0
  205. package/dist/routing/turn-context.js +45 -0
  206. package/dist/routing/turn-context.js.map +1 -0
  207. package/dist/routing/types.d.ts +15 -1
  208. package/dist/runtime/answer-contracts.d.ts +82 -0
  209. package/dist/runtime/answer-contracts.js +442 -0
  210. package/dist/runtime/answer-contracts.js.map +1 -0
  211. package/dist/runtime/artifact-contracts.d.ts +14 -0
  212. package/dist/runtime/artifact-contracts.js +57 -0
  213. package/dist/runtime/artifact-contracts.js.map +1 -0
  214. package/dist/runtime/planning-evidence.d.ts +99 -0
  215. package/dist/runtime/planning-evidence.js +466 -0
  216. package/dist/runtime/planning-evidence.js.map +1 -0
  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 +29 -3
  224. package/dist/runtime/session-coordinator.js +204 -31
  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 +1 -3
  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 +9 -11
  243. package/dist/sentiment/index.js +9 -20
  244. package/dist/sentiment/index.js.map +1 -1
  245. package/dist/sentiment/keywords.js +26 -4
  246. package/dist/sentiment/keywords.js.map +1 -1
  247. package/dist/sentiment/pipeline.d.ts +2 -2
  248. package/dist/sentiment/pipeline.js +1 -1
  249. package/dist/sentiment/pipeline.js.map +1 -1
  250. package/dist/sentiment/scorer.js +1 -1
  251. package/dist/sentiment/store.d.ts +1 -1
  252. package/dist/sentiment/store.js +1 -1
  253. package/dist/sentiment/store.js.map +1 -1
  254. package/dist/sentiment/trends.d.ts +1 -1
  255. package/dist/sentiment/trends.js.map +1 -1
  256. package/dist/sentiment/types.js.map +1 -1
  257. package/dist/system-prompt.js +7 -3
  258. package/dist/system-prompt.js.map +1 -1
  259. package/dist/tool-kit.d.ts +7 -7
  260. package/dist/tool-kit.js +4 -4
  261. package/dist/tool-kit.js.map +1 -1
  262. package/dist/tools/fundamentals/company-overview.js +12 -7
  263. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  264. package/dist/tools/fundamentals/comps.js +19 -10
  265. package/dist/tools/fundamentals/comps.js.map +1 -1
  266. package/dist/tools/fundamentals/dcf.js +24 -12
  267. package/dist/tools/fundamentals/dcf.js.map +1 -1
  268. package/dist/tools/fundamentals/earnings.js +9 -4
  269. package/dist/tools/fundamentals/earnings.js.map +1 -1
  270. package/dist/tools/fundamentals/financials.js +9 -4
  271. package/dist/tools/fundamentals/financials.js.map +1 -1
  272. package/dist/tools/fundamentals/sec-filings.d.ts +1 -0
  273. package/dist/tools/fundamentals/sec-filings.js +36 -4
  274. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  275. package/dist/tools/index.d.ts +23 -18
  276. package/dist/tools/index.js +53 -38
  277. package/dist/tools/index.js.map +1 -1
  278. package/dist/tools/interaction/ask-user.js +15 -3
  279. package/dist/tools/interaction/ask-user.js.map +1 -1
  280. package/dist/tools/interaction/twitter-login.js +13 -3
  281. package/dist/tools/interaction/twitter-login.js.map +1 -1
  282. package/dist/tools/macro/fear-greed.js +1 -1
  283. package/dist/tools/macro/fear-greed.js.map +1 -1
  284. package/dist/tools/macro/fred-data.d.ts +1 -1
  285. package/dist/tools/macro/fred-data.js +44 -9
  286. package/dist/tools/macro/fred-data.js.map +1 -1
  287. package/dist/tools/market/crypto-history.js +21 -3
  288. package/dist/tools/market/crypto-history.js.map +1 -1
  289. package/dist/tools/market/crypto-price.js +4 -2
  290. package/dist/tools/market/crypto-price.js.map +1 -1
  291. package/dist/tools/market/screen-stocks.d.ts +18 -0
  292. package/dist/tools/market/screen-stocks.js +252 -0
  293. package/dist/tools/market/screen-stocks.js.map +1 -0
  294. package/dist/tools/market/search-ticker.js +161 -9
  295. package/dist/tools/market/search-ticker.js.map +1 -1
  296. package/dist/tools/market/stock-history.d.ts +2 -2
  297. package/dist/tools/market/stock-history.js +27 -8
  298. package/dist/tools/market/stock-history.js.map +1 -1
  299. package/dist/tools/market/stock-quote.js +6 -4
  300. package/dist/tools/market/stock-quote.js.map +1 -1
  301. package/dist/tools/options/greeks.js +1 -2
  302. package/dist/tools/options/greeks.js.map +1 -1
  303. package/dist/tools/options/option-chain.js +27 -9
  304. package/dist/tools/options/option-chain.js.map +1 -1
  305. package/dist/tools/portfolio/alerts.d.ts +15 -0
  306. package/dist/tools/portfolio/alerts.js +357 -0
  307. package/dist/tools/portfolio/alerts.js.map +1 -0
  308. package/dist/tools/portfolio/correlation.d.ts +1 -1
  309. package/dist/tools/portfolio/correlation.js +34 -14
  310. package/dist/tools/portfolio/correlation.js.map +1 -1
  311. package/dist/tools/portfolio/daily-report.d.ts +8 -0
  312. package/dist/tools/portfolio/daily-report.js +83 -0
  313. package/dist/tools/portfolio/daily-report.js.map +1 -0
  314. package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
  315. package/dist/tools/portfolio/holdings-overlap.js +112 -0
  316. package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
  317. package/dist/tools/portfolio/notifications.d.ts +7 -0
  318. package/dist/tools/portfolio/notifications.js +43 -0
  319. package/dist/tools/portfolio/notifications.js.map +1 -0
  320. package/dist/tools/portfolio/predictions.d.ts +12 -6
  321. package/dist/tools/portfolio/predictions.js +338 -88
  322. package/dist/tools/portfolio/predictions.js.map +1 -1
  323. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  324. package/dist/tools/portfolio/risk-analysis.js +46 -7
  325. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  326. package/dist/tools/portfolio/tracker.d.ts +4 -3
  327. package/dist/tools/portfolio/tracker.js +247 -102
  328. package/dist/tools/portfolio/tracker.js.map +1 -1
  329. package/dist/tools/portfolio/watchlist.d.ts +6 -4
  330. package/dist/tools/portfolio/watchlist.js +209 -101
  331. package/dist/tools/portfolio/watchlist.js.map +1 -1
  332. package/dist/tools/sentiment/reddit-sentiment.js +24 -11
  333. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  334. package/dist/tools/sentiment/sentiment-summary.js +71 -14
  335. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  336. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  337. package/dist/tools/sentiment/sentiment-trend.js +12 -2
  338. package/dist/tools/sentiment/sentiment-trend.js.map +1 -1
  339. package/dist/tools/sentiment/twitter-sentiment.js +13 -6
  340. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  341. package/dist/tools/sentiment/untrusted-text.d.ts +2 -0
  342. package/dist/tools/sentiment/untrusted-text.js +17 -0
  343. package/dist/tools/sentiment/untrusted-text.js.map +1 -0
  344. package/dist/tools/sentiment/web-search.js +37 -12
  345. package/dist/tools/sentiment/web-search.js.map +1 -1
  346. package/dist/tools/sentiment/web-sentiment.js +16 -4
  347. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  348. package/dist/tools/technical/backtest.d.ts +3 -3
  349. package/dist/tools/technical/backtest.js +65 -44
  350. package/dist/tools/technical/backtest.js.map +1 -1
  351. package/dist/tools/technical/indicators.js +24 -8
  352. package/dist/tools/technical/indicators.js.map +1 -1
  353. package/dist/types/index.d.ts +3 -3
  354. package/dist/types/index.js.map +1 -1
  355. package/dist/types/market.d.ts +1 -0
  356. package/dist/types/options.d.ts +10 -0
  357. package/dist/types/portfolio.d.ts +41 -4
  358. package/dist/workflows/compare-assets.d.ts +0 -3
  359. package/dist/workflows/compare-assets.js +55 -10
  360. package/dist/workflows/compare-assets.js.map +1 -1
  361. package/dist/workflows/index.d.ts +3 -4
  362. package/dist/workflows/index.js +3 -3
  363. package/dist/workflows/index.js.map +1 -1
  364. package/dist/workflows/options-screener.d.ts +0 -3
  365. package/dist/workflows/options-screener.js +88 -14
  366. package/dist/workflows/options-screener.js.map +1 -1
  367. package/dist/workflows/portfolio-builder.d.ts +0 -3
  368. package/dist/workflows/portfolio-builder.js +7 -11
  369. package/dist/workflows/portfolio-builder.js.map +1 -1
  370. package/gui/server/ask-user-bridge.ts +82 -0
  371. package/gui/server/automation-heartbeat.ts +97 -0
  372. package/gui/server/background-quotes.ts +97 -1
  373. package/gui/server/chat-event-adapter.ts +32 -10
  374. package/gui/server/chat-run-session.ts +16 -0
  375. package/gui/server/gui-session-manager.ts +5 -0
  376. package/gui/server/invoke-tool.ts +144 -1
  377. package/gui/server/live-chat-event-adapter.ts +21 -6
  378. package/gui/server/market-state-api.ts +315 -0
  379. package/gui/server/model-setup.ts +149 -2
  380. package/gui/server/private-api-access.ts +62 -0
  381. package/gui/server/projector.ts +58 -11
  382. package/gui/server/prompt-observation.ts +58 -0
  383. package/gui/server/quote-snapshot-store.ts +50 -0
  384. package/gui/server/server.ts +236 -376
  385. package/gui/server/session-actions.ts +186 -1
  386. package/gui/server/session-entry-wait.ts +81 -0
  387. package/gui/server/shutdown.ts +47 -0
  388. package/gui/server/tool-invoke-ack.ts +49 -0
  389. package/gui/server/tool-metadata.ts +23 -10
  390. package/gui/server/websocket.ts +13 -3
  391. package/gui/server/writer-lock.ts +6 -2
  392. package/gui/server/ws-hub.ts +292 -0
  393. package/gui/shared/chat-events.ts +16 -1
  394. package/gui/shared/event-reducer.ts +24 -6
  395. package/gui/web/dist/assets/CatalogOverlay-eJ2cBk33.js +1 -0
  396. package/gui/web/dist/assets/index-2KZtKBmu.css +1 -0
  397. package/gui/web/dist/assets/index-CveNgtDg.js +69 -0
  398. package/gui/web/dist/index.html +2 -2
  399. package/package.json +22 -12
  400. package/src/analysts/contracts.ts +10 -23
  401. package/src/analysts/orchestrator.ts +8 -43
  402. package/src/cli.ts +37 -13
  403. package/src/config.ts +99 -7
  404. package/src/index.ts +1 -1
  405. package/src/infra/browser.ts +4 -2
  406. package/src/infra/cache.ts +41 -30
  407. package/src/infra/http-client.ts +72 -6
  408. package/src/infra/index.ts +7 -10
  409. package/src/infra/native-dependencies.ts +8 -3
  410. package/src/infra/node-version.ts +3 -1
  411. package/src/infra/opencandle-paths.ts +3 -14
  412. package/src/infra/rate-limiter.ts +32 -20
  413. package/src/market-state/alert-conditions.ts +82 -0
  414. package/src/market-state/alert-runner.ts +863 -0
  415. package/src/market-state/daily-report.ts +247 -0
  416. package/src/market-state/local-automation-service.ts +162 -0
  417. package/src/market-state/notification-delivery.ts +158 -0
  418. package/src/market-state/resolve-for-mutation.ts +24 -0
  419. package/src/market-state/resolve.ts +112 -0
  420. package/src/market-state/service.ts +2344 -0
  421. package/src/memory/index.ts +7 -7
  422. package/src/memory/manager.ts +57 -26
  423. package/src/memory/retrieval.ts +8 -7
  424. package/src/memory/sqlite.ts +407 -6
  425. package/src/memory/storage.ts +8 -17
  426. package/src/memory/tool-defaults.ts +60 -39
  427. package/src/memory/types.ts +7 -3
  428. package/src/monitor.ts +121 -0
  429. package/src/onboarding/connect.ts +10 -33
  430. package/src/onboarding/credential-interceptor.ts +3 -15
  431. package/src/onboarding/degradation-accumulator.ts +1 -3
  432. package/src/onboarding/providers.ts +9 -40
  433. package/src/onboarding/state.ts +4 -15
  434. package/src/onboarding/tool-helpers.ts +2 -9
  435. package/src/onboarding/tool-tags.ts +6 -6
  436. package/src/onboarding/validation.ts +14 -20
  437. package/src/pi/opencandle-extension.ts +795 -120
  438. package/src/pi/session.ts +7 -5
  439. package/src/pi/setup.ts +61 -33
  440. package/src/pi/tool-adapter.ts +5 -2
  441. package/src/prompts/context-builder.ts +143 -21
  442. package/src/prompts/disclaimer.ts +1 -1
  443. package/src/prompts/policy-cards.ts +220 -0
  444. package/src/prompts/sections.ts +4 -4
  445. package/src/prompts/symbol-preflight.ts +80 -0
  446. package/src/prompts/workflow-prompts.ts +231 -28
  447. package/src/providers/alpha-vantage.ts +82 -40
  448. package/src/providers/coingecko.ts +2 -5
  449. package/src/providers/errors.ts +9 -0
  450. package/src/providers/exa-search.ts +24 -22
  451. package/src/providers/fear-greed.ts +1 -1
  452. package/src/providers/finnhub.ts +7 -6
  453. package/src/providers/fred.ts +3 -3
  454. package/src/providers/index.ts +14 -6
  455. package/src/providers/reddit.ts +17 -6
  456. package/src/providers/sec-edgar.ts +235 -5
  457. package/src/providers/tradingview.ts +399 -0
  458. package/src/providers/twitter.ts +6 -8
  459. package/src/providers/web-search.ts +30 -20
  460. package/src/providers/with-fallback.ts +8 -7
  461. package/src/providers/wrap-provider.ts +15 -10
  462. package/src/providers/yahoo-finance.ts +292 -20
  463. package/src/routing/classify-intent.ts +186 -4
  464. package/src/routing/defaults.ts +4 -4
  465. package/src/routing/entity-extractor.ts +428 -28
  466. package/src/routing/fund-symbols.ts +58 -0
  467. package/src/routing/horizon.ts +7 -0
  468. package/src/routing/index.ts +60 -16
  469. package/src/routing/legacy-rule-router.ts +13 -0
  470. package/src/routing/planning.ts +823 -0
  471. package/src/routing/route-manifest.ts +309 -0
  472. package/src/routing/router-llm-client.ts +4 -4
  473. package/src/routing/router-prompt.ts +52 -52
  474. package/src/routing/router-types.ts +18 -0
  475. package/src/routing/router.ts +717 -20
  476. package/src/routing/slot-resolver.ts +75 -14
  477. package/src/routing/symbol-disambiguator.ts +72 -0
  478. package/src/routing/turn-context.ts +108 -0
  479. package/src/routing/types.ts +15 -1
  480. package/src/runtime/answer-contracts.ts +672 -0
  481. package/src/runtime/artifact-contracts.ts +77 -0
  482. package/src/runtime/planning-evidence.ts +682 -0
  483. package/src/runtime/prompt-step.ts +1 -16
  484. package/src/runtime/run-context.ts +12 -2
  485. package/src/runtime/session-coordinator.ts +297 -56
  486. package/src/runtime/session-title.ts +60 -0
  487. package/src/runtime/tool-defaults-wrapper.ts +1 -3
  488. package/src/runtime/validation.ts +1 -4
  489. package/src/runtime/workflow-events.ts +7 -7
  490. package/src/runtime/workflow-runner.ts +5 -11
  491. package/src/sentiment/adapters/finnhub.ts +7 -2
  492. package/src/sentiment/adapters/reddit.ts +2 -2
  493. package/src/sentiment/adapters/twitter.ts +1 -1
  494. package/src/sentiment/adapters/web.ts +1 -1
  495. package/src/sentiment/index.ts +16 -26
  496. package/src/sentiment/keywords.ts +26 -4
  497. package/src/sentiment/pipeline.ts +15 -4
  498. package/src/sentiment/scorer.ts +1 -1
  499. package/src/sentiment/store.ts +2 -2
  500. package/src/sentiment/trends.ts +9 -3
  501. package/src/sentiment/types.ts +5 -4
  502. package/src/system-prompt.ts +7 -3
  503. package/src/tool-kit.ts +10 -9
  504. package/src/tools/fundamentals/company-overview.ts +20 -10
  505. package/src/tools/fundamentals/comps.ts +69 -56
  506. package/src/tools/fundamentals/dcf.ts +146 -96
  507. package/src/tools/fundamentals/earnings.ts +17 -7
  508. package/src/tools/fundamentals/financials.ts +17 -8
  509. package/src/tools/fundamentals/sec-filings.ts +52 -8
  510. package/src/tools/index.ts +53 -38
  511. package/src/tools/interaction/ask-user.ts +22 -10
  512. package/src/tools/interaction/twitter-login.ts +17 -5
  513. package/src/tools/macro/fear-greed.ts +2 -2
  514. package/src/tools/macro/fred-data.ts +80 -42
  515. package/src/tools/market/crypto-history.ts +25 -4
  516. package/src/tools/market/crypto-price.ts +7 -7
  517. package/src/tools/market/screen-stocks.ts +279 -0
  518. package/src/tools/market/search-ticker.ts +219 -18
  519. package/src/tools/market/stock-history.ts +38 -13
  520. package/src/tools/market/stock-quote.ts +11 -8
  521. package/src/tools/options/greeks.ts +5 -6
  522. package/src/tools/options/option-chain.ts +47 -18
  523. package/src/tools/portfolio/alerts.ts +457 -0
  524. package/src/tools/portfolio/correlation.ts +48 -21
  525. package/src/tools/portfolio/daily-report.ts +101 -0
  526. package/src/tools/portfolio/holdings-overlap.ts +139 -0
  527. package/src/tools/portfolio/notifications.ts +45 -0
  528. package/src/tools/portfolio/predictions.ts +407 -107
  529. package/src/tools/portfolio/risk-analysis.ts +47 -8
  530. package/src/tools/portfolio/tracker.ts +271 -110
  531. package/src/tools/portfolio/watchlist.ts +251 -116
  532. package/src/tools/sentiment/reddit-sentiment.ts +51 -25
  533. package/src/tools/sentiment/sentiment-summary.ts +116 -35
  534. package/src/tools/sentiment/sentiment-trend.ts +24 -7
  535. package/src/tools/sentiment/twitter-sentiment.ts +23 -16
  536. package/src/tools/sentiment/untrusted-text.ts +21 -0
  537. package/src/tools/sentiment/web-search.ts +52 -16
  538. package/src/tools/sentiment/web-sentiment.ts +27 -11
  539. package/src/tools/technical/backtest.ts +78 -47
  540. package/src/tools/technical/indicators.ts +40 -17
  541. package/src/types/index.ts +8 -3
  542. package/src/types/market.ts +1 -0
  543. package/src/types/options.ts +17 -0
  544. package/src/types/portfolio.ts +46 -4
  545. package/src/types/sentiment.ts +2 -2
  546. package/src/workflows/compare-assets.ts +67 -19
  547. package/src/workflows/index.ts +3 -4
  548. package/src/workflows/options-screener.ts +98 -22
  549. package/src/workflows/portfolio-builder.ts +40 -29
  550. package/dist/runtime/index.d.ts +0 -16
  551. package/dist/runtime/index.js +0 -10
  552. package/dist/runtime/index.js.map +0 -1
  553. package/dist/runtime/provider-ids.d.ts +0 -14
  554. package/dist/runtime/provider-ids.js +0 -14
  555. package/dist/runtime/provider-ids.js.map +0 -1
  556. package/dist/workflows/types.d.ts +0 -4
  557. package/dist/workflows/types.js +0 -2
  558. package/dist/workflows/types.js.map +0 -1
  559. package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +0 -1
  560. package/gui/web/dist/assets/index-DBrWq43L.css +0 -1
  561. package/gui/web/dist/assets/index-RflHaj0y.js +0 -67
  562. package/src/runtime/index.ts +0 -55
  563. package/src/runtime/provider-ids.ts +0 -15
  564. 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";
@@ -12,30 +13,45 @@ import {
12
13
  SettingsManager,
13
14
  } from "@earendil-works/pi-coding-agent";
14
15
  import { createOpenCandleSession } from "../../src/index.js";
15
- import { continueOpenCandleSession } from "../../src/pi/session-storage.js";
16
- import { getAllTools } from "../../src/tools/index.js";
17
- import { persistProviderCredential } from "../../src/onboarding/connect.js";
16
+ import type { ChatEvent } from "../shared/chat-events.js";
17
+ import { createAskUserBridge } from "./ask-user-bridge.js";
18
18
  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";
19
+ createLocalAutomationHeartbeat,
20
+ normalizeAutomationHeartbeatMs,
21
+ } from "./automation-heartbeat.js";
22
+ import {
23
+ type BackgroundQuotePoller,
24
+ BackgroundQuoteRefreshes,
25
+ createBackgroundQuotePoller,
26
+ } from "./background-quotes.js";
31
27
  import { sessionEntriesToChatEvents } from "./chat-event-adapter.js";
28
+ import { chatRunSessionConflict } from "./chat-run-session.js";
29
+ import { createInitialGuiSessionManager } from "./gui-session-manager.js";
30
+ import { createToolInvokeController } from "./invoke-tool.js";
32
31
  import { createLiveChatEventAdapter } from "./live-chat-event-adapter.js";
33
- import { BackgroundQuoteRefreshes } from "./background-quotes.js";
34
- import type { ChatEvent } from "../shared/chat-events.js";
32
+ import {
33
+ buildMarketStateQuoteSnapshot,
34
+ buildMarketStateSnapshot,
35
+ searchInstrumentCandidates,
36
+ } from "./market-state-api.js";
37
+ import { buildModelSetupState, createModelSetupController } from "./model-setup.js";
38
+ import { isTrustedPrivateApiRequest, privateApiCookieHeader } from "./private-api-access.js";
39
+ import { createPromptObservation, observePromptEvent } from "./prompt-observation.js";
40
+ import { QuoteSnapshotStore } from "./quote-snapshot-store.js";
41
+ import { createSessionActionsController, promptAndSettle } from "./session-actions.js";
42
+ import { waitForNewEntryId } from "./session-entry-wait.js";
43
+ import { createGracefulShutdown } from "./shutdown.js";
44
+ import { acquireWriterLock, refreshWriterLock, releaseWriterLock } from "./writer-lock.js";
45
+ import { createWsHub, type WsHub } from "./ws-hub.js";
35
46
 
36
47
  const cwd = process.cwd();
37
48
  const host = process.env.OPENCANDLE_GUI_HOST ?? "127.0.0.1";
38
49
  const port = Number(process.env.OPENCANDLE_GUI_PORT ?? 14567);
50
+ const automationHeartbeatMs = normalizeAutomationHeartbeatMs(
51
+ process.env.OPENCANDLE_AUTOMATION_HEARTBEAT_MS,
52
+ );
53
+ const allowRemotePrivateApi = process.env.OPENCANDLE_GUI_ALLOW_REMOTE_PRIVATE_API === "1";
54
+ const privateApiSessionToken = randomBytes(32).toString("base64url");
39
55
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
40
56
  const webDist = resolve(__dirname, "../web/dist");
41
57
 
@@ -43,10 +59,16 @@ const agentDir = getAgentDir();
43
59
  const authStorage = AuthStorage.create();
44
60
  const modelRegistry = ModelRegistry.create(authStorage);
45
61
  const settingsManager = SettingsManager.create(cwd, agentDir);
46
- const initialSessionManager = continueOpenCandleSession(cwd);
62
+ const initialSessionManager = createInitialGuiSessionManager(cwd);
47
63
  let sessionManager = initialSessionManager;
48
64
  const sessionDir = sessionManager.getSessionDir();
49
65
  const lockResult = await acquireWriterLock(sessionDir, "gui");
66
+ let wsHub: WsHub;
67
+ let quotePoller: BackgroundQuotePoller;
68
+ const askUserBridge = createAskUserBridge({
69
+ broadcast: (message) => wsHub.broadcast(message),
70
+ getSessionId: () => sessionManager.getSessionId(),
71
+ });
50
72
  const runtime = await createAgentSessionRuntime(
51
73
  async (opts) => {
52
74
  const services = await createAgentSessionServices({
@@ -63,24 +85,77 @@ const runtime = await createAgentSessionRuntime(
63
85
  modelRegistry,
64
86
  settingsManager,
65
87
  sessionManager: opts.sessionManager,
88
+ askUserHandler: askUserBridge.ask,
66
89
  });
67
90
  return { ...result, services, diagnostics: services.diagnostics };
68
91
  },
69
92
  { cwd, agentDir, sessionManager },
70
93
  );
71
94
  let session = runtime.session;
72
- const clients = new Set<WsClient>();
73
95
  const heartbeat = setInterval(() => refreshWriterLock(sessionDir), 5000);
74
96
  const backgroundQuoteRefreshes = new BackgroundQuoteRefreshes();
75
- let poller: NodeJS.Timeout | null = null;
76
- let quotePollInFlight = false;
97
+ const quoteSnapshotStore = new QuoteSnapshotStore(() => buildMarketStateQuoteSnapshot());
98
+ quotePoller = createBackgroundQuotePoller({
99
+ getClientCount: () => wsHub.getClientCount(),
100
+ getSessionManager: () => sessionManager,
101
+ refreshes: backgroundQuoteRefreshes,
102
+ broadcastState: () => wsHub.broadcastState(),
103
+ });
104
+ const localAutomationHeartbeat = createLocalAutomationHeartbeat({
105
+ role: lockResult.role,
106
+ getSessionId: () => sessionManager.getSessionId(),
107
+ intervalMs: automationHeartbeatMs,
108
+ });
109
+ const modelSetupController = createModelSetupController({
110
+ role: lockResult.role,
111
+ getSession: () => session,
112
+ getSessionManager: () => sessionManager,
113
+ broadcastState: () => wsHub.broadcastState(),
114
+ });
115
+ const toolInvokeController = createToolInvokeController({
116
+ role: lockResult.role,
117
+ getSessionManager: () => sessionManager,
118
+ broadcastState: () => wsHub.broadcastState(),
119
+ onMarketStateChanged: () => quoteSnapshotStore.invalidate(),
120
+ });
121
+ const sessionActionsController = createSessionActionsController({
122
+ role: lockResult.role,
123
+ cwd,
124
+ sessionDir,
125
+ getSession: () => session,
126
+ getSessionManager: () => sessionManager,
127
+ getModelSetupState: () => modelSetupController.buildCurrentModelSetupState(),
128
+ askUserBridge,
129
+ runtime,
130
+ sendBoot: (client) => wsHub.sendBoot(client),
131
+ broadcastState: () => wsHub.broadcastState(),
132
+ broadcastSessions: () => wsHub.broadcastSessions(),
133
+ });
134
+ wsHub = createWsHub({
135
+ role: lockResult.role,
136
+ lock: lockResult.lock,
137
+ cwd,
138
+ sessionDir,
139
+ getSession: () => session,
140
+ getSessionManager: () => sessionManager,
141
+ backgroundQuoteRefreshes,
142
+ askUserBridge,
143
+ modelSetupController,
144
+ toolInvokeController,
145
+ sessionActionsController,
146
+ onClientCountChanged: () => quotePoller.updatePoller(),
147
+ isTrustedRequest: (req) =>
148
+ isTrustedPrivateApiRequest(req.headers, privateApiSessionToken, req.socket.remoteAddress, {
149
+ allowRemote: allowRemotePrivateApi,
150
+ }),
151
+ });
77
152
 
78
- let unsubscribeSession = subscribeToSessionEvents();
153
+ let unsubscribeSession = wsHub.subscribeToSessionEvents();
79
154
  runtime.setRebindSession(async (nextSession) => {
80
155
  unsubscribeSession();
81
156
  session = nextSession;
82
157
  sessionManager = nextSession.sessionManager;
83
- unsubscribeSession = subscribeToSessionEvents();
158
+ unsubscribeSession = wsHub.subscribeToSessionEvents();
84
159
  });
85
160
 
86
161
  const server = createServer((req, res) => {
@@ -94,7 +169,27 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
94
169
  return;
95
170
  }
96
171
 
172
+ if (url.pathname === "/api/bootstrap" && req.method === "GET") {
173
+ if (!allowTrustedGuiRequest(req, res, "Bootstrap API")) return;
174
+ writeJson(res, await wsHub.buildBootstrapPayload());
175
+ return;
176
+ }
177
+
178
+ if (url.pathname === "/api/session/new" && req.method === "POST") {
179
+ if (!allowTrustedGuiRequest(req, res, "Session API")) return;
180
+ if (lockResult.role !== "writer") {
181
+ writeJson(res, { error: "Read-only follower mode" }, 409);
182
+ return;
183
+ }
184
+ await sessionActionsController.handleNewSession();
185
+ wsHub.broadcastState();
186
+ wsHub.broadcastSessions();
187
+ writeJson(res, await wsHub.buildBootstrapPayload());
188
+ return;
189
+ }
190
+
97
191
  if (url.pathname === "/api/sessions" && req.method === "GET") {
192
+ if (!allowTrustedGuiRequest(req, res, "Session API")) return;
98
193
  writeJson(res, {
99
194
  currentSessionId: sessionManager.getSessionId(),
100
195
  role: lockResult.role,
@@ -104,15 +199,35 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
104
199
  }
105
200
 
106
201
  if (url.pathname === "/api/session/events" && req.method === "GET") {
202
+ if (!allowTrustedGuiRequest(req, res, "Session API")) return;
107
203
  writeJson(res, {
108
204
  sessionId: sessionManager.getSessionId(),
109
205
  role: lockResult.role,
110
- events: currentChatEvents(),
206
+ events: wsHub.currentChatEvents(),
111
207
  });
112
208
  return;
113
209
  }
114
210
 
211
+ if (url.pathname === "/api/market-state" && req.method === "GET") {
212
+ if (!allowTrustedGuiRequest(req, res, "Market-state API")) return;
213
+ writeJson(res, buildMarketStateSnapshot());
214
+ return;
215
+ }
216
+
217
+ if (url.pathname === "/api/market-state/quotes" && req.method === "GET") {
218
+ if (!allowTrustedGuiRequest(req, res, "Market-state API")) return;
219
+ writeJson(res, await quoteSnapshotStore.get());
220
+ return;
221
+ }
222
+
223
+ if (url.pathname === "/api/instruments/search" && req.method === "GET") {
224
+ if (!allowTrustedGuiRequest(req, res, "Market-state API")) return;
225
+ writeJson(res, await searchInstrumentCandidates(url.searchParams.get("q") ?? ""));
226
+ return;
227
+ }
228
+
115
229
  if (url.pathname === "/api/chat/run" && req.method === "POST") {
230
+ if (!allowTrustedGuiRequest(req, res, "Chat run API")) return;
116
231
  await handleSseChatRun(req, res);
117
232
  return;
118
233
  }
@@ -122,7 +237,7 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
122
237
  if (!path.startsWith(webDist) || !existsSync(path)) {
123
238
  const fallback = resolve(join(webDist, "index.html"));
124
239
  if (!extname(requested) && fallback.startsWith(webDist) && existsSync(fallback)) {
125
- res.writeHead(200, { "content-type": "text/html; charset=utf-8" });
240
+ res.writeHead(200, privateGuiHeaders("text/html; charset=utf-8"));
126
241
  createReadStream(fallback).pipe(res);
127
242
  return;
128
243
  }
@@ -130,278 +245,43 @@ async function handleHttpRequest(req: IncomingMessage, res: ServerResponse): Pro
130
245
  return;
131
246
  }
132
247
 
133
- res.writeHead(200, { "content-type": contentType(path) });
248
+ const type = contentType(path);
249
+ res.writeHead(200, privateGuiHeaders(type));
134
250
  createReadStream(path).pipe(res);
135
251
  }
136
252
 
137
- server.on("upgrade", (req, socket) => {
138
- if (req.url !== "/ws") {
139
- socket.destroy();
140
- return;
141
- }
142
-
143
- const client = acceptWebSocket(req, socket);
144
- clients.add(client);
145
- client.onClose(() => {
146
- clients.delete(client);
147
- updatePoller();
148
- });
149
- client.onMessage((message) => void handleClientMessage(client, message));
150
- sendBoot(client);
151
- updatePoller();
152
- });
253
+ server.on("upgrade", (req, socket) => wsHub.handleUpgrade(req, socket));
153
254
 
154
255
  server.listen(port, host, () => {
155
256
  console.log(`OpenCandle GUI listening on http://${host}:${port}`);
156
257
  if (host === "0.0.0.0") {
157
258
  console.log(`OpenCandle GUI is accepting LAN/Tailscale connections on port ${port}`);
158
259
  }
159
- console.log(`Writer role: ${lockResult.role}`);
160
- });
161
-
162
- process.on("SIGINT", shutdown);
163
- process.on("SIGTERM", shutdown);
164
-
165
- async function handleClientMessage(client: WsClient, message: unknown): Promise<void> {
166
- const data = asRecord(message);
167
- try {
168
- switch (data.type) {
169
- case "chat.prompt":
170
- await handlePrompt(String(data.prompt ?? ""));
171
- break;
172
- case "tool.invoke":
173
- await handleToolInvoke(String(data.toolName ?? ""), asRecord(data.args));
174
- break;
175
- case "tool.enabled":
176
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
177
- setToolEnabled(String(data.toolName), Boolean(data.enabled));
178
- broadcast({ type: "catalog", catalog: buildCatalog(), restartRequired: true });
179
- break;
180
- case "catalog.refresh":
181
- client.send({ type: "catalog", catalog: buildCatalog() });
182
- break;
183
- case "model.setup.refresh":
184
- session.modelRegistry.refresh();
185
- broadcastModelSetup();
186
- break;
187
- case "model.setup.save_api_key":
188
- await handleSaveModelApiKey(String(data.provider ?? ""), String(data.apiKey ?? ""));
189
- broadcastModelSetup();
190
- break;
191
- case "model.setup.select_model":
192
- await handleSelectModel(String(data.provider ?? ""), String(data.modelId ?? ""));
193
- broadcastModelSetup();
194
- break;
195
- case "provider.save_api_key":
196
- await handleSaveProviderApiKey(String(data.providerId ?? ""), String(data.apiKey ?? ""));
197
- broadcast({ type: "catalog", catalog: buildCatalog() });
198
- break;
199
- case "session.new":
200
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
201
- await handleNewSession();
202
- sendBoot(client);
203
- broadcastState();
204
- broadcastSessions();
205
- break;
206
- case "session.open":
207
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
208
- await handleOpenSession(String(data.path ?? ""));
209
- sendBoot(client);
210
- broadcastState();
211
- broadcastSessions();
212
- break;
213
- case "session.rename":
214
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
215
- await handleRenameSession(String(data.path ?? ""), String(data.name ?? ""));
216
- broadcastState();
217
- broadcastSessions();
218
- break;
219
- case "session.delete":
220
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
221
- await handleDeleteSession(client, String(data.path ?? ""));
222
- break;
223
- }
224
- } catch (error) {
225
- const message = error instanceof Error ? error.message : String(error);
226
- client.send({
227
- type: "error",
228
- message,
229
- });
230
- }
231
- }
232
-
233
- async function handlePrompt(prompt: string): Promise<void> {
234
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
235
-
236
- const modelSetup = buildCurrentModelSetupState();
237
- const trimmedPrompt = prompt.trim();
238
- if (!trimmedPrompt.startsWith("/") && modelSetup.requirement !== "ready") {
239
- sessionManager.appendMessage({ role: "user", content: prompt, timestamp: Date.now() });
240
- broadcastState();
241
- const message =
242
- modelSetup.requirement === "select_model"
243
- ? "Choose an available model before chat can run. OpenCandle found configured credentials but no active model."
244
- : "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
245
- sessionManager.appendCustomMessageEntry(
246
- "opencandle-model-setup",
247
- message,
248
- true,
249
- { source: "gui", requirement: modelSetup.requirement },
250
- );
251
- broadcastState();
252
- return;
253
- }
254
-
255
- await session.prompt(prompt);
256
- broadcastState();
257
- }
258
-
259
- async function handleNewSession(): Promise<void> {
260
- const result = await runtime.newSession();
261
- if (result.cancelled) throw new Error("Session switch cancelled");
262
- }
263
-
264
- async function handleOpenSession(path: string): Promise<void> {
265
- const sessions = await SessionManager.list(cwd, sessionDir);
266
- const match = sessions.find((candidate) => candidate.path === path);
267
- if (!match) throw new Error("Unknown saved session");
268
- const result = await runtime.switchSession(match.path);
269
- if (result.cancelled) throw new Error("Session switch cancelled");
270
- }
271
-
272
- async function handleRenameSession(path: string, name: string): Promise<void> {
273
- const nextName = name.trim();
274
- if (!nextName) throw new Error("Session name cannot be empty");
275
- if (sessionManager.getSessionFile() === path) {
276
- sessionManager.appendSessionInfo(nextName);
277
- return;
278
- }
279
- await renameSessionFile(cwd, sessionDir, path, nextName);
280
- }
281
-
282
- async function handleDeleteSession(client: WsClient, path: string): Promise<void> {
283
- const deletingCurrent = sessionManager.getSessionFile() === path;
284
- await deleteSessionFile(cwd, sessionDir, path);
285
- if (deletingCurrent) {
286
- await handleNewSession();
287
- sendBoot(client);
288
- broadcastState();
289
- }
290
- broadcastSessions();
291
- }
292
-
293
- async function handleSaveModelApiKey(providerId: string, apiKey: string): Promise<void> {
294
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
295
-
296
- const provider = modelSetupProviders.find((candidate) => candidate.id === providerId);
297
- if (!provider) throw new Error(`Unknown model provider: ${providerId}`);
298
-
299
- const trimmed = apiKey.trim();
300
- if (!trimmed) throw new Error(`Paste a ${provider.label} API key first.`);
301
-
302
- session.modelRegistry.authStorage.set(provider.id, { type: "api_key", key: trimmed });
303
- session.modelRegistry.refresh();
304
-
305
- const model = findPreferredModel(session.modelRegistry, provider);
306
- if (!model) {
307
- throw new Error(`Saved the ${provider.label} key, but no ${provider.label} models are available yet.`);
308
- }
309
-
310
- await session.setModel(model);
311
- await session.settingsManager.flush();
312
- sessionManager.appendCustomMessageEntry(
313
- "opencandle-model-setup",
314
- `Connected ${provider.label} and selected ${model.provider}/${model.id}.`,
315
- true,
316
- { source: "gui", provider: provider.id, model: `${model.provider}/${model.id}` },
317
- );
318
- broadcastState();
319
- }
320
-
321
- async function handleSaveProviderApiKey(providerId: string, apiKey: string): Promise<void> {
322
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
323
-
324
- const descriptor = PROVIDERS.find((candidate) => candidate.id === providerId);
325
- if (!descriptor) throw new Error(`Unknown provider: ${providerId}`);
326
-
327
- if (getCredentialSource(descriptor.id) === "env") {
328
- throw new Error(
329
- `${descriptor.displayName} is set via the ${descriptor.envVar} environment variable. Unset it to override here.`,
330
- );
331
- }
332
-
333
- const trimmed = apiKey.trim();
334
- if (!trimmed) throw new Error(`Paste a ${descriptor.displayName} API key first.`);
335
-
336
- const validation = await validateCredential(descriptor.id as ProviderId, trimmed);
337
- if (validation.status === "invalid") {
338
- const statusHint = validation.httpStatus !== undefined ? ` (HTTP ${validation.httpStatus})` : "";
339
- const messageHint = validation.message ? ` — ${validation.message}` : "";
340
- throw new Error(
341
- `${descriptor.displayName} rejected the key${statusHint}${messageHint}. The existing configuration was not changed.`,
260
+ if (allowRemotePrivateApi) {
261
+ console.log(
262
+ "OpenCandle GUI private market-state API accepts cookie-authenticated remote requests.",
342
263
  );
343
264
  }
265
+ console.log(`Writer role: ${lockResult.role}`);
266
+ localAutomationHeartbeat.start();
267
+ });
344
268
 
345
- persistProviderCredential(descriptor.id as ProviderId, trimmed);
346
-
347
- const verifiedNote =
348
- validation.status === "transient"
349
- ? `Saved ${descriptor.displayName} key but couldn't verify it (${validation.reason}). The next request will surface any issue.`
350
- : `Connected ${descriptor.displayName}. Key saved to ~/.opencandle/config.json.`;
351
-
352
- sessionManager.appendCustomMessageEntry(
353
- "opencandle-provider-setup",
354
- verifiedNote,
355
- true,
356
- { source: "gui", provider: descriptor.id, status: validation.status },
357
- );
358
- broadcastState();
359
- }
360
-
361
- async function handleSelectModel(provider: string, modelId: string): Promise<void> {
362
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
363
- session.modelRegistry.refresh();
364
- const model = session.modelRegistry.find(provider, modelId);
365
- if (!model) throw new Error(`Unknown model: ${provider}/${modelId}`);
366
- await session.setModel(model);
367
- await session.settingsManager.flush();
368
- }
369
-
370
- async function handleToolInvoke(toolName: string, args: Record<string, unknown>): Promise<void> {
371
- if (lockResult.role !== "writer") throw new Error("Read-only follower mode");
372
- const tool = getAllTools().find((candidate) => candidate.name === toolName);
373
- if (!tool) throw new Error(`Unknown tool: ${toolName}`);
374
- await invokeToolFromUi(sessionManager, tool, args, "ui");
375
- broadcastState();
376
- }
377
-
378
- function sendBoot(client: WsClient): void {
379
- const snapshot = buildStateSnapshot();
380
- client.send({
381
- type: "boot",
382
- role: lockResult.role,
383
- lock: lockResult.lock,
384
- sessionId: sessionManager.getSessionId(),
385
- catalog: buildCatalog(),
386
- modelSetup: buildCurrentModelSetupState(),
387
- });
388
- client.send({
389
- type: "state.snapshot",
390
- ...snapshot,
391
- });
392
- void SessionManager.list(cwd, sessionDir).then((sessions) => client.send({ type: "sessions", sessions }));
393
- }
394
-
395
- function broadcastModelSetup(): void {
396
- broadcast({ type: "model.setup", modelSetup: buildCurrentModelSetupState() });
397
- }
269
+ const shutdown = createGracefulShutdown({
270
+ server,
271
+ cleanup: async () => {
272
+ clearInterval(heartbeat);
273
+ quotePoller.stop();
274
+ localAutomationHeartbeat.stop();
275
+ wsHub.closeClients();
276
+ unsubscribeSession();
277
+ releaseWriterLock(sessionDir);
278
+ await runtime.dispose();
279
+ },
280
+ exit: (code) => process.exit(code),
281
+ });
398
282
 
399
- function broadcastState(): void {
400
- broadcast({
401
- type: "state.snapshot",
402
- ...buildStateSnapshot(),
403
- });
404
- }
283
+ process.once("SIGINT", shutdown);
284
+ process.once("SIGTERM", shutdown);
405
285
 
406
286
  async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Promise<void> {
407
287
  if (lockResult.role !== "writer") {
@@ -416,6 +296,15 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
416
296
  return;
417
297
  }
418
298
 
299
+ const sessionConflict = chatRunSessionConflict(
300
+ asRecord(body).sessionId,
301
+ sessionManager.getSessionId(),
302
+ );
303
+ if (sessionConflict) {
304
+ writeJson(res, sessionConflict, 409);
305
+ return;
306
+ }
307
+
419
308
  res.writeHead(200, {
420
309
  "content-type": "text/event-stream; charset=utf-8",
421
310
  "cache-control": "no-cache, no-transform",
@@ -424,9 +313,17 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
424
313
 
425
314
  let seq = 1;
426
315
  const runId = `gui-run-${Date.now()}`;
427
- const sessionId = sessionManager.getSessionId();
428
- const beforeEntries = sessionManager.getEntries();
316
+ const runSession = session;
317
+ const runSessionManager = sessionManager;
318
+ const sessionId = runSessionManager.getSessionId();
319
+ // Name new sessions by the user's words before any workflow transform
320
+ // replaces the turn text, so the sidebar shows what the user actually asked.
321
+ if (!prompt.startsWith("/") && !runSessionManager.getSessionName()) {
322
+ runSessionManager.appendSessionInfo(prompt.length > 80 ? `${prompt.slice(0, 77)}...` : prompt);
323
+ }
324
+ const beforeEntries = runSessionManager.getEntries();
429
325
  const beforeCount = beforeEntries.length;
326
+ const beforeIds = new Set(beforeEntries.map((entry) => entry.id));
430
327
  writeSse(res, { type: "run.started", runId, sessionId, seq: seq++ });
431
328
  res.flushHeaders?.();
432
329
  const liveStartSeq = seq;
@@ -435,14 +332,41 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
435
332
  sessionId,
436
333
  startSeq: seq,
437
334
  emit: (event) => writeSse(res, event),
335
+ originalPrompt: prompt,
336
+ });
337
+ const observation = createPromptObservation();
338
+ const unsubscribeLive = runSession.subscribe((event) => {
339
+ liveAdapter.handle(event);
340
+ observePromptEvent(observation, event);
438
341
  });
439
- const unsubscribeLive = session.subscribe((event) => liveAdapter.handle(event));
440
342
 
441
343
  try {
442
- await handlePrompt(prompt);
344
+ const modelSetup = buildModelSetupState(runSession.modelRegistry, runSession.model);
345
+ if (!prompt.startsWith("/") && modelSetup.requirement !== "ready") {
346
+ runSessionManager.appendMessage({ role: "user", content: prompt, timestamp: Date.now() });
347
+ const message =
348
+ modelSetup.requirement === "select_model"
349
+ ? "Choose an available model before chat can run. OpenCandle found configured credentials but no active model."
350
+ : "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
351
+ runSessionManager.appendCustomMessageEntry("opencandle-model-setup", message, true, {
352
+ source: "gui",
353
+ requirement: modelSetup.requirement,
354
+ });
355
+ wsHub.broadcastState();
356
+ } else {
357
+ await promptAndSettle(runSession, prompt, beforeIds, observation);
358
+ wsHub.broadcastState();
359
+ }
443
360
  seq = liveAdapter.nextSeq();
444
361
  if (seq === liveStartSeq) {
445
- const newEntries = sessionManager.getEntries().slice(beforeCount);
362
+ await waitForNewEntryId(
363
+ () => runSessionManager.getEntries().map((entry) => entry.id),
364
+ beforeIds,
365
+ );
366
+ const newEntries = runSessionManager
367
+ .getEntries()
368
+ .slice(beforeCount)
369
+ .filter((entry) => !beforeIds.has(entry.id));
446
370
  const events = sessionEntriesToChatEvents(newEntries, {
447
371
  sessionId,
448
372
  updatedAt: new Date().toISOString(),
@@ -464,88 +388,33 @@ async function handleSseChatRun(req: IncomingMessage, res: ServerResponse): Prom
464
388
  }
465
389
  }
466
390
 
467
- function buildStateSnapshot() {
468
- const sessionId = sessionManager.getSessionId();
469
- const entries = sessionManager.getEntries();
470
- return {
471
- sessionId,
472
- state: projectDashboard(backgroundQuoteRefreshes.withEntries(entries), sessionId),
473
- entries,
474
- events: currentChatEvents(entries),
475
- };
476
- }
477
-
478
- function currentChatEvents(entries = sessionManager.getEntries()): ChatEvent[] {
479
- return sessionEntriesToChatEvents(entries, {
480
- sessionId: sessionManager.getSessionId(),
481
- title: sessionManager.getSessionName(),
482
- });
483
- }
484
-
485
- function broadcastSessions(): void {
486
- void SessionManager.list(cwd, sessionDir).then((sessions) => broadcast({ type: "sessions", sessions }));
487
- }
488
-
489
- function buildCurrentModelSetupState() {
490
- return buildModelSetupState(session.modelRegistry, session.model);
491
- }
492
-
493
- function broadcast(message: unknown): void {
494
- for (const client of clients) client.send(message);
495
- }
496
-
497
- function subscribeToSessionEvents(): () => void {
498
- return session.subscribe((event) => {
499
- broadcast({ type: "session.event", event });
500
- broadcastState();
501
- });
391
+ function writeJson(res: ServerResponse, value: unknown, status = 200): void {
392
+ res.writeHead(status, { "content-type": "application/json" });
393
+ res.end(JSON.stringify(value));
502
394
  }
503
395
 
504
- function updatePoller(): void {
505
- if (clients.size > 0 && !poller) {
506
- poller = setInterval(() => void pollVisibleQuotes(), 30000);
507
- }
508
- if (clients.size === 0 && poller) {
509
- clearInterval(poller);
510
- poller = null;
511
- }
396
+ function allowTrustedGuiRequest(req: IncomingMessage, res: ServerResponse, label: string): boolean {
397
+ if (
398
+ isTrustedPrivateApiRequest(req.headers, privateApiSessionToken, req.socket.remoteAddress, {
399
+ allowRemote: allowRemotePrivateApi,
400
+ })
401
+ )
402
+ return true;
403
+ res.writeHead(403, { "content-type": "application/json" });
404
+ res.end(
405
+ JSON.stringify({
406
+ error: `${label} is only available to trusted GUI browser sessions.`,
407
+ }),
408
+ );
409
+ return false;
512
410
  }
513
411
 
514
- async function pollVisibleQuotes(): Promise<void> {
515
- if (quotePollInFlight) return;
516
- quotePollInFlight = true;
517
- try {
518
- const state = projectDashboard(sessionManager.getEntries(), sessionManager.getSessionId());
519
- const tool = getAllTools().find((candidate) => candidate.name === "get_stock_quote");
520
- if (!tool) return;
521
- for (const row of state.watchlist.filter((item) => item.pinned || item.quote)) {
522
- const result = await invokeToolFromUi(
523
- sessionManager,
524
- tool,
525
- { symbol: row.symbol },
526
- "background",
527
- { recordTranscript: false },
528
- );
529
- backgroundQuoteRefreshes.upsert({
530
- symbol: row.symbol,
531
- toolName: tool.name,
532
- args: { symbol: row.symbol },
533
- value: result.result.details,
534
- content: result.result.content,
535
- isError: result.isError,
536
- });
537
- }
538
- broadcastState();
539
- } catch (error) {
540
- console.warn(`Background quote refresh failed: ${error instanceof Error ? error.message : String(error)}`);
541
- } finally {
542
- quotePollInFlight = false;
412
+ function privateGuiHeaders(contentTypeValue: string): Record<string, string> {
413
+ const headers: Record<string, string> = { "content-type": contentTypeValue };
414
+ if (contentTypeValue.startsWith("text/html")) {
415
+ headers["set-cookie"] = privateApiCookieHeader(privateApiSessionToken);
543
416
  }
544
- }
545
-
546
- function writeJson(res: ServerResponse, value: unknown): void {
547
- res.writeHead(200, { "content-type": "application/json" });
548
- res.end(JSON.stringify(value));
417
+ return headers;
549
418
  }
550
419
 
551
420
  function writeSse(res: ServerResponse, event: ChatEvent): void {
@@ -578,15 +447,6 @@ function contentType(path: string): string {
578
447
 
579
448
  function asRecord(value: unknown): Record<string, unknown> {
580
449
  return typeof value === "object" && value !== null && !Array.isArray(value)
581
- ? value as Record<string, unknown>
450
+ ? (value as Record<string, unknown>)
582
451
  : {};
583
452
  }
584
-
585
- function shutdown(): void {
586
- clearInterval(heartbeat);
587
- if (poller) clearInterval(poller);
588
- releaseWriterLock(sessionDir);
589
- void runtime.dispose().finally(() => {
590
- server.close(() => process.exit(0));
591
- });
592
- }