opencandle 0.12.0 → 0.13.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 (75) hide show
  1. package/README.md +48 -180
  2. package/dist/cli-main.js +9 -8
  3. package/dist/config.d.ts +4 -0
  4. package/dist/config.js +1 -0
  5. package/dist/doctor/cli-command.js +7 -4
  6. package/dist/doctor/report.js +13 -0
  7. package/dist/gui/server/chat-event-adapter.js +145 -20
  8. package/dist/gui/server/history-snapshot-store.d.ts +8 -0
  9. package/dist/gui/server/history-snapshot-store.js +43 -0
  10. package/dist/gui/server/http-routes.d.ts +2 -0
  11. package/dist/gui/server/http-routes.js +32 -7
  12. package/dist/gui/server/live-chat-event-adapter.d.ts +2 -0
  13. package/dist/gui/server/live-chat-event-adapter.js +33 -15
  14. package/dist/gui/server/market-indices-api.d.ts +21 -0
  15. package/dist/gui/server/market-indices-api.js +26 -0
  16. package/dist/gui/server/market-indices-snapshot-store.d.ts +12 -0
  17. package/dist/gui/server/market-indices-snapshot-store.js +56 -0
  18. package/dist/gui/server/market-state-api.d.ts +112 -0
  19. package/dist/gui/server/market-state-api.js +246 -4
  20. package/dist/gui/server/model-setup.d.ts +2 -11
  21. package/dist/gui/server/model-setup.js +10 -6
  22. package/dist/gui/server/server.js +12 -9
  23. package/dist/gui/server/tool-metadata.d.ts +14 -14
  24. package/dist/gui/server/ws-hub.js +1 -1
  25. package/dist/infra/cache.d.ts +4 -0
  26. package/dist/infra/cache.js +4 -0
  27. package/dist/infra/lse-byte-budget.d.ts +10 -0
  28. package/dist/infra/lse-byte-budget.js +47 -0
  29. package/dist/infra/rate-limiter.js +1 -0
  30. package/dist/onboarding/providers.d.ts +16 -1
  31. package/dist/onboarding/providers.js +20 -0
  32. package/dist/onboarding/validate-model-key.js +33 -1
  33. package/dist/onboarding/validation.js +8 -0
  34. package/dist/pi/opencandle-extension.d.ts +2 -1
  35. package/dist/pi/opencandle-extension.js +2 -2
  36. package/dist/pi/session.d.ts +2 -3
  37. package/dist/pi/session.js +7 -5
  38. package/dist/pi/setup.d.ts +3 -3
  39. package/dist/pi/setup.js +51 -59
  40. package/dist/prompts/workflow-prompts.js +16 -14
  41. package/dist/providers/index.d.ts +1 -0
  42. package/dist/providers/index.js +1 -0
  43. package/dist/providers/lse.d.ts +35 -0
  44. package/dist/providers/lse.js +284 -0
  45. package/dist/providers/polymarket.js +22 -5
  46. package/dist/providers/wrap-provider.js +1 -0
  47. package/dist/providers/yahoo-finance.js +1 -0
  48. package/dist/routing/route-manifest.js +1 -0
  49. package/dist/routing/router.js +8 -1
  50. package/dist/runtime/evidence.d.ts +1 -0
  51. package/dist/runtime/evidence.js +1 -1
  52. package/dist/runtime/session-coordinator.d.ts +2 -2
  53. package/dist/runtime/session-coordinator.js +2 -2
  54. package/dist/tools/fundamentals/dcf.js +37 -8
  55. package/dist/tools/fundamentals/financials.js +88 -10
  56. package/dist/tools/index.d.ts +10 -5
  57. package/dist/tools/index.js +3 -0
  58. package/dist/tools/market/price-comparison.d.ts +30 -0
  59. package/dist/tools/market/price-comparison.js +202 -0
  60. package/dist/tools/market/stock-history.d.ts +18 -3
  61. package/dist/tools/market/stock-history.js +132 -17
  62. package/dist/tools/portfolio/correlation.d.ts +1 -1
  63. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  64. package/dist/types/market.d.ts +2 -0
  65. package/gui/web/dist/assets/CatalogOverlay-Cy8Fq0fn.js +1 -0
  66. package/gui/web/dist/assets/allocation-donut-DgBRIKCk.js +52 -0
  67. package/gui/web/dist/assets/index-DIlFyIOO.js +66 -0
  68. package/gui/web/dist/assets/index-DS4jESOB.css +2 -0
  69. package/gui/web/dist/assets/market-chart-DammaCjH.js +1 -0
  70. package/gui/web/dist/assets/utils-CnADgYgh.js +1 -0
  71. package/gui/web/dist/index.html +3 -2
  72. package/package.json +15 -17
  73. package/gui/web/dist/assets/CatalogOverlay-CAc7e3Pf.js +0 -1
  74. package/gui/web/dist/assets/index-BOEKd9wT.css +0 -2
  75. package/gui/web/dist/assets/index-C-H05cQ2.js +0 -65
package/README.md CHANGED
@@ -1,38 +1,28 @@
1
1
  # OpenCandle
2
2
 
3
- OpenCandle is an open source financial investigator: a terminal agent and local browser workbench for market research that starts from real provider data, shows source gaps, and keeps risk visible.
3
+ OpenCandle is an open source financial investigator: a terminal agent and local browser workbench for market research that uses real-time provider data to try to accurately answer financial questions.
4
4
 
5
- Requires Node.js 22.19+ or 24–26. macOS and Linux are fully supported; Windows is best-effort (WSL recommended).
5
+ [![CI](https://img.shields.io/github/actions/workflow/status/Kahtaf/OpenCandle/ci.yml?branch=main&label=CI)](https://github.com/Kahtaf/OpenCandle/actions/workflows/ci.yml)
6
+ [![npm](https://img.shields.io/npm/v/opencandle)](https://www.npmjs.com/package/opencandle)
7
+ [![Node](https://img.shields.io/node/v/opencandle)](https://www.npmjs.com/package/opencandle)
8
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
9
 
7
10
  ```bash
8
- npx opencandle
9
- # or
10
- npx opencandle gui
11
+ npx opencandle # terminal
12
+ npx opencandle gui # browser UI at http://127.0.0.1:14567
11
13
  ```
12
14
 
13
- [![CI](https://github.com/Kahtaf/OpenCandle/actions/workflows/ci.yml/badge.svg)](https://github.com/Kahtaf/OpenCandle/actions/workflows/ci.yml)
14
- [![npm](https://img.shields.io/npm/v/opencandle)](https://www.npmjs.com/package/opencandle)
15
- [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/Kahtaf/OpenCandle/blob/main/LICENSE)
16
-
17
15
  [Docs](https://opencandle.app/docs/) | [First run](https://opencandle.app/docs/first-run.html) | [GUI quickstart](https://opencandle.app/docs/gui-quickstart.html) | [Data sources](https://opencandle.app/docs/data-sources.html) | [Build a tool](https://opencandle.app/docs/build-a-tool.html)
18
16
 
19
17
  ## See It Work
20
18
 
21
- ### Terminal UI
22
-
23
- [![OpenCandle terminal UI demo](https://cdn.jsdelivr.net/gh/Kahtaf/OpenCandle@v0.11.1/assets/opencandle-tui-poster.png)](https://cdn.jsdelivr.net/gh/Kahtaf/OpenCandle@v0.11.1/assets/opencandle-tui.mp4)
24
-
25
- [Watch the terminal UI demo](https://cdn.jsdelivr.net/gh/Kahtaf/OpenCandle@v0.11.1/assets/opencandle-tui.mp4) | [Download MP4](https://github.com/Kahtaf/OpenCandle/raw/refs/heads/main/assets/opencandle-tui.mp4)
26
-
27
- ### Local GUI
19
+ https://github.com/user-attachments/assets/334956b1-18b4-4d6f-92b5-3f739824cd29
28
20
 
29
- [![OpenCandle local GUI demo](https://cdn.jsdelivr.net/gh/Kahtaf/OpenCandle@v0.11.1/assets/opencandle-gui-poster.png)](https://cdn.jsdelivr.net/gh/Kahtaf/OpenCandle@v0.11.1/assets/opencandle-gui.mp4)
30
-
31
- [Watch the local GUI demo](https://cdn.jsdelivr.net/gh/Kahtaf/OpenCandle@v0.11.1/assets/opencandle-gui.mp4) | [Download MP4](https://github.com/Kahtaf/OpenCandle/raw/refs/heads/main/assets/opencandle-gui.mp4)
21
+ ![OpenCandle GUI answering "How is NVDA doing today?" with a quote table, cited news sources, and the research steps panel open.](https://cdn.jsdelivr.net/gh/Kahtaf/OpenCandle@main/docs/images/gui-chat-research.png)
32
22
 
33
23
  ## Why OpenCandle
34
24
 
35
- Most finance agents answer too early. OpenCandle is built around the opposite loop: understand the question, gather market evidence, disclose missing or stale data, then synthesize.
25
+ Generic LLMs answer too early. OpenCandle is built around the opposite loop: understand the question, gather market evidence, disclose missing or stale data, then synthesize.
36
26
 
37
27
  Use it when a question needs inspectable evidence:
38
28
 
@@ -44,32 +34,27 @@ Use it when a question needs inspectable evidence:
44
34
 
45
35
  OpenCandle is read-only research software. It does not place trades, route orders, or provide financial advice.
46
36
 
47
- ## Highlights
37
+ ## Features
48
38
 
49
- | Capability | What it gives you |
50
- | --- | --- |
51
- | Terminal agent | Fast keyboard-driven research inside the bundled [Pi](https://github.com/earendil-works/pi) local TUI, with sessions, slash commands, model setup, and saved transcripts. Pi is the bundled agent runtime; no separate Pi install is needed. |
52
- | Local browser GUI | Chat, session history, provider setup, tool discovery, workflow launches, and richer financial result cards at `http://127.0.0.1:14567`. |
53
- | Evidence-first answers | Tools fetch and format data; the model synthesizes only after evidence is gathered. |
54
- | Finance routing | Quote lookup, comparison, portfolio review, options strategy, filing checks, macro questions, sentiment reads, and educational prompts route differently. |
55
- | Provider transparency | Missing keys, degraded sources, stale cache, and unavailable data are surfaced instead of hidden. |
56
- | Local state | OpenCandle user state lives under `~/.opencandle/` unless `OPENCANDLE_HOME` is set; durable market state is stored in SQLite at `~/.opencandle/state.db` (or `$OPENCANDLE_HOME/state.db`). |
57
- | Extensible tools | TypeScript tool APIs, provider boundaries, workflow builders, and package exports for add-on tools. |
58
- | Eval harness | Unit tests, live provider checks, CLI e2e, GUI browser smoke tests, and competitive finance evals. |
39
+ - **Terminal agent** keyboard-driven research in the bundled [Pi](https://github.com/earendil-works/pi) TUI, with sessions, slash commands, and saved transcripts.
40
+ - **Local browser GUI** — chat with result cards and interactive charts, a market dashboard, per-ticker symbol pages, watchlists and portfolios with live sparklines.
41
+ - **Evidence-first answers** tools fetch and format data; the model synthesizes only after evidence is gathered.
42
+ - **Finance routing** quotes, comparisons, portfolio reviews, options, filings, macro, sentiment, and education each get a purpose-built research path.
43
+ - **Provider transparency** missing keys, degraded sources, and stale data are disclosed instead of hidden.
44
+ - **Local state** watchlists, portfolios, and alerts stay on your machine under `~/.opencandle/`; sessions in Pi's local storage.
45
+ - **Extensible** typed TypeScript tool APIs for building and publishing add-on tools.
59
46
 
60
47
  ## Quick Start
61
48
 
62
- On first run, OpenCandle walks you through model setup. [Pi](https://github.com/earendil-works/pi) is the bundled agent runtime that handles model setup, the terminal shell, and saved sessions; no separate Pi install is needed. In the terminal, you can use Pi sign-in when available or provide a model API key. In the GUI, use the API-key setup panel or complete terminal `/setup` first and refresh the browser. Data-provider keys are separate and optional.
63
-
64
- Start the GUI instead:
49
+ Requires [Node.js](https://nodejs.org/) 22.19+ or 24–26. macOS and Linux are fully supported; Windows is best-effort (WSL recommended).
65
50
 
66
51
  ```bash
67
- npx opencandle gui
52
+ npx opencandle
68
53
  ```
69
54
 
70
- Then open `http://127.0.0.1:14567`.
55
+ On first run, OpenCandle walks you through model setup — use Pi sign-in when offered, or provide a model API key (in the GUI, use the API-key setup panel). Data-provider keys are separate and optional. For the five-minute path from install to a first answer, see [First Run](https://opencandle.app/docs/first-run.html).
71
56
 
72
- For a five-minute path from install to a successful answer, see [First Run](https://opencandle.app/docs/first-run.html).
57
+ Check your setup anytime with `npx opencandle doctor` (or the GUI's `/diagnostics` page).
73
58
 
74
59
  ## Example Prompts
75
60
 
@@ -83,81 +68,24 @@ Run risk analysis on SPY
83
68
  /analyze NVDA # deep research: multi-analyst debate, takes a few minutes
84
69
  ```
85
70
 
86
- Useful slash commands:
87
-
88
- ```text
89
- /setup
90
- /login
91
- /model
92
- /connect
93
- /analyze AAPL # deep research: multi-analyst debate, takes a few minutes
94
- ```
95
-
96
- Health diagnostics run from your shell:
97
-
98
- ```bash
99
- npx opencandle doctor
100
- npx opencandle doctor --json
101
- npx opencandle doctor --sessions
102
- npx opencandle doctor --full
103
- npx opencandle doctor --enable twitter
104
- ```
105
-
106
- The GUI also has a Diagnostics page at `/diagnostics` with the same health report, provider setup links, model setup actions, and an explicit browser-session check for Reddit and Twitter/X.
107
-
108
71
  ## Data Sources
109
72
 
110
- | Area | Examples | Source |
111
- | --- | --- | --- |
112
- | Market data | Quotes, history, ticker search, screeners, crypto price and history | Yahoo Finance, TradingView scanner, Alpha Vantage fallback when configured, CoinGecko |
113
- | Options | Chains, open interest, IV, locally computed Greeks | Yahoo Finance plus local calculations |
114
- | Fundamentals | Overview, financials, earnings, DCF, comparable companies | Alpha Vantage |
115
- | Macro | Rates, CPI, GDP, unemployment, event probabilities, crypto Fear & Greed | FRED, Polymarket Gamma API, alternative.me |
116
- | Technical | Indicators and strategy backtests | Local calculations over market history |
117
- | Sentiment | Reddit, Twitter/X, Finnhub news, and web sentiment | `rdt-cli` and `twitter-cli` using your normal browser sessions, Finnhub, Exa, Brave, DuckDuckGo |
118
- | Filings | SEC filing search | SEC EDGAR |
119
- | Portfolio | Watchlists, holdings, correlation, risk | Local state plus market data |
73
+ | Area | Examples | Source |
74
+ | ------------ | ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------- |
75
+ | Market data | Quotes, history, indexed price comparisons, ticker search, screeners, crypto price and history | Yahoo Finance, TradingView scanner, Alpha Vantage and London Strategic Edge fallbacks when configured, CoinGecko |
76
+ | Options | Chains, open interest, IV, locally computed Greeks | Yahoo Finance plus local calculations |
77
+ | Fundamentals | Overview, financials, earnings, DCF, comparable companies | Alpha Vantage; optional London Strategic Edge for financial statements and DCF, with Yahoo Finance fallbacks |
78
+ | Macro | Rates, CPI, GDP, unemployment, event probabilities, crypto Fear & Greed | FRED, Polymarket Gamma API, alternative.me |
79
+ | Technical | Indicators and strategy backtests | Local calculations over market history |
80
+ | Sentiment | Reddit, Twitter/X, Finnhub news, and web sentiment | `rdt-cli` and `twitter-cli` using your normal browser sessions, Finnhub, Exa, Brave, DuckDuckGo |
81
+ | Filings | SEC filing search | SEC EDGAR |
82
+ | Portfolio | Watchlists, holdings, correlation, risk | Local state plus market data |
120
83
 
121
- Yahoo Finance, TradingView scanner, Polymarket Gamma, CoinGecko, SEC EDGAR, DuckDuckGo search, and the alternative.me crypto Fear & Greed index do not require OpenCandle provider keys. Reddit sentiment requires `rdt-cli` (`uv tool install rdt-cli`) plus an active Reddit session in your normal browser. Twitter/X sentiment requires `twitter-cli` (`uv tool install twitter-cli`) plus an active x.com session in your normal browser. Alpha Vantage, FRED, Brave, Exa, and Finnhub unlock deeper coverage when configured.
84
+ Most sources work without API keys. Reddit and Twitter/X sentiment use `rdt-cli` / `twitter-cli` (`uv tool install ...`) with your normal browser sessions. Alpha Vantage, FRED, Brave, Exa, Finnhub, and London Strategic Edge unlock deeper coverage when configured — see [Data Sources](https://opencandle.app/docs/data-sources.html).
122
85
 
123
86
  ## Configuration
124
87
 
125
- Model access comes from Pi. Market data provider keys can be set in the environment, through `/connect`, through the GUI provider setup flow, or in `~/.opencandle/config.json`.
126
-
127
- | Key | Used For |
128
- | --- | --- |
129
- | `GEMINI_API_KEY` | Google models through Pi |
130
- | `OPENAI_API_KEY` | OpenAI models through Pi |
131
- | `ANTHROPIC_API_KEY` | Anthropic models through Pi |
132
- | `ALPHA_VANTAGE_API_KEY` | Fundamentals, earnings, financial statements, DCF, comps |
133
- | `FRED_API_KEY` | Macro series such as rates, CPI, GDP, unemployment |
134
- | `BRAVE_API_KEY` | Brave web search fallback |
135
- | `EXA_API_KEY` | Exa web search |
136
- | `FINNHUB_API_KEY` | Finnhub company news for sentiment summaries |
137
- | `OPENCANDLE_HOME` | Override OpenCandle state directory |
138
- | `OPENCANDLE_GUI_HOST` | GUI bind host, default `127.0.0.1` |
139
- | `OPENCANDLE_GUI_PORT` | GUI port, default `14567` |
140
- | `OPENCANDLE_NOTIFICATION_WEBHOOK_URL` | Optional local webhook target for alert/report notification delivery attempts |
141
-
142
- Environment variables override `~/.opencandle/config.json`. See [Configuration](https://opencandle.app/docs/configuration.html) for the full reference, including advanced routing and diagnostic switches.
143
-
144
- ## From Source
145
-
146
- ```bash
147
- git clone https://github.com/Kahtaf/OpenCandle.git
148
- cd OpenCandle
149
- npm install
150
- cp .env.example .env
151
- npm start
152
- ```
153
-
154
- On Windows Command Prompt, use `copy .env.example .env` instead of `cp .env.example .env`.
155
-
156
- Run the local GUI from a checkout:
157
-
158
- ```bash
159
- npm run gui
160
- ```
88
+ Model access is configured through Pi on first run (`GEMINI_API_KEY`, `OPENAI_API_KEY`, `ANTHROPIC_API_KEY`, or Pi sign-in). Data-provider keys are optional and can be set in the environment, through `/connect`, through the GUI provider setup flow, or in `~/.opencandle/config.json`; environment variables override the config file. See [Configuration](https://opencandle.app/docs/configuration.html) for the full key reference and advanced switches.
161
89
 
162
90
  ## How It Fits Together
163
91
 
@@ -172,81 +100,21 @@ User prompt
172
100
  -> terminal or GUI session records the trace
173
101
  ```
174
102
 
175
- Project shape:
176
-
177
- ```text
178
- src/
179
- |-- providers/ API clients
180
- |-- tools/ Tool implementations by domain
181
- |-- infra/ Cache, rate limiter, HTTP client, paths
182
- |-- routing/ Request understanding, entity extraction, slot resolution
183
- |-- workflows/ WorkflowDefinition builders
184
- |-- runtime/ Session coordinator, workflow runner, runtime context
185
- |-- market-state/ Durable watchlists, portfolios, alerts, reports
186
- |-- memory/ SQLite-backed state and retrieval
187
- |-- sentiment/ Cross-source sentiment pipeline, scoring, adapters, trends
188
- |-- analysts/ Multi-analyst orchestration
189
- |-- pi/ Pi integration and session wiring
190
- |-- cli.ts CLI entry point
191
- |-- monitor.ts Local automation heartbeat command
192
- |-- tool-kit.ts Public add-on tool helpers
193
- `-- index.ts Public package exports
194
- ```
195
-
196
- Package exports:
197
-
198
- - `opencandle`
199
- - `opencandle/tool-kit`
200
- - `opencandle/infra`
201
- - `opencandle/types`
202
- - `opencandle/providers`
203
- - `opencandle/tools`
204
- - `opencandle/workflows`
205
-
206
- If you want to add a new first-party tool or publish an add-on package, start with [Build a Tool](https://opencandle.app/docs/build-a-tool.html).
103
+ To add a first-party tool or publish an add-on package, start with [Build a Tool](https://opencandle.app/docs/build-a-tool.html).
207
104
 
208
105
  ## Development
209
106
 
210
107
  ```bash
211
- npm start
212
- npm run gui
213
- npm run docs:site:build
214
- npm test
215
- npm run test:watch
216
- npm run test:e2e
217
- npm run test:e2e:cli
218
- npm run test:e2e:providers
219
- npm run eval
220
- npm run eval -- cases
221
- npm run eval -- product
222
- npm run eval -- competitive
223
- ```
224
-
225
- Baseline check:
226
-
227
- ```bash
228
- npm test
229
- ```
230
-
231
- The e2e, provider, and eval commands can hit live APIs, live model providers, or local agent CLIs. `npm run eval` lists every eval suite; run suites through `npm run eval -- <suite>`. Run them intentionally; see [Testing and Evals](https://opencandle.app/docs/testing-and-evals.html).
232
-
233
- Repository rules that matter:
234
-
235
- - Unit tests mock `globalThis.fetch` and use fixture JSON.
236
- - External calls go through shared cache and rate-limiter infrastructure.
237
- - Tools fetch and format; analysts and prompts synthesize.
238
- - Relative TypeScript imports use `.js` extensions.
239
- - Behavior changes should be test-first.
240
-
241
- For scripted full-session driving with file-based IPC:
242
-
243
- ```bash
244
- npx tsx tests/harness/cli.ts run --prompt "What is AAPL trading at?" --ipc /tmp/opencandle-ipc &
245
- npx tsx tests/harness/cli.ts wait --ipc /tmp/opencandle-ipc
246
- npx tsx tests/harness/cli.ts trace --ipc /tmp/opencandle-ipc
108
+ git clone https://github.com/Kahtaf/OpenCandle.git
109
+ cd OpenCandle
110
+ npm install
111
+ cp .env.example .env # optional provider keys (copy .env.example .env on Windows CMD)
112
+ npm start # terminal agent
113
+ npm run gui # browser GUI
114
+ npm test # unit tests
247
115
  ```
248
116
 
249
- See [tests/harness/README.md](https://github.com/Kahtaf/OpenCandle/blob/main/tests/harness/README.md) for the full flow.
117
+ Contributor conventions live in [AGENTS.md](AGENTS.md) and [CONTRIBUTING.md](CONTRIBUTING.md). The e2e, provider, and eval suites can hit live APIs and model providers — run them intentionally; see [Testing and Evals](https://opencandle.app/docs/testing-and-evals.html).
250
118
 
251
119
  ## Documentation
252
120
 
@@ -259,12 +127,12 @@ See [tests/harness/README.md](https://github.com/Kahtaf/OpenCandle/blob/main/tes
259
127
  - [Configuration](https://opencandle.app/docs/configuration.html)
260
128
  - [System Architecture](https://opencandle.app/docs/system-architecture.html)
261
129
  - [Testing and Evals](https://opencandle.app/docs/testing-and-evals.html)
262
- - [Comparisons](https://opencandle.app/docs/comparisons.html)
130
+ - [Why OpenCandle](https://opencandle.app/docs/comparisons.html)
263
131
 
264
- ## License
132
+ ## Community and Contributing
265
133
 
266
- MIT
134
+ Questions and bug reports are welcome in [GitHub Issues](https://github.com/Kahtaf/OpenCandle/issues) — redact API keys, account identifiers, holdings, and local state from public issues. Before opening large changes or reporting sensitive issues, see [Contributing](CONTRIBUTING.md), [Security](https://opencandle.app/docs/security.html), and the [Code of Conduct](CODE_OF_CONDUCT.md).
267
135
 
268
- ## Contributing and Security
136
+ ## License
269
137
 
270
- See [Contributing](https://opencandle.app/docs/contributing.html), [Security](https://opencandle.app/docs/security.html), and the [Code of Conduct](https://github.com/Kahtaf/OpenCandle/blob/main/CODE_OF_CONDUCT.md) before opening large changes or reporting sensitive issues. Redact API keys, account identifiers, holdings, and local state from public issues.
138
+ MIT see [LICENSE](LICENSE).
package/dist/cli-main.js CHANGED
@@ -5,7 +5,7 @@ import { createRequire } from "node:module";
5
5
  import { dirname, resolve } from "node:path";
6
6
  import { createInterface } from "node:readline/promises";
7
7
  import { fileURLToPath } from "node:url";
8
- import { AuthStorage, createAgentSessionRuntime, createAgentSessionServices, DefaultPackageManager, getAgentDir, InteractiveMode, initTheme, ModelRegistry, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
8
+ import { createAgentSessionRuntime, createAgentSessionServices, DefaultPackageManager, getAgentDir, InteractiveMode, initTheme, ModelRegistry, ModelRuntime, SessionManager, SettingsManager, } from "@earendil-works/pi-coding-agent";
9
9
  import { loadEnv } from "./config.js";
10
10
  import { handleDoctorCommand } from "./doctor/cli-command.js";
11
11
  import { createOpenCandleSession } from "./pi/session.js";
@@ -167,8 +167,11 @@ async function main() {
167
167
  }
168
168
  // Default: start the OpenCandle interactive agent
169
169
  const settingsManager = SettingsManager.create(cwd, agentDir);
170
- const authStorage = AuthStorage.create();
171
- const modelRegistry = ModelRegistry.create(authStorage);
170
+ const modelRuntime = await ModelRuntime.create({
171
+ authPath: resolve(agentDir, "auth.json"),
172
+ modelsPath: resolve(agentDir, "models.json"),
173
+ });
174
+ const modelRegistry = new ModelRegistry(modelRuntime);
172
175
  const shouldSuppressFallbackMessage = modelRegistry.getAvailable().length === 0;
173
176
  initTheme(settingsManager.getTheme(), true);
174
177
  const sessionManager = continueOpenCandleSession(cwd);
@@ -190,7 +193,7 @@ async function main() {
190
193
  if (!model) {
191
194
  return "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
192
195
  }
193
- if (!session.modelRegistry.hasConfiguredAuth(model)) {
196
+ if (!session.modelRuntime.hasConfiguredAuth(model.provider)) {
194
197
  return "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
195
198
  }
196
199
  return null;
@@ -239,16 +242,14 @@ async function main() {
239
242
  const services = await createAgentSessionServices({
240
243
  cwd: opts.cwd,
241
244
  agentDir: opts.agentDir,
242
- authStorage,
243
245
  settingsManager,
244
- modelRegistry,
246
+ modelRuntime,
245
247
  });
246
248
  const result = await createOpenCandleSession({
247
249
  cwd: opts.cwd,
248
250
  agentDir: opts.agentDir,
249
251
  settingsManager,
250
- authStorage,
251
- modelRegistry,
252
+ modelRuntime,
252
253
  sessionManager: opts.sessionManager,
253
254
  bindExtensions: false,
254
255
  });
package/dist/config.d.ts CHANGED
@@ -19,6 +19,7 @@ export interface Config {
19
19
  braveApiKey?: string;
20
20
  exaApiKey?: string;
21
21
  finnhubApiKey?: string;
22
+ lseApiKey?: string;
22
23
  /**
23
24
  * Intent-router mode. The LLM router is the only production routing path;
24
25
  * `OPENCANDLE_ROUTER_MODE` accepts only `"llm"` (or unset). The removed
@@ -56,6 +57,9 @@ export interface OpenCandleFileConfig {
56
57
  finnhub?: {
57
58
  apiKey?: string;
58
59
  };
60
+ lse?: {
61
+ apiKey?: string;
62
+ };
59
63
  };
60
64
  sentiment?: {
61
65
  retentionDays?: number;
package/dist/config.js CHANGED
@@ -108,6 +108,7 @@ function resolveConfig(fileConfig) {
108
108
  braveApiKey: process.env.BRAVE_API_KEY ?? fileConfig.providers?.brave?.apiKey,
109
109
  exaApiKey: process.env.EXA_API_KEY ?? fileConfig.providers?.exa?.apiKey,
110
110
  finnhubApiKey: process.env.FINNHUB_API_KEY ?? fileConfig.providers?.finnhub?.apiKey,
111
+ lseApiKey: process.env.LSE_API_KEY ?? fileConfig.providers?.lse?.apiKey,
111
112
  routerMode: resolveRouterMode(),
112
113
  toolScopeMode: resolveToolScopeMode(),
113
114
  planningMigrationStatuses: resolvePlanningMigrationStatuses(),
@@ -1,4 +1,4 @@
1
- import { AuthStorage, ModelRegistry, SettingsManager } from "@earendil-works/pi-coding-agent";
1
+ import { ModelRegistry, ModelRuntime, SettingsManager } from "@earendil-works/pi-coding-agent";
2
2
  import { getProvider } from "../onboarding/providers.js";
3
3
  import { clearProviderOnboardingEntry, loadOnboardingState, saveOnboardingState, } from "../onboarding/state.js";
4
4
  import { renderDoctorReport } from "./render.js";
@@ -27,8 +27,11 @@ export async function handleDoctorCommand(args, cwd, agentDir) {
27
27
  if (!json)
28
28
  console.log(`Re-enabled ${providerId}.`);
29
29
  }
30
- const authStorage = AuthStorage.create();
31
- const modelRegistry = ModelRegistry.create(authStorage);
30
+ const modelRuntime = await ModelRuntime.create({
31
+ authPath: `${agentDir}/auth.json`,
32
+ modelsPath: `${agentDir}/models.json`,
33
+ });
34
+ const modelRegistry = new ModelRegistry(modelRuntime);
32
35
  const settingsManager = SettingsManager.create(cwd, agentDir);
33
36
  const includeSessions = args.includes("--sessions");
34
37
  if (includeSessions) {
@@ -47,7 +50,7 @@ export async function handleDoctorCommand(args, cwd, agentDir) {
47
50
  return true;
48
51
  }
49
52
  function buildCliModelSetupState(modelRegistry, settingsManager) {
50
- modelRegistry.refresh();
53
+ void modelRegistry.refresh();
51
54
  const provider = settingsManager.getDefaultProvider();
52
55
  const modelId = settingsManager.getDefaultModel();
53
56
  const activeModel = provider && modelId ? modelRegistry.find(provider, modelId) : undefined;
@@ -2,6 +2,8 @@ import { accessSync, constants, existsSync, readFileSync, statSync } from "node:
2
2
  import { createRequire } from "node:module";
3
3
  import { dirname, resolve } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { getConfig } from "../config.js";
6
+ import { getUsage, isOverSoftThreshold, LSE_MONTHLY_BYTE_CAP } from "../infra/lse-byte-budget.js";
5
7
  import { getUnsupportedNodeVersionMessage } from "../infra/node-version.js";
6
8
  import { getConfigPath, getOnboardingPath, getOpenCandleHomeDir, getStateDbPath, resolveOpenCandlePath, } from "../infra/opencandle-paths.js";
7
9
  import { probeAllProviderStatuses, probeProviderStatus, } from "../onboarding/provider-status.js";
@@ -262,6 +264,17 @@ async function buildProviderChecks(options) {
262
264
  respectSkipped: true,
263
265
  }));
264
266
  const checks = statuses.map((status) => providerStatusCheck(status));
267
+ if (getConfig().lseApiKey && isOverSoftThreshold()) {
268
+ const percentUsed = Math.round((getUsage().bytesUsed / LSE_MONTHLY_BYTE_CAP) * 100);
269
+ checks.push({
270
+ id: "provider.lse.byte_budget",
271
+ label: "London Strategic Edge",
272
+ status: "warn",
273
+ capability: "optional",
274
+ summary: `London Strategic Edge monthly data allowance is ${percentUsed}% used; answers use Yahoo/Alpha Vantage until the month resets.`,
275
+ remediation: "Wait for the monthly allowance to reset before relying on this provider again.",
276
+ });
277
+ }
265
278
  for (const provider of providers.filter(isExternalToolProvider)) {
266
279
  const installStatus = statuses.find((status) => status.providerId === provider.id && status.kind === "external-tool");
267
280
  if (installStatus?.state === "skipped")
@@ -10,6 +10,7 @@ export function sessionEntriesToChatEvents(entries, options) {
10
10
  let pendingOriginalAttachments = [];
11
11
  let lastEntryWasUserMessage = false;
12
12
  let lastUserCompletedEventIndex = null;
13
+ const workflowSteps = workflowStepMetadata(entries);
13
14
  const updatedAt = options.updatedAt ?? entries.at(-1)?.timestamp ?? new Date().toISOString();
14
15
  events.push({
15
16
  type: "session.updated",
@@ -53,26 +54,43 @@ export function sessionEntriesToChatEvents(entries, options) {
53
54
  const message = entry.message;
54
55
  const messageId = entry.id;
55
56
  if (message.role === "user") {
56
- events.push({
57
- type: "message.created",
58
- sessionId: options.sessionId,
59
- messageId,
60
- role: "user",
61
- seq: seq++,
62
- });
63
- const completedEvent = {
64
- type: "message.completed",
65
- sessionId: options.sessionId,
66
- messageId,
67
- content: userMessageContent(message.content, pendingOriginalInput),
68
- ...(pendingOriginalAttachments.length > 0
69
- ? { attachments: pendingOriginalAttachments }
70
- : {}),
71
- seq: seq++,
72
- };
73
- events.push(completedEvent);
74
- lastUserCompletedEventIndex = events.length - 1;
75
- lastEntryWasUserMessage = true;
57
+ const workflowStep = workflowSteps.get(messageId);
58
+ if (!workflowStep || workflowStep.preserveUserTurn) {
59
+ events.push({
60
+ type: "message.created",
61
+ sessionId: options.sessionId,
62
+ messageId,
63
+ role: "user",
64
+ seq: seq++,
65
+ });
66
+ const completedEvent = {
67
+ type: "message.completed",
68
+ sessionId: options.sessionId,
69
+ messageId,
70
+ content: userMessageContent(message.content, pendingOriginalInput),
71
+ ...(pendingOriginalAttachments.length > 0
72
+ ? { attachments: pendingOriginalAttachments }
73
+ : {}),
74
+ seq: seq++,
75
+ };
76
+ events.push(completedEvent);
77
+ lastUserCompletedEventIndex = events.length - 1;
78
+ lastEntryWasUserMessage = true;
79
+ }
80
+ else {
81
+ lastEntryWasUserMessage = false;
82
+ }
83
+ if (workflowStep) {
84
+ events.push({
85
+ type: "custom.message",
86
+ sessionId: options.sessionId,
87
+ messageId: `workflow-step-${messageId}`,
88
+ customType: "opencandle-workflow-step",
89
+ content: userMessageContent(message.content, null),
90
+ details: workflowStep,
91
+ seq: seq++,
92
+ });
93
+ }
76
94
  pendingOriginalInput = null;
77
95
  pendingOriginalAttachments = [];
78
96
  continue;
@@ -165,6 +183,113 @@ export function sessionEntriesToChatEvents(entries, options) {
165
183
  }
166
184
  return events;
167
185
  }
186
+ function workflowStepMetadata(entries) {
187
+ const groups = [];
188
+ let activeGroup = null;
189
+ let pendingOriginalInput = false;
190
+ let awaitingValidationPrompt = false;
191
+ for (const entry of entries) {
192
+ if (isCustomEntry(entry, "opencandle-workflow")) {
193
+ activeGroup = [];
194
+ groups.push(activeGroup);
195
+ pendingOriginalInput = false;
196
+ awaitingValidationPrompt = false;
197
+ continue;
198
+ }
199
+ if (isOriginalInputEntry(entry)) {
200
+ if (awaitingValidationPrompt) {
201
+ activeGroup = null;
202
+ awaitingValidationPrompt = false;
203
+ }
204
+ if (activeGroup)
205
+ pendingOriginalInput = true;
206
+ continue;
207
+ }
208
+ if (entry.type === "message" && entry.message.role === "user") {
209
+ if (activeGroup) {
210
+ const candidate = {
211
+ messageId: entry.id,
212
+ preserveUserTurn: pendingOriginalInput,
213
+ };
214
+ if (awaitingValidationPrompt) {
215
+ candidate.stage = "validation";
216
+ candidate.label = "Validation and synthesis";
217
+ }
218
+ activeGroup.push(candidate);
219
+ if (awaitingValidationPrompt) {
220
+ activeGroup = null;
221
+ awaitingValidationPrompt = false;
222
+ }
223
+ }
224
+ pendingOriginalInput = false;
225
+ continue;
226
+ }
227
+ if (isCustomEntry(entry, "opencandle-analyst-step")) {
228
+ const data = customEntryData(entry);
229
+ const stage = stringField(data, "stage") ?? "analysis";
230
+ assignPendingWorkflowSteps(activeGroup, stage, workflowStepLabel(stage, data));
231
+ continue;
232
+ }
233
+ if (isCustomEntry(entry, "opencandle-validation")) {
234
+ assignPendingWorkflowSteps(activeGroup, "validation", "Validation and synthesis");
235
+ awaitingValidationPrompt = activeGroup !== null;
236
+ pendingOriginalInput = false;
237
+ }
238
+ }
239
+ const metadata = new Map();
240
+ for (const group of groups) {
241
+ group.forEach((candidate, index) => {
242
+ metadata.set(candidate.messageId, {
243
+ label: candidate.label ?? "Workflow step",
244
+ stage: candidate.stage ?? "workflow",
245
+ step: index + 1,
246
+ total: group.length,
247
+ preserveUserTurn: candidate.preserveUserTurn,
248
+ });
249
+ });
250
+ }
251
+ return metadata;
252
+ }
253
+ function assignPendingWorkflowSteps(group, stage, label) {
254
+ if (!group)
255
+ return;
256
+ for (let index = group.length - 1; index >= 0; index -= 1) {
257
+ const candidate = group[index];
258
+ if (candidate.stage)
259
+ continue;
260
+ candidate.stage = stage;
261
+ candidate.label = label;
262
+ return;
263
+ }
264
+ }
265
+ function workflowStepLabel(stage, data) {
266
+ const role = stringField(data, "role");
267
+ if (stage.startsWith("analyst_")) {
268
+ return `${sentenceCase(role ?? stage.slice("analyst_".length))} analyst`;
269
+ }
270
+ if (stage === "debate_bull")
271
+ return "Bull researcher";
272
+ if (stage === "debate_bear")
273
+ return "Bear researcher";
274
+ if (stage === "debate_rebuttal")
275
+ return "Debate rebuttal";
276
+ return sentenceCase(stage.replaceAll("_", " "));
277
+ }
278
+ function sentenceCase(value) {
279
+ const normalized = value.trim().toLowerCase();
280
+ return normalized ? `${normalized[0].toUpperCase()}${normalized.slice(1)}` : "Workflow step";
281
+ }
282
+ function isCustomEntry(entry, customType) {
283
+ return entry.type === "custom" && entry.customType === customType;
284
+ }
285
+ function customEntryData(entry) {
286
+ const data = entry.data;
287
+ return typeof data === "object" && data !== null ? data : {};
288
+ }
289
+ function stringField(data, field) {
290
+ const value = data[field];
291
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
292
+ }
168
293
  function applyOriginalInput(event, originalInput, attachments) {
169
294
  if (event?.type !== "message.completed")
170
295
  return;
@@ -0,0 +1,8 @@
1
+ export declare class HistorySnapshotStore<T> {
2
+ private readonly maxAgeMs;
3
+ private readonly now;
4
+ private readonly entries;
5
+ constructor(maxAgeMs?: number, now?: () => number);
6
+ get(key: string, build: () => Promise<T>): Promise<T>;
7
+ clear(): void;
8
+ }