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,4 +1,7 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { persistProviderCredential } from "../../src/onboarding/connect.js";
3
+ import { getCredentialSource, PROVIDERS, type ProviderId } from "../../src/onboarding/providers.js";
4
+ import { validateCredential } from "../../src/onboarding/validation.js";
2
5
 
3
6
  export type ModelSetupRequirement = "ready" | "select_model" | "connect_auth";
4
7
 
@@ -24,6 +27,47 @@ export interface ModelSetupRegistry {
24
27
  hasConfiguredAuth(model: Model<Api>): boolean;
25
28
  }
26
29
 
30
+ interface ModelSetupAuthStorage {
31
+ set(provider: string, credential: { type: "api_key"; key: string }): void;
32
+ }
33
+
34
+ interface MutableModelSetupRegistry extends ModelSetupRegistry {
35
+ authStorage: ModelSetupAuthStorage;
36
+ find(provider: string, modelId: string): Model<Api> | undefined;
37
+ }
38
+
39
+ interface ModelSetupSession {
40
+ modelRegistry: MutableModelSetupRegistry;
41
+ model?: Model<Api>;
42
+ setModel(model: Model<Api>): Promise<void>;
43
+ settingsManager: {
44
+ flush(): Promise<void>;
45
+ };
46
+ }
47
+
48
+ interface ModelSetupSessionManager {
49
+ appendCustomMessageEntry(
50
+ customType: string,
51
+ content: string,
52
+ isActive: boolean,
53
+ data: Record<string, unknown>,
54
+ ): void;
55
+ }
56
+
57
+ export interface ModelSetupController {
58
+ buildCurrentModelSetupState(): ModelSetupState;
59
+ handleSaveModelApiKey(providerId: string, apiKey: string): Promise<void>;
60
+ handleSaveProviderApiKey(providerId: string, apiKey: string): Promise<void>;
61
+ handleSelectModel(provider: string, modelId: string): Promise<void>;
62
+ }
63
+
64
+ export interface ModelSetupControllerOptions {
65
+ role: string;
66
+ getSession: () => ModelSetupSession;
67
+ getSessionManager: () => ModelSetupSessionManager;
68
+ broadcastState: () => void;
69
+ }
70
+
27
71
  export const modelSetupProviders: ModelSetupProvider[] = [
28
72
  {
29
73
  id: "google",
@@ -83,8 +127,7 @@ export function findPreferredModel(
83
127
  const available = sortModels(registry.getAvailable(), provider.defaultProvider);
84
128
  return (
85
129
  available.find(
86
- (model) =>
87
- model.provider === provider.defaultProvider && model.id === provider.defaultModel,
130
+ (model) => model.provider === provider.defaultProvider && model.id === provider.defaultModel,
88
131
  ) ?? available.find((model) => model.provider === provider.defaultProvider)
89
132
  );
90
133
  }
@@ -98,3 +141,107 @@ export function sortModels(models: Model<Api>[], preferredProvider?: string): Mo
98
141
  return byProvider !== 0 ? byProvider : a.id.localeCompare(b.id);
99
142
  });
100
143
  }
144
+
145
+ export function createModelSetupController({
146
+ role,
147
+ getSession,
148
+ getSessionManager,
149
+ broadcastState,
150
+ }: ModelSetupControllerOptions): ModelSetupController {
151
+ function ensureWriter(): void {
152
+ if (role !== "writer") throw new Error("Read-only follower mode");
153
+ }
154
+
155
+ function buildCurrentModelSetupState(): ModelSetupState {
156
+ const session = getSession();
157
+ return buildModelSetupState(session.modelRegistry, session.model);
158
+ }
159
+
160
+ async function handleSaveModelApiKey(providerId: string, apiKey: string): Promise<void> {
161
+ ensureWriter();
162
+
163
+ const provider = modelSetupProviders.find((candidate) => candidate.id === providerId);
164
+ if (!provider) throw new Error(`Unknown model provider: ${providerId}`);
165
+
166
+ const trimmed = apiKey.trim();
167
+ if (!trimmed) throw new Error(`Paste a ${provider.label} API key first.`);
168
+
169
+ const session = getSession();
170
+ session.modelRegistry.authStorage.set(provider.id, { type: "api_key", key: trimmed });
171
+ session.modelRegistry.refresh();
172
+
173
+ const model = findPreferredModel(session.modelRegistry, provider);
174
+ if (!model) {
175
+ throw new Error(
176
+ `Saved the ${provider.label} key, but no ${provider.label} models are available yet.`,
177
+ );
178
+ }
179
+
180
+ await session.setModel(model);
181
+ await session.settingsManager.flush();
182
+ getSessionManager().appendCustomMessageEntry(
183
+ "opencandle-model-setup",
184
+ `Connected ${provider.label} and selected ${model.provider}/${model.id}.`,
185
+ true,
186
+ { source: "gui", provider: provider.id, model: `${model.provider}/${model.id}` },
187
+ );
188
+ broadcastState();
189
+ }
190
+
191
+ async function handleSaveProviderApiKey(providerId: string, apiKey: string): Promise<void> {
192
+ ensureWriter();
193
+
194
+ const descriptor = PROVIDERS.find((candidate) => candidate.id === providerId);
195
+ if (!descriptor) throw new Error(`Unknown provider: ${providerId}`);
196
+
197
+ if (getCredentialSource(descriptor.id) === "env") {
198
+ throw new Error(
199
+ `${descriptor.displayName} is set via the ${descriptor.envVar} environment variable. Unset it to override here.`,
200
+ );
201
+ }
202
+
203
+ const trimmed = apiKey.trim();
204
+ if (!trimmed) throw new Error(`Paste a ${descriptor.displayName} API key first.`);
205
+
206
+ const validation = await validateCredential(descriptor.id as ProviderId, trimmed);
207
+ if (validation.status === "invalid") {
208
+ const statusHint =
209
+ validation.httpStatus !== undefined ? ` (HTTP ${validation.httpStatus})` : "";
210
+ const messageHint = validation.message ? ` — ${validation.message}` : "";
211
+ throw new Error(
212
+ `${descriptor.displayName} rejected the key${statusHint}${messageHint}. The existing configuration was not changed.`,
213
+ );
214
+ }
215
+
216
+ persistProviderCredential(descriptor.id as ProviderId, trimmed);
217
+
218
+ const verifiedNote =
219
+ validation.status === "transient"
220
+ ? `Saved ${descriptor.displayName} key but couldn't verify it (${validation.reason}). The next request will surface any issue.`
221
+ : `Connected ${descriptor.displayName}. Key saved to ~/.opencandle/config.json.`;
222
+
223
+ getSessionManager().appendCustomMessageEntry("opencandle-provider-setup", verifiedNote, true, {
224
+ source: "gui",
225
+ provider: descriptor.id,
226
+ status: validation.status,
227
+ });
228
+ broadcastState();
229
+ }
230
+
231
+ async function handleSelectModel(provider: string, modelId: string): Promise<void> {
232
+ ensureWriter();
233
+ const session = getSession();
234
+ session.modelRegistry.refresh();
235
+ const model = session.modelRegistry.find(provider, modelId);
236
+ if (!model) throw new Error(`Unknown model: ${provider}/${modelId}`);
237
+ await session.setModel(model);
238
+ await session.settingsManager.flush();
239
+ }
240
+
241
+ return {
242
+ buildCurrentModelSetupState,
243
+ handleSaveModelApiKey,
244
+ handleSaveProviderApiKey,
245
+ handleSelectModel,
246
+ };
247
+ }
@@ -0,0 +1,62 @@
1
+ import type { IncomingHttpHeaders } from "node:http";
2
+
3
+ const PRIVATE_API_COOKIE = "opencandle_gui_session";
4
+
5
+ export function isLoopbackAddress(remoteAddress: string | undefined): boolean {
6
+ if (remoteAddress == null || remoteAddress === "") return false;
7
+ return (
8
+ remoteAddress === "::1" ||
9
+ remoteAddress === "localhost" ||
10
+ remoteAddress.startsWith("127.") ||
11
+ remoteAddress.startsWith("::ffff:127.")
12
+ );
13
+ }
14
+
15
+ export function isTrustedPrivateApiRequest(
16
+ headers: IncomingHttpHeaders,
17
+ sessionToken: string,
18
+ remoteAddress: string | undefined,
19
+ options: { allowRemote?: boolean } = {},
20
+ ): boolean {
21
+ if (!options.allowRemote && !isLoopbackAddress(remoteAddress)) return false;
22
+ if (cookieValue(headers.cookie, PRIVATE_API_COOKIE) !== sessionToken) return false;
23
+
24
+ const fetchSite = headerValue(headers["sec-fetch-site"]);
25
+ if (fetchSite != null && fetchSite !== "same-origin" && fetchSite !== "none") return false;
26
+
27
+ const origin = headerValue(headers.origin);
28
+ const host = headerValue(headers.host);
29
+ if (origin != null && host != null) {
30
+ try {
31
+ if (new URL(origin).host !== host) return false;
32
+ } catch {
33
+ return false;
34
+ }
35
+ }
36
+
37
+ return true;
38
+ }
39
+
40
+ export function privateApiCookieHeader(sessionToken: string): string {
41
+ return `${PRIVATE_API_COOKIE}=${encodeURIComponent(sessionToken)}; Path=/; HttpOnly; SameSite=Strict`;
42
+ }
43
+
44
+ function cookieValue(header: string | string[] | undefined, name: string): string | undefined {
45
+ const value = Array.isArray(header) ? header.join("; ") : header;
46
+ if (value == null) return undefined;
47
+ for (const part of value.split(";")) {
48
+ const [rawKey, ...rawValue] = part.trim().split("=");
49
+ if (rawKey === name) {
50
+ try {
51
+ return decodeURIComponent(rawValue.join("="));
52
+ } catch {
53
+ return undefined;
54
+ }
55
+ }
56
+ }
57
+ return undefined;
58
+ }
59
+
60
+ function headerValue(value: string | string[] | undefined): string | undefined {
61
+ return Array.isArray(value) ? value[0] : value;
62
+ }
@@ -1,5 +1,5 @@
1
- import type { SessionEntry } from "@earendil-works/pi-coding-agent";
2
1
  import type { Message, ToolResultMessage } from "@earendil-works/pi-ai";
2
+ import type { SessionEntry } from "@earendil-works/pi-coding-agent";
3
3
 
4
4
  export interface DashboardState {
5
5
  watchlist: Array<{
@@ -28,6 +28,16 @@ export interface DashboardState {
28
28
  };
29
29
  }
30
30
 
31
+ const DIRECT_TOOL_GAP_PROVIDERS: Record<string, string> = {
32
+ get_company_overview: "alpha_vantage",
33
+ get_financials: "alpha_vantage",
34
+ get_earnings: "alpha_vantage",
35
+ compute_dcf: "alpha_vantage",
36
+ compare_companies: "alpha_vantage",
37
+ get_economic_data: "fred",
38
+ get_twitter_sentiment: "twitter",
39
+ };
40
+
31
41
  export function createEmptyDashboardState(): DashboardState {
32
42
  return {
33
43
  watchlist: [],
@@ -71,15 +81,15 @@ export function projectDashboard(entries: SessionEntry[], sessionId = "local"):
71
81
  const annotation = stringValue(data.annotation);
72
82
  if (annotation) {
73
83
  for (const provider of parseSkippedProviders(annotation)) {
74
- state.dataQuality.softGaps.push({ provider, lastSeen: entry.timestamp });
84
+ upsertProviderGap(state.dataQuality.softGaps, provider, entry.timestamp);
75
85
  }
76
86
  }
77
87
  for (const gap of asArray(data.softGaps)) {
78
88
  const provider = stringValue(asRecord(gap).provider);
79
- if (provider) state.dataQuality.softGaps.push({ provider, lastSeen: entry.timestamp });
89
+ if (provider) upsertProviderGap(state.dataQuality.softGaps, provider, entry.timestamp);
80
90
  }
81
91
  const provider = stringValue(data.provider);
82
- if (provider) state.dataQuality.softGaps.push({ provider, lastSeen: entry.timestamp });
92
+ if (provider) upsertProviderGap(state.dataQuality.softGaps, provider, entry.timestamp);
83
93
  }
84
94
 
85
95
  if (entry.type === "custom" && entry.customType === "opencandle-quote-refresh") {
@@ -137,8 +147,31 @@ function projectToolResult(
137
147
  .filter((part) => part.type === "text")
138
148
  .map((part) => part.text)
139
149
  .join("\n");
150
+ for (const provider of parseSoftGapProviders(text)) {
151
+ upsertProviderGap(state.dataQuality.softGaps, provider, timestamp);
152
+ }
140
153
  for (const provider of parseCredentialRequiredProviders(text)) {
141
- state.dataQuality.hardSkips.push({ provider, lastSeen: timestamp });
154
+ upsertProviderGap(state.dataQuality.hardSkips, provider, timestamp);
155
+ }
156
+ const provider = inferDirectToolGapProvider(message.toolName, text);
157
+ if (provider) upsertProviderGap(state.dataQuality.softGaps, provider, timestamp);
158
+ }
159
+
160
+ function upsertProviderGap(
161
+ gaps: Array<{ provider: string; lastSeen: string }>,
162
+ provider: string,
163
+ lastSeen: string,
164
+ ): void {
165
+ const existing = gaps.find((gap) => gap.provider === provider);
166
+ if (!existing) {
167
+ gaps.push({ provider, lastSeen });
168
+ return;
169
+ }
170
+
171
+ const existingTime = Date.parse(existing.lastSeen);
172
+ const nextTime = Date.parse(lastSeen);
173
+ if (!Number.isFinite(existingTime) || (Number.isFinite(nextTime) && nextTime > existingTime)) {
174
+ existing.lastSeen = lastSeen;
142
175
  }
143
176
  }
144
177
 
@@ -166,23 +199,37 @@ function projectQuote(
166
199
  function parseCredentialRequiredProviders(text: string): string[] {
167
200
  const providers: string[] = [];
168
201
  const re = /\[OPENCANDLE_CREDENTIAL_REQUIRED[^\]]*provider=([a-z0-9_-]+)/gi;
169
- let match: RegExpExecArray | null;
170
- while ((match = re.exec(text)) !== null) {
202
+ while (true) {
203
+ const match = re.exec(text);
204
+ if (!match) break;
171
205
  providers.push(match[1]);
172
206
  }
173
207
  return providers;
174
208
  }
175
209
 
176
210
  function parseSkippedProviders(text: string): string[] {
211
+ return parseSoftGapProviders(text);
212
+ }
213
+
214
+ function parseSoftGapProviders(text: string): string[] {
177
215
  const providers: string[] = [];
178
- const re = /\[OPENCANDLE_SKIPPED[^\]]*provider=([a-z0-9_-]+)/gi;
179
- let match: RegExpExecArray | null;
180
- while ((match = re.exec(text)) !== null) {
216
+ const re = /\[OPENCANDLE_(?:SKIPPED|SOFT_DEGRADED)[^\]]*provider=([a-z0-9_-]+)/gi;
217
+ while (true) {
218
+ const match = re.exec(text);
219
+ if (!match) break;
181
220
  providers.push(match[1]);
182
221
  }
183
222
  return providers;
184
223
  }
185
224
 
225
+ function inferDirectToolGapProvider(
226
+ toolName: string | undefined,
227
+ text: string,
228
+ ): string | undefined {
229
+ if (!toolName || !/(?:⚠|unavailable|No .*data found|LOGIN_NEEDED)/i.test(text)) return undefined;
230
+ return DIRECT_TOOL_GAP_PROVIDERS[toolName];
231
+ }
232
+
186
233
  function inferSymbolFromContent(content: ToolResultMessage["content"]): string | undefined {
187
234
  const text = content.find((part) => part.type === "text")?.text;
188
235
  const match = text?.match(/^([A-Z]{1,8})\b/);
@@ -191,7 +238,7 @@ function inferSymbolFromContent(content: ToolResultMessage["content"]): string |
191
238
 
192
239
  function asRecord(value: unknown): Record<string, unknown> {
193
240
  return typeof value === "object" && value !== null && !Array.isArray(value)
194
- ? value as Record<string, unknown>
241
+ ? (value as Record<string, unknown>)
195
242
  : {};
196
243
  }
197
244
 
@@ -0,0 +1,58 @@
1
+ import type { AgentSessionEvent } from "@earendil-works/pi-coding-agent";
2
+
3
+ export interface PromptObservation {
4
+ userTexts: string[];
5
+ sawAssistantOrTool: boolean;
6
+ }
7
+
8
+ export function createPromptObservation(): PromptObservation {
9
+ return {
10
+ userTexts: [],
11
+ sawAssistantOrTool: false,
12
+ };
13
+ }
14
+
15
+ export function observePromptEvent(observation: PromptObservation, event: AgentSessionEvent): void {
16
+ if (event.type === "message_start") {
17
+ const message = event.message as { role?: unknown; content?: unknown };
18
+ if (message.role === "user") {
19
+ observation.userTexts.push(messageTextFromContent(message.content));
20
+ return;
21
+ }
22
+ if (message.role === "assistant") {
23
+ observation.sawAssistantOrTool = true;
24
+ }
25
+ return;
26
+ }
27
+
28
+ if (event.type === "tool_execution_start") {
29
+ observation.sawAssistantOrTool = true;
30
+ }
31
+ }
32
+
33
+ export function selectReplayPrompt(
34
+ observation: PromptObservation,
35
+ originalPrompt: string,
36
+ ): string | undefined {
37
+ if (observation.sawAssistantOrTool) return undefined;
38
+
39
+ const original = originalPrompt.trim();
40
+ const text = [...observation.userTexts]
41
+ .reverse()
42
+ .map((candidate) => candidate.trim())
43
+ .find((candidate) => candidate.length > 0 && candidate !== original);
44
+
45
+ return text || undefined;
46
+ }
47
+
48
+ function messageTextFromContent(content: unknown): string {
49
+ if (typeof content === "string") return content;
50
+ if (!Array.isArray(content)) return "";
51
+ return content
52
+ .map((part) =>
53
+ typeof part === "object" && part !== null && "text" in part && typeof part.text === "string"
54
+ ? part.text
55
+ : "",
56
+ )
57
+ .join("");
58
+ }
@@ -0,0 +1,50 @@
1
+ import type { MarketStateQuoteSnapshot } from "./market-state-api.js";
2
+
3
+ export class QuoteSnapshotStore {
4
+ private snapshot: MarketStateQuoteSnapshot | null = null;
5
+ private fetchedAtMs = 0;
6
+ private inFlight: Promise<MarketStateQuoteSnapshot> | null = null;
7
+ private invalidationVersion = 0;
8
+
9
+ constructor(
10
+ private readonly build: () => Promise<MarketStateQuoteSnapshot>,
11
+ private readonly maxAgeMs = 60_000,
12
+ private readonly now: () => number = Date.now,
13
+ ) {}
14
+
15
+ async get(): Promise<MarketStateQuoteSnapshot> {
16
+ if (this.snapshot == null) return this.refresh();
17
+ if (this.now() - this.fetchedAtMs >= this.maxAgeMs) {
18
+ // Stale-while-revalidate: serve the old snapshot now, refresh in the background.
19
+ this.refresh().catch(() => {});
20
+ }
21
+ return this.snapshot;
22
+ }
23
+
24
+ invalidate(): void {
25
+ this.invalidationVersion += 1;
26
+ this.snapshot = null;
27
+ this.fetchedAtMs = 0;
28
+ this.inFlight = null;
29
+ }
30
+
31
+ private refresh(): Promise<MarketStateQuoteSnapshot> {
32
+ if (this.inFlight) return this.inFlight;
33
+ const version = this.invalidationVersion;
34
+ const inFlight = this.build()
35
+ .then((snapshot) => {
36
+ if (version === this.invalidationVersion) {
37
+ this.snapshot = snapshot;
38
+ this.fetchedAtMs = this.now();
39
+ }
40
+ return snapshot;
41
+ })
42
+ .finally(() => {
43
+ if (this.inFlight === inFlight) {
44
+ this.inFlight = null;
45
+ }
46
+ });
47
+ this.inFlight = inFlight;
48
+ return this.inFlight;
49
+ }
50
+ }