opencandle 0.2.0 → 0.4.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 (401) hide show
  1. package/README.md +110 -87
  2. package/assets/logo.svg +187 -0
  3. package/dist/analysts/orchestrator.js +1 -2
  4. package/dist/analysts/orchestrator.js.map +1 -1
  5. package/dist/cli.d.ts +1 -1
  6. package/dist/cli.js +38 -2
  7. package/dist/cli.js.map +1 -1
  8. package/dist/config.d.ts +34 -5
  9. package/dist/config.js +29 -8
  10. package/dist/config.js.map +1 -1
  11. package/dist/infra/browser.d.ts +10 -0
  12. package/dist/infra/browser.js +1 -0
  13. package/dist/infra/browser.js.map +1 -1
  14. package/dist/infra/cache.d.ts +4 -0
  15. package/dist/infra/cache.js +4 -0
  16. package/dist/infra/cache.js.map +1 -1
  17. package/dist/infra/native-dependencies.d.ts +1 -0
  18. package/dist/infra/native-dependencies.js +10 -0
  19. package/dist/infra/native-dependencies.js.map +1 -0
  20. package/dist/infra/node-version.d.ts +2 -0
  21. package/dist/infra/node-version.js +23 -0
  22. package/dist/infra/node-version.js.map +1 -0
  23. package/dist/infra/rate-limiter.js +6 -0
  24. package/dist/infra/rate-limiter.js.map +1 -1
  25. package/dist/memory/index.d.ts +2 -0
  26. package/dist/memory/index.js +1 -0
  27. package/dist/memory/index.js.map +1 -1
  28. package/dist/memory/sqlite.js +42 -4
  29. package/dist/memory/sqlite.js.map +1 -1
  30. package/dist/memory/storage.d.ts +6 -0
  31. package/dist/memory/storage.js +3 -3
  32. package/dist/memory/storage.js.map +1 -1
  33. package/dist/memory/tool-defaults.d.ts +8 -0
  34. package/dist/memory/tool-defaults.js +59 -0
  35. package/dist/memory/tool-defaults.js.map +1 -0
  36. package/dist/onboarding/connect.d.ts +35 -0
  37. package/dist/onboarding/connect.js +118 -0
  38. package/dist/onboarding/connect.js.map +1 -0
  39. package/dist/onboarding/credential-interceptor.d.ts +44 -0
  40. package/dist/onboarding/credential-interceptor.js +72 -0
  41. package/dist/onboarding/credential-interceptor.js.map +1 -0
  42. package/dist/onboarding/degradation-accumulator.d.ts +21 -0
  43. package/dist/onboarding/degradation-accumulator.js +55 -0
  44. package/dist/onboarding/degradation-accumulator.js.map +1 -0
  45. package/dist/onboarding/prompt-user.d.ts +23 -0
  46. package/dist/onboarding/prompt-user.js +61 -0
  47. package/dist/onboarding/prompt-user.js.map +1 -0
  48. package/dist/onboarding/providers.d.ts +116 -0
  49. package/dist/onboarding/providers.js +239 -0
  50. package/dist/onboarding/providers.js.map +1 -0
  51. package/dist/onboarding/state.d.ts +31 -2
  52. package/dist/onboarding/state.js +141 -13
  53. package/dist/onboarding/state.js.map +1 -1
  54. package/dist/onboarding/tool-helpers.d.ts +34 -0
  55. package/dist/onboarding/tool-helpers.js +80 -0
  56. package/dist/onboarding/tool-helpers.js.map +1 -0
  57. package/dist/onboarding/tool-tags.d.ts +37 -0
  58. package/dist/onboarding/tool-tags.js +149 -0
  59. package/dist/onboarding/tool-tags.js.map +1 -0
  60. package/dist/onboarding/validation.d.ts +19 -0
  61. package/dist/onboarding/validation.js +117 -0
  62. package/dist/onboarding/validation.js.map +1 -0
  63. package/dist/pi/opencandle-extension.d.ts +7 -1
  64. package/dist/pi/opencandle-extension.js +488 -13
  65. package/dist/pi/opencandle-extension.js.map +1 -1
  66. package/dist/pi/session-storage.d.ts +2 -0
  67. package/dist/pi/session-storage.js +5 -0
  68. package/dist/pi/session-storage.js.map +1 -0
  69. package/dist/pi/session.d.ts +4 -1
  70. package/dist/pi/session.js +25 -3
  71. package/dist/pi/session.js.map +1 -1
  72. package/dist/pi/setup.d.ts +1 -2
  73. package/dist/pi/setup.js +67 -120
  74. package/dist/pi/setup.js.map +1 -1
  75. package/dist/pi/tool-adapter.d.ts +2 -2
  76. package/dist/pi/tool-adapter.js +14 -1
  77. package/dist/pi/tool-adapter.js.map +1 -1
  78. package/dist/prompts/context-builder.d.ts +22 -0
  79. package/dist/prompts/context-builder.js +47 -11
  80. package/dist/prompts/context-builder.js.map +1 -1
  81. package/dist/prompts/disclaimer.d.ts +6 -0
  82. package/dist/prompts/disclaimer.js +9 -0
  83. package/dist/prompts/disclaimer.js.map +1 -0
  84. package/dist/prompts/workflow-prompts.d.ts +8 -0
  85. package/dist/prompts/workflow-prompts.js +39 -5
  86. package/dist/prompts/workflow-prompts.js.map +1 -1
  87. package/dist/providers/alpha-vantage.js +20 -1
  88. package/dist/providers/alpha-vantage.js.map +1 -1
  89. package/dist/providers/exa-search.d.ts +39 -0
  90. package/dist/providers/exa-search.js +276 -0
  91. package/dist/providers/exa-search.js.map +1 -0
  92. package/dist/providers/finnhub.d.ts +17 -0
  93. package/dist/providers/finnhub.js +94 -0
  94. package/dist/providers/finnhub.js.map +1 -0
  95. package/dist/providers/fred.js +13 -1
  96. package/dist/providers/fred.js.map +1 -1
  97. package/dist/providers/index.d.ts +2 -0
  98. package/dist/providers/index.js +1 -0
  99. package/dist/providers/index.js.map +1 -1
  100. package/dist/providers/provider-credential-error.d.ts +8 -0
  101. package/dist/providers/provider-credential-error.js +22 -0
  102. package/dist/providers/provider-credential-error.js.map +1 -0
  103. package/dist/providers/reddit.d.ts +8 -0
  104. package/dist/providers/reddit.js +36 -9
  105. package/dist/providers/reddit.js.map +1 -1
  106. package/dist/providers/twitter.js +2 -8
  107. package/dist/providers/twitter.js.map +1 -1
  108. package/dist/providers/web-search.d.ts +17 -0
  109. package/dist/providers/web-search.js +224 -0
  110. package/dist/providers/web-search.js.map +1 -0
  111. package/dist/providers/wrap-provider.d.ts +7 -0
  112. package/dist/providers/wrap-provider.js +15 -0
  113. package/dist/providers/wrap-provider.js.map +1 -1
  114. package/dist/providers/yahoo-finance.js +70 -33
  115. package/dist/providers/yahoo-finance.js.map +1 -1
  116. package/dist/routing/classify-intent.js +22 -0
  117. package/dist/routing/classify-intent.js.map +1 -1
  118. package/dist/routing/defaults.js +1 -1
  119. package/dist/routing/defaults.js.map +1 -1
  120. package/dist/routing/index.d.ts +4 -0
  121. package/dist/routing/index.js +3 -0
  122. package/dist/routing/index.js.map +1 -1
  123. package/dist/routing/router-llm-client.d.ts +11 -0
  124. package/dist/routing/router-llm-client.js +42 -0
  125. package/dist/routing/router-llm-client.js.map +1 -0
  126. package/dist/routing/router-prompt.d.ts +2 -0
  127. package/dist/routing/router-prompt.js +138 -0
  128. package/dist/routing/router-prompt.js.map +1 -0
  129. package/dist/routing/router-types.d.ts +62 -0
  130. package/dist/routing/router-types.js +2 -0
  131. package/dist/routing/router-types.js.map +1 -0
  132. package/dist/routing/router.d.ts +10 -0
  133. package/dist/routing/router.js +194 -0
  134. package/dist/routing/router.js.map +1 -0
  135. package/dist/runtime/session-coordinator.d.ts +63 -4
  136. package/dist/runtime/session-coordinator.js +155 -4
  137. package/dist/runtime/session-coordinator.js.map +1 -1
  138. package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
  139. package/dist/runtime/tool-defaults-wrapper.js +25 -0
  140. package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
  141. package/dist/sentiment/adapters/finnhub.d.ts +7 -0
  142. package/dist/sentiment/adapters/finnhub.js +39 -0
  143. package/dist/sentiment/adapters/finnhub.js.map +1 -0
  144. package/dist/sentiment/adapters/reddit.d.ts +11 -0
  145. package/dist/sentiment/adapters/reddit.js +54 -0
  146. package/dist/sentiment/adapters/reddit.js.map +1 -0
  147. package/dist/sentiment/adapters/twitter.d.ts +9 -0
  148. package/dist/sentiment/adapters/twitter.js +32 -0
  149. package/dist/sentiment/adapters/twitter.js.map +1 -0
  150. package/dist/sentiment/adapters/web.d.ts +9 -0
  151. package/dist/sentiment/adapters/web.js +40 -0
  152. package/dist/sentiment/adapters/web.js.map +1 -0
  153. package/dist/sentiment/index.d.ts +16 -0
  154. package/dist/sentiment/index.js +44 -0
  155. package/dist/sentiment/index.js.map +1 -0
  156. package/dist/sentiment/keywords.d.ts +2 -0
  157. package/dist/sentiment/keywords.js +9 -0
  158. package/dist/sentiment/keywords.js.map +1 -0
  159. package/dist/sentiment/pipeline.d.ts +9 -0
  160. package/dist/sentiment/pipeline.js +57 -0
  161. package/dist/sentiment/pipeline.js.map +1 -0
  162. package/dist/sentiment/scorer.d.ts +9 -0
  163. package/dist/sentiment/scorer.js +64 -0
  164. package/dist/sentiment/scorer.js.map +1 -0
  165. package/dist/sentiment/store.d.ts +24 -0
  166. package/dist/sentiment/store.js +182 -0
  167. package/dist/sentiment/store.js.map +1 -0
  168. package/dist/sentiment/trends.d.ts +13 -0
  169. package/dist/sentiment/trends.js +73 -0
  170. package/dist/sentiment/trends.js.map +1 -0
  171. package/dist/sentiment/types.d.ts +66 -0
  172. package/dist/sentiment/types.js +54 -0
  173. package/dist/sentiment/types.js.map +1 -0
  174. package/dist/system-prompt.js +29 -13
  175. package/dist/system-prompt.js.map +1 -1
  176. package/dist/tool-kit.d.ts +4 -4
  177. package/dist/tools/fundamentals/company-overview.d.ts +4 -2
  178. package/dist/tools/fundamentals/company-overview.js +27 -27
  179. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  180. package/dist/tools/fundamentals/comps.d.ts +1 -1
  181. package/dist/tools/fundamentals/comps.js +45 -45
  182. package/dist/tools/fundamentals/comps.js.map +1 -1
  183. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  184. package/dist/tools/fundamentals/dcf.js +82 -82
  185. package/dist/tools/fundamentals/dcf.js.map +1 -1
  186. package/dist/tools/fundamentals/earnings.d.ts +4 -2
  187. package/dist/tools/fundamentals/earnings.js +25 -25
  188. package/dist/tools/fundamentals/earnings.js.map +1 -1
  189. package/dist/tools/fundamentals/financials.d.ts +4 -2
  190. package/dist/tools/fundamentals/financials.js +23 -23
  191. package/dist/tools/fundamentals/financials.js.map +1 -1
  192. package/dist/tools/fundamentals/sec-filings.d.ts +1 -1
  193. package/dist/tools/index.d.ts +28 -1
  194. package/dist/tools/index.js +35 -2
  195. package/dist/tools/index.js.map +1 -1
  196. package/dist/tools/interaction/ask-user.d.ts +1 -1
  197. package/dist/tools/interaction/ask-user.js +28 -64
  198. package/dist/tools/interaction/ask-user.js.map +1 -1
  199. package/dist/tools/interaction/twitter-login.d.ts +1 -1
  200. package/dist/tools/macro/fear-greed.d.ts +1 -1
  201. package/dist/tools/macro/fred-data.d.ts +4 -2
  202. package/dist/tools/macro/fred-data.js +26 -26
  203. package/dist/tools/macro/fred-data.js.map +1 -1
  204. package/dist/tools/market/crypto-history.d.ts +1 -1
  205. package/dist/tools/market/crypto-price.d.ts +1 -1
  206. package/dist/tools/market/search-ticker.d.ts +1 -1
  207. package/dist/tools/market/stock-history.d.ts +1 -1
  208. package/dist/tools/market/stock-quote.d.ts +1 -1
  209. package/dist/tools/options/option-chain.d.ts +1 -1
  210. package/dist/tools/options/option-chain.js +4 -1
  211. package/dist/tools/options/option-chain.js.map +1 -1
  212. package/dist/tools/portfolio/correlation.d.ts +1 -1
  213. package/dist/tools/portfolio/predictions.d.ts +1 -1
  214. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  215. package/dist/tools/portfolio/tracker.d.ts +1 -1
  216. package/dist/tools/portfolio/watchlist.d.ts +1 -1
  217. package/dist/tools/sentiment/reddit-sentiment.d.ts +4 -2
  218. package/dist/tools/sentiment/reddit-sentiment.js +107 -22
  219. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  220. package/dist/tools/sentiment/sentiment-summary.d.ts +7 -0
  221. package/dist/tools/sentiment/sentiment-summary.js +230 -0
  222. package/dist/tools/sentiment/sentiment-summary.js.map +1 -0
  223. package/dist/tools/sentiment/sentiment-trend.d.ts +22 -0
  224. package/dist/tools/sentiment/sentiment-trend.js +39 -0
  225. package/dist/tools/sentiment/sentiment-trend.js.map +1 -0
  226. package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
  227. package/dist/tools/sentiment/twitter-sentiment.js +17 -0
  228. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  229. package/dist/tools/sentiment/web-search.d.ts +11 -0
  230. package/dist/tools/sentiment/web-search.js +115 -0
  231. package/dist/tools/sentiment/web-search.js.map +1 -0
  232. package/dist/tools/sentiment/web-sentiment.d.ts +8 -0
  233. package/dist/tools/sentiment/web-sentiment.js +66 -0
  234. package/dist/tools/sentiment/web-sentiment.js.map +1 -0
  235. package/dist/tools/technical/backtest.d.ts +1 -1
  236. package/dist/tools/technical/indicators.d.ts +1 -1
  237. package/dist/tools/technical/indicators.js +7 -1
  238. package/dist/tools/technical/indicators.js.map +1 -1
  239. package/dist/types/index.d.ts +1 -1
  240. package/dist/types/sentiment.d.ts +21 -0
  241. package/dist/workflows/options-screener.js +7 -2
  242. package/dist/workflows/options-screener.js.map +1 -1
  243. package/dist/workflows/portfolio-builder.js +3 -3
  244. package/dist/workflows/portfolio-builder.js.map +1 -1
  245. package/gui/server/background-quotes.ts +31 -0
  246. package/gui/server/chat-event-adapter.ts +142 -0
  247. package/gui/server/invoke-tool.ts +89 -0
  248. package/gui/server/live-chat-event-adapter.ts +181 -0
  249. package/gui/server/model-setup.ts +100 -0
  250. package/gui/server/package.json +5 -0
  251. package/gui/server/projector.ts +212 -0
  252. package/gui/server/server.ts +592 -0
  253. package/gui/server/session-actions.ts +31 -0
  254. package/gui/server/tool-metadata.ts +88 -0
  255. package/gui/server/websocket.ts +128 -0
  256. package/gui/server/writer-lock.ts +118 -0
  257. package/gui/shared/chat-events.ts +118 -0
  258. package/gui/shared/event-reducer.ts +186 -0
  259. package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +1 -0
  260. package/gui/web/dist/assets/index-DBrWq43L.css +1 -0
  261. package/gui/web/dist/assets/index-RflHaj0y.js +67 -0
  262. package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
  263. package/gui/web/dist/index.html +17 -0
  264. package/package.json +62 -20
  265. package/src/analysts/contracts.ts +189 -0
  266. package/src/analysts/orchestrator.ts +300 -0
  267. package/src/cli.ts +205 -0
  268. package/src/config.ts +161 -0
  269. package/src/index.ts +5 -0
  270. package/src/infra/browser.ts +111 -0
  271. package/src/infra/cache.ts +103 -0
  272. package/src/infra/http-client.ts +68 -0
  273. package/src/infra/index.ts +18 -0
  274. package/src/infra/native-dependencies.ts +12 -0
  275. package/src/infra/node-version.ts +24 -0
  276. package/src/infra/open-url.ts +28 -0
  277. package/src/infra/opencandle-paths.ts +64 -0
  278. package/src/infra/rate-limiter.ts +64 -0
  279. package/src/memory/index.ts +10 -0
  280. package/src/memory/manager.ts +159 -0
  281. package/src/memory/preference-extractor.ts +106 -0
  282. package/src/memory/retrieval.ts +70 -0
  283. package/src/memory/sqlite.ts +172 -0
  284. package/src/memory/storage.ts +204 -0
  285. package/src/memory/tool-defaults.ts +87 -0
  286. package/src/memory/types.ts +67 -0
  287. package/src/onboarding/connect.ts +184 -0
  288. package/src/onboarding/credential-interceptor.ts +134 -0
  289. package/src/onboarding/degradation-accumulator.ts +79 -0
  290. package/src/onboarding/prompt-user.ts +85 -0
  291. package/src/onboarding/providers.ts +315 -0
  292. package/src/onboarding/state.ts +218 -0
  293. package/src/onboarding/tool-helpers.ts +111 -0
  294. package/src/onboarding/tool-tags.ts +201 -0
  295. package/src/onboarding/validation.ts +158 -0
  296. package/src/pi/opencandle-extension.ts +724 -0
  297. package/src/pi/session-storage.ts +5 -0
  298. package/src/pi/session.ts +81 -0
  299. package/src/pi/setup.ts +371 -0
  300. package/src/pi/tool-adapter.ts +36 -0
  301. package/src/prompts/context-builder.ts +204 -0
  302. package/src/prompts/disclaimer.ts +9 -0
  303. package/src/prompts/sections.ts +46 -0
  304. package/src/prompts/workflow-prompts.ts +279 -0
  305. package/src/providers/alpha-vantage.ts +292 -0
  306. package/src/providers/coingecko.ts +96 -0
  307. package/src/providers/exa-search.ts +373 -0
  308. package/src/providers/fear-greed.ts +45 -0
  309. package/src/providers/finnhub.ts +124 -0
  310. package/src/providers/fred.ts +83 -0
  311. package/src/providers/index.ts +9 -0
  312. package/src/providers/provider-credential-error.ts +23 -0
  313. package/src/providers/reddit.ts +151 -0
  314. package/src/providers/sec-edgar.ts +96 -0
  315. package/src/providers/twitter.ts +173 -0
  316. package/src/providers/web-search.ts +293 -0
  317. package/src/providers/with-fallback.ts +41 -0
  318. package/src/providers/wrap-provider.ts +64 -0
  319. package/src/providers/yahoo-finance.ts +367 -0
  320. package/src/routing/classify-intent.ts +194 -0
  321. package/src/routing/defaults.ts +29 -0
  322. package/src/routing/entity-extractor.ts +140 -0
  323. package/src/routing/index.ts +26 -0
  324. package/src/routing/router-llm-client.ts +51 -0
  325. package/src/routing/router-prompt.ts +159 -0
  326. package/src/routing/router-types.ts +66 -0
  327. package/src/routing/router.ts +213 -0
  328. package/src/routing/slot-resolver.ts +152 -0
  329. package/src/routing/types.ts +63 -0
  330. package/src/runtime/evidence.ts +77 -0
  331. package/src/runtime/index.ts +55 -0
  332. package/src/runtime/prompt-step.ts +75 -0
  333. package/src/runtime/provider-ids.ts +15 -0
  334. package/src/runtime/provider-tracker.ts +40 -0
  335. package/src/runtime/run-context.ts +22 -0
  336. package/src/runtime/session-coordinator.ts +406 -0
  337. package/src/runtime/tool-defaults-wrapper.ts +35 -0
  338. package/src/runtime/validation.ts +214 -0
  339. package/src/runtime/workflow-events.ts +75 -0
  340. package/src/runtime/workflow-runner.ts +188 -0
  341. package/src/runtime/workflow-types.ts +102 -0
  342. package/src/sentiment/adapters/finnhub.ts +44 -0
  343. package/src/sentiment/adapters/reddit.ts +65 -0
  344. package/src/sentiment/adapters/twitter.ts +36 -0
  345. package/src/sentiment/adapters/web.ts +44 -0
  346. package/src/sentiment/index.ts +58 -0
  347. package/src/sentiment/keywords.ts +9 -0
  348. package/src/sentiment/pipeline.ts +68 -0
  349. package/src/sentiment/scorer.ts +78 -0
  350. package/src/sentiment/store.ts +260 -0
  351. package/src/sentiment/trends.ts +90 -0
  352. package/src/sentiment/types.ts +108 -0
  353. package/src/system-prompt.ts +115 -0
  354. package/src/tool-kit.ts +68 -0
  355. package/src/tools/AGENTS.md +36 -0
  356. package/src/tools/fundamentals/company-overview.ts +54 -0
  357. package/src/tools/fundamentals/comps.ts +156 -0
  358. package/src/tools/fundamentals/dcf.ts +267 -0
  359. package/src/tools/fundamentals/earnings.ts +47 -0
  360. package/src/tools/fundamentals/financials.ts +54 -0
  361. package/src/tools/fundamentals/sec-filings.ts +61 -0
  362. package/src/tools/index.ts +88 -0
  363. package/src/tools/interaction/ask-user.ts +81 -0
  364. package/src/tools/interaction/twitter-login.ts +93 -0
  365. package/src/tools/macro/fear-greed.ts +41 -0
  366. package/src/tools/macro/fred-data.ts +54 -0
  367. package/src/tools/market/crypto-history.ts +51 -0
  368. package/src/tools/market/crypto-price.ts +53 -0
  369. package/src/tools/market/search-ticker.ts +53 -0
  370. package/src/tools/market/stock-history.ts +79 -0
  371. package/src/tools/market/stock-quote.ts +64 -0
  372. package/src/tools/options/greeks.ts +82 -0
  373. package/src/tools/options/option-chain.ts +91 -0
  374. package/src/tools/portfolio/correlation.ts +162 -0
  375. package/src/tools/portfolio/predictions.ts +253 -0
  376. package/src/tools/portfolio/risk-analysis.ts +134 -0
  377. package/src/tools/portfolio/tracker.ts +147 -0
  378. package/src/tools/portfolio/watchlist.ts +153 -0
  379. package/src/tools/sentiment/reddit-sentiment.ts +164 -0
  380. package/src/tools/sentiment/sentiment-summary.ts +256 -0
  381. package/src/tools/sentiment/sentiment-trend.ts +58 -0
  382. package/src/tools/sentiment/twitter-sentiment.ts +96 -0
  383. package/src/tools/sentiment/web-search.ts +150 -0
  384. package/src/tools/sentiment/web-sentiment.ts +76 -0
  385. package/src/tools/technical/backtest.ts +246 -0
  386. package/src/tools/technical/indicators.ts +258 -0
  387. package/src/types/fundamentals.ts +46 -0
  388. package/src/types/index.ts +20 -0
  389. package/src/types/macro.ts +27 -0
  390. package/src/types/market.ts +43 -0
  391. package/src/types/options.ts +35 -0
  392. package/src/types/portfolio.ts +41 -0
  393. package/src/types/sentiment.ts +70 -0
  394. package/src/workflows/compare-assets.ts +39 -0
  395. package/src/workflows/index.ts +4 -0
  396. package/src/workflows/options-screener.ts +49 -0
  397. package/src/workflows/portfolio-builder.ts +52 -0
  398. package/src/workflows/types.ts +4 -0
  399. package/dist/tools/sentiment/news-sentiment.d.ts +0 -7
  400. package/dist/tools/sentiment/news-sentiment.js +0 -55
  401. package/dist/tools/sentiment/news-sentiment.js.map +0 -1
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Shared stealth browser infrastructure using Camoufox (anti-detection Firefox).
3
+ * Provides a singleton browser instance that any tool can use for scraping.
4
+ *
5
+ * Usage:
6
+ * import { StealthBrowser } from "../infra/browser.js";
7
+ * const data = await StealthBrowser.fetchJson<MyType>(url);
8
+ * const result = await StealthBrowser.evaluate(url, () => document.title);
9
+ */
10
+ import "./node-version.js";
11
+ import { Camoufox } from "camoufox-js";
12
+ import type { Browser, Page } from "playwright-core";
13
+
14
+ let browser: Browser | null = null;
15
+ let page: Page | null = null;
16
+ let launching: Promise<void> | null = null;
17
+ let pageQueue: Promise<void> = Promise.resolve();
18
+
19
+ async function ensureBrowser(): Promise<Page> {
20
+ if (page && browser?.isConnected()) return page;
21
+
22
+ // Prevent concurrent launches
23
+ if (launching) {
24
+ await launching;
25
+ if (page && browser?.isConnected()) return page;
26
+ }
27
+
28
+ launching = (async () => {
29
+ const b = await Camoufox({ headless: true });
30
+ browser = b;
31
+ page = await b.newPage();
32
+ })();
33
+
34
+ await launching;
35
+ launching = null;
36
+ return page!;
37
+ }
38
+
39
+ async function withPage<T>(fn: (p: Page) => Promise<T>): Promise<T> {
40
+ let resolve!: () => void;
41
+ const next = new Promise<void>((r) => { resolve = r; });
42
+ const prev = pageQueue;
43
+ pageQueue = next;
44
+ await prev;
45
+ try {
46
+ const p = await ensureBrowser();
47
+ return await fn(p);
48
+ } finally {
49
+ resolve();
50
+ }
51
+ }
52
+
53
+ export const StealthBrowser = {
54
+ /**
55
+ * Navigate to a URL, run a JS function in the page context, and return the result.
56
+ */
57
+ async evaluate<T>(url: string, fn: () => T | Promise<T>): Promise<T> {
58
+ return withPage(async (p) => {
59
+ await p.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
60
+ return p.evaluate(fn);
61
+ });
62
+ },
63
+
64
+ /**
65
+ * Fetch JSON from a URL using the browser's session (cookies, TLS fingerprint).
66
+ * Useful for APIs that block Node.js fetch but allow real browsers.
67
+ */
68
+ async fetchJson<T>(url: string, options?: { cookies?: string }): Promise<T> {
69
+ return withPage(async (p) => {
70
+ const result = await p.evaluate(async (fetchUrl: string) => {
71
+ const res = await fetch(fetchUrl, { credentials: "include" });
72
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
73
+ return res.json();
74
+ }, url);
75
+ return result as T;
76
+ });
77
+ },
78
+
79
+ /**
80
+ * Run a custom async function in the browser page context.
81
+ * The page must already be on a relevant domain for cookies to work.
82
+ */
83
+ async run<T>(fn: (page: Page) => Promise<T>): Promise<T> {
84
+ return withPage(async (p) => fn(p));
85
+ },
86
+
87
+ /**
88
+ * Navigate to a URL and establish session cookies for that domain.
89
+ */
90
+ async initSession(url: string): Promise<void> {
91
+ return withPage(async (p) => {
92
+ await p.goto(url, { waitUntil: "domcontentloaded", timeout: 15000 });
93
+ });
94
+ },
95
+
96
+ /**
97
+ * Close the browser. It will be re-launched on next use.
98
+ */
99
+ async close(): Promise<void> {
100
+ if (browser) {
101
+ await browser.close().catch(() => {});
102
+ browser = null;
103
+ page = null;
104
+ }
105
+ },
106
+ };
107
+
108
+ // Clean up on process exit
109
+ process.on("exit", () => {
110
+ browser?.close().catch(() => {});
111
+ });
@@ -0,0 +1,103 @@
1
+ interface CacheEntry<T> {
2
+ value: T;
3
+ expiresAt: number;
4
+ cachedAt: number;
5
+ }
6
+
7
+ export interface StaleResult<T> {
8
+ value: T;
9
+ stale: true;
10
+ cachedAt: number;
11
+ }
12
+
13
+ export class Cache {
14
+ private store = new Map<string, CacheEntry<unknown>>();
15
+ private lastStaleHit = false;
16
+ private lastStaleCachedAt = 0;
17
+
18
+ get<T>(key: string): T | undefined {
19
+ const entry = this.store.get(key);
20
+ if (!entry) return undefined;
21
+ if (Date.now() > entry.expiresAt) return undefined;
22
+ return entry.value as T;
23
+ }
24
+
25
+ /**
26
+ * Return an expired entry if it exists and is within the stale limit.
27
+ * Unlike get(), this does not require the entry to be within its TTL.
28
+ * Entries beyond the stale limit are deleted.
29
+ */
30
+ getStale<T>(key: string, staleLimitMs: number): StaleResult<T> | undefined {
31
+ const entry = this.store.get(key);
32
+ if (!entry) return undefined;
33
+
34
+ if (Date.now() > entry.cachedAt + staleLimitMs) {
35
+ this.store.delete(key);
36
+ return undefined;
37
+ }
38
+
39
+ this.lastStaleHit = true;
40
+ this.lastStaleCachedAt = entry.cachedAt;
41
+ return { value: entry.value as T, stale: true, cachedAt: entry.cachedAt };
42
+ }
43
+
44
+ /**
45
+ * Consume the stale flag set by the last getStale() hit.
46
+ * Returns { stale: true, cachedAt } if the last getStale() found data,
47
+ * then resets the flag. Used by wrapProvider to propagate stale metadata.
48
+ */
49
+ consumeStaleFlag(): { stale: boolean; cachedAt: number } {
50
+ const result = { stale: this.lastStaleHit, cachedAt: this.lastStaleCachedAt };
51
+ this.lastStaleHit = false;
52
+ this.lastStaleCachedAt = 0;
53
+ return result;
54
+ }
55
+
56
+ set<T>(key: string, value: T, ttlMs: number): void {
57
+ this.store.set(key, { value, expiresAt: Date.now() + ttlMs, cachedAt: Date.now() });
58
+ }
59
+
60
+ invalidate(pattern: string): void {
61
+ for (const key of this.store.keys()) {
62
+ if (key.includes(pattern)) {
63
+ this.store.delete(key);
64
+ }
65
+ }
66
+ }
67
+
68
+ clear(): void {
69
+ this.store.clear();
70
+ }
71
+
72
+ get size(): number {
73
+ return this.store.size;
74
+ }
75
+ }
76
+
77
+ // Shared cache instance
78
+ export const cache = new Cache();
79
+
80
+ // Default TTLs
81
+ export const TTL = {
82
+ QUOTE: 60_000, // 1 minute
83
+ HISTORY: 3_600_000, // 1 hour
84
+ FUNDAMENTALS: 86_400_000, // 24 hours
85
+ MACRO: 3_600_000, // 1 hour
86
+ SENTIMENT: 300_000, // 5 minutes
87
+ OPTIONS_CHAIN: 120_000, // 2 minutes
88
+ CRUMB: 900_000, // 15 minutes
89
+ WEB_SEARCH: 300_000, // 5 minutes
90
+ FINNHUB_NEWS: 300_000, // 5 minutes
91
+ } as const;
92
+
93
+ // Stale limits — how long past TTL expiry a cached value is still useful as fallback
94
+ export const STALE_LIMIT = {
95
+ QUOTE: 15 * 60_000, // 15 minutes
96
+ HISTORY: 24 * 3_600_000, // 24 hours
97
+ FUNDAMENTALS: 7 * 86_400_000, // 7 days
98
+ MACRO: 24 * 3_600_000, // 24 hours
99
+ SENTIMENT: 3_600_000, // 1 hour
100
+ OPTIONS_CHAIN: 30 * 60_000, // 30 minutes
101
+ WEB_SEARCH: 3_600_000, // 1 hour
102
+ FINNHUB_NEWS: 3_600_000, // 1 hour
103
+ } as const;
@@ -0,0 +1,68 @@
1
+ export interface HttpClientOptions {
2
+ timeoutMs?: number;
3
+ maxRetries?: number;
4
+ retryDelayMs?: number;
5
+ headers?: Record<string, string>;
6
+ }
7
+
8
+ const DEFAULT_OPTIONS: Required<HttpClientOptions> = {
9
+ timeoutMs: 10_000,
10
+ maxRetries: 2,
11
+ retryDelayMs: 1_000,
12
+ headers: {},
13
+ };
14
+
15
+ export class HttpError extends Error {
16
+ constructor(
17
+ public readonly status: number,
18
+ public readonly statusText: string,
19
+ public readonly body: string,
20
+ ) {
21
+ super(`HTTP ${status} ${statusText}`);
22
+ this.name = "HttpError";
23
+ }
24
+ }
25
+
26
+ export async function httpGet<T>(
27
+ url: string,
28
+ options: HttpClientOptions = {},
29
+ ): Promise<T> {
30
+ const opts = { ...DEFAULT_OPTIONS, ...options };
31
+ let lastError: Error | undefined;
32
+
33
+ for (let attempt = 0; attempt <= opts.maxRetries; attempt++) {
34
+ if (attempt > 0) {
35
+ await sleep(opts.retryDelayMs * attempt);
36
+ }
37
+
38
+ const controller = new AbortController();
39
+ const timeout = setTimeout(() => controller.abort(), opts.timeoutMs);
40
+
41
+ try {
42
+ const response = await fetch(url, {
43
+ signal: controller.signal,
44
+ headers: opts.headers,
45
+ });
46
+
47
+ if (!response.ok) {
48
+ const body = await response.text().catch(() => "");
49
+ throw new HttpError(response.status, response.statusText, body);
50
+ }
51
+
52
+ return (await response.json()) as T;
53
+ } catch (error) {
54
+ lastError = error as Error;
55
+ if (error instanceof HttpError && error.status >= 400 && error.status < 500) {
56
+ throw error; // Don't retry client errors
57
+ }
58
+ } finally {
59
+ clearTimeout(timeout);
60
+ }
61
+ }
62
+
63
+ throw lastError!;
64
+ }
65
+
66
+ function sleep(ms: number): Promise<void> {
67
+ return new Promise((resolve) => setTimeout(resolve, ms));
68
+ }
@@ -0,0 +1,18 @@
1
+ export { Cache, cache, TTL } from "./cache.js";
2
+ export { RateLimiter, rateLimiter } from "./rate-limiter.js";
3
+ export { httpGet, HttpError, type HttpClientOptions } from "./http-client.js";
4
+ export { StealthBrowser } from "./browser.js";
5
+ export {
6
+ getOpenCandleHomeDir,
7
+ ensureOpenCandleHomeDir,
8
+ resolveOpenCandlePath,
9
+ ensureParentDir,
10
+ getWatchlistPath,
11
+ getPortfolioPath,
12
+ getPredictionsPath,
13
+ getConfigPath,
14
+ getOnboardingPath,
15
+ getStateDbPath,
16
+ getLogsDir,
17
+ getBrowserProfileDir,
18
+ } from "./opencandle-paths.js";
@@ -0,0 +1,12 @@
1
+ export function getNativeDependencyErrorMessage(error: unknown, dependencyName: string): string | null {
2
+ const message = error instanceof Error ? error.message : String(error);
3
+ if (
4
+ !message.includes("NODE_MODULE_VERSION") &&
5
+ !message.includes("was compiled against a different Node.js version")
6
+ ) {
7
+ return null;
8
+ }
9
+
10
+ return `${dependencyName} native binding was built for a different Node ABI than the active Node ${process.versions.node}. ` +
11
+ `Run \`npm rebuild ${dependencyName}\` or reinstall dependencies under the active Node with \`npm install\`.`;
12
+ }
@@ -0,0 +1,24 @@
1
+ const SUPPORTED_NODE_RANGE = "20.19+, 22.12+, or 24.x-26.x";
2
+
3
+ function isSupportedNodeVersion(version: string): boolean {
4
+ const [majorRaw, minorRaw] = version.split(".");
5
+ const major = Number(majorRaw);
6
+ const minor = Number(minorRaw);
7
+
8
+ if (major === 20) return minor >= 19;
9
+ if (major === 22) return minor >= 12;
10
+ return major >= 24 && major < 27;
11
+ }
12
+
13
+ export function getUnsupportedNodeVersionMessage(version: string = process.versions.node): string | null {
14
+ if (isSupportedNodeVersion(version)) return null;
15
+
16
+ return `OpenCandle supports Node ${SUPPORTED_NODE_RANGE}. Current Node is ${version}. Use Node ${SUPPORTED_NODE_RANGE}; the repo default is Node 22.22.0 via \`nvm use\`. After switching Node versions, reinstall dependencies under the active Node with \`npm install\` or rebuild native modules with \`npm rebuild better-sqlite3\`.`;
17
+ }
18
+
19
+ export function assertSupportedNodeVersion(version?: string): void {
20
+ const message = getUnsupportedNodeVersionMessage(version);
21
+ if (message) throw new Error(message);
22
+ }
23
+
24
+ assertSupportedNodeVersion();
@@ -0,0 +1,28 @@
1
+ import { execFile } from "node:child_process";
2
+
3
+ export function openInBrowser(url: string): Promise<void> {
4
+ return new Promise<void>((resolve, reject) => {
5
+ let command: string;
6
+ let args: string[];
7
+
8
+ if (process.platform === "darwin") {
9
+ command = "open";
10
+ args = [url];
11
+ } else if (process.platform === "win32") {
12
+ command = "cmd";
13
+ args = ["/c", "start", "", url];
14
+ } else {
15
+ command = "xdg-open";
16
+ args = [url];
17
+ }
18
+
19
+ const child = execFile(command, args, { timeout: 5000 }, (error) => {
20
+ if (error) {
21
+ reject(error);
22
+ } else {
23
+ resolve();
24
+ }
25
+ });
26
+ child.unref();
27
+ });
28
+ }
@@ -0,0 +1,64 @@
1
+ import { existsSync, mkdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
3
+ import { dirname, join, resolve } from "node:path";
4
+
5
+ const OPENCANDLE_HOME_ENV = "OPENCANDLE_HOME";
6
+
7
+ function ensureDir(path: string): void {
8
+ if (!existsSync(path)) {
9
+ mkdirSync(path, { recursive: true });
10
+ }
11
+ }
12
+
13
+ export function getOpenCandleHomeDir(): string {
14
+ const override = process.env[OPENCANDLE_HOME_ENV];
15
+ return override ? resolve(override) : join(homedir(), ".opencandle");
16
+ }
17
+
18
+ export function ensureOpenCandleHomeDir(): string {
19
+ const home = getOpenCandleHomeDir();
20
+ ensureDir(home);
21
+ return home;
22
+ }
23
+
24
+ export function resolveOpenCandlePath(...segments: string[]): string {
25
+ return join(getOpenCandleHomeDir(), ...segments);
26
+ }
27
+
28
+ export function ensureParentDir(path: string): string {
29
+ const parent = dirname(path);
30
+ ensureDir(parent);
31
+ return parent;
32
+ }
33
+
34
+ export function getWatchlistPath(): string {
35
+ return resolveOpenCandlePath("watchlist.json");
36
+ }
37
+
38
+ export function getPortfolioPath(): string {
39
+ return resolveOpenCandlePath("portfolio.json");
40
+ }
41
+
42
+ export function getPredictionsPath(): string {
43
+ return resolveOpenCandlePath("predictions.json");
44
+ }
45
+
46
+ export function getConfigPath(): string {
47
+ return resolveOpenCandlePath("config.json");
48
+ }
49
+
50
+ export function getOnboardingPath(): string {
51
+ return resolveOpenCandlePath("onboarding.json");
52
+ }
53
+
54
+ export function getStateDbPath(): string {
55
+ return resolveOpenCandlePath("state.db");
56
+ }
57
+
58
+ export function getLogsDir(): string {
59
+ return resolveOpenCandlePath("logs");
60
+ }
61
+
62
+ export function getBrowserProfileDir(): string {
63
+ return resolveOpenCandlePath("browser-profile");
64
+ }
@@ -0,0 +1,64 @@
1
+ interface BucketConfig {
2
+ maxTokens: number;
3
+ refillRate: number; // tokens per second
4
+ }
5
+
6
+ interface Bucket {
7
+ tokens: number;
8
+ lastRefill: number;
9
+ config: BucketConfig;
10
+ }
11
+
12
+ export class RateLimiter {
13
+ private buckets = new Map<string, Bucket>();
14
+
15
+ configure(provider: string, maxTokens: number, refillRate: number): void {
16
+ this.buckets.set(provider, {
17
+ tokens: maxTokens,
18
+ lastRefill: Date.now(),
19
+ config: { maxTokens, refillRate },
20
+ });
21
+ }
22
+
23
+ async acquire(provider: string): Promise<void> {
24
+ const bucket = this.buckets.get(provider);
25
+ if (!bucket) return; // No limit configured
26
+
27
+ this.refill(bucket);
28
+
29
+ if (bucket.tokens >= 1) {
30
+ bucket.tokens -= 1;
31
+ return;
32
+ }
33
+
34
+ // Wait until a token is available
35
+ const waitMs = ((1 - bucket.tokens) / bucket.config.refillRate) * 1000;
36
+ await new Promise((resolve) => setTimeout(resolve, Math.ceil(waitMs)));
37
+ this.refill(bucket);
38
+ bucket.tokens -= 1;
39
+ }
40
+
41
+ private refill(bucket: Bucket): void {
42
+ const now = Date.now();
43
+ const elapsed = (now - bucket.lastRefill) / 1000;
44
+ bucket.tokens = Math.min(
45
+ bucket.config.maxTokens,
46
+ bucket.tokens + elapsed * bucket.config.refillRate,
47
+ );
48
+ bucket.lastRefill = now;
49
+ }
50
+ }
51
+
52
+ // Shared instance with default provider limits
53
+ export const rateLimiter = new RateLimiter();
54
+ rateLimiter.configure("yahoo", 5, 5); // 5 req/s
55
+ rateLimiter.configure("coingecko", 10, 0.167); // 10 req/min
56
+ rateLimiter.configure("alphavantage", 5, 0.083); // 5 req/min (free tier)
57
+ rateLimiter.configure("fred", 120, 2); // 120 req/min
58
+ rateLimiter.configure("twitter", 5, 0.167); // 5 req, ~10 req/min
59
+ rateLimiter.configure("reddit", 5, 0.167); // 5 req, ~10 req/min
60
+ rateLimiter.configure("reddit_comments", 10, 0.5); // 10 req, ~30 req/min
61
+ rateLimiter.configure("ddg", 3, 0.1); // 3 req, ~6 req/min
62
+ rateLimiter.configure("brave_search", 5, 0.083); // 5 req, ~5 req/min
63
+ rateLimiter.configure("exa", 5, 0.1); // 5 req, ~6 req/min
64
+ rateLimiter.configure("finnhub", 60, 1); // 60 req/min (free tier)
@@ -0,0 +1,10 @@
1
+ export { initDatabase, initDefaultDatabase, getTableNames, getSchemaVersion } from "./sqlite.js";
2
+ export { MemoryStorage } from "./storage.js";
3
+ export type { WorkflowPreferences } from "./storage.js";
4
+ export { buildMemoryContext } from "./retrieval.js";
5
+ export { extractPreferences } from "./preference-extractor.js";
6
+ export { MemoryManager } from "./manager.js";
7
+ export { getAllDefaults, getDefaults, setDefault, clearDefault } from "./tool-defaults.js";
8
+ export type { ToolDefaults } from "./tool-defaults.js";
9
+ export type { MemoryCategory, MemoryEntry } from "./types.js";
10
+ export { isStale, STALENESS_THRESHOLDS, NEVER_TRUST_FROM_MEMORY } from "./types.js";
@@ -0,0 +1,159 @@
1
+ import type { MemoryStorage } from "./storage.js";
2
+ import type { MemoryCategory, MemoryEntry } from "./types.js";
3
+ import {
4
+ KEY_TO_CATEGORY,
5
+ WORKFLOW_RELEVANT_CATEGORIES,
6
+ NEVER_TRUST_FROM_MEMORY,
7
+ isStale,
8
+ } from "./types.js";
9
+
10
+ /** Slot name → preference key(s) mapping for suppression. */
11
+ const SLOT_TO_PREF_KEYS: Record<string, string[]> = {
12
+ riskProfile: ["risk_profile"],
13
+ assetScope: ["asset_scope"],
14
+ timeHorizon: ["time_horizon"],
15
+ dteTarget: ["dte_target"],
16
+ moneynessPreference: ["moneyness_preference"],
17
+ liquidityMinimum: ["liquidity_minimum", "options_liquidity"],
18
+ };
19
+
20
+ const MAX_WORKFLOW_HISTORY_PER_TYPE = 3;
21
+ const MAX_PREFERENCE_LINES = 15;
22
+
23
+ /**
24
+ * Selective, typed memory retrieval with staleness rules
25
+ * and override suppression.
26
+ */
27
+ export class MemoryManager {
28
+ constructor(private readonly storage: MemoryStorage) {}
29
+
30
+ /**
31
+ * Retrieve memory entries relevant to the given workflow type,
32
+ * filtering by category, staleness, and overrides.
33
+ */
34
+ retrieve(
35
+ workflowType: string,
36
+ overriddenSlots?: string[],
37
+ now: Date = new Date(),
38
+ ): MemoryEntry[] {
39
+ const relevantCategories = WORKFLOW_RELEVANT_CATEGORIES[workflowType] ??
40
+ WORKFLOW_RELEVANT_CATEGORIES["unclassified"];
41
+
42
+ // Build set of preference keys to suppress
43
+ const suppressedKeys = new Set<string>();
44
+ if (overriddenSlots) {
45
+ for (const slot of overriddenSlots) {
46
+ const keys = SLOT_TO_PREF_KEYS[slot];
47
+ if (keys) keys.forEach((k) => suppressedKeys.add(k));
48
+ }
49
+ }
50
+
51
+ const entries: MemoryEntry[] = [];
52
+
53
+ // Preferences as investor_profile entries
54
+ if (relevantCategories.includes("investor_profile")) {
55
+ const prefs = this.storage.getPreferencesByNamespace("global");
56
+ for (const pref of prefs) {
57
+ const key = String(pref.key);
58
+ if (suppressedKeys.has(key)) continue;
59
+ if (NEVER_TRUST_FROM_MEMORY.has(key)) continue;
60
+
61
+ const category = KEY_TO_CATEGORY[key] ?? "investor_profile";
62
+ if (!relevantCategories.includes(category)) continue;
63
+
64
+ const entry: MemoryEntry = {
65
+ key,
66
+ value: tryParseValue(String(pref.value_json ?? "")),
67
+ category,
68
+ recordedAt: String(pref.updated_at ?? pref.created_at ?? now.toISOString()),
69
+ confidence: pref.confidence != null ? String(pref.confidence) : undefined,
70
+ source: pref.source != null ? String(pref.source) : undefined,
71
+ };
72
+
73
+ if (!isStale(entry, now)) {
74
+ entries.push(entry);
75
+ }
76
+ }
77
+ }
78
+
79
+ // Workflow history
80
+ if (relevantCategories.includes("workflow_history")) {
81
+ const runs = this.storage.getRecentWorkflowRuns(MAX_WORKFLOW_HISTORY_PER_TYPE * 4);
82
+ const countsByType = new Map<string, number>();
83
+
84
+ for (const run of runs) {
85
+ const wfType = String(run.workflow_type);
86
+ const count = countsByType.get(wfType) ?? 0;
87
+ if (count >= MAX_WORKFLOW_HISTORY_PER_TYPE) continue;
88
+ countsByType.set(wfType, count + 1);
89
+
90
+ const recordedAt = String(run.created_at ?? now.toISOString());
91
+ const entry: MemoryEntry = {
92
+ key: `workflow_run_${run.id}`,
93
+ value: run.output_summary
94
+ ? `${wfType}: ${run.output_summary}`
95
+ : wfType,
96
+ category: "workflow_history",
97
+ recordedAt,
98
+ };
99
+
100
+ if (!isStale(entry, now)) {
101
+ entries.push(entry);
102
+ }
103
+ }
104
+ }
105
+
106
+ return entries.slice(0, MAX_PREFERENCE_LINES + MAX_WORKFLOW_HISTORY_PER_TYPE * 4);
107
+ }
108
+
109
+ /**
110
+ * Build compact text context from retrieved memory entries.
111
+ */
112
+ buildContext(
113
+ workflowType: string,
114
+ overriddenSlots?: string[],
115
+ now: Date = new Date(),
116
+ ): string {
117
+ const entries = this.retrieve(workflowType, overriddenSlots, now);
118
+ if (entries.length === 0) return "";
119
+
120
+ const sections: string[] = [];
121
+
122
+ // Group by category
123
+ const byCategory = new Map<MemoryCategory, MemoryEntry[]>();
124
+ for (const entry of entries) {
125
+ const list = byCategory.get(entry.category) ?? [];
126
+ list.push(entry);
127
+ byCategory.set(entry.category, list);
128
+ }
129
+
130
+ const profileEntries = byCategory.get("investor_profile");
131
+ if (profileEntries && profileEntries.length > 0) {
132
+ const lines = profileEntries.map((e) => `- ${e.key}: ${e.value}`);
133
+ sections.push("User Preferences:\n" + lines.join("\n"));
134
+ }
135
+
136
+ const historyEntries = byCategory.get("workflow_history");
137
+ if (historyEntries && historyEntries.length > 0) {
138
+ const lines = historyEntries.map((e) => `- ${e.value} (${e.recordedAt})`);
139
+ sections.push("Recent Workflows:\n" + lines.join("\n"));
140
+ }
141
+
142
+ const feedbackEntries = byCategory.get("interaction_feedback");
143
+ if (feedbackEntries && feedbackEntries.length > 0) {
144
+ const lines = feedbackEntries.map((e) => `- ${e.key}: ${e.value}`);
145
+ sections.push("Feedback:\n" + lines.join("\n"));
146
+ }
147
+
148
+ return sections.join("\n\n");
149
+ }
150
+ }
151
+
152
+ function tryParseValue(json: string): string {
153
+ try {
154
+ const parsed = JSON.parse(json);
155
+ return typeof parsed === "string" ? parsed : JSON.stringify(parsed);
156
+ } catch {
157
+ return json;
158
+ }
159
+ }