opencandle 0.3.0 → 0.5.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 (408) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +106 -14
  3. package/assets/logo.svg +187 -0
  4. package/dist/cli.d.ts +1 -1
  5. package/dist/cli.js +40 -3
  6. package/dist/cli.js.map +1 -1
  7. package/dist/config.d.ts +25 -0
  8. package/dist/config.js +72 -0
  9. package/dist/config.js.map +1 -1
  10. package/dist/infra/browser.d.ts +11 -3
  11. package/dist/infra/browser.js +2 -1
  12. package/dist/infra/browser.js.map +1 -1
  13. package/dist/infra/native-dependencies.d.ts +1 -0
  14. package/dist/infra/native-dependencies.js +10 -0
  15. package/dist/infra/native-dependencies.js.map +1 -0
  16. package/dist/infra/node-version.d.ts +2 -0
  17. package/dist/infra/node-version.js +23 -0
  18. package/dist/infra/node-version.js.map +1 -0
  19. package/dist/infra/rate-limiter.d.ts +4 -0
  20. package/dist/infra/rate-limiter.js +5 -1
  21. package/dist/infra/rate-limiter.js.map +1 -1
  22. package/dist/memory/index.d.ts +2 -0
  23. package/dist/memory/index.js +1 -0
  24. package/dist/memory/index.js.map +1 -1
  25. package/dist/memory/manager.d.ts +9 -0
  26. package/dist/memory/manager.js +28 -11
  27. package/dist/memory/manager.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 +7 -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/memory/types.js +4 -0
  37. package/dist/memory/types.js.map +1 -1
  38. package/dist/onboarding/connect.d.ts +13 -1
  39. package/dist/onboarding/connect.js +21 -10
  40. package/dist/onboarding/connect.js.map +1 -1
  41. package/dist/onboarding/prompt-user.d.ts +1 -1
  42. package/dist/onboarding/providers.d.ts +7 -0
  43. package/dist/onboarding/providers.js +6 -3
  44. package/dist/onboarding/providers.js.map +1 -1
  45. package/dist/onboarding/tool-helpers.d.ts +1 -1
  46. package/dist/pi/opencandle-extension.d.ts +7 -1
  47. package/dist/pi/opencandle-extension.js +391 -21
  48. package/dist/pi/opencandle-extension.js.map +1 -1
  49. package/dist/pi/session-storage.d.ts +2 -0
  50. package/dist/pi/session-storage.js +5 -0
  51. package/dist/pi/session-storage.js.map +1 -0
  52. package/dist/pi/session.d.ts +4 -1
  53. package/dist/pi/session.js +25 -3
  54. package/dist/pi/session.js.map +1 -1
  55. package/dist/pi/setup.d.ts +1 -1
  56. package/dist/pi/setup.js +11 -1
  57. package/dist/pi/setup.js.map +1 -1
  58. package/dist/pi/tool-adapter.d.ts +2 -2
  59. package/dist/pi/tool-adapter.js +14 -1
  60. package/dist/pi/tool-adapter.js.map +1 -1
  61. package/dist/prompts/context-builder.d.ts +40 -3
  62. package/dist/prompts/context-builder.js +140 -19
  63. package/dist/prompts/context-builder.js.map +1 -1
  64. package/dist/prompts/disclaimer.d.ts +6 -0
  65. package/dist/prompts/disclaimer.js +9 -0
  66. package/dist/prompts/disclaimer.js.map +1 -0
  67. package/dist/prompts/policy-cards.d.ts +13 -0
  68. package/dist/prompts/policy-cards.js +197 -0
  69. package/dist/prompts/policy-cards.js.map +1 -0
  70. package/dist/prompts/sections.js +3 -3
  71. package/dist/prompts/sections.js.map +1 -1
  72. package/dist/prompts/workflow-prompts.d.ts +8 -0
  73. package/dist/prompts/workflow-prompts.js +208 -22
  74. package/dist/prompts/workflow-prompts.js.map +1 -1
  75. package/dist/providers/alpha-vantage.js +23 -1
  76. package/dist/providers/alpha-vantage.js.map +1 -1
  77. package/dist/providers/sec-edgar.d.ts +8 -1
  78. package/dist/providers/sec-edgar.js +172 -5
  79. package/dist/providers/sec-edgar.js.map +1 -1
  80. package/dist/providers/yahoo-finance.d.ts +2 -0
  81. package/dist/providers/yahoo-finance.js +203 -35
  82. package/dist/providers/yahoo-finance.js.map +1 -1
  83. package/dist/routing/classify-intent.d.ts +3 -0
  84. package/dist/routing/classify-intent.js +82 -3
  85. package/dist/routing/classify-intent.js.map +1 -1
  86. package/dist/routing/defaults.js +4 -4
  87. package/dist/routing/defaults.js.map +1 -1
  88. package/dist/routing/entity-extractor.d.ts +1 -0
  89. package/dist/routing/entity-extractor.js +158 -12
  90. package/dist/routing/entity-extractor.js.map +1 -1
  91. package/dist/routing/index.d.ts +10 -0
  92. package/dist/routing/index.js +7 -0
  93. package/dist/routing/index.js.map +1 -1
  94. package/dist/routing/legacy-rule-router.d.ts +9 -0
  95. package/dist/routing/legacy-rule-router.js +12 -0
  96. package/dist/routing/legacy-rule-router.js.map +1 -0
  97. package/dist/routing/planning.d.ts +54 -0
  98. package/dist/routing/planning.js +531 -0
  99. package/dist/routing/planning.js.map +1 -0
  100. package/dist/routing/route-manifest.d.ts +35 -0
  101. package/dist/routing/route-manifest.js +221 -0
  102. package/dist/routing/route-manifest.js.map +1 -0
  103. package/dist/routing/router-llm-client.d.ts +11 -0
  104. package/dist/routing/router-llm-client.js +42 -0
  105. package/dist/routing/router-llm-client.js.map +1 -0
  106. package/dist/routing/router-prompt.d.ts +2 -0
  107. package/dist/routing/router-prompt.js +141 -0
  108. package/dist/routing/router-prompt.js.map +1 -0
  109. package/dist/routing/router-types.d.ts +71 -0
  110. package/dist/routing/router-types.js +2 -0
  111. package/dist/routing/router-types.js.map +1 -0
  112. package/dist/routing/router.d.ts +11 -0
  113. package/dist/routing/router.js +638 -0
  114. package/dist/routing/router.js.map +1 -0
  115. package/dist/routing/slot-resolver.js +46 -6
  116. package/dist/routing/slot-resolver.js.map +1 -1
  117. package/dist/routing/turn-context.d.ts +44 -0
  118. package/dist/routing/turn-context.js +45 -0
  119. package/dist/routing/turn-context.js.map +1 -0
  120. package/dist/routing/types.d.ts +13 -1
  121. package/dist/runtime/answer-contracts.d.ts +82 -0
  122. package/dist/runtime/answer-contracts.js +414 -0
  123. package/dist/runtime/answer-contracts.js.map +1 -0
  124. package/dist/runtime/artifact-contracts.d.ts +14 -0
  125. package/dist/runtime/artifact-contracts.js +57 -0
  126. package/dist/runtime/artifact-contracts.js.map +1 -0
  127. package/dist/runtime/planning-evidence.d.ts +99 -0
  128. package/dist/runtime/planning-evidence.js +445 -0
  129. package/dist/runtime/planning-evidence.js.map +1 -0
  130. package/dist/runtime/session-coordinator.d.ts +81 -3
  131. package/dist/runtime/session-coordinator.js +201 -17
  132. package/dist/runtime/session-coordinator.js.map +1 -1
  133. package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
  134. package/dist/runtime/tool-defaults-wrapper.js +25 -0
  135. package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
  136. package/dist/sentiment/store.js +5 -0
  137. package/dist/sentiment/store.js.map +1 -1
  138. package/dist/system-prompt.js +23 -12
  139. package/dist/system-prompt.js.map +1 -1
  140. package/dist/tool-kit.d.ts +4 -4
  141. package/dist/tools/fundamentals/company-overview.d.ts +1 -1
  142. package/dist/tools/fundamentals/company-overview.js +1 -1
  143. package/dist/tools/fundamentals/company-overview.js.map +1 -1
  144. package/dist/tools/fundamentals/comps.d.ts +1 -1
  145. package/dist/tools/fundamentals/comps.js +1 -1
  146. package/dist/tools/fundamentals/comps.js.map +1 -1
  147. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  148. package/dist/tools/fundamentals/dcf.js +1 -1
  149. package/dist/tools/fundamentals/dcf.js.map +1 -1
  150. package/dist/tools/fundamentals/earnings.d.ts +1 -1
  151. package/dist/tools/fundamentals/earnings.js +1 -1
  152. package/dist/tools/fundamentals/earnings.js.map +1 -1
  153. package/dist/tools/fundamentals/financials.d.ts +1 -1
  154. package/dist/tools/fundamentals/financials.js +1 -1
  155. package/dist/tools/fundamentals/financials.js.map +1 -1
  156. package/dist/tools/fundamentals/sec-filings.d.ts +2 -1
  157. package/dist/tools/fundamentals/sec-filings.js +19 -2
  158. package/dist/tools/fundamentals/sec-filings.js.map +1 -1
  159. package/dist/tools/index.d.ts +29 -1
  160. package/dist/tools/index.js +30 -0
  161. package/dist/tools/index.js.map +1 -1
  162. package/dist/tools/interaction/ask-user.d.ts +1 -1
  163. package/dist/tools/interaction/twitter-login.d.ts +1 -1
  164. package/dist/tools/macro/fear-greed.d.ts +1 -1
  165. package/dist/tools/macro/fear-greed.js +1 -1
  166. package/dist/tools/macro/fear-greed.js.map +1 -1
  167. package/dist/tools/macro/fred-data.d.ts +1 -1
  168. package/dist/tools/macro/fred-data.js +29 -5
  169. package/dist/tools/macro/fred-data.js.map +1 -1
  170. package/dist/tools/market/crypto-history.d.ts +1 -1
  171. package/dist/tools/market/crypto-history.js +18 -2
  172. package/dist/tools/market/crypto-history.js.map +1 -1
  173. package/dist/tools/market/crypto-price.d.ts +1 -1
  174. package/dist/tools/market/crypto-price.js +1 -1
  175. package/dist/tools/market/crypto-price.js.map +1 -1
  176. package/dist/tools/market/search-ticker.d.ts +1 -1
  177. package/dist/tools/market/search-ticker.js +1 -1
  178. package/dist/tools/market/search-ticker.js.map +1 -1
  179. package/dist/tools/market/stock-history.d.ts +1 -1
  180. package/dist/tools/market/stock-history.js +1 -1
  181. package/dist/tools/market/stock-history.js.map +1 -1
  182. package/dist/tools/market/stock-quote.d.ts +1 -1
  183. package/dist/tools/market/stock-quote.js +1 -1
  184. package/dist/tools/market/stock-quote.js.map +1 -1
  185. package/dist/tools/options/greeks.js +0 -1
  186. package/dist/tools/options/greeks.js.map +1 -1
  187. package/dist/tools/options/option-chain.d.ts +1 -1
  188. package/dist/tools/options/option-chain.js +13 -5
  189. package/dist/tools/options/option-chain.js.map +1 -1
  190. package/dist/tools/portfolio/correlation.d.ts +1 -1
  191. package/dist/tools/portfolio/correlation.js +1 -1
  192. package/dist/tools/portfolio/correlation.js.map +1 -1
  193. package/dist/tools/portfolio/holdings-overlap.d.ts +8 -0
  194. package/dist/tools/portfolio/holdings-overlap.js +105 -0
  195. package/dist/tools/portfolio/holdings-overlap.js.map +1 -0
  196. package/dist/tools/portfolio/predictions.d.ts +1 -1
  197. package/dist/tools/portfolio/predictions.js +1 -1
  198. package/dist/tools/portfolio/predictions.js.map +1 -1
  199. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  200. package/dist/tools/portfolio/risk-analysis.js +1 -1
  201. package/dist/tools/portfolio/risk-analysis.js.map +1 -1
  202. package/dist/tools/portfolio/tracker.d.ts +1 -1
  203. package/dist/tools/portfolio/tracker.js +1 -1
  204. package/dist/tools/portfolio/tracker.js.map +1 -1
  205. package/dist/tools/portfolio/watchlist.d.ts +1 -1
  206. package/dist/tools/portfolio/watchlist.js +12 -4
  207. package/dist/tools/portfolio/watchlist.js.map +1 -1
  208. package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
  209. package/dist/tools/sentiment/reddit-sentiment.js +1 -1
  210. package/dist/tools/sentiment/reddit-sentiment.js.map +1 -1
  211. package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
  212. package/dist/tools/sentiment/sentiment-summary.js +57 -2
  213. package/dist/tools/sentiment/sentiment-summary.js.map +1 -1
  214. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  215. package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
  216. package/dist/tools/sentiment/twitter-sentiment.js +1 -1
  217. package/dist/tools/sentiment/twitter-sentiment.js.map +1 -1
  218. package/dist/tools/sentiment/web-search.d.ts +1 -1
  219. package/dist/tools/sentiment/web-search.js +32 -3
  220. package/dist/tools/sentiment/web-search.js.map +1 -1
  221. package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
  222. package/dist/tools/sentiment/web-sentiment.js +1 -1
  223. package/dist/tools/sentiment/web-sentiment.js.map +1 -1
  224. package/dist/tools/technical/backtest.d.ts +3 -3
  225. package/dist/tools/technical/backtest.js +41 -27
  226. package/dist/tools/technical/backtest.js.map +1 -1
  227. package/dist/tools/technical/indicators.d.ts +1 -1
  228. package/dist/tools/technical/indicators.js +8 -4
  229. package/dist/tools/technical/indicators.js.map +1 -1
  230. package/dist/types/options.d.ts +10 -0
  231. package/dist/types/portfolio.d.ts +27 -0
  232. package/dist/workflows/compare-assets.js +38 -2
  233. package/dist/workflows/compare-assets.js.map +1 -1
  234. package/dist/workflows/options-screener.js +94 -8
  235. package/dist/workflows/options-screener.js.map +1 -1
  236. package/dist/workflows/portfolio-builder.js +9 -5
  237. package/dist/workflows/portfolio-builder.js.map +1 -1
  238. package/gui/server/ask-user-bridge.ts +82 -0
  239. package/gui/server/background-quotes.ts +31 -0
  240. package/gui/server/chat-event-adapter.ts +142 -0
  241. package/gui/server/gui-session-manager.ts +5 -0
  242. package/gui/server/invoke-tool.ts +89 -0
  243. package/gui/server/live-chat-event-adapter.ts +181 -0
  244. package/gui/server/model-setup.ts +100 -0
  245. package/gui/server/package.json +5 -0
  246. package/gui/server/projector.ts +254 -0
  247. package/gui/server/prompt-observation.ts +61 -0
  248. package/gui/server/server.ts +703 -0
  249. package/gui/server/session-actions.ts +31 -0
  250. package/gui/server/session-entry-wait.ts +81 -0
  251. package/gui/server/tool-metadata.ts +88 -0
  252. package/gui/server/websocket.ts +128 -0
  253. package/gui/server/writer-lock.ts +118 -0
  254. package/gui/shared/chat-events.ts +118 -0
  255. package/gui/shared/event-reducer.ts +186 -0
  256. package/gui/web/dist/assets/CatalogOverlay-Bmp6Knu7.js +1 -0
  257. package/gui/web/dist/assets/index-Bxt9QpLX.css +1 -0
  258. package/gui/web/dist/assets/index-CZ9DHZYy.js +67 -0
  259. package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
  260. package/gui/web/dist/index.html +17 -0
  261. package/package.json +50 -18
  262. package/src/analysts/contracts.ts +189 -0
  263. package/src/analysts/orchestrator.ts +300 -0
  264. package/src/cli.ts +206 -0
  265. package/src/config.ts +245 -0
  266. package/src/index.ts +5 -0
  267. package/src/infra/browser.ts +111 -0
  268. package/src/infra/cache.ts +103 -0
  269. package/src/infra/http-client.ts +68 -0
  270. package/src/infra/index.ts +18 -0
  271. package/src/infra/native-dependencies.ts +12 -0
  272. package/src/infra/node-version.ts +24 -0
  273. package/src/infra/open-url.ts +28 -0
  274. package/src/infra/opencandle-paths.ts +64 -0
  275. package/src/infra/rate-limiter.ts +73 -0
  276. package/src/memory/index.ts +10 -0
  277. package/src/memory/manager.ts +192 -0
  278. package/src/memory/preference-extractor.ts +106 -0
  279. package/src/memory/retrieval.ts +70 -0
  280. package/src/memory/sqlite.ts +172 -0
  281. package/src/memory/storage.ts +205 -0
  282. package/src/memory/tool-defaults.ts +87 -0
  283. package/src/memory/types.ts +71 -0
  284. package/src/onboarding/connect.ts +184 -0
  285. package/src/onboarding/credential-interceptor.ts +134 -0
  286. package/src/onboarding/degradation-accumulator.ts +79 -0
  287. package/src/onboarding/prompt-user.ts +85 -0
  288. package/src/onboarding/providers.ts +315 -0
  289. package/src/onboarding/state.ts +218 -0
  290. package/src/onboarding/tool-helpers.ts +111 -0
  291. package/src/onboarding/tool-tags.ts +201 -0
  292. package/src/onboarding/validation.ts +158 -0
  293. package/src/pi/opencandle-extension.ts +955 -0
  294. package/src/pi/session-storage.ts +5 -0
  295. package/src/pi/session.ts +81 -0
  296. package/src/pi/setup.ts +381 -0
  297. package/src/pi/tool-adapter.ts +36 -0
  298. package/src/prompts/context-builder.ts +315 -0
  299. package/src/prompts/disclaimer.ts +9 -0
  300. package/src/prompts/policy-cards.ts +220 -0
  301. package/src/prompts/sections.ts +46 -0
  302. package/src/prompts/workflow-prompts.ts +433 -0
  303. package/src/providers/alpha-vantage.ts +315 -0
  304. package/src/providers/coingecko.ts +96 -0
  305. package/src/providers/exa-search.ts +373 -0
  306. package/src/providers/fear-greed.ts +45 -0
  307. package/src/providers/finnhub.ts +124 -0
  308. package/src/providers/fred.ts +83 -0
  309. package/src/providers/index.ts +9 -0
  310. package/src/providers/provider-credential-error.ts +23 -0
  311. package/src/providers/reddit.ts +151 -0
  312. package/src/providers/sec-edgar.ts +312 -0
  313. package/src/providers/twitter.ts +173 -0
  314. package/src/providers/web-search.ts +293 -0
  315. package/src/providers/with-fallback.ts +41 -0
  316. package/src/providers/wrap-provider.ts +64 -0
  317. package/src/providers/yahoo-finance.ts +534 -0
  318. package/src/routing/classify-intent.ts +285 -0
  319. package/src/routing/defaults.ts +29 -0
  320. package/src/routing/entity-extractor.ts +291 -0
  321. package/src/routing/index.ts +70 -0
  322. package/src/routing/legacy-rule-router.ts +13 -0
  323. package/src/routing/planning.ts +732 -0
  324. package/src/routing/route-manifest.ts +287 -0
  325. package/src/routing/router-llm-client.ts +51 -0
  326. package/src/routing/router-prompt.ts +163 -0
  327. package/src/routing/router-types.ts +87 -0
  328. package/src/routing/router.ts +712 -0
  329. package/src/routing/slot-resolver.ts +190 -0
  330. package/src/routing/turn-context.ts +111 -0
  331. package/src/routing/types.ts +75 -0
  332. package/src/runtime/answer-contracts.ts +633 -0
  333. package/src/runtime/artifact-contracts.ts +76 -0
  334. package/src/runtime/evidence.ts +77 -0
  335. package/src/runtime/planning-evidence.ts +591 -0
  336. package/src/runtime/prompt-step.ts +75 -0
  337. package/src/runtime/provider-tracker.ts +40 -0
  338. package/src/runtime/run-context.ts +22 -0
  339. package/src/runtime/session-coordinator.ts +472 -0
  340. package/src/runtime/tool-defaults-wrapper.ts +35 -0
  341. package/src/runtime/validation.ts +214 -0
  342. package/src/runtime/workflow-events.ts +75 -0
  343. package/src/runtime/workflow-runner.ts +188 -0
  344. package/src/runtime/workflow-types.ts +102 -0
  345. package/src/sentiment/adapters/finnhub.ts +44 -0
  346. package/src/sentiment/adapters/reddit.ts +65 -0
  347. package/src/sentiment/adapters/twitter.ts +36 -0
  348. package/src/sentiment/adapters/web.ts +44 -0
  349. package/src/sentiment/index.ts +58 -0
  350. package/src/sentiment/keywords.ts +9 -0
  351. package/src/sentiment/pipeline.ts +68 -0
  352. package/src/sentiment/scorer.ts +78 -0
  353. package/src/sentiment/store.ts +260 -0
  354. package/src/sentiment/trends.ts +90 -0
  355. package/src/sentiment/types.ts +108 -0
  356. package/src/system-prompt.ts +118 -0
  357. package/src/tool-kit.ts +68 -0
  358. package/src/tools/AGENTS.md +36 -0
  359. package/src/tools/fundamentals/company-overview.ts +54 -0
  360. package/src/tools/fundamentals/comps.ts +156 -0
  361. package/src/tools/fundamentals/dcf.ts +267 -0
  362. package/src/tools/fundamentals/earnings.ts +47 -0
  363. package/src/tools/fundamentals/financials.ts +54 -0
  364. package/src/tools/fundamentals/sec-filings.ts +84 -0
  365. package/src/tools/index.ts +91 -0
  366. package/src/tools/interaction/ask-user.ts +81 -0
  367. package/src/tools/interaction/twitter-login.ts +93 -0
  368. package/src/tools/macro/fear-greed.ts +41 -0
  369. package/src/tools/macro/fred-data.ts +80 -0
  370. package/src/tools/market/crypto-history.ts +67 -0
  371. package/src/tools/market/crypto-price.ts +53 -0
  372. package/src/tools/market/search-ticker.ts +53 -0
  373. package/src/tools/market/stock-history.ts +79 -0
  374. package/src/tools/market/stock-quote.ts +64 -0
  375. package/src/tools/options/greeks.ts +81 -0
  376. package/src/tools/options/option-chain.ts +96 -0
  377. package/src/tools/portfolio/correlation.ts +162 -0
  378. package/src/tools/portfolio/holdings-overlap.ts +123 -0
  379. package/src/tools/portfolio/predictions.ts +253 -0
  380. package/src/tools/portfolio/risk-analysis.ts +134 -0
  381. package/src/tools/portfolio/tracker.ts +147 -0
  382. package/src/tools/portfolio/watchlist.ts +159 -0
  383. package/src/tools/sentiment/reddit-sentiment.ts +164 -0
  384. package/src/tools/sentiment/sentiment-summary.ts +316 -0
  385. package/src/tools/sentiment/sentiment-trend.ts +58 -0
  386. package/src/tools/sentiment/twitter-sentiment.ts +96 -0
  387. package/src/tools/sentiment/web-search.ts +183 -0
  388. package/src/tools/sentiment/web-sentiment.ts +76 -0
  389. package/src/tools/technical/backtest.ts +267 -0
  390. package/src/tools/technical/indicators.ts +256 -0
  391. package/src/types/fundamentals.ts +46 -0
  392. package/src/types/index.ts +20 -0
  393. package/src/types/macro.ts +27 -0
  394. package/src/types/market.ts +43 -0
  395. package/src/types/options.ts +52 -0
  396. package/src/types/portfolio.ts +73 -0
  397. package/src/types/sentiment.ts +70 -0
  398. package/src/workflows/compare-assets.ts +75 -0
  399. package/src/workflows/index.ts +4 -0
  400. package/src/workflows/options-screener.ts +127 -0
  401. package/src/workflows/portfolio-builder.ts +56 -0
  402. package/src/workflows/types.ts +4 -0
  403. package/dist/runtime/index.d.ts +0 -16
  404. package/dist/runtime/index.js +0 -10
  405. package/dist/runtime/index.js.map +0 -1
  406. package/dist/runtime/provider-ids.d.ts +0 -14
  407. package/dist/runtime/provider-ids.js +0 -14
  408. package/dist/runtime/provider-ids.js.map +0 -1
package/src/config.ts ADDED
@@ -0,0 +1,245 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { ensureParentDir, getConfigPath } from "./infra/opencandle-paths.js";
3
+ import type { PlanningBehaviorMode, TaskFamily } from "./routing/planning.js";
4
+
5
+ export interface SentimentConfig {
6
+ retentionDays: number;
7
+ defaultSubreddits: string[];
8
+ commentsPerPost: number;
9
+ divergenceThreshold: number;
10
+ }
11
+
12
+ export type RouterMode = "rules" | "llm";
13
+ export type ToolScopeMode = "observe" | "enforce";
14
+ export type PlanningMigrationStatuses = Partial<Record<TaskFamily, PlanningBehaviorMode>>;
15
+
16
+ export interface Config {
17
+ alphaVantageApiKey?: string;
18
+ fredApiKey?: string;
19
+ braveApiKey?: string;
20
+ exaApiKey?: string;
21
+ finnhubApiKey?: string;
22
+ /** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
23
+ debate?: boolean;
24
+ /**
25
+ * Intent-router mode. `"llm"` (default) runs the LLM router ahead of prompt
26
+ * assembly. `"rules"` is the explicit legacy rule-router rollback path
27
+ * (`classifyIntent` + `extractPreferences`). Controlled by
28
+ * `OPENCANDLE_ROUTER_MODE`.
29
+ */
30
+ routerMode: RouterMode;
31
+ /**
32
+ * Route-selected tool scope mode. `"observe"` (default) records selected
33
+ * bundles and active-tool candidates. `"enforce"` applies Pi active tools
34
+ * for the turn via `pi.setActiveTools`.
35
+ */
36
+ toolScopeMode: ToolScopeMode;
37
+ /**
38
+ * Per-task planning behavior rollback/activation overrides. Controlled by
39
+ * `OPENCANDLE_PLANNING_MIGRATION_STATUSES`, e.g.
40
+ * `asset_compare=dual_run,single_asset_decision=observe_only`.
41
+ */
42
+ planningMigrationStatuses?: PlanningMigrationStatuses;
43
+ sentiment?: SentimentConfig;
44
+ }
45
+
46
+ export interface OpenCandleFileConfig {
47
+ providers?: {
48
+ alphaVantage?: {
49
+ apiKey?: string;
50
+ };
51
+ fred?: {
52
+ apiKey?: string;
53
+ };
54
+ brave?: {
55
+ apiKey?: string;
56
+ };
57
+ exa?: {
58
+ apiKey?: string;
59
+ };
60
+ finnhub?: {
61
+ apiKey?: string;
62
+ };
63
+ };
64
+ /** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
65
+ debate?: boolean;
66
+ sentiment?: {
67
+ retentionDays?: number;
68
+ defaultSubreddits?: string[];
69
+ commentsPerPost?: number;
70
+ divergenceThreshold?: number;
71
+ };
72
+ }
73
+
74
+ export function loadEnv(path = ".env"): void {
75
+ let content: string;
76
+ try {
77
+ content = readFileSync(path, "utf-8");
78
+ } catch {
79
+ return;
80
+ }
81
+ for (const line of content.split("\n")) {
82
+ const trimmed = line.trim();
83
+ if (!trimmed || trimmed.startsWith("#")) continue;
84
+ const eqIndex = trimmed.indexOf("=");
85
+ if (eqIndex === -1) continue;
86
+ const key = trimmed.slice(0, eqIndex).trim();
87
+ const value = trimmed.slice(eqIndex + 1).trim();
88
+ if (key && value) {
89
+ process.env[key] = value;
90
+ }
91
+ }
92
+ }
93
+
94
+ let cachedConfig: Config | null = null;
95
+
96
+ const SENTIMENT_DEFAULTS: SentimentConfig = {
97
+ retentionDays: 30,
98
+ defaultSubreddits: ["wallstreetbets", "stocks", "investing", "options"],
99
+ commentsPerPost: 5,
100
+ divergenceThreshold: 0.4,
101
+ };
102
+
103
+ const PLANNING_TASK_FAMILIES = [
104
+ "single_asset_decision",
105
+ "asset_compare",
106
+ "portfolio_build",
107
+ "portfolio_review",
108
+ "macro_allocation_review",
109
+ "options_strategy",
110
+ "current_event_explanation",
111
+ "ticker_disambiguation",
112
+ "filing_thesis_review",
113
+ "sentiment_snapshot",
114
+ "concept_explainer",
115
+ "retail_finance_tradeoff",
116
+ "stateful_tracking_update",
117
+ "backtest_review",
118
+ "general_fallback",
119
+ ] as const satisfies readonly TaskFamily[];
120
+
121
+ const PLANNING_BEHAVIOR_MODES = [
122
+ "observe_only",
123
+ "dual_run",
124
+ "replacement_active",
125
+ ] as const satisfies readonly PlanningBehaviorMode[];
126
+
127
+ function resolveRouterMode(): RouterMode {
128
+ const raw = process.env.OPENCANDLE_ROUTER_MODE;
129
+ if (raw === undefined || raw === "") return "llm";
130
+ if (raw === "rules" || raw === "llm") return raw;
131
+ throw new Error(
132
+ `Invalid OPENCANDLE_ROUTER_MODE="${raw}". Allowed values: "llm" (default) or "rules".`,
133
+ );
134
+ }
135
+
136
+ function resolveToolScopeMode(): ToolScopeMode {
137
+ const raw = process.env.OPENCANDLE_TOOL_SCOPE_MODE;
138
+ if (raw === undefined || raw === "") return "observe";
139
+ if (raw === "observe" || raw === "enforce") return raw;
140
+ throw new Error(
141
+ `Invalid OPENCANDLE_TOOL_SCOPE_MODE="${raw}". Allowed values: "observe" (default) or "enforce".`,
142
+ );
143
+ }
144
+
145
+ function resolvePlanningMigrationStatuses(): PlanningMigrationStatuses | undefined {
146
+ const raw = process.env.OPENCANDLE_PLANNING_MIGRATION_STATUSES;
147
+ if (raw === undefined || raw.trim() === "") return undefined;
148
+
149
+ const statuses: PlanningMigrationStatuses = {};
150
+ for (const entry of raw.split(",")) {
151
+ const trimmed = entry.trim();
152
+ if (!trimmed) continue;
153
+
154
+ const parts = trimmed.split("=");
155
+ const taskFamily = parts[0]?.trim();
156
+ const behaviorMode = parts[1]?.trim();
157
+ if (
158
+ parts.length !== 2 ||
159
+ !isPlanningTaskFamily(taskFamily) ||
160
+ !isPlanningBehaviorMode(behaviorMode)
161
+ ) {
162
+ throw new Error(`Invalid OPENCANDLE_PLANNING_MIGRATION_STATUSES entry "${trimmed}".`);
163
+ }
164
+ statuses[taskFamily] = behaviorMode;
165
+ }
166
+
167
+ return Object.keys(statuses).length > 0 ? statuses : undefined;
168
+ }
169
+
170
+ function isPlanningTaskFamily(value: string | undefined): value is TaskFamily {
171
+ return PLANNING_TASK_FAMILIES.includes(value as TaskFamily);
172
+ }
173
+
174
+ function isPlanningBehaviorMode(value: string | undefined): value is PlanningBehaviorMode {
175
+ return PLANNING_BEHAVIOR_MODES.includes(value as PlanningBehaviorMode);
176
+ }
177
+
178
+ function resolveConfig(fileConfig: OpenCandleFileConfig): Config {
179
+ const debateEnv = process.env.OPENCANDLE_DEBATE;
180
+ const fileSentiment = fileConfig.sentiment;
181
+ return {
182
+ alphaVantageApiKey:
183
+ process.env.ALPHA_VANTAGE_API_KEY ?? fileConfig.providers?.alphaVantage?.apiKey,
184
+ fredApiKey: process.env.FRED_API_KEY ?? fileConfig.providers?.fred?.apiKey,
185
+ braveApiKey: process.env.BRAVE_API_KEY ?? fileConfig.providers?.brave?.apiKey,
186
+ exaApiKey: process.env.EXA_API_KEY ?? fileConfig.providers?.exa?.apiKey,
187
+ finnhubApiKey: process.env.FINNHUB_API_KEY ?? fileConfig.providers?.finnhub?.apiKey,
188
+ debate: debateEnv !== undefined ? debateEnv !== "false" && debateEnv !== "0" : fileConfig.debate ?? true,
189
+ routerMode: resolveRouterMode(),
190
+ toolScopeMode: resolveToolScopeMode(),
191
+ planningMigrationStatuses: resolvePlanningMigrationStatuses(),
192
+ sentiment: {
193
+ retentionDays: fileSentiment?.retentionDays ?? SENTIMENT_DEFAULTS.retentionDays,
194
+ defaultSubreddits: fileSentiment?.defaultSubreddits ?? SENTIMENT_DEFAULTS.defaultSubreddits,
195
+ commentsPerPost: fileSentiment?.commentsPerPost ?? SENTIMENT_DEFAULTS.commentsPerPost,
196
+ divergenceThreshold: fileSentiment?.divergenceThreshold ?? SENTIMENT_DEFAULTS.divergenceThreshold,
197
+ },
198
+ };
199
+ }
200
+
201
+ export function loadFileConfig(path = getConfigPath()): OpenCandleFileConfig {
202
+ if (!existsSync(path)) {
203
+ return {};
204
+ }
205
+
206
+ let content: string;
207
+ try {
208
+ content = readFileSync(path, "utf-8");
209
+ } catch (error) {
210
+ const message = error instanceof Error ? error.message : String(error);
211
+ throw new Error(`Unable to read OpenCandle config at ${path}: ${message}`);
212
+ }
213
+
214
+ try {
215
+ const parsed = JSON.parse(content) as OpenCandleFileConfig;
216
+ return parsed && typeof parsed === "object" ? parsed : {};
217
+ } catch (error) {
218
+ const message = error instanceof Error ? error.message : String(error);
219
+ throw new Error(`Invalid OpenCandle config at ${path}: ${message}`);
220
+ }
221
+ }
222
+
223
+ export function saveFileConfig(config: OpenCandleFileConfig, path = getConfigPath()): void {
224
+ ensureParentDir(path);
225
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
226
+ }
227
+
228
+ export function loadConfig(): Config {
229
+ loadEnv();
230
+ cachedConfig = resolveConfig(loadFileConfig());
231
+
232
+ return cachedConfig;
233
+ }
234
+
235
+ export function getConfig(): Config {
236
+ if (!cachedConfig) {
237
+ return loadConfig();
238
+ }
239
+ return cachedConfig;
240
+ }
241
+
242
+ /** Test-only: clear the memoized config so the next `getConfig()` re-reads env. */
243
+ export function resetConfigCache(): void {
244
+ cachedConfig = null;
245
+ }
package/src/index.ts ADDED
@@ -0,0 +1,5 @@
1
+ export { createOpenCandleSession, type CreateOpenCandleSessionOptions } from "./pi/session.js";
2
+ export { default as openCandleExtension } from "./pi/opencandle-extension.js";
3
+ export { agentToolToPiTool, getOpenCandleToolDefinitions } from "./pi/tool-adapter.js";
4
+ export { registerTools } from "./tool-kit.js";
5
+ export { getAllTools } from "./tools/index.js";
@@ -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): 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
+ }