opencandle 0.3.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 (283) hide show
  1. package/assets/logo.svg +187 -0
  2. package/dist/cli.d.ts +1 -1
  3. package/dist/cli.js +38 -2
  4. package/dist/cli.js.map +1 -1
  5. package/dist/config.d.ts +9 -0
  6. package/dist/config.js +13 -0
  7. package/dist/config.js.map +1 -1
  8. package/dist/infra/browser.d.ts +10 -0
  9. package/dist/infra/browser.js +1 -0
  10. package/dist/infra/browser.js.map +1 -1
  11. package/dist/infra/native-dependencies.d.ts +1 -0
  12. package/dist/infra/native-dependencies.js +10 -0
  13. package/dist/infra/native-dependencies.js.map +1 -0
  14. package/dist/infra/node-version.d.ts +2 -0
  15. package/dist/infra/node-version.js +23 -0
  16. package/dist/infra/node-version.js.map +1 -0
  17. package/dist/memory/index.d.ts +2 -0
  18. package/dist/memory/index.js +1 -0
  19. package/dist/memory/index.js.map +1 -1
  20. package/dist/memory/sqlite.js +42 -4
  21. package/dist/memory/sqlite.js.map +1 -1
  22. package/dist/memory/storage.d.ts +6 -0
  23. package/dist/memory/storage.js +3 -3
  24. package/dist/memory/storage.js.map +1 -1
  25. package/dist/memory/tool-defaults.d.ts +8 -0
  26. package/dist/memory/tool-defaults.js +59 -0
  27. package/dist/memory/tool-defaults.js.map +1 -0
  28. package/dist/onboarding/connect.d.ts +13 -1
  29. package/dist/onboarding/connect.js +21 -10
  30. package/dist/onboarding/connect.js.map +1 -1
  31. package/dist/onboarding/prompt-user.d.ts +1 -1
  32. package/dist/onboarding/providers.d.ts +7 -0
  33. package/dist/onboarding/providers.js +6 -3
  34. package/dist/onboarding/providers.js.map +1 -1
  35. package/dist/onboarding/tool-helpers.d.ts +1 -1
  36. package/dist/pi/opencandle-extension.d.ts +7 -1
  37. package/dist/pi/opencandle-extension.js +186 -10
  38. package/dist/pi/opencandle-extension.js.map +1 -1
  39. package/dist/pi/session-storage.d.ts +2 -0
  40. package/dist/pi/session-storage.js +5 -0
  41. package/dist/pi/session-storage.js.map +1 -0
  42. package/dist/pi/session.d.ts +4 -1
  43. package/dist/pi/session.js +25 -3
  44. package/dist/pi/session.js.map +1 -1
  45. package/dist/pi/setup.d.ts +1 -1
  46. package/dist/pi/setup.js +1 -1
  47. package/dist/pi/setup.js.map +1 -1
  48. package/dist/pi/tool-adapter.d.ts +2 -2
  49. package/dist/pi/tool-adapter.js +14 -1
  50. package/dist/pi/tool-adapter.js.map +1 -1
  51. package/dist/prompts/context-builder.d.ts +22 -0
  52. package/dist/prompts/context-builder.js +45 -10
  53. package/dist/prompts/context-builder.js.map +1 -1
  54. package/dist/prompts/disclaimer.d.ts +6 -0
  55. package/dist/prompts/disclaimer.js +9 -0
  56. package/dist/prompts/disclaimer.js.map +1 -0
  57. package/dist/prompts/workflow-prompts.d.ts +8 -0
  58. package/dist/prompts/workflow-prompts.js +39 -5
  59. package/dist/prompts/workflow-prompts.js.map +1 -1
  60. package/dist/providers/yahoo-finance.js +70 -33
  61. package/dist/providers/yahoo-finance.js.map +1 -1
  62. package/dist/routing/defaults.js +1 -1
  63. package/dist/routing/defaults.js.map +1 -1
  64. package/dist/routing/index.d.ts +4 -0
  65. package/dist/routing/index.js +3 -0
  66. package/dist/routing/index.js.map +1 -1
  67. package/dist/routing/router-llm-client.d.ts +11 -0
  68. package/dist/routing/router-llm-client.js +42 -0
  69. package/dist/routing/router-llm-client.js.map +1 -0
  70. package/dist/routing/router-prompt.d.ts +2 -0
  71. package/dist/routing/router-prompt.js +138 -0
  72. package/dist/routing/router-prompt.js.map +1 -0
  73. package/dist/routing/router-types.d.ts +62 -0
  74. package/dist/routing/router-types.js +2 -0
  75. package/dist/routing/router-types.js.map +1 -0
  76. package/dist/routing/router.d.ts +10 -0
  77. package/dist/routing/router.js +194 -0
  78. package/dist/routing/router.js.map +1 -0
  79. package/dist/runtime/session-coordinator.d.ts +63 -3
  80. package/dist/runtime/session-coordinator.js +155 -4
  81. package/dist/runtime/session-coordinator.js.map +1 -1
  82. package/dist/runtime/tool-defaults-wrapper.d.ts +3 -0
  83. package/dist/runtime/tool-defaults-wrapper.js +25 -0
  84. package/dist/runtime/tool-defaults-wrapper.js.map +1 -0
  85. package/dist/sentiment/store.js +5 -0
  86. package/dist/sentiment/store.js.map +1 -1
  87. package/dist/system-prompt.js +20 -12
  88. package/dist/system-prompt.js.map +1 -1
  89. package/dist/tool-kit.d.ts +4 -4
  90. package/dist/tools/fundamentals/company-overview.d.ts +1 -1
  91. package/dist/tools/fundamentals/comps.d.ts +1 -1
  92. package/dist/tools/fundamentals/dcf.d.ts +1 -1
  93. package/dist/tools/fundamentals/earnings.d.ts +1 -1
  94. package/dist/tools/fundamentals/financials.d.ts +1 -1
  95. package/dist/tools/fundamentals/sec-filings.d.ts +1 -1
  96. package/dist/tools/index.d.ts +28 -1
  97. package/dist/tools/index.js +27 -0
  98. package/dist/tools/index.js.map +1 -1
  99. package/dist/tools/interaction/ask-user.d.ts +1 -1
  100. package/dist/tools/interaction/twitter-login.d.ts +1 -1
  101. package/dist/tools/macro/fear-greed.d.ts +1 -1
  102. package/dist/tools/macro/fred-data.d.ts +1 -1
  103. package/dist/tools/market/crypto-history.d.ts +1 -1
  104. package/dist/tools/market/crypto-price.d.ts +1 -1
  105. package/dist/tools/market/search-ticker.d.ts +1 -1
  106. package/dist/tools/market/stock-history.d.ts +1 -1
  107. package/dist/tools/market/stock-quote.d.ts +1 -1
  108. package/dist/tools/options/option-chain.d.ts +1 -1
  109. package/dist/tools/options/option-chain.js +4 -1
  110. package/dist/tools/options/option-chain.js.map +1 -1
  111. package/dist/tools/portfolio/correlation.d.ts +1 -1
  112. package/dist/tools/portfolio/predictions.d.ts +1 -1
  113. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  114. package/dist/tools/portfolio/tracker.d.ts +1 -1
  115. package/dist/tools/portfolio/watchlist.d.ts +1 -1
  116. package/dist/tools/sentiment/reddit-sentiment.d.ts +1 -1
  117. package/dist/tools/sentiment/sentiment-summary.d.ts +1 -1
  118. package/dist/tools/sentiment/sentiment-trend.d.ts +1 -1
  119. package/dist/tools/sentiment/twitter-sentiment.d.ts +1 -1
  120. package/dist/tools/sentiment/web-search.d.ts +1 -1
  121. package/dist/tools/sentiment/web-sentiment.d.ts +1 -1
  122. package/dist/tools/technical/backtest.d.ts +1 -1
  123. package/dist/tools/technical/indicators.d.ts +1 -1
  124. package/dist/tools/technical/indicators.js +7 -1
  125. package/dist/tools/technical/indicators.js.map +1 -1
  126. package/dist/workflows/options-screener.js +7 -2
  127. package/dist/workflows/options-screener.js.map +1 -1
  128. package/dist/workflows/portfolio-builder.js +3 -3
  129. package/dist/workflows/portfolio-builder.js.map +1 -1
  130. package/gui/server/background-quotes.ts +31 -0
  131. package/gui/server/chat-event-adapter.ts +142 -0
  132. package/gui/server/invoke-tool.ts +89 -0
  133. package/gui/server/live-chat-event-adapter.ts +181 -0
  134. package/gui/server/model-setup.ts +100 -0
  135. package/gui/server/package.json +5 -0
  136. package/gui/server/projector.ts +212 -0
  137. package/gui/server/server.ts +592 -0
  138. package/gui/server/session-actions.ts +31 -0
  139. package/gui/server/tool-metadata.ts +88 -0
  140. package/gui/server/websocket.ts +128 -0
  141. package/gui/server/writer-lock.ts +118 -0
  142. package/gui/shared/chat-events.ts +118 -0
  143. package/gui/shared/event-reducer.ts +186 -0
  144. package/gui/web/dist/assets/CatalogOverlay-D1ImSJTe.js +1 -0
  145. package/gui/web/dist/assets/index-DBrWq43L.css +1 -0
  146. package/gui/web/dist/assets/index-RflHaj0y.js +67 -0
  147. package/gui/web/dist/assets/logo-CWpt6Y2a.svg +187 -0
  148. package/gui/web/dist/index.html +17 -0
  149. package/package.json +44 -18
  150. package/src/analysts/contracts.ts +189 -0
  151. package/src/analysts/orchestrator.ts +300 -0
  152. package/src/cli.ts +205 -0
  153. package/src/config.ts +161 -0
  154. package/src/index.ts +5 -0
  155. package/src/infra/browser.ts +111 -0
  156. package/src/infra/cache.ts +103 -0
  157. package/src/infra/http-client.ts +68 -0
  158. package/src/infra/index.ts +18 -0
  159. package/src/infra/native-dependencies.ts +12 -0
  160. package/src/infra/node-version.ts +24 -0
  161. package/src/infra/open-url.ts +28 -0
  162. package/src/infra/opencandle-paths.ts +64 -0
  163. package/src/infra/rate-limiter.ts +64 -0
  164. package/src/memory/index.ts +10 -0
  165. package/src/memory/manager.ts +159 -0
  166. package/src/memory/preference-extractor.ts +106 -0
  167. package/src/memory/retrieval.ts +70 -0
  168. package/src/memory/sqlite.ts +172 -0
  169. package/src/memory/storage.ts +204 -0
  170. package/src/memory/tool-defaults.ts +87 -0
  171. package/src/memory/types.ts +67 -0
  172. package/src/onboarding/connect.ts +184 -0
  173. package/src/onboarding/credential-interceptor.ts +134 -0
  174. package/src/onboarding/degradation-accumulator.ts +79 -0
  175. package/src/onboarding/prompt-user.ts +85 -0
  176. package/src/onboarding/providers.ts +315 -0
  177. package/src/onboarding/state.ts +218 -0
  178. package/src/onboarding/tool-helpers.ts +111 -0
  179. package/src/onboarding/tool-tags.ts +201 -0
  180. package/src/onboarding/validation.ts +158 -0
  181. package/src/pi/opencandle-extension.ts +724 -0
  182. package/src/pi/session-storage.ts +5 -0
  183. package/src/pi/session.ts +81 -0
  184. package/src/pi/setup.ts +371 -0
  185. package/src/pi/tool-adapter.ts +36 -0
  186. package/src/prompts/context-builder.ts +204 -0
  187. package/src/prompts/disclaimer.ts +9 -0
  188. package/src/prompts/sections.ts +46 -0
  189. package/src/prompts/workflow-prompts.ts +279 -0
  190. package/src/providers/alpha-vantage.ts +292 -0
  191. package/src/providers/coingecko.ts +96 -0
  192. package/src/providers/exa-search.ts +373 -0
  193. package/src/providers/fear-greed.ts +45 -0
  194. package/src/providers/finnhub.ts +124 -0
  195. package/src/providers/fred.ts +83 -0
  196. package/src/providers/index.ts +9 -0
  197. package/src/providers/provider-credential-error.ts +23 -0
  198. package/src/providers/reddit.ts +151 -0
  199. package/src/providers/sec-edgar.ts +96 -0
  200. package/src/providers/twitter.ts +173 -0
  201. package/src/providers/web-search.ts +293 -0
  202. package/src/providers/with-fallback.ts +41 -0
  203. package/src/providers/wrap-provider.ts +64 -0
  204. package/src/providers/yahoo-finance.ts +367 -0
  205. package/src/routing/classify-intent.ts +194 -0
  206. package/src/routing/defaults.ts +29 -0
  207. package/src/routing/entity-extractor.ts +140 -0
  208. package/src/routing/index.ts +26 -0
  209. package/src/routing/router-llm-client.ts +51 -0
  210. package/src/routing/router-prompt.ts +159 -0
  211. package/src/routing/router-types.ts +66 -0
  212. package/src/routing/router.ts +213 -0
  213. package/src/routing/slot-resolver.ts +152 -0
  214. package/src/routing/types.ts +63 -0
  215. package/src/runtime/evidence.ts +77 -0
  216. package/src/runtime/index.ts +55 -0
  217. package/src/runtime/prompt-step.ts +75 -0
  218. package/src/runtime/provider-ids.ts +15 -0
  219. package/src/runtime/provider-tracker.ts +40 -0
  220. package/src/runtime/run-context.ts +22 -0
  221. package/src/runtime/session-coordinator.ts +406 -0
  222. package/src/runtime/tool-defaults-wrapper.ts +35 -0
  223. package/src/runtime/validation.ts +214 -0
  224. package/src/runtime/workflow-events.ts +75 -0
  225. package/src/runtime/workflow-runner.ts +188 -0
  226. package/src/runtime/workflow-types.ts +102 -0
  227. package/src/sentiment/adapters/finnhub.ts +44 -0
  228. package/src/sentiment/adapters/reddit.ts +65 -0
  229. package/src/sentiment/adapters/twitter.ts +36 -0
  230. package/src/sentiment/adapters/web.ts +44 -0
  231. package/src/sentiment/index.ts +58 -0
  232. package/src/sentiment/keywords.ts +9 -0
  233. package/src/sentiment/pipeline.ts +68 -0
  234. package/src/sentiment/scorer.ts +78 -0
  235. package/src/sentiment/store.ts +260 -0
  236. package/src/sentiment/trends.ts +90 -0
  237. package/src/sentiment/types.ts +108 -0
  238. package/src/system-prompt.ts +115 -0
  239. package/src/tool-kit.ts +68 -0
  240. package/src/tools/AGENTS.md +36 -0
  241. package/src/tools/fundamentals/company-overview.ts +54 -0
  242. package/src/tools/fundamentals/comps.ts +156 -0
  243. package/src/tools/fundamentals/dcf.ts +267 -0
  244. package/src/tools/fundamentals/earnings.ts +47 -0
  245. package/src/tools/fundamentals/financials.ts +54 -0
  246. package/src/tools/fundamentals/sec-filings.ts +61 -0
  247. package/src/tools/index.ts +88 -0
  248. package/src/tools/interaction/ask-user.ts +81 -0
  249. package/src/tools/interaction/twitter-login.ts +93 -0
  250. package/src/tools/macro/fear-greed.ts +41 -0
  251. package/src/tools/macro/fred-data.ts +54 -0
  252. package/src/tools/market/crypto-history.ts +51 -0
  253. package/src/tools/market/crypto-price.ts +53 -0
  254. package/src/tools/market/search-ticker.ts +53 -0
  255. package/src/tools/market/stock-history.ts +79 -0
  256. package/src/tools/market/stock-quote.ts +64 -0
  257. package/src/tools/options/greeks.ts +82 -0
  258. package/src/tools/options/option-chain.ts +91 -0
  259. package/src/tools/portfolio/correlation.ts +162 -0
  260. package/src/tools/portfolio/predictions.ts +253 -0
  261. package/src/tools/portfolio/risk-analysis.ts +134 -0
  262. package/src/tools/portfolio/tracker.ts +147 -0
  263. package/src/tools/portfolio/watchlist.ts +153 -0
  264. package/src/tools/sentiment/reddit-sentiment.ts +164 -0
  265. package/src/tools/sentiment/sentiment-summary.ts +256 -0
  266. package/src/tools/sentiment/sentiment-trend.ts +58 -0
  267. package/src/tools/sentiment/twitter-sentiment.ts +96 -0
  268. package/src/tools/sentiment/web-search.ts +150 -0
  269. package/src/tools/sentiment/web-sentiment.ts +76 -0
  270. package/src/tools/technical/backtest.ts +246 -0
  271. package/src/tools/technical/indicators.ts +258 -0
  272. package/src/types/fundamentals.ts +46 -0
  273. package/src/types/index.ts +20 -0
  274. package/src/types/macro.ts +27 -0
  275. package/src/types/market.ts +43 -0
  276. package/src/types/options.ts +35 -0
  277. package/src/types/portfolio.ts +41 -0
  278. package/src/types/sentiment.ts +70 -0
  279. package/src/workflows/compare-assets.ts +39 -0
  280. package/src/workflows/index.ts +4 -0
  281. package/src/workflows/options-screener.ts +49 -0
  282. package/src/workflows/portfolio-builder.ts +52 -0
  283. package/src/workflows/types.ts +4 -0
@@ -0,0 +1,300 @@
1
+ export type AnalystRole =
2
+ | "valuation"
3
+ | "momentum"
4
+ | "options"
5
+ | "contrarian"
6
+ | "risk";
7
+
8
+ const SYMBOL_CAPTURE = "(\\$?[A-Za-z]{1,5}(?:[./-][A-Za-z]{1,2})?)";
9
+ const NORMALIZED_SYMBOL_PATTERN = /^[A-Z]{1,5}(?:[./-][A-Z]{1,2})?$/;
10
+
11
+ const VOTING_INSTRUCTION = `
12
+
13
+ End your analysis with this exact format:
14
+ SIGNAL: BUY | HOLD | SELL
15
+ CONVICTION: [1-10]
16
+ THESIS: [one sentence summary of your position]`;
17
+
18
+ const EXECUTION_GUARDRAILS = `
19
+ Execution rules:
20
+ - Reuse tool outputs that were already fetched earlier in the session. Do not call the same tool again for the same symbol unless you need a missing field.
21
+ - If a required provider returns unavailable or missing data, stop that leg quickly, label the missing metrics as unavailable, and continue with the remaining evidence.
22
+ - Do not retry the same failing fundamentals call multiple times.`;
23
+
24
+ const ANALYST_PROMPTS: Record<AnalystRole, (symbol: string) => string> = {
25
+ valuation: (symbol) =>
26
+ `**[Valuation Analyst]** You are a Damodaran-style valuation analyst. Your approach: connect the company's narrative to numbers, then compute intrinsic value. Analyze ${symbol}:
27
+ 1. Start with get_company_overview for P/E, forward P/E, EPS, profit margin, and market cap.
28
+ 2. If overview data is available, use get_financials for revenue, income, and free cash flow trends across years.
29
+ 3. If financial statements are available, use get_earnings for EPS surprise patterns and growth trajectory.
30
+ 4. Only use compute_dcf once you have the inputs needed to estimate intrinsic value.
31
+ Assess: What growth rate is the market implicitly pricing in? Is the current price above or below your intrinsic value range? Cite specific numbers with their source tool. Keep reasoning data-driven — every claim must reference a fetched number.${EXECUTION_GUARDRAILS}${VOTING_INSTRUCTION}`,
32
+
33
+ momentum: (symbol) =>
34
+ `**[Momentum Analyst]** You are a CAN SLIM-style momentum analyst. Price action and volume are your primary evidence. Analyze ${symbol}:
35
+ 1. Use get_stock_history with 1y range, then get_technical_indicators.
36
+ 2. Focus on: Is price making new highs or breaking down from a base? Is OBV rising (volume confirming) or diverging? Where is price relative to VWAP?
37
+ 3. Check RSI (overbought >70 / oversold <30) and MACD histogram direction.
38
+ 4. Identify key support/resistance from Bollinger Bands and SMA(20)/SMA(50).
39
+ 5. Use get_earnings to check if earnings are accelerating quarter over quarter.
40
+ State specific price levels. A breakout on rising volume is bullish; a breakdown on high volume is bearish. No vague language — cite the numbers.${EXECUTION_GUARDRAILS}${VOTING_INSTRUCTION}`,
41
+
42
+ options: (symbol) =>
43
+ `**[Options Analyst]** You analyze what the derivatives market is pricing in. Analyze ${symbol}:
44
+ 1. Use get_option_chain to review the full chain with strikes, volume, open interest, IV, and Greeks.
45
+ 2. Compute the put/call ratio from volume data — above 1.0 is bearish bias, below 0.7 is bullish.
46
+ 3. Look for unusually high volume contracts (>3x average OI) that signal institutional positioning.
47
+ 4. Note the overall IV level — is it elevated (expecting a move) or compressed (quiet period)?
48
+ 5. Check if smart money is positioning via deep ITM or OTM options with high volume.
49
+ What is the options market pricing in that the stock price alone doesn't show?${EXECUTION_GUARDRAILS}${VOTING_INSTRUCTION}`,
50
+
51
+ contrarian: (symbol) =>
52
+ `**[Contrarian Analyst]** You are a Burry-style contrarian. Your job is to find what the crowd is missing. Be terse and data-driven — cite concrete numbers like "FCF yield 14.7%" or "P/E 8.3x vs sector 22x." Analyze ${symbol}:
53
+ 1. Use get_fear_greed for overall market mood — extreme readings signal opportunity.
54
+ 2. Use get_reddit_sentiment on wallstreetbets and stocks — check the sentiment score. Extreme bullishness from retail is a warning; extreme bearishness may be opportunity.
55
+ 3. Cross-reference: Is sentiment overly bullish while fundamentals (revenue, margins, FCF) are deteriorating? Is everyone bearish while the numbers quietly improve?
56
+ 5. Reuse get_company_overview or other fundamentals already fetched earlier in the session if available. If fundamentals are unavailable, say so and base the contrarian view on sentiment and price only.
57
+ Where is the consensus wrong? What is the market over-pricing or under-pricing?${EXECUTION_GUARDRAILS}${VOTING_INSTRUCTION}`,
58
+
59
+ risk: (symbol) =>
60
+ `**[Risk Manager]** You are the final check before capital is deployed. Your job is to quantify downside, not to have an opinion on direction. Analyze ${symbol}:
61
+ 1. Use analyze_risk to compute annualized volatility, Sharpe ratio, max drawdown, and VaR(95%).
62
+ 2. Position sizing: Using the 2% portfolio risk rule, compute max position size. Formula: position_size = (0.02 * portfolio_value) / (entry_price * stop_loss_pct). Assume $100K portfolio.
63
+ 3. Risk/reward: Is potential upside at least 2x the max drawdown? If not, the trade is unfavorable regardless of thesis.
64
+ 4. Correlation: If this is in a portfolio, would it add diversification or concentration risk?
65
+ 5. Scenario analysis: What is the max realistic downside in a 1-sigma and 2-sigma move?
66
+ Be quantitative. Every assessment must include a number.${EXECUTION_GUARDRAILS}${VOTING_INSTRUCTION}`,
67
+ };
68
+
69
+ export function buildBullPrompt(symbol: string): string {
70
+ return `**[Bull Researcher]** You have received five analyst perspectives above for ${symbol}.
71
+ Build the strongest possible case FOR this position.
72
+
73
+ Rules:
74
+ - Cite analyst outputs and underlying tool evidence where available.
75
+ - Address any bearish signals (SELL votes, high VaR, negative sentiment) and explain why they are less concerning than they appear.
76
+ - You may call up to 2 tools if you identify a specific gap in the existing evidence. State the gap before calling the tool.
77
+ - Reuse data already fetched in the session.
78
+ ${EXECUTION_GUARDRAILS}
79
+
80
+ End with this exact format:
81
+ BULL THESIS: [2-3 sentences building the case for the position]
82
+ KEY RISK TO THIS THESIS: [one sentence — the single thing that would invalidate your case]`;
83
+ }
84
+
85
+ export function buildBearPrompt(symbol: string): string {
86
+ return `**[Bear Researcher]** You have received five analyst perspectives and a bull case above for ${symbol}. Your job is to dismantle the bull thesis.
87
+
88
+ Rules:
89
+ - Attack the weakest assumptions in the bull case above.
90
+ - Cite analyst outputs and underlying tool evidence where available.
91
+ - If the bull case ignored negative data points, surface them.
92
+ - You may call up to 2 tools if you identify a specific gap in the existing evidence. State the gap before calling the tool.
93
+ - Reuse data already fetched in the session.
94
+ ${EXECUTION_GUARDRAILS}
95
+
96
+ End with this exact format:
97
+ BEAR THESIS: [2-3 sentences arguing against the position]
98
+ WHAT WOULD CHANGE MY MIND: [one sentence — what data would make you concede to the bull]`;
99
+ }
100
+
101
+ export function buildRebuttalPrompt(symbol: string): string {
102
+ return `**[Bull Rebuttal]** First, check the five analyst SIGNAL: lines above for ${symbol} (each analyst ended with "SIGNAL: BUY", "SIGNAL: HOLD", or "SIGNAL: SELL").
103
+ If there is NO case where at least one analyst said SIGNAL: BUY and at least one said SIGNAL: SELL, respond with ONLY:
104
+ REBUTTAL SKIPPED — consensus reached.
105
+
106
+ Otherwise, the bear raised specific concerns above. Address each one directly.
107
+
108
+ Rules:
109
+ - Concede any point where the bear is factually correct.
110
+ - For points you rebut, cite specific data from the analysts above.
111
+ - Do not repeat your original thesis — respond to the bear's NEW arguments.
112
+ - No tool calls in the rebuttal. Work with existing evidence only.
113
+ ${EXECUTION_GUARDRAILS}
114
+
115
+ End with this exact format:
116
+ CONCESSIONS: [bullet list of points you concede]
117
+ REMAINING CONVICTION: [1-10, where 10 = fully confident despite bear case]`;
118
+ }
119
+
120
+ export function buildSynthesisPrompt(symbol: string): string {
121
+ return `**[Synthesis]** You have received five analyst signals with conviction scores for ${symbol}, a bull case arguing FOR the position, and a bear case arguing AGAINST.
122
+ If a bull rebuttal with concessions appears above (not a line starting with "REBUTTAL SKIPPED"), treat the concessions as validated risks that must be addressed.
123
+
124
+ Your job is NOT to average opinions. Your job is to RESOLVE THE DEBATE.
125
+
126
+ 1. **Vote Tally**: X BUY, Y HOLD, Z SELL — weighted average conviction
127
+ 2. **Verdict**: BUY, HOLD, or SELL
128
+ 3. **Debate winner**: Which side had the stronger argument, and why
129
+ 4. **Strongest counterpoint**: Address the losing side's best argument directly — explain why it's outweighed, or acknowledge it as a real risk
130
+ 5. **Reversal condition**: State the SPECIFIC, TESTABLE condition under which your verdict would reverse (the bear's "what would change my mind" or the bull's "key risk")
131
+ 6. **Key levels**: Entry, stop-loss, and target prices
132
+ 7. **Position sizing**: Based on risk manager's analysis
133
+
134
+ Be direct and actionable. This is your final word on ${symbol}.
135
+
136
+ End with this exact format:
137
+ VERDICT: [BUY|HOLD|SELL]
138
+ CONFIDENCE: [1-10]
139
+ DEBATE WINNER: [BULL|BEAR]
140
+ REVERSAL CONDITION: [specific, testable condition]`;
141
+ }
142
+
143
+ const VALIDATION_PROMPT_DEBATE = (symbol: string) =>
144
+ `**[Validation Check]** Review the complete analysis of ${symbol} above, including the debate. For each specific number cited by any analyst, bull, or bear researcher, verify it matches tool output data received in the session. Flag any inconsistencies. If a number was stated without being fetched first, call it out as UNVERIFIED.
145
+
146
+ Additionally check:
147
+ 1. Did the bull/bear cite real numbers from analyst outputs (not hallucinated)?
148
+ 2. If a rebuttal occurred (not a line starting with "REBUTTAL SKIPPED"), are the concessions genuine (did the bull actually give ground on the bear's specific points)?
149
+ 3. Is the reversal condition specific and testable (not vague like "if macro deteriorates")?
150
+
151
+ Output: VALIDATED if all checks pass, or list specific corrections needed.`;
152
+
153
+ const SYNTHESIS_PROMPT_NO_DEBATE = (symbol: string) =>
154
+ `**[Synthesis]** You have received five analyst signals above for ${symbol}. Tally the SIGNAL votes (BUY/HOLD/SELL) and weight them by CONVICTION scores. Then provide:
155
+ 1. **Vote Tally**: X BUY, Y HOLD, Z SELL — weighted average conviction
156
+ 2. **Verdict**: Buy, Hold, or Sell — based on the signal consensus
157
+ 3. **Key thesis** in 2-3 sentences
158
+ 4. **Bull case** — what could go right
159
+ 5. **Bear case** — what could go wrong
160
+ 6. **Key levels** — entry, stop-loss, and target prices
161
+ 7. **Position sizing recommendation** based on risk profile
162
+
163
+ Be direct and actionable. This is your final word on ${symbol}.`;
164
+
165
+ const VALIDATION_PROMPT_NO_DEBATE = (symbol: string) =>
166
+ `**[Validation Check]** Review your complete analysis of ${symbol} above. For each specific number you cited (price, P/E, revenue, RSI, intrinsic value, etc.), verify it matches the tool output data you received. Flag any inconsistencies. If you stated a number without fetching it first, call that out as UNVERIFIED. Output: VALIDATED if all numbers check out, or list specific corrections needed.`;
167
+
168
+ export function getInitialAnalysisPrompt(symbol: string): string {
169
+ return `Begin comprehensive analysis of ${symbol}. Start by getting the current stock quote.`;
170
+ }
171
+
172
+ export interface ComprehensiveAnalysisOptions {
173
+ debate?: boolean;
174
+ }
175
+
176
+ export function getComprehensiveAnalysisPrompts(symbol: string, options?: ComprehensiveAnalysisOptions): string[] {
177
+ const debate = options?.debate ?? true;
178
+ const roles: AnalystRole[] = ["valuation", "momentum", "options", "contrarian", "risk"];
179
+ const prompts = [getInitialAnalysisPrompt(symbol)];
180
+
181
+ for (const role of roles) {
182
+ prompts.push(ANALYST_PROMPTS[role](symbol));
183
+ }
184
+
185
+ if (debate) {
186
+ prompts.push(buildBullPrompt(symbol));
187
+ prompts.push(buildBearPrompt(symbol));
188
+ prompts.push(buildRebuttalPrompt(symbol));
189
+ prompts.push(buildSynthesisPrompt(symbol));
190
+ prompts.push(VALIDATION_PROMPT_DEBATE(symbol));
191
+ } else {
192
+ prompts.push(SYNTHESIS_PROMPT_NO_DEBATE(symbol));
193
+ prompts.push(VALIDATION_PROMPT_NO_DEBATE(symbol));
194
+ }
195
+
196
+ return prompts;
197
+ }
198
+
199
+ import type { WorkflowDefinition } from "../runtime/prompt-step.js";
200
+ import { promptStep } from "../runtime/prompt-step.js";
201
+
202
+ export function buildComprehensiveAnalysisDefinition(symbol: string, options?: ComprehensiveAnalysisOptions): WorkflowDefinition {
203
+ const debate = options?.debate ?? true;
204
+ const roles: AnalystRole[] = ["valuation", "momentum", "options", "contrarian", "risk"];
205
+
206
+ const analystOutputs = roles.map((r) => `${r}_signal`);
207
+
208
+ const analystSteps = [
209
+ promptStep("initial_fetch", "Fetch initial quote data", getInitialAnalysisPrompt(symbol), {
210
+ expectedOutputs: ["quote"],
211
+ }),
212
+ ...roles.map((role) =>
213
+ promptStep(`analyst_${role}`, `${role} analysis`, ANALYST_PROMPTS[role](symbol), {
214
+ skippable: true,
215
+ requiredInputs: ["quote"],
216
+ expectedOutputs: [`${role}_signal`],
217
+ }),
218
+ ),
219
+ ];
220
+
221
+ if (debate) {
222
+ return {
223
+ workflowType: "comprehensive_analysis",
224
+ steps: [
225
+ ...analystSteps,
226
+ promptStep("debate_bull", "Bull researcher case", buildBullPrompt(symbol), {
227
+ requiredInputs: analystOutputs,
228
+ expectedOutputs: ["bull_thesis"],
229
+ }),
230
+ promptStep("debate_bear", "Bear researcher case", buildBearPrompt(symbol), {
231
+ requiredInputs: [...analystOutputs, "bull_thesis"],
232
+ expectedOutputs: ["bear_thesis"],
233
+ }),
234
+ promptStep("debate_rebuttal", "Bull rebuttal (self-gating)", buildRebuttalPrompt(symbol), {
235
+ requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis"],
236
+ expectedOutputs: ["rebuttal"],
237
+ }),
238
+ promptStep("synthesis", "Resolve the debate", buildSynthesisPrompt(symbol), {
239
+ requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis", "rebuttal"],
240
+ expectedOutputs: ["verdict"],
241
+ }),
242
+ promptStep("validation", "Validate cited numbers", VALIDATION_PROMPT_DEBATE(symbol), {
243
+ skippable: true,
244
+ requiredInputs: ["verdict"],
245
+ expectedOutputs: ["validation_result"],
246
+ }),
247
+ ],
248
+ };
249
+ }
250
+
251
+ return {
252
+ workflowType: "comprehensive_analysis",
253
+ steps: [
254
+ ...analystSteps,
255
+ promptStep("synthesis", "Synthesize analyst signals", SYNTHESIS_PROMPT_NO_DEBATE(symbol), {
256
+ requiredInputs: analystOutputs,
257
+ expectedOutputs: ["verdict"],
258
+ }),
259
+ promptStep("validation", "Validate cited numbers", VALIDATION_PROMPT_NO_DEBATE(symbol), {
260
+ skippable: true,
261
+ requiredInputs: ["verdict"],
262
+ expectedOutputs: ["validation_result"],
263
+ }),
264
+ ],
265
+ };
266
+ }
267
+
268
+ export function runComprehensiveAnalysis(
269
+ enqueueFollowUp: (prompt: string) => void,
270
+ symbol: string,
271
+ options?: ComprehensiveAnalysisOptions,
272
+ ): void {
273
+ for (const prompt of getComprehensiveAnalysisPrompts(symbol, options).slice(1)) {
274
+ enqueueFollowUp(prompt);
275
+ }
276
+ }
277
+
278
+ export function isAnalysisRequest(input: string): { match: boolean; symbol?: string } {
279
+ const patterns = [
280
+ new RegExp(`^analyze\\s+${SYMBOL_CAPTURE}\\s*$`, "i"),
281
+ new RegExp(`^full\\s+analysis\\s+(?:of\\s+)?${SYMBOL_CAPTURE}\\s*$`, "i"),
282
+ new RegExp(`^deep\\s+dive\\s+(?:on\\s+)?${SYMBOL_CAPTURE}\\s*$`, "i"),
283
+ ];
284
+
285
+ for (const pattern of patterns) {
286
+ const match = input.match(pattern);
287
+ if (match) {
288
+ return { match: true, symbol: match[1].replace(/\$/g, "").toUpperCase() };
289
+ }
290
+ }
291
+
292
+ return { match: false };
293
+ }
294
+
295
+ export function normalizeSymbol(input: string): string | undefined {
296
+ const trimmed = input.trim();
297
+ if (!trimmed) return undefined;
298
+ const candidate = trimmed.replace(/\$/g, "").toUpperCase();
299
+ return NORMALIZED_SYMBOL_PATTERN.test(candidate) ? candidate : undefined;
300
+ }
package/src/cli.ts ADDED
@@ -0,0 +1,205 @@
1
+ #!/usr/bin/env node
2
+ import "./infra/node-version.js";
3
+ import { spawn } from "node:child_process";
4
+ import { createRequire } from "node:module";
5
+ import { dirname, resolve } from "node:path";
6
+ import { parseArgs } from "node:util";
7
+ import { fileURLToPath } from "node:url";
8
+ import {
9
+ AuthStorage,
10
+ DefaultPackageManager,
11
+ InteractiveMode,
12
+ ModelRegistry,
13
+ SettingsManager,
14
+ createAgentSessionRuntime,
15
+ createAgentSessionServices,
16
+ getAgentDir,
17
+ initTheme,
18
+ } from "@earendil-works/pi-coding-agent";
19
+ import { createOpenCandleSession } from "./pi/session.js";
20
+ import { continueOpenCandleSession } from "./pi/session-storage.js";
21
+ import { loadEnv } from "./config.js";
22
+
23
+ const require = createRequire(import.meta.url);
24
+ const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
25
+
26
+ async function handlePackageCommand(
27
+ args: string[],
28
+ cwd: string,
29
+ agentDir: string,
30
+ ): Promise<boolean> {
31
+ const [command, ...rest] = args;
32
+ if (
33
+ !command ||
34
+ !["install", "remove", "uninstall", "list", "update"].includes(command)
35
+ ) {
36
+ return false;
37
+ }
38
+
39
+ const settingsManager = SettingsManager.create(cwd, agentDir);
40
+ const packageManager = new DefaultPackageManager({
41
+ cwd,
42
+ agentDir,
43
+ settingsManager,
44
+ });
45
+ packageManager.setProgressCallback((event) => {
46
+ if (event.type === "start" || event.type === "progress") {
47
+ process.stdout.write(`${event.message}\n`);
48
+ }
49
+ });
50
+
51
+ const source = rest.find((a) => !a.startsWith("-"));
52
+ const local = rest.includes("-l") || rest.includes("--local");
53
+
54
+ switch (command === "uninstall" ? "remove" : command) {
55
+ case "install": {
56
+ if (!source) {
57
+ console.error("Usage: opencandle install <source> [-l]");
58
+ process.exitCode = 1;
59
+ return true;
60
+ }
61
+ await packageManager.install(source, { local });
62
+ packageManager.addSourceToSettings(source, { local });
63
+ console.log(`Installed ${source}`);
64
+ return true;
65
+ }
66
+ case "remove": {
67
+ if (!source) {
68
+ console.error("Usage: opencandle remove <source> [-l]");
69
+ process.exitCode = 1;
70
+ return true;
71
+ }
72
+ await packageManager.remove(source, { local });
73
+ const removed = packageManager.removeSourceFromSettings(source, {
74
+ local,
75
+ });
76
+ if (!removed) {
77
+ console.error(`No matching package found for ${source}`);
78
+ process.exitCode = 1;
79
+ } else {
80
+ console.log(`Removed ${source}`);
81
+ }
82
+ return true;
83
+ }
84
+ case "list": {
85
+ const globalPkgs = settingsManager.getGlobalSettings().packages ?? [];
86
+ const projectPkgs = settingsManager.getProjectSettings().packages ?? [];
87
+ if (globalPkgs.length === 0 && projectPkgs.length === 0) {
88
+ console.log("No packages installed.");
89
+ return true;
90
+ }
91
+ if (globalPkgs.length > 0) {
92
+ console.log("User packages:");
93
+ for (const pkg of globalPkgs) {
94
+ const s = typeof pkg === "string" ? pkg : pkg.source;
95
+ const path = packageManager.getInstalledPath(s, "user");
96
+ console.log(` ${s}${path ? `\n ${path}` : ""}`);
97
+ }
98
+ }
99
+ if (projectPkgs.length > 0) {
100
+ console.log("Project packages:");
101
+ for (const pkg of projectPkgs) {
102
+ const s = typeof pkg === "string" ? pkg : pkg.source;
103
+ const path = packageManager.getInstalledPath(s, "project");
104
+ console.log(` ${s}${path ? `\n ${path}` : ""}`);
105
+ }
106
+ }
107
+ return true;
108
+ }
109
+ case "update": {
110
+ await packageManager.update(source);
111
+ console.log(source ? `Updated ${source}` : "All packages updated.");
112
+ return true;
113
+ }
114
+ }
115
+ return false;
116
+ }
117
+
118
+ async function handleGuiCommand(args: string[], cwd: string): Promise<boolean> {
119
+ if (args[0] !== "gui") return false;
120
+
121
+ const tsxCli = require.resolve("tsx/cli");
122
+ const serverPath = resolve(packageRoot, "gui/server/server.ts");
123
+ const child = spawn(process.execPath, [tsxCli, serverPath, ...args.slice(1)], {
124
+ cwd,
125
+ env: process.env,
126
+ stdio: "inherit",
127
+ });
128
+
129
+ const exitCode = await new Promise<number>((resolveExit) => {
130
+ child.on("close", (code, signal) => {
131
+ if (signal) {
132
+ resolveExit(1);
133
+ } else {
134
+ resolveExit(code ?? 0);
135
+ }
136
+ });
137
+ });
138
+ process.exitCode = exitCode;
139
+ return true;
140
+ }
141
+
142
+ async function main(): Promise<void> {
143
+ const { positionals } = parseArgs({ allowPositionals: true, strict: false });
144
+ const cwd = process.cwd();
145
+ const agentDir = getAgentDir();
146
+
147
+ if (await handleGuiCommand(positionals, cwd)) {
148
+ return;
149
+ }
150
+
151
+ if (await handlePackageCommand(positionals, cwd, agentDir)) {
152
+ return;
153
+ }
154
+
155
+ // Default: start the OpenCandle interactive agent
156
+ loadEnv();
157
+ const settingsManager = SettingsManager.create(cwd, agentDir);
158
+ const authStorage = AuthStorage.create();
159
+ const modelRegistry = ModelRegistry.create(authStorage);
160
+ const shouldSuppressFallbackMessage = modelRegistry.getAvailable().length === 0;
161
+
162
+ initTheme(settingsManager.getTheme(), true);
163
+
164
+ const sessionManager = continueOpenCandleSession(cwd);
165
+
166
+ const runtime = await createAgentSessionRuntime(
167
+ async (opts) => {
168
+ const services = await createAgentSessionServices({
169
+ cwd: opts.cwd,
170
+ agentDir: opts.agentDir,
171
+ authStorage,
172
+ settingsManager,
173
+ modelRegistry,
174
+ });
175
+ const result = await createOpenCandleSession({
176
+ cwd: opts.cwd,
177
+ agentDir: opts.agentDir,
178
+ settingsManager,
179
+ authStorage,
180
+ modelRegistry,
181
+ sessionManager: opts.sessionManager,
182
+ bindExtensions: false,
183
+ });
184
+ return {
185
+ ...result,
186
+ services,
187
+ diagnostics: services.diagnostics,
188
+ };
189
+ },
190
+ { cwd, agentDir, sessionManager },
191
+ );
192
+
193
+ try {
194
+ const interactiveMode = new InteractiveMode(runtime, {
195
+ modelFallbackMessage: shouldSuppressFallbackMessage
196
+ ? undefined
197
+ : runtime.modelFallbackMessage,
198
+ });
199
+ await interactiveMode.run();
200
+ } finally {
201
+ await runtime.dispose();
202
+ }
203
+ }
204
+
205
+ await main();
package/src/config.ts ADDED
@@ -0,0 +1,161 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { ensureParentDir, getConfigPath } from "./infra/opencandle-paths.js";
3
+
4
+ export interface SentimentConfig {
5
+ retentionDays: number;
6
+ defaultSubreddits: string[];
7
+ commentsPerPost: number;
8
+ divergenceThreshold: number;
9
+ }
10
+
11
+ export type RouterMode = "rules" | "llm";
12
+
13
+ export interface Config {
14
+ alphaVantageApiKey?: string;
15
+ fredApiKey?: string;
16
+ braveApiKey?: string;
17
+ exaApiKey?: string;
18
+ finnhubApiKey?: string;
19
+ /** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
20
+ debate?: boolean;
21
+ /**
22
+ * Intent-router rollout flag. `"rules"` (default) runs the legacy regex
23
+ * `classifyIntent` + `extractPreferences` path. `"llm"` runs the LLM router
24
+ * ahead of prompt assembly. Controlled by `OPENCANDLE_ROUTER_MODE`.
25
+ */
26
+ routerMode: RouterMode;
27
+ sentiment?: SentimentConfig;
28
+ }
29
+
30
+ export interface OpenCandleFileConfig {
31
+ providers?: {
32
+ alphaVantage?: {
33
+ apiKey?: string;
34
+ };
35
+ fred?: {
36
+ apiKey?: string;
37
+ };
38
+ brave?: {
39
+ apiKey?: string;
40
+ };
41
+ exa?: {
42
+ apiKey?: string;
43
+ };
44
+ finnhub?: {
45
+ apiKey?: string;
46
+ };
47
+ };
48
+ /** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
49
+ debate?: boolean;
50
+ sentiment?: {
51
+ retentionDays?: number;
52
+ defaultSubreddits?: string[];
53
+ commentsPerPost?: number;
54
+ divergenceThreshold?: number;
55
+ };
56
+ }
57
+
58
+ export function loadEnv(path = ".env"): void {
59
+ let content: string;
60
+ try {
61
+ content = readFileSync(path, "utf-8");
62
+ } catch {
63
+ return;
64
+ }
65
+ for (const line of content.split("\n")) {
66
+ const trimmed = line.trim();
67
+ if (!trimmed || trimmed.startsWith("#")) continue;
68
+ const eqIndex = trimmed.indexOf("=");
69
+ if (eqIndex === -1) continue;
70
+ const key = trimmed.slice(0, eqIndex).trim();
71
+ const value = trimmed.slice(eqIndex + 1).trim();
72
+ if (key && value) {
73
+ process.env[key] = value;
74
+ }
75
+ }
76
+ }
77
+
78
+ let cachedConfig: Config | null = null;
79
+
80
+ const SENTIMENT_DEFAULTS: SentimentConfig = {
81
+ retentionDays: 30,
82
+ defaultSubreddits: ["wallstreetbets", "stocks", "investing", "options"],
83
+ commentsPerPost: 5,
84
+ divergenceThreshold: 0.4,
85
+ };
86
+
87
+ function resolveRouterMode(): RouterMode {
88
+ const raw = process.env.OPENCANDLE_ROUTER_MODE;
89
+ if (raw === undefined || raw === "") return "rules";
90
+ if (raw === "rules" || raw === "llm") return raw;
91
+ throw new Error(
92
+ `Invalid OPENCANDLE_ROUTER_MODE="${raw}". Allowed values: "rules" (default) or "llm".`,
93
+ );
94
+ }
95
+
96
+ function resolveConfig(fileConfig: OpenCandleFileConfig): Config {
97
+ const debateEnv = process.env.OPENCANDLE_DEBATE;
98
+ const fileSentiment = fileConfig.sentiment;
99
+ return {
100
+ alphaVantageApiKey:
101
+ process.env.ALPHA_VANTAGE_API_KEY ?? fileConfig.providers?.alphaVantage?.apiKey,
102
+ fredApiKey: process.env.FRED_API_KEY ?? fileConfig.providers?.fred?.apiKey,
103
+ braveApiKey: process.env.BRAVE_API_KEY ?? fileConfig.providers?.brave?.apiKey,
104
+ exaApiKey: process.env.EXA_API_KEY ?? fileConfig.providers?.exa?.apiKey,
105
+ finnhubApiKey: process.env.FINNHUB_API_KEY ?? fileConfig.providers?.finnhub?.apiKey,
106
+ debate: debateEnv !== undefined ? debateEnv !== "false" && debateEnv !== "0" : fileConfig.debate ?? true,
107
+ routerMode: resolveRouterMode(),
108
+ sentiment: {
109
+ retentionDays: fileSentiment?.retentionDays ?? SENTIMENT_DEFAULTS.retentionDays,
110
+ defaultSubreddits: fileSentiment?.defaultSubreddits ?? SENTIMENT_DEFAULTS.defaultSubreddits,
111
+ commentsPerPost: fileSentiment?.commentsPerPost ?? SENTIMENT_DEFAULTS.commentsPerPost,
112
+ divergenceThreshold: fileSentiment?.divergenceThreshold ?? SENTIMENT_DEFAULTS.divergenceThreshold,
113
+ },
114
+ };
115
+ }
116
+
117
+ export function loadFileConfig(path = getConfigPath()): OpenCandleFileConfig {
118
+ if (!existsSync(path)) {
119
+ return {};
120
+ }
121
+
122
+ let content: string;
123
+ try {
124
+ content = readFileSync(path, "utf-8");
125
+ } catch (error) {
126
+ const message = error instanceof Error ? error.message : String(error);
127
+ throw new Error(`Unable to read OpenCandle config at ${path}: ${message}`);
128
+ }
129
+
130
+ try {
131
+ const parsed = JSON.parse(content) as OpenCandleFileConfig;
132
+ return parsed && typeof parsed === "object" ? parsed : {};
133
+ } catch (error) {
134
+ const message = error instanceof Error ? error.message : String(error);
135
+ throw new Error(`Invalid OpenCandle config at ${path}: ${message}`);
136
+ }
137
+ }
138
+
139
+ export function saveFileConfig(config: OpenCandleFileConfig, path = getConfigPath()): void {
140
+ ensureParentDir(path);
141
+ writeFileSync(path, `${JSON.stringify(config, null, 2)}\n`, "utf-8");
142
+ }
143
+
144
+ export function loadConfig(): Config {
145
+ loadEnv();
146
+ cachedConfig = resolveConfig(loadFileConfig());
147
+
148
+ return cachedConfig;
149
+ }
150
+
151
+ export function getConfig(): Config {
152
+ if (!cachedConfig) {
153
+ return loadConfig();
154
+ }
155
+ return cachedConfig;
156
+ }
157
+
158
+ /** Test-only: clear the memoized config so the next `getConfig()` re-reads env. */
159
+ export function resetConfigCache(): void {
160
+ cachedConfig = null;
161
+ }
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";