opencandle 0.11.1 → 0.12.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 (653) hide show
  1. package/README.md +20 -24
  2. package/dist/analysts/contracts.js +0 -1
  3. package/dist/analysts/orchestrator.js +0 -1
  4. package/dist/cli-main.js +13 -7
  5. package/dist/cli-options.d.ts +4 -0
  6. package/dist/cli-options.js +33 -0
  7. package/dist/cli.js +8 -3
  8. package/dist/config.js +0 -1
  9. package/dist/doctor/cli-command.js +2 -1
  10. package/dist/doctor/render.js +0 -1
  11. package/dist/doctor/report.js +40 -11
  12. package/dist/gui/server/ask-user-bridge.d.ts +19 -0
  13. package/dist/gui/server/ask-user-bridge.js +55 -0
  14. package/dist/gui/server/automation-heartbeat.d.ts +25 -0
  15. package/dist/gui/server/automation-heartbeat.js +61 -0
  16. package/dist/gui/server/background-quotes.d.ts +39 -0
  17. package/dist/gui/server/background-quotes.js +71 -0
  18. package/dist/gui/server/chat-event-adapter.d.ts +16 -0
  19. package/dist/gui/server/chat-event-adapter.js +263 -0
  20. package/dist/gui/server/gui-session-manager.d.ts +2 -0
  21. package/dist/gui/server/gui-session-manager.js +4 -0
  22. package/dist/gui/server/http-routes.d.ts +69 -0
  23. package/dist/gui/server/http-routes.js +1008 -0
  24. package/dist/gui/server/invoke-tool.d.ts +47 -0
  25. package/dist/gui/server/invoke-tool.js +373 -0
  26. package/dist/gui/server/live-chat-event-adapter.d.ts +19 -0
  27. package/dist/gui/server/live-chat-event-adapter.js +206 -0
  28. package/dist/gui/server/local-session-coordinator.d.ts +26 -0
  29. package/dist/gui/server/local-session-coordinator.js +53 -0
  30. package/dist/gui/server/market-state-api.d.ts +138 -0
  31. package/dist/gui/server/market-state-api.js +355 -0
  32. package/dist/gui/server/model-setup.d.ts +64 -0
  33. package/dist/gui/server/model-setup.js +150 -0
  34. package/dist/gui/server/private-api-access.d.ts +6 -0
  35. package/dist/gui/server/private-api-access.js +53 -0
  36. package/dist/gui/server/projector.d.ts +49 -0
  37. package/dist/gui/server/projector.js +296 -0
  38. package/dist/gui/server/prompt-observation.d.ts +8 -0
  39. package/dist/gui/server/prompt-observation.js +43 -0
  40. package/dist/gui/server/quote-snapshot-store.d.ts +14 -0
  41. package/dist/gui/server/quote-snapshot-store.js +49 -0
  42. package/dist/gui/server/server.d.ts +1 -0
  43. package/dist/gui/server/server.js +259 -0
  44. package/dist/gui/server/session-actions.d.ts +60 -0
  45. package/dist/gui/server/session-actions.js +218 -0
  46. package/dist/gui/server/session-entry-wait.d.ts +27 -0
  47. package/dist/gui/server/session-entry-wait.js +129 -0
  48. package/dist/gui/server/session-list.d.ts +2 -0
  49. package/dist/gui/server/session-list.js +11 -0
  50. package/dist/gui/server/shutdown.d.ts +10 -0
  51. package/dist/gui/server/shutdown.js +29 -0
  52. package/dist/gui/server/tool-invoke-ack.d.ts +17 -0
  53. package/dist/gui/server/tool-invoke-ack.js +26 -0
  54. package/dist/gui/server/tool-metadata.d.ts +93 -0
  55. package/dist/gui/server/tool-metadata.js +148 -0
  56. package/dist/gui/server/websocket.d.ts +9 -0
  57. package/dist/gui/server/websocket.js +124 -0
  58. package/dist/gui/server/writer-lock.d.ts +1 -0
  59. package/dist/gui/server/writer-lock.js +1 -0
  60. package/dist/gui/server/ws-hub.d.ts +50 -0
  61. package/dist/gui/server/ws-hub.js +284 -0
  62. package/dist/gui/shared/chat-events.d.ts +174 -0
  63. package/dist/gui/shared/chat-events.js +13 -0
  64. package/dist/gui/shared/event-reducer.d.ts +3 -0
  65. package/dist/gui/shared/event-reducer.js +187 -0
  66. package/dist/index.js +0 -1
  67. package/dist/infra/cache.js +0 -1
  68. package/dist/infra/freshness.js +0 -1
  69. package/dist/infra/http-client.js +1 -2
  70. package/dist/infra/index.js +0 -1
  71. package/dist/infra/market-calendar.js +0 -1
  72. package/dist/infra/native-dependencies.js +0 -1
  73. package/dist/infra/node-version.js +0 -1
  74. package/dist/infra/open-url.js +0 -1
  75. package/dist/infra/opencandle-paths.js +0 -1
  76. package/dist/infra/rate-limiter.js +0 -1
  77. package/dist/market-state/alert-conditions.js +0 -1
  78. package/dist/market-state/alert-runner.d.ts +1 -0
  79. package/dist/market-state/alert-runner.js +5 -4
  80. package/dist/market-state/daily-report.js +0 -1
  81. package/dist/market-state/local-automation-service.js +0 -1
  82. package/dist/market-state/notification-delivery.js +0 -1
  83. package/dist/market-state/resolve-for-mutation.js +0 -1
  84. package/dist/market-state/resolve.js +0 -1
  85. package/dist/market-state/service.d.ts +26 -21
  86. package/dist/market-state/service.js +175 -36
  87. package/dist/market-state/summaries.js +1 -13
  88. package/dist/memory/index.js +0 -1
  89. package/dist/memory/manager.js +0 -1
  90. package/dist/memory/preference-extractor.js +0 -1
  91. package/dist/memory/retrieval.js +0 -1
  92. package/dist/memory/sqlite.js +60 -11
  93. package/dist/memory/storage.js +0 -1
  94. package/dist/memory/tool-defaults.js +0 -1
  95. package/dist/memory/types.js +0 -1
  96. package/dist/monitor.js +0 -1
  97. package/dist/onboarding/connect.js +0 -1
  98. package/dist/onboarding/credential-interceptor.js +0 -1
  99. package/dist/onboarding/degradation-accumulator.js +0 -1
  100. package/dist/onboarding/prompt-user.d.ts +1 -1
  101. package/dist/onboarding/prompt-user.js +0 -1
  102. package/dist/onboarding/provider-status.js +0 -1
  103. package/dist/onboarding/providers.js +0 -1
  104. package/dist/onboarding/state.js +0 -1
  105. package/dist/onboarding/tool-helpers.js +0 -1
  106. package/dist/onboarding/tool-tags.js +0 -1
  107. package/dist/onboarding/validate-model-key.d.ts +17 -0
  108. package/dist/onboarding/validate-model-key.js +54 -0
  109. package/dist/onboarding/validation.js +0 -1
  110. package/dist/pi/opencandle-extension.js +2 -2
  111. package/dist/pi/session-action-dedupe.js +0 -1
  112. package/dist/pi/session-storage.js +0 -1
  113. package/dist/pi/session-writer-lock.js +0 -1
  114. package/dist/pi/session.js +0 -1
  115. package/dist/pi/setup.d.ts +2 -0
  116. package/dist/pi/setup.js +10 -3
  117. package/dist/pi/tool-adapter.js +0 -1
  118. package/dist/pi/tui-session-coordinator.js +0 -1
  119. package/dist/prompts/context-builder.js +1 -2
  120. package/dist/prompts/disclaimer.js +0 -1
  121. package/dist/prompts/policy-cards.js +0 -1
  122. package/dist/prompts/sections.js +0 -1
  123. package/dist/prompts/symbol-preflight.js +0 -1
  124. package/dist/prompts/workflow-prompts.js +0 -1
  125. package/dist/providers/alpha-vantage.js +2 -3
  126. package/dist/providers/coingecko.js +0 -1
  127. package/dist/providers/errors.js +0 -1
  128. package/dist/providers/exa-search.js +11 -10
  129. package/dist/providers/external-tool-command.js +0 -1
  130. package/dist/providers/external-tool-error.js +0 -1
  131. package/dist/providers/fear-greed.js +0 -1
  132. package/dist/providers/finnhub.js +0 -1
  133. package/dist/providers/fred.js +0 -1
  134. package/dist/providers/index.js +0 -1
  135. package/dist/providers/polymarket.js +0 -1
  136. package/dist/providers/provider-credential-error.js +0 -1
  137. package/dist/providers/reddit-cli.js +0 -1
  138. package/dist/providers/reddit.js +0 -1
  139. package/dist/providers/sec-edgar.js +0 -1
  140. package/dist/providers/tradingview.js +1 -2
  141. package/dist/providers/twitter-cli.js +0 -1
  142. package/dist/providers/twitter.js +0 -1
  143. package/dist/providers/web-search.js +15 -12
  144. package/dist/providers/with-fallback.js +0 -1
  145. package/dist/providers/wrap-provider.js +0 -1
  146. package/dist/providers/yahoo-finance.js +67 -4
  147. package/dist/routing/classify-intent.js +0 -1
  148. package/dist/routing/defaults.js +0 -1
  149. package/dist/routing/entity-extractor.js +2 -3
  150. package/dist/routing/fund-symbols.js +0 -1
  151. package/dist/routing/horizon.js +0 -1
  152. package/dist/routing/index.js +0 -1
  153. package/dist/routing/legacy-rule-router.js +0 -1
  154. package/dist/routing/planning.js +0 -1
  155. package/dist/routing/route-manifest.js +0 -1
  156. package/dist/routing/router-llm-client.d.ts +2 -2
  157. package/dist/routing/router-llm-client.js +0 -1
  158. package/dist/routing/router-prompt.js +0 -1
  159. package/dist/routing/router-types.js +0 -1
  160. package/dist/routing/router.js +1 -2
  161. package/dist/routing/slot-resolver.js +0 -1
  162. package/dist/routing/symbol-disambiguator.js +0 -1
  163. package/dist/routing/turn-context.js +0 -1
  164. package/dist/routing/types.js +0 -1
  165. package/dist/runtime/answer-contracts.js +0 -1
  166. package/dist/runtime/artifact-contracts.js +0 -1
  167. package/dist/runtime/evidence.js +0 -1
  168. package/dist/runtime/numeric-claims.js +0 -1
  169. package/dist/runtime/planning-evidence.js +0 -1
  170. package/dist/runtime/prompt-step.js +0 -1
  171. package/dist/runtime/provider-tracker.js +0 -1
  172. package/dist/runtime/run-context.js +0 -1
  173. package/dist/runtime/session-coordinator.js +0 -1
  174. package/dist/runtime/session-title.js +0 -1
  175. package/dist/runtime/tool-defaults-wrapper.js +0 -1
  176. package/dist/runtime/validation.js +0 -1
  177. package/dist/runtime/workflow-events.js +0 -1
  178. package/dist/runtime/workflow-runner.d.ts +1 -0
  179. package/dist/runtime/workflow-runner.js +8 -3
  180. package/dist/runtime/workflow-types.js +0 -1
  181. package/dist/sentiment/adapters/finnhub.js +0 -1
  182. package/dist/sentiment/adapters/reddit.js +0 -1
  183. package/dist/sentiment/adapters/twitter.js +0 -1
  184. package/dist/sentiment/adapters/web.js +0 -1
  185. package/dist/sentiment/index.js +4 -2
  186. package/dist/sentiment/insights.js +0 -1
  187. package/dist/sentiment/keywords.js +0 -1
  188. package/dist/sentiment/pipeline.js +0 -1
  189. package/dist/sentiment/scorer.js +0 -1
  190. package/dist/sentiment/store.js +0 -1
  191. package/dist/sentiment/trends.js +0 -1
  192. package/dist/sentiment/types.js +0 -1
  193. package/dist/system-prompt.js +0 -1
  194. package/dist/tool-kit.js +0 -1
  195. package/dist/tools/fundamentals/company-overview.d.ts +1 -1
  196. package/dist/tools/fundamentals/company-overview.js +2 -1
  197. package/dist/tools/fundamentals/comps.js +6 -4
  198. package/dist/tools/fundamentals/dcf.js +0 -1
  199. package/dist/tools/fundamentals/earnings.d.ts +1 -1
  200. package/dist/tools/fundamentals/earnings.js +2 -1
  201. package/dist/tools/fundamentals/financials.js +2 -1
  202. package/dist/tools/fundamentals/sec-filings.js +0 -1
  203. package/dist/tools/index.d.ts +116 -2
  204. package/dist/tools/index.js +0 -1
  205. package/dist/tools/interaction/ask-user.js +11 -1
  206. package/dist/tools/macro/event-probabilities.js +0 -1
  207. package/dist/tools/macro/fear-greed.d.ts +1 -1
  208. package/dist/tools/macro/fear-greed.js +0 -1
  209. package/dist/tools/macro/fred-data.d.ts +1 -1
  210. package/dist/tools/macro/fred-data.js +2 -1
  211. package/dist/tools/market/crypto-history.js +0 -1
  212. package/dist/tools/market/crypto-price.d.ts +2 -2
  213. package/dist/tools/market/crypto-price.js +0 -1
  214. package/dist/tools/market/screen-stocks.js +0 -1
  215. package/dist/tools/market/search-ticker.js +1 -2
  216. package/dist/tools/market/stock-history.js +0 -1
  217. package/dist/tools/market/stock-quote.d.ts +2 -2
  218. package/dist/tools/market/stock-quote.js +0 -1
  219. package/dist/tools/options/greeks.js +0 -1
  220. package/dist/tools/options/option-chain.d.ts +2 -2
  221. package/dist/tools/options/option-chain.js +0 -1
  222. package/dist/tools/portfolio/alerts.d.ts +2 -1
  223. package/dist/tools/portfolio/alerts.js +228 -2
  224. package/dist/tools/portfolio/correlation.js +23 -5
  225. package/dist/tools/portfolio/daily-report.js +0 -1
  226. package/dist/tools/portfolio/holdings-overlap.js +0 -1
  227. package/dist/tools/portfolio/notifications.js +0 -1
  228. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  229. package/dist/tools/portfolio/risk-analysis.js +0 -1
  230. package/dist/tools/portfolio/tracker.d.ts +3 -1
  231. package/dist/tools/portfolio/tracker.js +57 -10
  232. package/dist/tools/portfolio/watchlist.d.ts +3 -6
  233. package/dist/tools/portfolio/watchlist.js +101 -110
  234. package/dist/tools/sentiment/insight-format.js +0 -1
  235. package/dist/tools/sentiment/query-match.js +0 -1
  236. package/dist/tools/sentiment/reddit-sentiment.js +0 -1
  237. package/dist/tools/sentiment/sentiment-summary.js +8 -4
  238. package/dist/tools/sentiment/sentiment-trend.d.ts +5 -1
  239. package/dist/tools/sentiment/sentiment-trend.js +6 -2
  240. package/dist/tools/sentiment/twitter-sentiment.js +0 -1
  241. package/dist/tools/sentiment/untrusted-text.js +0 -1
  242. package/dist/tools/sentiment/web-search.d.ts +1 -1
  243. package/dist/tools/sentiment/web-search.js +0 -1
  244. package/dist/tools/sentiment/web-sentiment.js +0 -1
  245. package/dist/tools/technical/backtest.js +0 -1
  246. package/dist/tools/technical/indicators.js +0 -1
  247. package/dist/types/fundamentals.js +0 -1
  248. package/dist/types/index.js +0 -1
  249. package/dist/types/macro.js +0 -1
  250. package/dist/types/market.d.ts +6 -0
  251. package/dist/types/market.js +0 -1
  252. package/dist/types/options.js +0 -1
  253. package/dist/types/portfolio.js +0 -1
  254. package/dist/types/prediction-markets.js +0 -1
  255. package/dist/types/sentiment.js +0 -1
  256. package/dist/workflows/compare-assets.js +0 -1
  257. package/dist/workflows/index.js +0 -1
  258. package/dist/workflows/options-screener.js +0 -1
  259. package/dist/workflows/portfolio-builder.js +0 -1
  260. package/gui/web/dist/assets/CatalogOverlay-CAc7e3Pf.js +1 -0
  261. package/gui/web/dist/assets/index-BOEKd9wT.css +2 -0
  262. package/gui/web/dist/assets/index-C-H05cQ2.js +65 -0
  263. package/gui/web/dist/index.html +2 -2
  264. package/package.json +11 -12
  265. package/dist/analysts/contracts.js.map +0 -1
  266. package/dist/analysts/orchestrator.js.map +0 -1
  267. package/dist/cli-main.js.map +0 -1
  268. package/dist/cli.js.map +0 -1
  269. package/dist/config.js.map +0 -1
  270. package/dist/doctor/cli-command.js.map +0 -1
  271. package/dist/doctor/render.js.map +0 -1
  272. package/dist/doctor/report.js.map +0 -1
  273. package/dist/index.js.map +0 -1
  274. package/dist/infra/cache.js.map +0 -1
  275. package/dist/infra/freshness.js.map +0 -1
  276. package/dist/infra/http-client.js.map +0 -1
  277. package/dist/infra/index.js.map +0 -1
  278. package/dist/infra/market-calendar.js.map +0 -1
  279. package/dist/infra/native-dependencies.js.map +0 -1
  280. package/dist/infra/node-version.js.map +0 -1
  281. package/dist/infra/open-url.js.map +0 -1
  282. package/dist/infra/opencandle-paths.js.map +0 -1
  283. package/dist/infra/rate-limiter.js.map +0 -1
  284. package/dist/market-state/alert-conditions.js.map +0 -1
  285. package/dist/market-state/alert-runner.js.map +0 -1
  286. package/dist/market-state/daily-report.js.map +0 -1
  287. package/dist/market-state/local-automation-service.js.map +0 -1
  288. package/dist/market-state/notification-delivery.js.map +0 -1
  289. package/dist/market-state/resolve-for-mutation.js.map +0 -1
  290. package/dist/market-state/resolve.js.map +0 -1
  291. package/dist/market-state/service.js.map +0 -1
  292. package/dist/market-state/summaries.js.map +0 -1
  293. package/dist/memory/index.js.map +0 -1
  294. package/dist/memory/manager.js.map +0 -1
  295. package/dist/memory/preference-extractor.js.map +0 -1
  296. package/dist/memory/retrieval.js.map +0 -1
  297. package/dist/memory/sqlite.js.map +0 -1
  298. package/dist/memory/storage.js.map +0 -1
  299. package/dist/memory/tool-defaults.js.map +0 -1
  300. package/dist/memory/types.js.map +0 -1
  301. package/dist/monitor.js.map +0 -1
  302. package/dist/onboarding/connect.js.map +0 -1
  303. package/dist/onboarding/credential-interceptor.js.map +0 -1
  304. package/dist/onboarding/degradation-accumulator.js.map +0 -1
  305. package/dist/onboarding/prompt-user.js.map +0 -1
  306. package/dist/onboarding/provider-status.js.map +0 -1
  307. package/dist/onboarding/providers.js.map +0 -1
  308. package/dist/onboarding/state.js.map +0 -1
  309. package/dist/onboarding/tool-helpers.js.map +0 -1
  310. package/dist/onboarding/tool-tags.js.map +0 -1
  311. package/dist/onboarding/validation.js.map +0 -1
  312. package/dist/pi/opencandle-extension.js.map +0 -1
  313. package/dist/pi/session-action-dedupe.js.map +0 -1
  314. package/dist/pi/session-storage.js.map +0 -1
  315. package/dist/pi/session-writer-lock.js.map +0 -1
  316. package/dist/pi/session.js.map +0 -1
  317. package/dist/pi/setup.js.map +0 -1
  318. package/dist/pi/tool-adapter.js.map +0 -1
  319. package/dist/pi/tui-session-coordinator.js.map +0 -1
  320. package/dist/prompts/context-builder.js.map +0 -1
  321. package/dist/prompts/disclaimer.js.map +0 -1
  322. package/dist/prompts/policy-cards.js.map +0 -1
  323. package/dist/prompts/sections.js.map +0 -1
  324. package/dist/prompts/symbol-preflight.js.map +0 -1
  325. package/dist/prompts/workflow-prompts.js.map +0 -1
  326. package/dist/providers/alpha-vantage.js.map +0 -1
  327. package/dist/providers/coingecko.js.map +0 -1
  328. package/dist/providers/errors.js.map +0 -1
  329. package/dist/providers/exa-search.js.map +0 -1
  330. package/dist/providers/external-tool-command.js.map +0 -1
  331. package/dist/providers/external-tool-error.js.map +0 -1
  332. package/dist/providers/fear-greed.js.map +0 -1
  333. package/dist/providers/finnhub.js.map +0 -1
  334. package/dist/providers/fred.js.map +0 -1
  335. package/dist/providers/index.js.map +0 -1
  336. package/dist/providers/polymarket.js.map +0 -1
  337. package/dist/providers/provider-credential-error.js.map +0 -1
  338. package/dist/providers/reddit-cli.js.map +0 -1
  339. package/dist/providers/reddit.js.map +0 -1
  340. package/dist/providers/sec-edgar.js.map +0 -1
  341. package/dist/providers/tradingview.js.map +0 -1
  342. package/dist/providers/twitter-cli.js.map +0 -1
  343. package/dist/providers/twitter.js.map +0 -1
  344. package/dist/providers/web-search.js.map +0 -1
  345. package/dist/providers/with-fallback.js.map +0 -1
  346. package/dist/providers/wrap-provider.js.map +0 -1
  347. package/dist/providers/yahoo-finance.js.map +0 -1
  348. package/dist/routing/classify-intent.js.map +0 -1
  349. package/dist/routing/defaults.js.map +0 -1
  350. package/dist/routing/entity-extractor.js.map +0 -1
  351. package/dist/routing/fund-symbols.js.map +0 -1
  352. package/dist/routing/horizon.js.map +0 -1
  353. package/dist/routing/index.js.map +0 -1
  354. package/dist/routing/legacy-rule-router.js.map +0 -1
  355. package/dist/routing/planning.js.map +0 -1
  356. package/dist/routing/route-manifest.js.map +0 -1
  357. package/dist/routing/router-llm-client.js.map +0 -1
  358. package/dist/routing/router-prompt.js.map +0 -1
  359. package/dist/routing/router-types.js.map +0 -1
  360. package/dist/routing/router.js.map +0 -1
  361. package/dist/routing/slot-resolver.js.map +0 -1
  362. package/dist/routing/symbol-disambiguator.js.map +0 -1
  363. package/dist/routing/turn-context.js.map +0 -1
  364. package/dist/routing/types.js.map +0 -1
  365. package/dist/runtime/answer-contracts.js.map +0 -1
  366. package/dist/runtime/artifact-contracts.js.map +0 -1
  367. package/dist/runtime/evidence.js.map +0 -1
  368. package/dist/runtime/numeric-claims.js.map +0 -1
  369. package/dist/runtime/planning-evidence.js.map +0 -1
  370. package/dist/runtime/prompt-step.js.map +0 -1
  371. package/dist/runtime/provider-tracker.js.map +0 -1
  372. package/dist/runtime/run-context.js.map +0 -1
  373. package/dist/runtime/session-coordinator.js.map +0 -1
  374. package/dist/runtime/session-title.js.map +0 -1
  375. package/dist/runtime/tool-defaults-wrapper.js.map +0 -1
  376. package/dist/runtime/validation.js.map +0 -1
  377. package/dist/runtime/workflow-events.js.map +0 -1
  378. package/dist/runtime/workflow-runner.js.map +0 -1
  379. package/dist/runtime/workflow-types.js.map +0 -1
  380. package/dist/sentiment/adapters/finnhub.js.map +0 -1
  381. package/dist/sentiment/adapters/reddit.js.map +0 -1
  382. package/dist/sentiment/adapters/twitter.js.map +0 -1
  383. package/dist/sentiment/adapters/web.js.map +0 -1
  384. package/dist/sentiment/index.js.map +0 -1
  385. package/dist/sentiment/insights.js.map +0 -1
  386. package/dist/sentiment/keywords.js.map +0 -1
  387. package/dist/sentiment/pipeline.js.map +0 -1
  388. package/dist/sentiment/scorer.js.map +0 -1
  389. package/dist/sentiment/store.js.map +0 -1
  390. package/dist/sentiment/trends.js.map +0 -1
  391. package/dist/sentiment/types.js.map +0 -1
  392. package/dist/system-prompt.js.map +0 -1
  393. package/dist/tool-kit.js.map +0 -1
  394. package/dist/tools/fundamentals/company-overview.js.map +0 -1
  395. package/dist/tools/fundamentals/comps.js.map +0 -1
  396. package/dist/tools/fundamentals/dcf.js.map +0 -1
  397. package/dist/tools/fundamentals/earnings.js.map +0 -1
  398. package/dist/tools/fundamentals/financials.js.map +0 -1
  399. package/dist/tools/fundamentals/sec-filings.js.map +0 -1
  400. package/dist/tools/index.js.map +0 -1
  401. package/dist/tools/interaction/ask-user.js.map +0 -1
  402. package/dist/tools/macro/event-probabilities.js.map +0 -1
  403. package/dist/tools/macro/fear-greed.js.map +0 -1
  404. package/dist/tools/macro/fred-data.js.map +0 -1
  405. package/dist/tools/market/crypto-history.js.map +0 -1
  406. package/dist/tools/market/crypto-price.js.map +0 -1
  407. package/dist/tools/market/screen-stocks.js.map +0 -1
  408. package/dist/tools/market/search-ticker.js.map +0 -1
  409. package/dist/tools/market/stock-history.js.map +0 -1
  410. package/dist/tools/market/stock-quote.js.map +0 -1
  411. package/dist/tools/options/greeks.js.map +0 -1
  412. package/dist/tools/options/option-chain.js.map +0 -1
  413. package/dist/tools/portfolio/alerts.js.map +0 -1
  414. package/dist/tools/portfolio/correlation.js.map +0 -1
  415. package/dist/tools/portfolio/daily-report.js.map +0 -1
  416. package/dist/tools/portfolio/holdings-overlap.js.map +0 -1
  417. package/dist/tools/portfolio/notifications.js.map +0 -1
  418. package/dist/tools/portfolio/risk-analysis.js.map +0 -1
  419. package/dist/tools/portfolio/tracker.js.map +0 -1
  420. package/dist/tools/portfolio/watchlist.js.map +0 -1
  421. package/dist/tools/sentiment/insight-format.js.map +0 -1
  422. package/dist/tools/sentiment/query-match.js.map +0 -1
  423. package/dist/tools/sentiment/reddit-sentiment.js.map +0 -1
  424. package/dist/tools/sentiment/sentiment-summary.js.map +0 -1
  425. package/dist/tools/sentiment/sentiment-trend.js.map +0 -1
  426. package/dist/tools/sentiment/twitter-sentiment.js.map +0 -1
  427. package/dist/tools/sentiment/untrusted-text.js.map +0 -1
  428. package/dist/tools/sentiment/web-search.js.map +0 -1
  429. package/dist/tools/sentiment/web-sentiment.js.map +0 -1
  430. package/dist/tools/technical/backtest.js.map +0 -1
  431. package/dist/tools/technical/indicators.js.map +0 -1
  432. package/dist/types/fundamentals.js.map +0 -1
  433. package/dist/types/index.js.map +0 -1
  434. package/dist/types/macro.js.map +0 -1
  435. package/dist/types/market.js.map +0 -1
  436. package/dist/types/options.js.map +0 -1
  437. package/dist/types/portfolio.js.map +0 -1
  438. package/dist/types/prediction-markets.js.map +0 -1
  439. package/dist/types/sentiment.js.map +0 -1
  440. package/dist/workflows/compare-assets.js.map +0 -1
  441. package/dist/workflows/index.js.map +0 -1
  442. package/dist/workflows/options-screener.js.map +0 -1
  443. package/dist/workflows/portfolio-builder.js.map +0 -1
  444. package/gui/server/ask-user-bridge.ts +0 -89
  445. package/gui/server/automation-heartbeat.ts +0 -97
  446. package/gui/server/background-quotes.ts +0 -127
  447. package/gui/server/chat-event-adapter.ts +0 -294
  448. package/gui/server/gui-session-manager.ts +0 -5
  449. package/gui/server/http-routes.ts +0 -1201
  450. package/gui/server/invoke-tool.ts +0 -490
  451. package/gui/server/live-chat-event-adapter.ts +0 -243
  452. package/gui/server/local-session-coordinator.ts +0 -97
  453. package/gui/server/market-state-api.ts +0 -331
  454. package/gui/server/model-setup.ts +0 -254
  455. package/gui/server/package.json +0 -5
  456. package/gui/server/private-api-access.ts +0 -62
  457. package/gui/server/projector.ts +0 -392
  458. package/gui/server/prompt-observation.ts +0 -58
  459. package/gui/server/quote-snapshot-store.ts +0 -50
  460. package/gui/server/server.ts +0 -289
  461. package/gui/server/session-actions.ts +0 -323
  462. package/gui/server/session-entry-wait.ts +0 -179
  463. package/gui/server/shutdown.ts +0 -47
  464. package/gui/server/tool-invoke-ack.ts +0 -49
  465. package/gui/server/tool-metadata.ts +0 -167
  466. package/gui/server/websocket.ts +0 -138
  467. package/gui/server/writer-lock.ts +0 -1
  468. package/gui/server/ws-hub.ts +0 -403
  469. package/gui/shared/chat-events.ts +0 -165
  470. package/gui/shared/event-reducer.ts +0 -220
  471. package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +0 -1
  472. package/gui/web/dist/assets/index-D4F9AJnn.css +0 -2
  473. package/gui/web/dist/assets/index-DtqMMBTr.js +0 -65
  474. package/src/analysts/contracts.ts +0 -176
  475. package/src/analysts/orchestrator.ts +0 -244
  476. package/src/cli-main.ts +0 -510
  477. package/src/cli.ts +0 -17
  478. package/src/config.ts +0 -272
  479. package/src/doctor/cli-command.ts +0 -83
  480. package/src/doctor/render.ts +0 -37
  481. package/src/doctor/report.ts +0 -638
  482. package/src/index.ts +0 -5
  483. package/src/infra/cache.ts +0 -127
  484. package/src/infra/freshness.ts +0 -165
  485. package/src/infra/http-client.ts +0 -134
  486. package/src/infra/index.ts +0 -14
  487. package/src/infra/market-calendar.ts +0 -193
  488. package/src/infra/native-dependencies.ts +0 -84
  489. package/src/infra/node-version.ts +0 -23
  490. package/src/infra/open-url.ts +0 -28
  491. package/src/infra/opencandle-paths.ts +0 -53
  492. package/src/infra/rate-limiter.ts +0 -77
  493. package/src/market-state/alert-conditions.ts +0 -82
  494. package/src/market-state/alert-runner.ts +0 -863
  495. package/src/market-state/daily-report.ts +0 -237
  496. package/src/market-state/local-automation-service.ts +0 -162
  497. package/src/market-state/notification-delivery.ts +0 -158
  498. package/src/market-state/resolve-for-mutation.ts +0 -24
  499. package/src/market-state/resolve.ts +0 -112
  500. package/src/market-state/service.ts +0 -2208
  501. package/src/market-state/summaries.ts +0 -75
  502. package/src/memory/index.ts +0 -10
  503. package/src/memory/manager.ts +0 -190
  504. package/src/memory/preference-extractor.ts +0 -106
  505. package/src/memory/retrieval.ts +0 -71
  506. package/src/memory/sqlite.ts +0 -577
  507. package/src/memory/storage.ts +0 -195
  508. package/src/memory/tool-defaults.ts +0 -108
  509. package/src/memory/types.ts +0 -71
  510. package/src/monitor.ts +0 -123
  511. package/src/onboarding/connect.ts +0 -177
  512. package/src/onboarding/credential-interceptor.ts +0 -122
  513. package/src/onboarding/degradation-accumulator.ts +0 -77
  514. package/src/onboarding/prompt-user.ts +0 -85
  515. package/src/onboarding/provider-status.ts +0 -382
  516. package/src/onboarding/providers.ts +0 -442
  517. package/src/onboarding/state.ts +0 -216
  518. package/src/onboarding/tool-helpers.ts +0 -104
  519. package/src/onboarding/tool-tags.ts +0 -244
  520. package/src/onboarding/validation.ts +0 -152
  521. package/src/pi/opencandle-extension.ts +0 -1294
  522. package/src/pi/session-action-dedupe.ts +0 -155
  523. package/src/pi/session-storage.ts +0 -5
  524. package/src/pi/session-writer-lock.ts +0 -426
  525. package/src/pi/session.ts +0 -84
  526. package/src/pi/setup.ts +0 -399
  527. package/src/pi/tool-adapter.ts +0 -49
  528. package/src/pi/tui-session-coordinator.ts +0 -351
  529. package/src/prompts/context-builder.ts +0 -326
  530. package/src/prompts/disclaimer.ts +0 -9
  531. package/src/prompts/policy-cards.ts +0 -228
  532. package/src/prompts/sections.ts +0 -46
  533. package/src/prompts/symbol-preflight.ts +0 -80
  534. package/src/prompts/workflow-prompts.ts +0 -504
  535. package/src/providers/alpha-vantage.ts +0 -340
  536. package/src/providers/coingecko.ts +0 -95
  537. package/src/providers/errors.ts +0 -9
  538. package/src/providers/exa-search.ts +0 -375
  539. package/src/providers/external-tool-command.ts +0 -164
  540. package/src/providers/external-tool-error.ts +0 -20
  541. package/src/providers/fear-greed.ts +0 -45
  542. package/src/providers/finnhub.ts +0 -125
  543. package/src/providers/fred.ts +0 -83
  544. package/src/providers/index.ts +0 -17
  545. package/src/providers/polymarket.ts +0 -214
  546. package/src/providers/provider-credential-error.ts +0 -23
  547. package/src/providers/reddit-cli.ts +0 -286
  548. package/src/providers/reddit.ts +0 -106
  549. package/src/providers/sec-edgar.ts +0 -326
  550. package/src/providers/tradingview.ts +0 -399
  551. package/src/providers/twitter-cli.ts +0 -202
  552. package/src/providers/twitter.ts +0 -102
  553. package/src/providers/web-search.ts +0 -303
  554. package/src/providers/with-fallback.ts +0 -42
  555. package/src/providers/wrap-provider.ts +0 -105
  556. package/src/providers/yahoo-finance.ts +0 -984
  557. package/src/routing/classify-intent.ts +0 -380
  558. package/src/routing/defaults.ts +0 -29
  559. package/src/routing/entity-extractor.ts +0 -557
  560. package/src/routing/fund-symbols.ts +0 -58
  561. package/src/routing/horizon.ts +0 -7
  562. package/src/routing/index.ts +0 -70
  563. package/src/routing/legacy-rule-router.ts +0 -13
  564. package/src/routing/planning.ts +0 -829
  565. package/src/routing/route-manifest.ts +0 -308
  566. package/src/routing/router-llm-client.ts +0 -64
  567. package/src/routing/router-prompt.ts +0 -159
  568. package/src/routing/router-types.ts +0 -84
  569. package/src/routing/router.ts +0 -1332
  570. package/src/routing/slot-resolver.ts +0 -213
  571. package/src/routing/symbol-disambiguator.ts +0 -72
  572. package/src/routing/turn-context.ts +0 -108
  573. package/src/routing/types.ts +0 -77
  574. package/src/runtime/answer-contracts.ts +0 -693
  575. package/src/runtime/artifact-contracts.ts +0 -77
  576. package/src/runtime/evidence.ts +0 -78
  577. package/src/runtime/numeric-claims.ts +0 -108
  578. package/src/runtime/planning-evidence.ts +0 -597
  579. package/src/runtime/prompt-step.ts +0 -185
  580. package/src/runtime/provider-tracker.ts +0 -40
  581. package/src/runtime/run-context.ts +0 -32
  582. package/src/runtime/session-coordinator.ts +0 -1037
  583. package/src/runtime/session-title.ts +0 -60
  584. package/src/runtime/tool-defaults-wrapper.ts +0 -43
  585. package/src/runtime/validation.ts +0 -211
  586. package/src/runtime/workflow-events.ts +0 -77
  587. package/src/runtime/workflow-runner.ts +0 -182
  588. package/src/runtime/workflow-types.ts +0 -105
  589. package/src/sentiment/adapters/finnhub.ts +0 -49
  590. package/src/sentiment/adapters/reddit.ts +0 -65
  591. package/src/sentiment/adapters/twitter.ts +0 -36
  592. package/src/sentiment/adapters/web.ts +0 -44
  593. package/src/sentiment/index.ts +0 -49
  594. package/src/sentiment/insights.ts +0 -269
  595. package/src/sentiment/keywords.ts +0 -31
  596. package/src/sentiment/pipeline.ts +0 -91
  597. package/src/sentiment/scorer.ts +0 -89
  598. package/src/sentiment/store.ts +0 -260
  599. package/src/sentiment/trends.ts +0 -96
  600. package/src/sentiment/types.ts +0 -112
  601. package/src/system-prompt.ts +0 -115
  602. package/src/tool-kit.ts +0 -69
  603. package/src/tools/AGENTS.md +0 -36
  604. package/src/tools/fundamentals/company-overview.ts +0 -64
  605. package/src/tools/fundamentals/comps.ts +0 -180
  606. package/src/tools/fundamentals/dcf.ts +0 -447
  607. package/src/tools/fundamentals/earnings.ts +0 -57
  608. package/src/tools/fundamentals/financials.ts +0 -63
  609. package/src/tools/fundamentals/sec-filings.ts +0 -105
  610. package/src/tools/index.ts +0 -104
  611. package/src/tools/interaction/ask-user.ts +0 -93
  612. package/src/tools/macro/event-probabilities.ts +0 -141
  613. package/src/tools/macro/fear-greed.ts +0 -41
  614. package/src/tools/macro/fred-data.ts +0 -92
  615. package/src/tools/market/crypto-history.ts +0 -72
  616. package/src/tools/market/crypto-price.ts +0 -65
  617. package/src/tools/market/screen-stocks.ts +0 -290
  618. package/src/tools/market/search-ticker.ts +0 -254
  619. package/src/tools/market/stock-history.ts +0 -104
  620. package/src/tools/market/stock-quote.ts +0 -73
  621. package/src/tools/options/greeks.ts +0 -81
  622. package/src/tools/options/option-chain.ts +0 -134
  623. package/src/tools/portfolio/alerts.ts +0 -457
  624. package/src/tools/portfolio/correlation.ts +0 -189
  625. package/src/tools/portfolio/daily-report.ts +0 -107
  626. package/src/tools/portfolio/holdings-overlap.ts +0 -139
  627. package/src/tools/portfolio/notifications.ts +0 -45
  628. package/src/tools/portfolio/risk-analysis.ts +0 -173
  629. package/src/tools/portfolio/tracker.ts +0 -308
  630. package/src/tools/portfolio/watchlist.ts +0 -288
  631. package/src/tools/sentiment/insight-format.ts +0 -50
  632. package/src/tools/sentiment/query-match.ts +0 -117
  633. package/src/tools/sentiment/reddit-sentiment.ts +0 -403
  634. package/src/tools/sentiment/sentiment-summary.ts +0 -383
  635. package/src/tools/sentiment/sentiment-trend.ts +0 -75
  636. package/src/tools/sentiment/twitter-sentiment.ts +0 -287
  637. package/src/tools/sentiment/untrusted-text.ts +0 -21
  638. package/src/tools/sentiment/web-search.ts +0 -186
  639. package/src/tools/sentiment/web-sentiment.ts +0 -96
  640. package/src/tools/technical/backtest.ts +0 -336
  641. package/src/tools/technical/indicators.ts +0 -281
  642. package/src/types/fundamentals.ts +0 -47
  643. package/src/types/index.ts +0 -26
  644. package/src/types/macro.ts +0 -27
  645. package/src/types/market.ts +0 -46
  646. package/src/types/options.ts +0 -53
  647. package/src/types/portfolio.ts +0 -83
  648. package/src/types/prediction-markets.ts +0 -13
  649. package/src/types/sentiment.ts +0 -129
  650. package/src/workflows/compare-assets.ts +0 -89
  651. package/src/workflows/index.ts +0 -3
  652. package/src/workflows/options-screener.ts +0 -125
  653. package/src/workflows/portfolio-builder.ts +0 -63
@@ -1,1294 +0,0 @@
1
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
- import {
3
- buildComprehensiveAnalysisDefinition,
4
- isAnalysisRequest,
5
- normalizeSymbol,
6
- } from "../analysts/orchestrator.js";
7
- import { getConfig } from "../config.js";
8
- import type { InstrumentCandidate } from "../market-state/resolve.js";
9
- import { runProviderConnect } from "../onboarding/connect.js";
10
- import { resolveCredentialRequired } from "../onboarding/credential-interceptor.js";
11
- import { createDegradationAccumulator } from "../onboarding/degradation-accumulator.js";
12
- import { promptUser } from "../onboarding/prompt-user.js";
13
- import { getProvider, type ProviderId } from "../onboarding/providers.js";
14
- import {
15
- loadOnboardingState,
16
- markProviderNeverAsk,
17
- markProviderSnoozed,
18
- markWelcomeShown,
19
- saveOnboardingState,
20
- shouldShowWelcome,
21
- } from "../onboarding/state.js";
22
- import { buildConnectedTag, buildSkippedTag, parseToolTag } from "../onboarding/tool-tags.js";
23
- import { DISCLAIMER_TEXT } from "../prompts/disclaimer.js";
24
- import { formatPreflightDropAnnotation, preflightSymbols } from "../prompts/symbol-preflight.js";
25
- import { buildAssumptionsBlockFromRouter } from "../prompts/workflow-prompts.js";
26
- import {
27
- buildResolvedTurnContext,
28
- createPiAiRouterClient,
29
- resolveOptionsScreenerSlots,
30
- resolvePortfolioSlots,
31
- route as routeLlm,
32
- } from "../routing/index.js";
33
- import type { RouterInputContext, RouterLlmClient, RouterOutput } from "../routing/router-types.js";
34
- import type { ResolvedTurnContext } from "../routing/turn-context.js";
35
- import type {
36
- CompareAssetsSlots,
37
- ExtractedEntities,
38
- SlotResolution,
39
- SlotSource,
40
- } from "../routing/types.js";
41
- import { SessionCoordinator } from "../runtime/session-coordinator.js";
42
- import { generateSessionTitle } from "../runtime/session-title.js";
43
- import { registerAskUserTool } from "../tools/interaction/ask-user.js";
44
- import type { AskUserHandler } from "../types/index.js";
45
- import {
46
- buildCompareAssetsWorkflowDefinition,
47
- buildOptionsScreenerWorkflowDefinition,
48
- buildPortfolioWorkflowDefinition,
49
- } from "../workflows/index.js";
50
- import { getOpenCandleToolDefinitions } from "./tool-adapter.js";
51
-
52
- export interface OpenCandleExtensionOptions {
53
- askUserHandler?: AskUserHandler;
54
- /**
55
- * Optional router LLM client. When provided, this instance is used instead
56
- * of the pi-ai-backed default. Intended for tests + offline eval runners.
57
- */
58
- routerLlmClient?: RouterLlmClient;
59
- symbolSearch?: (query: string) => Promise<InstrumentCandidate[]>;
60
- /**
61
- * Optional completion function for LLM session titles. When omitted, the
62
- * extension resolves a pi-ai client from the session's current model.
63
- * Intended for tests + offline runners.
64
- */
65
- titleCompletion?: (prompt: string) => Promise<string>;
66
- }
67
-
68
- interface OriginalInputMarkerContext {
69
- sessionManager?: { getBranch?: () => unknown[]; getEntries?: () => unknown[] };
70
- }
71
-
72
- export default function openCandleExtension(
73
- pi: ExtensionAPI,
74
- options?: OpenCandleExtensionOptions,
75
- ): void {
76
- const coordinator = new SessionCoordinator();
77
-
78
- // Workflow transforms replace the user's turn with the expanded prompt; this
79
- // marker lets the GUI render the user's original words instead.
80
- const markOriginalInput = (original: string, ctx?: OriginalInputMarkerContext): void => {
81
- if (hasUnconsumedOriginalInputMarker(ctx)) return;
82
- pi.appendEntry("opencandle-user-input", { original });
83
- };
84
-
85
- const appendComprehensiveAnalysisWorkflowEntry = (
86
- symbol: string,
87
- definition: ReturnType<typeof buildComprehensiveAnalysisDefinition>,
88
- ): void => {
89
- pi.appendEntry("opencandle-workflow", {
90
- workflow: "comprehensive_analysis",
91
- resolvedSlots: { symbol },
92
- analystsTotal: definition.steps.filter((step) => step.stepType.startsWith("analyst_")).length,
93
- });
94
- };
95
-
96
- // Credential-interception state. Lifetime:
97
- // `sessionPromptedSet` — cleared on session_start, persists across turns
98
- // within a session so users don't get re-prompted after picking
99
- // "continue without".
100
- // `hardPromptFiredInWorkflow` — reset on turn_start (the nearest clean
101
- // boundary for a single user request). Enforces the "at most one hard
102
- // prompt per workflow" cap.
103
- // `degradationAccumulator` — per-turn record of soft-tier providers that
104
- // fell back to their keyless alternative. Reset on turn_start; flushed
105
- // on turn_end via `pi.appendEntry("opencandle-turn-gap", ...)` for
106
- // session observability. Does NOT pause the workflow or mutate tool
107
- // results inline — soft-degraded tags remain visible to the LLM so its
108
- // final answer can surface them in a **Data gaps** section.
109
- const sessionPromptedSet = new Set<ProviderId>();
110
- let hardPromptFiredInWorkflow = false;
111
- const degradationAccumulator = createDegradationAccumulator();
112
- let activeToolSnapshot: string[] | null = null;
113
- let currentRouteToolContext: ResolvedTurnContext | null = null;
114
- // LLM session-title state: one title attempt per session per process.
115
- // Reset on session_start; set before the (async) title call fires so
116
- // overlapping turn_end events cannot double-title.
117
- let sessionTitleAttempted = false;
118
-
119
- // Register tools
120
- for (const tool of getOpenCandleToolDefinitions({ askUserHandler: options?.askUserHandler })) {
121
- pi.registerTool(tool);
122
- }
123
- registerAskUserTool(pi, options?.askUserHandler);
124
-
125
- // /analyze command
126
- pi.registerCommand("analyze", {
127
- description: "Run the multi-analyst OpenCandle workflow for a ticker symbol",
128
- handler: async (args, ctx) => {
129
- const symbol = normalizeSymbol(args);
130
- if (!symbol) {
131
- ctx.ui.notify("Usage: /analyze <ticker>", "warning");
132
- return;
133
- }
134
- const definition = buildComprehensiveAnalysisDefinition(symbol);
135
- appendComprehensiveAnalysisWorkflowEntry(symbol, definition);
136
- coordinator.executeWorkflow(pi, definition, ctx);
137
- },
138
- });
139
-
140
- // /setup command — reconfigure the OpenCandle AI model (sign-in / API key).
141
- // Data providers are configured separately via `/connect`.
142
- pi.registerCommand("setup", {
143
- description: "Reconfigure the OpenCandle AI model (sign-in or API key)",
144
- handler: async (_args, ctx) => {
145
- const result = await coordinator.runSetup(pi, ctx, { mode: "manual" });
146
- if (result === "ready") {
147
- ctx.ui.notify("OpenCandle setup complete.", "info");
148
- }
149
- },
150
- });
151
-
152
- // /connect command — reconfigurable sectioned setup for data providers.
153
- // `/connect` with no args opens a picker listing all providers.
154
- // `/connect <alias|id|category>` routes to a specific provider (or a
155
- // sub-picker for multi-provider categories like "search").
156
- pi.registerCommand("connect", {
157
- description: "Connect an API-key data provider (Alpha Vantage, FRED, Finnhub, Brave, Exa)",
158
- handler: async (args, ctx) => {
159
- const { listApiKeyProviders, resolveProviderFromArgument, hasCredential, isApiKeyProvider } =
160
- await import("../onboarding/providers.js");
161
-
162
- const formatState = (id: ProviderId): string => {
163
- const state = loadOnboardingState().providers[id];
164
- if (state?.status === "completed") return "Configured";
165
- if (state?.status === "snoozed") {
166
- return `Snoozed until ${state.snoozeUntil.slice(0, 10)}`;
167
- }
168
- if (state?.status === "never_ask") return "Never-ask";
169
- if (hasCredential(id)) return "Configured (via env)";
170
- return "Not configured";
171
- };
172
-
173
- const pickProvider = async (
174
- providers: readonly ReturnType<typeof getProvider>[],
175
- ): Promise<ProviderId | undefined> => {
176
- const labels = providers.map(
177
- (p) => `${p.displayName} — ${p.unlocks.slice(0, 2).join(", ")} [${formatState(p.id)}]`,
178
- );
179
- const choice = await ctx.ui.select("Which provider would you like to connect?", labels);
180
- if (choice === undefined) return undefined;
181
- const index = labels.indexOf(choice);
182
- return providers[index]?.id;
183
- };
184
-
185
- const trimmed = args.trim();
186
- let targetId: ProviderId | undefined;
187
-
188
- if (trimmed === "") {
189
- // Bare /connect → full picker.
190
- targetId = await pickProvider(listApiKeyProviders());
191
- } else {
192
- const resolved = resolveProviderFromArgument(trimmed);
193
- if (!resolved) {
194
- const all = listApiKeyProviders()
195
- .map((p) => ` ${p.displayName} (${p.aliases.join(", ")})`)
196
- .join("\n");
197
- ctx.ui.notify(`Unknown provider: "${trimmed}". Available:\n${all}`, "warning");
198
- return;
199
- }
200
- if (Array.isArray(resolved)) {
201
- // Multi-provider category — show a sub-picker.
202
- const apiKeyProviders = resolved.filter(isApiKeyProvider);
203
- if (apiKeyProviders.length === 0) {
204
- ctx.ui.notify(
205
- `"${trimmed}" does not use API-key setup. Run opencandle doctor for setup status.`,
206
- "warning",
207
- );
208
- return;
209
- }
210
- targetId = await pickProvider(apiKeyProviders);
211
- } else {
212
- const descriptor = resolved as ReturnType<typeof getProvider>;
213
- if (!isApiKeyProvider(descriptor)) {
214
- ctx.ui.notify(
215
- `${descriptor.displayName} does not use API-key setup. Run opencandle doctor for setup status.`,
216
- "warning",
217
- );
218
- return;
219
- }
220
- targetId = descriptor.id;
221
- }
222
- }
223
-
224
- if (!targetId) {
225
- ctx.ui.notify("Connect cancelled.", "info");
226
- return;
227
- }
228
-
229
- const result = await runProviderConnect(ctx, targetId);
230
- if (result.status === "connected") {
231
- ctx.ui.notify(`${getProvider(targetId).displayName} is now connected.`, "info");
232
- } else if (result.status === "cancelled") {
233
- ctx.ui.notify("Connect cancelled.", "info");
234
- }
235
- // "blocked_by_env" already notifies from inside runProviderConnect.
236
- },
237
- });
238
-
239
- // Session start
240
- pi.on("session_start", async (_event, ctx) => {
241
- coordinator.initSession(ctx.sessionManager.getSessionId());
242
- sessionPromptedSet.clear();
243
- hardPromptFiredInWorkflow = false;
244
- sessionTitleAttempted = false;
245
-
246
- if (!ctx.hasUI) return;
247
- // Pin the user-facing disclaimer in the UI footer for the entire session.
248
- // Using `setStatus` keeps it always visible to the user without ever
249
- // entering the LLM's conversation context (unlike `sendMessage`, which Pi
250
- // reinjects as a `role:"user"` message every turn).
251
- ctx.ui.setStatus("opencandle-disclaimer", DISCLAIMER_TEXT);
252
-
253
- const result = await coordinator.runSetup(pi, ctx, { mode: "startup" });
254
- if (result === "shutdown") {
255
- return;
256
- }
257
-
258
- // One-shot welcome on the very first session (gated on welcomeShownAt).
259
- // Uses `pi.sendMessage` with `display: true` so the welcome lands in the
260
- // chat transcript (persistent, scrollable) rather than a transient
261
- // `ctx.ui.notify` banner.
262
- const state = loadOnboardingState();
263
- if (shouldShowWelcome(state, ctx.hasUI)) {
264
- const WELCOME_BODY =
265
- "Welcome to OpenCandle. I'm your AI copilot for market analysis.\n\n" +
266
- "Try something like:\n" +
267
- " • analyze NVDA — full deep-dive on a ticker\n" +
268
- " • quote TSLA — just the price and daily move\n" +
269
- " • how's bitcoin? — crypto\n" +
270
- " • what's r/wallstreetbets saying about META? — social sentiment\n\n" +
271
- "You're running with just an LLM right now, which covers most of what\n" +
272
- "people want. For fundamentals, economic data, or premium news you'll\n" +
273
- "need a few free API keys — I'll offer to help when they'd actually\n" +
274
- "make a difference, or run /connect anytime.";
275
-
276
- pi.sendMessage({
277
- customType: "opencandle-welcome",
278
- content: [{ type: "text", text: WELCOME_BODY }],
279
- display: true,
280
- });
281
- saveOnboardingState(markWelcomeShown(state));
282
- } else {
283
- ctx.ui.notify(
284
- "OpenCandle ready. Try /analyze NVDA or /connect to add data providers.",
285
- "info",
286
- );
287
- }
288
- });
289
-
290
- // Reset the per-workflow prompt cap AND the degradation accumulator on each
291
- // new turn. One user request = one workflow invocation = at most one hard
292
- // prompt and one combined soft-degradation annotation.
293
- pi.on("turn_start", async () => {
294
- hardPromptFiredInWorkflow = false;
295
- degradationAccumulator.reset();
296
- });
297
-
298
- // At turn_end, flush the soft-degradation accumulator to a session entry so
299
- // downstream consumers (UI renderers, debug inspectors, later turns) can see
300
- // which soft providers fell back during this turn. The LLM has already
301
- // emitted its answer by the time this fires, so the per-tool-result
302
- // soft-degraded tags remain the primary carrier for the in-turn gap note.
303
- //
304
- // Also persist a per-turn disclaimer entry via `appendEntry`. `CustomEntry`
305
- // is NOT sent to LLM context (unlike `sendMessage`/`CustomMessage`, which
306
- // Pi's `convertToLlm` reinjects as a `role:"user"` message), so this keeps
307
- // the stance instruction-free while still producing a session record that
308
- // downstream renderers / exporters / tests can surface. The always-visible
309
- // footer status pinned at session_start is the primary user-visible channel.
310
- pi.on("turn_end", async (event) => {
311
- const msg = event.message;
312
- const isFinalAssistantTurn = msg.role === "assistant" && msg.stopReason === "stop";
313
- if (isFinalAssistantTurn) {
314
- pi.appendEntry("opencandle-disclaimer", { text: DISCLAIMER_TEXT });
315
- restoreRouteToolScope();
316
- }
317
-
318
- if (degradationAccumulator.isEmpty()) return;
319
- const state = loadOnboardingState();
320
- const annotation = degradationAccumulator.buildCombinedAnnotation(state);
321
- if (annotation !== null) {
322
- pi.appendEntry("opencandle-turn-gap", { annotation });
323
- }
324
- degradationAccumulator.reset();
325
- });
326
-
327
- // LLM session titles. After the first completed user↔assistant exchange,
328
- // replace the placeholder session name (the raw first prompt, set by the
329
- // GUI server / Pi's firstMessage fallback) with a short model-written
330
- // summary. Manual renames are left alone, and each session is attempted at
331
- // most once per process. Model failures are swallowed (placeholder stays)
332
- // but recorded as an `opencandle-title-error` entry for observability.
333
- pi.on("turn_end", async (event, ctx) => {
334
- if (sessionTitleAttempted) return;
335
- const msg = event.message as { role?: unknown; stopReason?: unknown };
336
- if (msg?.role !== "assistant" || msg?.stopReason !== "stop") return;
337
- const sessionManager = ctx?.sessionManager;
338
- if (typeof sessionManager?.getBranch !== "function") return;
339
-
340
- const branch = sessionManager.getBranch();
341
- let firstUserText: string | null = null;
342
- let firstAssistantText: string | null = null;
343
- let originalInput: string | null = null;
344
- for (const entry of branch) {
345
- if (
346
- originalInput === null &&
347
- entry.type === "custom" &&
348
- (entry as { customType?: unknown }).customType === "opencandle-user-input"
349
- ) {
350
- const original = (entry as { data?: { original?: unknown } }).data?.original;
351
- if (typeof original === "string" && original.trim().length > 0) {
352
- originalInput = original;
353
- }
354
- continue;
355
- }
356
- if (entry.type !== "message") continue;
357
- const message = (entry as { message?: { role?: unknown; content?: unknown } }).message;
358
- const text = extractMessageText(message?.content);
359
- if (text.trim().length === 0) continue;
360
- if (firstUserText === null && message?.role === "user") firstUserText = text;
361
- if (firstAssistantText === null && message?.role === "assistant") {
362
- firstAssistantText = text;
363
- }
364
- if (firstUserText !== null && firstAssistantText !== null) break;
365
- }
366
- // Fall back to the just-finished assistant message when the branch has
367
- // not surfaced it yet at turn_end time.
368
- if (firstAssistantText === null) {
369
- const eventText = extractMessageText((msg as { content?: unknown }).content);
370
- if (eventText.trim().length > 0) firstAssistantText = eventText;
371
- }
372
- if (firstUserText === null || firstAssistantText === null) return;
373
-
374
- const userText = originalInput ?? firstUserText;
375
- // A manual rename must be left alone — only replace the placeholder
376
- // (unset, the raw first prompt, or the GUI's "first 77 chars + ..." form).
377
- if (!isPlaceholderSessionName(pi.getSessionName(), [userText, firstUserText])) return;
378
-
379
- const completion =
380
- options?.titleCompletion ??
381
- (() => {
382
- const client = options?.routerLlmClient ?? resolveRouterLlmClient(ctx);
383
- return client ? (prompt: string) => client.complete(prompt) : null;
384
- })();
385
- if (!completion) return;
386
-
387
- sessionTitleAttempted = true;
388
- try {
389
- const title = await generateSessionTitle(
390
- { userText, assistantText: firstAssistantText.slice(0, 500) },
391
- completion,
392
- );
393
- if (title !== null) {
394
- pi.setSessionName(title);
395
- }
396
- } catch (err) {
397
- pi.appendEntry("opencandle-title-error", {
398
- message: err instanceof Error ? err.message : String(err),
399
- });
400
- }
401
- });
402
-
403
- // Intercept tool results for credential-required and soft-degraded tags.
404
- pi.on("tool_result", async (event, ctx) => {
405
- // First pass: record any soft-degradation tags in the per-turn accumulator
406
- // WITHOUT mutating the tool result (the inline tag stays visible to the
407
- // LLM so it can surface the gap in its final answer). Multiple tags per
408
- // tool-result block are deduplicated by the accumulator's Set.
409
- for (const block of event.content) {
410
- if (block.type !== "text") continue;
411
- const parsed = parseToolTag(block.text);
412
- if (parsed?.kind === "soft_degraded") {
413
- degradationAccumulator.record(parsed.provider);
414
- }
415
- }
416
-
417
- // Second pass: look for setup-required tags; on match, run the interception
418
- // decision and either replace the tool result or prompt the user. Only the
419
- // first setup tag in the content list is acted on.
420
- for (const block of event.content) {
421
- if (block.type !== "text") continue;
422
- const parsed = parseToolTag(block.text);
423
- if (
424
- !parsed ||
425
- (parsed.kind !== "credential_required" && parsed.kind !== "external_tool_required")
426
- ) {
427
- continue;
428
- }
429
-
430
- if (parsed.kind === "external_tool_required") {
431
- const state = loadOnboardingState();
432
- const descriptor = getProvider(parsed.provider);
433
- if (state.providers[parsed.provider]?.status === "never_ask") {
434
- return {
435
- content: [
436
- {
437
- type: "text",
438
- text:
439
- `${buildSkippedTag({
440
- provider: parsed.provider,
441
- reason: "credential_not_provided",
442
- remediation: `run ${parsed.installCmd} and ${parsed.loginCmd ?? "rdt login"} to enable ${descriptor.displayName} (silenced)`,
443
- silenced: true,
444
- })}\n\n` +
445
- `${descriptor.displayName} data was not fetched because you previously asked not to be reminded about this provider.`,
446
- },
447
- ],
448
- };
449
- }
450
- const setupLabel =
451
- parsed.reason === "not_installed"
452
- ? `Continue after installing ${descriptor.displayName}`
453
- : `Continue after logging in to ${descriptor.displayName}`;
454
- const skipLabel = `Skip ${descriptor.displayName} once`;
455
- const neverLabel = `Always skip ${descriptor.displayName}`;
456
- const installOrLogin =
457
- parsed.reason === "not_installed"
458
- ? `Install command: ${parsed.installCmd}.`
459
- : `Login command: ${parsed.loginCmd ?? "rdt login"}.`;
460
- const questionBody =
461
- `${descriptor.displayName} requires a local external tool before this data can be fetched. ` +
462
- `${installOrLogin} How would you like to proceed?`;
463
-
464
- sessionPromptedSet.add(parsed.provider);
465
- const promptResult = await promptUser(
466
- ctx,
467
- {
468
- question: questionBody,
469
- questionType: "select",
470
- options: [setupLabel, skipLabel, neverLabel],
471
- },
472
- options?.askUserHandler,
473
- );
474
-
475
- const answer = promptResult.cancelled ? skipLabel : (promptResult.answer ?? skipLabel);
476
- if (answer.startsWith("Always")) {
477
- saveOnboardingState(markProviderNeverAsk(state, parsed.provider));
478
- }
479
-
480
- if (answer.startsWith("Continue")) {
481
- return {
482
- content: [
483
- {
484
- type: "text",
485
- text:
486
- `${buildConnectedTag({ provider: parsed.provider })}\n\n` +
487
- `${descriptor.displayName} setup was acknowledged. Re-run the original ${descriptor.displayName} request now; if setup is still unavailable, continue without ${descriptor.displayName} and disclose the source gap.`,
488
- },
489
- ],
490
- };
491
- }
492
-
493
- const silenced = answer.startsWith("Always");
494
- const remediation = silenced
495
- ? `run ${parsed.installCmd} and ${parsed.loginCmd ?? "rdt login"} to enable ${descriptor.displayName} (silenced)`
496
- : `run ${parsed.installCmd} and ${parsed.loginCmd ?? "rdt login"} to enable ${descriptor.displayName}`;
497
- return {
498
- content: [
499
- {
500
- type: "text",
501
- text: `${buildSkippedTag({
502
- provider: parsed.provider,
503
- reason: "credential_not_provided",
504
- remediation,
505
- silenced,
506
- })}\n\n${descriptor.displayName} data was omitted per your choice.`,
507
- },
508
- ],
509
- };
510
- }
511
-
512
- const state = loadOnboardingState();
513
- const action = resolveCredentialRequired({
514
- provider: parsed.provider,
515
- reason: parsed.reason,
516
- state,
517
- sessionPromptedSet,
518
- hardPromptFiredInWorkflow,
519
- now: new Date(),
520
- });
521
-
522
- if (action.action === "skip") {
523
- // Replace content with a skipped placeholder so the LLM sees a
524
- // neutral gap note instead of the credential-required tag.
525
- const descriptor = getProvider(parsed.provider);
526
- const remediation = action.silenced
527
- ? `${action.remediation} (silenced)`
528
- : action.remediation;
529
- const tag = buildSkippedTag({
530
- provider: parsed.provider,
531
- reason: "credential_not_provided",
532
- remediation,
533
- silenced: action.silenced,
534
- });
535
- return {
536
- content: [
537
- {
538
- type: "text",
539
- text:
540
- `${tag}\n\n${descriptor.displayName} data was not fetched for this request. ` +
541
- (action.silenced
542
- ? `You previously asked not to be reminded about this provider.`
543
- : `To unlock ${descriptor.unlocks.join(", ")}, ${action.remediation}.`),
544
- },
545
- ],
546
- };
547
- }
548
-
549
- // action === "prompt": pause and ask the user via promptUser.
550
- const descriptor = getProvider(parsed.provider);
551
- const connectLabel = `Connect now — ${descriptor.instructionsHint}`;
552
- const continueLabel = descriptor.fallbackDescription
553
- ? `Continue with ${descriptor.fallbackDescription} for this run`
554
- : `Continue without ${descriptor.displayName} for this run`;
555
- const snoozeLabel = `Snooze ${descriptor.snoozeDurationDays} days`;
556
- const neverLabel = `Never ask again`;
557
- const questionBody =
558
- `${descriptor.displayName} unlocks ${descriptor.unlocks.join(", ")}. ` +
559
- `Free signup takes about 30 seconds. How would you like to proceed?`;
560
-
561
- // Mark that a hard-tier prompt has now fired in this workflow (for the cap).
562
- if (descriptor.tier === "hard") {
563
- hardPromptFiredInWorkflow = true;
564
- }
565
- sessionPromptedSet.add(parsed.provider);
566
-
567
- const promptResult = await promptUser(
568
- ctx,
569
- {
570
- question: questionBody,
571
- questionType: "select",
572
- options: [connectLabel, continueLabel, snoozeLabel, neverLabel],
573
- },
574
- options?.askUserHandler,
575
- );
576
-
577
- if (promptResult.cancelled) {
578
- // Treat cancel like "continue without".
579
- return {
580
- content: [
581
- {
582
- type: "text",
583
- text: `${buildSkippedTag({
584
- provider: parsed.provider,
585
- reason: "credential_not_provided",
586
- remediation: `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`,
587
- })}\n\nPrompt was cancelled.`,
588
- },
589
- ],
590
- };
591
- }
592
-
593
- const answer = promptResult.answer ?? "";
594
-
595
- if (answer.startsWith("Connect")) {
596
- const connectResult = await runProviderConnect(ctx, parsed.provider);
597
- if (connectResult.status === "connected") {
598
- // Pi has no tool re-dispatch API — emit a CONNECTED placeholder so
599
- // the LLM knows the key was just saved and can retry on the next
600
- // turn (or use whatever partial data is in context).
601
- return {
602
- content: [
603
- {
604
- type: "text",
605
- text:
606
- `${buildConnectedTag({ provider: parsed.provider })}\n\n` +
607
- `${descriptor.displayName} was just connected. Please re-run the previous request to fetch the data now that the credential is available.`,
608
- },
609
- ],
610
- };
611
- }
612
- // Cancelled, blocked-by-env, or invalid_key: fall through to skipped
613
- // with a result-specific explanation so the LLM can describe what
614
- // just happened in its final answer.
615
- const connectOutcomeDescription =
616
- connectResult.status === "blocked_by_env"
617
- ? "blocked by an existing environment variable"
618
- : connectResult.status === "invalid_key"
619
- ? `rejected by ${descriptor.displayName} (the key was invalid and nothing was saved)`
620
- : "cancelled";
621
- return {
622
- content: [
623
- {
624
- type: "text",
625
- text: `${buildSkippedTag({
626
- provider: parsed.provider,
627
- reason: "credential_not_provided",
628
- remediation: `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`,
629
- })}\n\n${descriptor.displayName} connect was ${connectOutcomeDescription}.`,
630
- },
631
- ],
632
- };
633
- }
634
-
635
- if (answer.startsWith("Snooze")) {
636
- saveOnboardingState(
637
- markProviderSnoozed(state, parsed.provider, descriptor.snoozeDurationDays),
638
- );
639
- } else if (answer.startsWith("Never")) {
640
- saveOnboardingState(markProviderNeverAsk(state, parsed.provider));
641
- }
642
- // "Continue" / fallthrough: no state mutation, just skip.
643
- const silenced = answer.startsWith("Never");
644
- const remediation = silenced
645
- ? `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock (silenced)`
646
- : `run /connect ${descriptor.aliases[0] ?? descriptor.id} to unlock`;
647
- return {
648
- content: [
649
- {
650
- type: "text",
651
- text: `${buildSkippedTag({
652
- provider: parsed.provider,
653
- reason: "credential_not_provided",
654
- remediation,
655
- silenced,
656
- })}\n\n${descriptor.displayName} data was omitted per your choice.`,
657
- },
658
- ],
659
- };
660
- }
661
-
662
- // No OpenCandle tag in this tool result — pass through.
663
- return undefined;
664
- });
665
-
666
- pi.on("tool_call", async (event) => {
667
- if (!currentRouteToolContext) return undefined;
668
- const allowed = new Set(currentRouteToolContext.activeToolNames);
669
- if (allowed.has(event.toolName)) return undefined;
670
-
671
- const diagnostic = {
672
- routeKind: currentRouteToolContext.routeKind,
673
- workflow: currentRouteToolContext.workflow,
674
- toolName: event.toolName,
675
- toolBundles: currentRouteToolContext.toolBundles,
676
- activeToolNames: currentRouteToolContext.activeToolNames,
677
- };
678
- pi.appendEntry("opencandle-tool-scope-violation", diagnostic);
679
-
680
- if (getConfig().toolScopeMode === "enforce") {
681
- return {
682
- block: true,
683
- reason: `Tool ${event.toolName} is outside the route-selected OpenCandle tool bundle.`,
684
- };
685
- }
686
- return undefined;
687
- });
688
-
689
- // Input handling — the LLM router is the single production routing path.
690
- pi.on("input", async (event, ctx) => {
691
- if (event.source === "extension") return;
692
- coordinator.clearTickerValidationCache();
693
-
694
- // Check for comprehensive analysis pattern before routing.
695
- const analysis = isAnalysisRequest(event.text);
696
- if (analysis.match && analysis.symbol) {
697
- const definition = buildComprehensiveAnalysisDefinition(analysis.symbol);
698
- appendComprehensiveAnalysisWorkflowEntry(analysis.symbol, definition);
699
- const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
700
- if (prompt) markOriginalInput(event.text, ctx);
701
- return prompt ? { action: "transform", text: prompt } : { action: "handled" };
702
- }
703
-
704
- const dispatched = await handleLlmRouterTurn(event.text, ctx);
705
- // Dispatched a workflow → the original user turn is now represented by
706
- // the workflow's queued prompts; tell Pi not to also forward it.
707
- // Fallback path (no dispatch) → let Pi pass the user turn through to the
708
- // main agent, which will run under the router-supplied fallback context.
709
- return dispatched || undefined;
710
- });
711
-
712
- /**
713
- * LLM-mode input handler. In this mode `classifyIntent` and
714
- * `extractPreferences` are NOT called — the router is the single source of
715
- * classification + preference extraction. Mirrors rule-mode dispatch for
716
- * identified workflows; for `fallback` turns, stashes a fallback context
717
- * for the next `before_agent_start` to inject.
718
- */
719
- async function handleLlmRouterTurn(
720
- text: string,
721
- ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
722
- ): Promise<{ action: "transform"; text: string } | false> {
723
- const storage = coordinator.getStorage();
724
- const { profileSnapshot, recentWorkflowRuns, priorTurns } = coordinator.buildRouterContextBase(
725
- ctx.sessionManager,
726
- );
727
- // priorTurns is not scrubbed for /forget — tracked in proposal.md follow-ups.
728
- const input: RouterInputContext = {
729
- text,
730
- priorTurns,
731
- profileSnapshot,
732
- recentWorkflowRuns,
733
- };
734
-
735
- const client = options?.routerLlmClient ?? resolveRouterLlmClient(ctx);
736
- if (!client) {
737
- pi.appendEntry("opencandle-router-error", {
738
- reason: "no_llm_client_available",
739
- text,
740
- });
741
- return false;
742
- }
743
-
744
- let output: RouterOutput;
745
- try {
746
- output = await routeLlm(input, client);
747
- } catch (err) {
748
- pi.appendEntry("opencandle-router-error", {
749
- reason: "route_failed",
750
- text,
751
- message: err instanceof Error ? err.message : String(err),
752
- });
753
- return false;
754
- }
755
-
756
- const availableToolNames = safeGetAllToolNames();
757
- const memory = coordinator.retrieveMemoryForRoute(
758
- output.routeKind,
759
- output.workflow,
760
- Object.keys(output.slots),
761
- );
762
- const resolvedTurnContext = buildResolvedTurnContext(input, output, {
763
- availableToolNames,
764
- memoryEntries: memory.entries,
765
- filteredMemory: memory.filtered.map(({ entry, reason }) => ({
766
- category: entry.category,
767
- key: entry.key,
768
- source: entry.source,
769
- recordedAt: entry.recordedAt,
770
- confidence: entry.confidence,
771
- filtered: true,
772
- filterReason: reason,
773
- })),
774
- planning: {
775
- migrationStatuses: getConfig().planningMigrationStatuses,
776
- },
777
- });
778
-
779
- pi.appendEntry("opencandle-router", { output });
780
- appendRouterSymbolDropEntries(output);
781
- pi.appendEntry("opencandle-route-context", resolvedTurnContext);
782
- coordinator.setPendingResolvedTurnContext(resolvedTurnContext);
783
- applyRouteToolScope(resolvedTurnContext);
784
-
785
- // Preference writes: HIGH-confidence only. Medium/low are logged for
786
- // observability even when no storage is available.
787
- const dropped: typeof output.preference_updates = [];
788
- for (const pref of output.preference_updates) {
789
- if (pref.confidence === "high") {
790
- storage?.upsertPreference({
791
- key: pref.key,
792
- valueJson: JSON.stringify(pref.value),
793
- confidence: pref.confidence,
794
- source: pref.source,
795
- });
796
- } else {
797
- dropped.push(pref);
798
- }
799
- }
800
- if (dropped.length > 0) {
801
- pi.appendEntry("opencandle-router-prefs-dropped", { dropped });
802
- }
803
-
804
- // Workflow dispatch for recognised workflows.
805
- if (output.routeKind === "workflow_dispatch" && output.workflow) {
806
- return dispatchRouterWorkflow(output, ctx, text);
807
- }
808
-
809
- if (output.routeKind === "pass_through") {
810
- return false;
811
- }
812
-
813
- // Fallback: record the turn and stash the fallback context for the
814
- // upcoming `before_agent_start` hook to render into the system prompt.
815
- coordinator.recordWorkflowRun(
816
- "fallback",
817
- output.entities,
818
- Object.fromEntries(Object.entries(output.slots).map(([k, v]) => [k, v.value])),
819
- [],
820
- output.routeKind,
821
- );
822
-
823
- const assumptionsBlock = buildAssumptionsBlockFromRouter(output.slots);
824
- coordinator.setPendingFallbackContext({
825
- assumptionsBlock,
826
- missingRequired: output.missing_required,
827
- extraContext:
828
- output.entities.symbols.length > 0
829
- ? `Router-extracted symbols: ${output.entities.symbols.join(", ")}.` +
830
- ` Route kind: ${output.routeKind}. Tool bundles: ${output.tool_bundles.join(", ") || "(none)"}.`
831
- : undefined,
832
- });
833
- return false;
834
- }
835
-
836
- async function dispatchRouterWorkflow(
837
- output: RouterOutput,
838
- ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
839
- originalText: string,
840
- ): Promise<{ action: "transform"; text: string } | false> {
841
- const workflow = output.workflow!;
842
- const storage = coordinator.getStorage();
843
- const workflowPrefs = storage?.getWorkflowPreferences("global") ?? {};
844
- const entities = mergeRouterSlotsIntoEntities(output);
845
-
846
- if (workflow === "portfolio_builder") {
847
- const resolution = withRouterSlotSources(
848
- resolvePortfolioSlots(entities, workflowPrefs),
849
- output,
850
- );
851
- coordinator.recordWorkflowRun(
852
- "portfolio_builder",
853
- entities,
854
- resolution.resolved,
855
- resolution.defaultsUsed,
856
- output.routeKind,
857
- );
858
- pi.appendEntry("opencandle-workflow", {
859
- workflow: "portfolio_builder",
860
- entities,
861
- resolved: resolution.resolved,
862
- });
863
- const definition = buildPortfolioWorkflowDefinition(resolution);
864
- const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
865
- if (prompt) markOriginalInput(originalText, ctx);
866
- return prompt ? { action: "transform", text: prompt } : false;
867
- }
868
- if (workflow === "options_screener") {
869
- const resolution = withRouterSlotSources(
870
- resolveOptionsScreenerSlots(entities, workflowPrefs),
871
- output,
872
- );
873
- // Router may emit missing_required; main agent handles via ask_user.
874
- // Still dispatch the workflow when symbol is present.
875
- if (resolution.missingRequired.length === 0) {
876
- coordinator.recordWorkflowRun(
877
- "options_screener",
878
- entities,
879
- resolution.resolved,
880
- resolution.defaultsUsed,
881
- output.routeKind,
882
- );
883
- pi.appendEntry("opencandle-workflow", {
884
- workflow: "options_screener",
885
- entities,
886
- resolved: resolution.resolved,
887
- });
888
- const definition = buildOptionsScreenerWorkflowDefinition(resolution);
889
- const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
890
- if (prompt) markOriginalInput(originalText, ctx);
891
- return prompt ? { action: "transform", text: prompt } : false;
892
- }
893
- // Missing required symbol — treat as fallback with ask_user directive.
894
- }
895
- if (workflow === "compare_assets" && entities.symbols.length >= 2) {
896
- const resolution: SlotResolution<CompareAssetsSlots> = {
897
- resolved: {
898
- symbols: entities.symbols,
899
- metrics: entities.compareMetrics,
900
- timeHorizon: entities.timeHorizon,
901
- budget: entities.budget,
902
- assetScope: entities.assetScope,
903
- },
904
- sources: {
905
- symbols: sourceForRouterSlot(output, "symbols", "user"),
906
- ...(entities.timeHorizon ? { timeHorizon: "user" as const } : {}),
907
- ...(entities.compareMetrics ? { metrics: "user" as const } : {}),
908
- ...(entities.budget !== undefined
909
- ? { budget: sourceForRouterSlot(output, "budget", "user") }
910
- : {}),
911
- ...(entities.assetScope ? { assetScope: "user" as const } : {}),
912
- },
913
- defaultsUsed: [],
914
- missingRequired: [],
915
- };
916
- coordinator.recordWorkflowRun(
917
- "compare_assets",
918
- entities,
919
- resolution.resolved,
920
- [],
921
- output.routeKind,
922
- );
923
- pi.appendEntry("opencandle-workflow", {
924
- workflow: "compare_assets",
925
- symbols: entities.symbols,
926
- });
927
- const preflight = await preflightCompareResolution(resolution);
928
- if (!preflight) {
929
- coordinator.recordWorkflowRun(
930
- "fallback",
931
- output.entities,
932
- Object.fromEntries(Object.entries(output.slots).map(([k, v]) => [k, v.value])),
933
- [],
934
- output.routeKind,
935
- );
936
- coordinator.setPendingResolvedTurnContext(null);
937
- coordinator.setPendingFallbackContext({
938
- assumptionsBlock: buildAssumptionsBlockFromRouter(output.slots),
939
- missingRequired: ["symbols"],
940
- extraContext:
941
- "Compare workflow aborted because ticker preflight left fewer than two valid symbols. Ask the user to clarify the intended tickers.",
942
- });
943
- return false;
944
- }
945
- const definition = buildCompareAssetsWorkflowDefinition(preflight.resolution);
946
- applyPreflightAnnotation(definition, preflight.dropped);
947
- const prompt = coordinator.transformWorkflowInput(pi, definition, ctx);
948
- if (prompt) markOriginalInput(originalText, ctx);
949
- return prompt ? { action: "transform", text: prompt } : false;
950
- }
951
-
952
- // single_asset_analysis / watchlist / general_qa + any workflow with
953
- // unmet required slots: fall through to fallback handling so the main
954
- // agent still gets an Assumptions block + ask_user directive.
955
- coordinator.recordWorkflowRun(
956
- "fallback",
957
- output.entities,
958
- Object.fromEntries(Object.entries(output.slots).map(([k, v]) => [k, v.value])),
959
- [],
960
- output.routeKind,
961
- );
962
- const assumptionsBlock = buildAssumptionsBlockFromRouter(output.slots);
963
- coordinator.setPendingFallbackContext({
964
- assumptionsBlock,
965
- missingRequired: output.missing_required,
966
- extraContext: `Router classified as ${workflow} but declined to dispatch. Symbols: ${entities.symbols.join(", ") || "(none)"}.`,
967
- });
968
- return false;
969
- }
970
-
971
- function appendRouterSymbolDropEntries(output: RouterOutput): void {
972
- for (const diagnostic of output.diagnostics) {
973
- if (diagnostic.code !== "symbol_dropped") continue;
974
- const details = diagnostic.details ?? {};
975
- appendSymbolDropEntries(
976
- [
977
- {
978
- token: String(details.token ?? ""),
979
- reason: String(details.reason ?? ""),
980
- signalsChecked: Array.isArray(details.signalsChecked)
981
- ? details.signalsChecked.map(String)
982
- : [],
983
- },
984
- ],
985
- String(details.source ?? "llm"),
986
- );
987
- }
988
- }
989
-
990
- function appendSymbolDropEntries(
991
- dropped: Array<{ token: string; reason: string; signalsChecked: string[] }>,
992
- source: string,
993
- ): void {
994
- for (const drop of dropped) {
995
- pi.appendEntry("opencandle-symbol-dropped", {
996
- token: drop.token,
997
- reason: drop.reason,
998
- signalsChecked: drop.signalsChecked,
999
- source,
1000
- });
1001
- }
1002
- }
1003
-
1004
- async function preflightCompareResolution(
1005
- resolution: SlotResolution<CompareAssetsSlots>,
1006
- ): Promise<{
1007
- resolution: SlotResolution<CompareAssetsSlots>;
1008
- dropped: Array<{ symbol: string; reason: string }>;
1009
- } | null> {
1010
- const result = await preflightSymbols(resolution.resolved.symbols, {
1011
- cache: coordinator.getTickerValidationCache(),
1012
- search: options?.symbolSearch,
1013
- });
1014
- for (const drop of result.dropped) {
1015
- pi.appendEntry("opencandle-symbol-preflight-dropped", drop);
1016
- }
1017
- if (result.valid.length < 2) {
1018
- pi.appendEntry("opencandle-workflow-aborted", {
1019
- reason: "preflight-insufficient-symbols",
1020
- dropped: result.dropped,
1021
- });
1022
- return null;
1023
- }
1024
- return {
1025
- resolution: {
1026
- ...resolution,
1027
- resolved: {
1028
- ...resolution.resolved,
1029
- symbols: result.valid,
1030
- },
1031
- },
1032
- dropped: result.dropped,
1033
- };
1034
- }
1035
-
1036
- function applyPreflightAnnotation(
1037
- definition: ReturnType<typeof buildCompareAssetsWorkflowDefinition>,
1038
- dropped: Array<{ symbol: string; reason: string }>,
1039
- ): void {
1040
- if (dropped.length === 0 || definition.steps.length === 0) return;
1041
- definition.steps[0] = {
1042
- ...definition.steps[0],
1043
- prompt: `${formatPreflightDropAnnotation(dropped)}\n\n${definition.steps[0].prompt}`,
1044
- };
1045
- }
1046
-
1047
- function mergeRouterSlotsIntoEntities(output: RouterOutput): ExtractedEntities {
1048
- const entities: ExtractedEntities = {
1049
- ...output.entities,
1050
- symbols: output.entities.symbols,
1051
- };
1052
-
1053
- if (entities.budget === undefined && typeof output.slots.budget?.value === "number") {
1054
- entities.budget = output.slots.budget.value;
1055
- }
1056
-
1057
- const droppedSymbols = droppedSymbolsFromDiagnostics(output);
1058
- const slotSymbols = symbolsFromRouterSlots(output).filter(
1059
- (symbol) => !droppedSymbols.has(symbol),
1060
- );
1061
- if (slotSymbols.length > 0 && slotSymbols.length > entities.symbols.length) {
1062
- entities.symbols = mergeSymbols(slotSymbols, entities.symbols);
1063
- }
1064
-
1065
- return entities;
1066
- }
1067
-
1068
- function droppedSymbolsFromDiagnostics(output: RouterOutput): Set<string> {
1069
- const dropped = new Set<string>();
1070
- for (const diagnostic of output.diagnostics) {
1071
- if (diagnostic.code !== "symbol_dropped") continue;
1072
- const token = diagnostic.details?.token;
1073
- if (typeof token === "string" && token.trim() !== "") {
1074
- dropped.add(token.toUpperCase());
1075
- }
1076
- }
1077
- return dropped;
1078
- }
1079
-
1080
- function withRouterSlotSources<T extends object>(
1081
- resolution: SlotResolution<T>,
1082
- output: RouterOutput,
1083
- ): SlotResolution<T> {
1084
- const sources: Record<string, SlotSource | undefined> = { ...resolution.sources };
1085
- if (output.entities.budget === undefined && output.slots.budget) {
1086
- sources.budget = output.slots.budget.source;
1087
- }
1088
- if (output.entities.symbols.length === 0 && output.slots.symbol) {
1089
- sources.symbol = output.slots.symbol.source;
1090
- }
1091
- if (output.entities.symbols.length < 2 && output.slots.symbols) {
1092
- sources.symbols = output.slots.symbols.source;
1093
- }
1094
- return { ...resolution, sources: sources as SlotResolution<T>["sources"] };
1095
- }
1096
-
1097
- function sourceForRouterSlot(
1098
- output: RouterOutput,
1099
- slotName: "symbol" | "symbols" | "budget",
1100
- fallback: SlotSource,
1101
- ): SlotSource {
1102
- return output.slots[slotName]?.source ?? fallback;
1103
- }
1104
-
1105
- function symbolsFromRouterSlots(output: RouterOutput): string[] {
1106
- const symbols: string[] = [];
1107
- const symbol = output.slots.symbol?.value;
1108
- if (typeof symbol === "string" && symbol.trim() !== "") {
1109
- symbols.push(symbol.toUpperCase());
1110
- }
1111
- const symbolList = output.slots.symbols?.value;
1112
- if (Array.isArray(symbolList)) {
1113
- for (const value of symbolList) {
1114
- if (typeof value === "string" && value.trim() !== "") {
1115
- symbols.push(value.toUpperCase());
1116
- }
1117
- }
1118
- }
1119
- return symbols;
1120
- }
1121
-
1122
- function mergeSymbols(primary: string[], secondary: string[]): string[] {
1123
- const merged: string[] = [];
1124
- for (const symbol of [...primary, ...secondary]) {
1125
- if (!merged.includes(symbol)) merged.push(symbol);
1126
- }
1127
- return merged;
1128
- }
1129
-
1130
- function safeGetAllToolNames(): string[] {
1131
- try {
1132
- return pi.getAllTools().map((tool) => tool.name);
1133
- } catch {
1134
- return [];
1135
- }
1136
- }
1137
-
1138
- function applyRouteToolScope(context: ResolvedTurnContext): void {
1139
- const mode = getConfig().toolScopeMode;
1140
- currentRouteToolContext = context;
1141
- pi.appendEntry("opencandle-tool-scope", {
1142
- mode,
1143
- routeKind: context.routeKind,
1144
- workflow: context.workflow,
1145
- toolBundles: context.toolBundles,
1146
- activeToolNames: context.activeToolNames,
1147
- enforced: false,
1148
- });
1149
-
1150
- if (mode !== "enforce") return;
1151
- if (context.activeToolNames.length === 0) return;
1152
-
1153
- try {
1154
- if (activeToolSnapshot === null) {
1155
- activeToolSnapshot = pi.getActiveTools();
1156
- }
1157
- pi.setActiveTools(context.activeToolNames);
1158
- pi.appendEntry("opencandle-tool-scope", {
1159
- mode,
1160
- routeKind: context.routeKind,
1161
- workflow: context.workflow,
1162
- toolBundles: context.toolBundles,
1163
- activeToolNames: context.activeToolNames,
1164
- enforced: true,
1165
- });
1166
- } catch (err) {
1167
- pi.appendEntry("opencandle-tool-scope", {
1168
- mode,
1169
- routeKind: context.routeKind,
1170
- workflow: context.workflow,
1171
- toolBundles: context.toolBundles,
1172
- activeToolNames: context.activeToolNames,
1173
- enforced: false,
1174
- diagnostic: err instanceof Error ? err.message : String(err),
1175
- });
1176
- }
1177
- }
1178
-
1179
- function restoreRouteToolScope(): void {
1180
- currentRouteToolContext = null;
1181
- if (activeToolSnapshot === null) return;
1182
- try {
1183
- pi.setActiveTools(activeToolSnapshot);
1184
- pi.appendEntry("opencandle-tool-scope", {
1185
- mode: getConfig().toolScopeMode,
1186
- restored: true,
1187
- activeToolNames: activeToolSnapshot,
1188
- });
1189
- } catch (err) {
1190
- pi.appendEntry("opencandle-tool-scope", {
1191
- mode: getConfig().toolScopeMode,
1192
- restored: false,
1193
- diagnostic: err instanceof Error ? err.message : String(err),
1194
- });
1195
- } finally {
1196
- activeToolSnapshot = null;
1197
- }
1198
- }
1199
-
1200
- function resolveRouterLlmClient(
1201
- ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1],
1202
- ): RouterLlmClient | null {
1203
- // `ctx.model` is the currently selected pi-ai model. When unset (no auth
1204
- // configured yet), we skip the router and the main agent runs with its
1205
- // default unrouted flow for the turn.
1206
- const model = (ctx as { model?: unknown }).model;
1207
- if (!model) return null;
1208
- // biome-ignore lint/suspicious/noExplicitAny: Pi typings keep Model generic
1209
- return createPiAiRouterClient(model as any);
1210
- }
1211
-
1212
- // System prompt assembly — delegate to coordinator. When a fallback context
1213
- // is pending (router-mode fallback turns), inject it into the prompt.
1214
- pi.on("before_agent_start", async (event) => {
1215
- const fallbackContext = coordinator.consumePendingFallbackContext() ?? undefined;
1216
- const resolvedTurnContext = coordinator.consumePendingResolvedTurnContext() ?? undefined;
1217
- return {
1218
- systemPrompt: coordinator.buildSystemPrompt(
1219
- event.systemPrompt,
1220
- coordinator.getActiveWorkflowType(),
1221
- fallbackContext,
1222
- resolvedTurnContext,
1223
- ),
1224
- };
1225
- });
1226
- }
1227
-
1228
- function hasUnconsumedOriginalInputMarker(ctx?: OriginalInputMarkerContext): boolean {
1229
- const manager = ctx?.sessionManager;
1230
- const entries =
1231
- typeof manager?.getBranch === "function"
1232
- ? manager.getBranch()
1233
- : typeof manager?.getEntries === "function"
1234
- ? manager.getEntries()
1235
- : [];
1236
- let markerAfterLastUser = false;
1237
- for (const entry of entries) {
1238
- if (!isSessionEntryRecord(entry)) continue;
1239
- if (entry.type === "message" && entry.message?.role === "user") {
1240
- markerAfterLastUser = false;
1241
- continue;
1242
- }
1243
- if (entry.type === "custom" && entry.customType === "opencandle-user-input") {
1244
- markerAfterLastUser = true;
1245
- continue;
1246
- }
1247
- if (markerAfterLastUser) {
1248
- markerAfterLastUser = false;
1249
- }
1250
- }
1251
- return markerAfterLastUser;
1252
- }
1253
-
1254
- function isSessionEntryRecord(
1255
- entry: unknown,
1256
- ): entry is { type?: unknown; customType?: unknown; message?: { role?: unknown } } {
1257
- return typeof entry === "object" && entry !== null;
1258
- }
1259
-
1260
- /** Concatenate text from a Pi message `content` (plain string or block array). */
1261
- function extractMessageText(content: unknown): string {
1262
- if (typeof content === "string") return content;
1263
- if (!Array.isArray(content)) return "";
1264
- let text = "";
1265
- for (const block of content) {
1266
- if (
1267
- block &&
1268
- typeof block === "object" &&
1269
- (block as { type?: unknown }).type === "text" &&
1270
- typeof (block as { text?: unknown }).text === "string"
1271
- ) {
1272
- text += (block as { text: string }).text;
1273
- }
1274
- }
1275
- return text;
1276
- }
1277
-
1278
- /**
1279
- * True when the session name is still an auto-set placeholder that the LLM
1280
- * title may replace: unset, exactly one of the candidate user texts, or the
1281
- * GUI server's truncated "first 77 chars + ..." form of one of them.
1282
- */
1283
- function isPlaceholderSessionName(name: string | undefined, candidates: string[]): boolean {
1284
- if (name === undefined || name.trim().length === 0) return true;
1285
- const trimmed = name.trim();
1286
- for (const candidate of candidates) {
1287
- const candidateTrimmed = candidate.trim();
1288
- if (trimmed === candidateTrimmed) return true;
1289
- if (trimmed.endsWith("...") && candidateTrimmed.startsWith(trimmed.slice(0, -3))) {
1290
- return true;
1291
- }
1292
- }
1293
- return false;
1294
- }