opencandle 0.10.0 → 0.11.1
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.
- package/README.md +12 -9
- package/dist/analysts/orchestrator.d.ts +3 -4
- package/dist/analysts/orchestrator.js +34 -44
- package/dist/analysts/orchestrator.js.map +1 -1
- package/dist/cli-main.js +242 -9
- package/dist/cli-main.js.map +1 -1
- package/dist/config.d.ts +4 -9
- package/dist/config.js +7 -10
- package/dist/config.js.map +1 -1
- package/dist/doctor/report.js +1 -1
- package/dist/doctor/report.js.map +1 -1
- package/dist/infra/cache.d.ts +6 -0
- package/dist/infra/cache.js +16 -6
- package/dist/infra/cache.js.map +1 -1
- package/dist/infra/freshness.d.ts +21 -0
- package/dist/infra/freshness.js +119 -0
- package/dist/infra/freshness.js.map +1 -0
- package/dist/infra/market-calendar.d.ts +14 -0
- package/dist/infra/market-calendar.js +137 -0
- package/dist/infra/market-calendar.js.map +1 -0
- package/dist/infra/rate-limiter.js +1 -0
- package/dist/infra/rate-limiter.js.map +1 -1
- package/dist/market-state/daily-report.d.ts +1 -1
- package/dist/market-state/daily-report.js +5 -16
- package/dist/market-state/daily-report.js.map +1 -1
- package/dist/market-state/service.d.ts +0 -35
- package/dist/market-state/service.js +0 -63
- package/dist/market-state/service.js.map +1 -1
- package/dist/market-state/summaries.d.ts +8 -0
- package/dist/market-state/summaries.js +70 -0
- package/dist/market-state/summaries.js.map +1 -0
- package/dist/memory/sqlite.js +20 -19
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/onboarding/providers.d.ts +25 -1
- package/dist/onboarding/providers.js +26 -0
- package/dist/onboarding/providers.js.map +1 -1
- package/dist/pi/opencandle-extension.js +57 -226
- package/dist/pi/opencandle-extension.js.map +1 -1
- package/dist/pi/session-action-dedupe.d.ts +6 -0
- package/dist/pi/session-action-dedupe.js +126 -0
- package/dist/pi/session-action-dedupe.js.map +1 -0
- package/dist/pi/session-writer-lock.d.ts +26 -2
- package/dist/pi/session-writer-lock.js +230 -18
- package/dist/pi/session-writer-lock.js.map +1 -1
- package/dist/pi/setup.js +5 -5
- package/dist/pi/setup.js.map +1 -1
- package/dist/pi/tui-session-coordinator.d.ts +15 -0
- package/dist/pi/tui-session-coordinator.js +283 -0
- package/dist/pi/tui-session-coordinator.js.map +1 -0
- package/dist/prompts/context-builder.js +1 -1
- package/dist/prompts/policy-cards.js +1 -1
- package/dist/prompts/policy-cards.js.map +1 -1
- package/dist/providers/alpha-vantage.js +6 -0
- package/dist/providers/alpha-vantage.js.map +1 -1
- package/dist/providers/coingecko.js +1 -0
- package/dist/providers/coingecko.js.map +1 -1
- package/dist/providers/polymarket.d.ts +2 -0
- package/dist/providers/polymarket.js +163 -0
- package/dist/providers/polymarket.js.map +1 -0
- package/dist/providers/wrap-provider.js +5 -3
- package/dist/providers/wrap-provider.js.map +1 -1
- package/dist/providers/yahoo-finance.d.ts +3 -0
- package/dist/providers/yahoo-finance.js +206 -10
- package/dist/providers/yahoo-finance.js.map +1 -1
- package/dist/routing/classify-intent.js +4 -5
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/entity-extractor.js +16 -0
- package/dist/routing/entity-extractor.js.map +1 -1
- package/dist/routing/route-manifest.js +1 -2
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-llm-client.js +18 -3
- package/dist/routing/router-llm-client.js.map +1 -1
- package/dist/routing/router-prompt.js +1 -1
- package/dist/routing/router.d.ts +1 -1
- package/dist/routing/router.js +309 -9
- package/dist/routing/router.js.map +1 -1
- package/dist/routing/slot-resolver.d.ts +1 -0
- package/dist/routing/slot-resolver.js +1 -1
- package/dist/routing/slot-resolver.js.map +1 -1
- package/dist/runtime/evidence.d.ts +1 -0
- package/dist/runtime/evidence.js.map +1 -1
- package/dist/runtime/numeric-claims.d.ts +23 -0
- package/dist/runtime/numeric-claims.js +100 -0
- package/dist/runtime/numeric-claims.js.map +1 -0
- package/dist/runtime/planning-evidence.js +1 -76
- package/dist/runtime/planning-evidence.js.map +1 -1
- package/dist/runtime/prompt-step.d.ts +10 -1
- package/dist/runtime/prompt-step.js +116 -2
- package/dist/runtime/prompt-step.js.map +1 -1
- package/dist/runtime/session-coordinator.d.ts +11 -1
- package/dist/runtime/session-coordinator.js +354 -62
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/runtime/validation.d.ts +3 -1
- package/dist/runtime/workflow-events.d.ts +1 -1
- package/dist/runtime/workflow-events.js.map +1 -1
- package/dist/runtime/workflow-types.d.ts +3 -0
- package/dist/runtime/workflow-types.js.map +1 -1
- package/dist/system-prompt.js +1 -1
- package/dist/tools/fundamentals/comps.d.ts +1 -0
- package/dist/tools/fundamentals/comps.js +20 -9
- package/dist/tools/fundamentals/comps.js.map +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +190 -76
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +1 -1
- package/dist/tools/index.js +3 -3
- package/dist/tools/index.js.map +1 -1
- package/dist/tools/macro/event-probabilities.d.ts +8 -0
- package/dist/tools/macro/event-probabilities.js +121 -0
- package/dist/tools/macro/event-probabilities.js.map +1 -0
- package/dist/tools/market/crypto-price.d.ts +4 -1
- package/dist/tools/market/crypto-price.js +10 -1
- package/dist/tools/market/crypto-price.js.map +1 -1
- package/dist/tools/market/screen-stocks.js +14 -3
- package/dist/tools/market/screen-stocks.js.map +1 -1
- package/dist/tools/market/stock-quote.d.ts +4 -1
- package/dist/tools/market/stock-quote.js +9 -4
- package/dist/tools/market/stock-quote.js.map +1 -1
- package/dist/tools/options/option-chain.d.ts +4 -1
- package/dist/tools/options/option-chain.js +12 -1
- package/dist/tools/options/option-chain.js.map +1 -1
- package/dist/tools/portfolio/daily-report.js +10 -4
- package/dist/tools/portfolio/daily-report.js.map +1 -1
- package/dist/tools/portfolio/risk-analysis.js +1 -1
- package/dist/tools/portfolio/risk-analysis.js.map +1 -1
- package/dist/tools/technical/backtest.d.ts +23 -5
- package/dist/tools/technical/backtest.js +131 -94
- package/dist/tools/technical/backtest.js.map +1 -1
- package/dist/tools/technical/indicators.js +1 -1
- package/dist/tools/technical/indicators.js.map +1 -1
- package/dist/types/fundamentals.d.ts +1 -0
- package/dist/types/index.d.ts +1 -0
- package/dist/types/market.d.ts +2 -0
- package/dist/types/options.d.ts +1 -0
- package/dist/types/prediction-markets.d.ts +13 -0
- package/dist/types/prediction-markets.js +2 -0
- package/dist/types/prediction-markets.js.map +1 -0
- package/gui/server/chat-event-adapter.ts +89 -5
- package/gui/server/http-routes.ts +661 -37
- package/gui/server/invoke-tool.ts +165 -16
- package/gui/server/live-chat-event-adapter.ts +37 -2
- package/gui/server/local-session-coordinator.ts +97 -0
- package/gui/server/market-state-api.ts +64 -48
- package/gui/server/projector.ts +132 -3
- package/gui/server/server.ts +51 -6
- package/gui/server/session-actions.ts +146 -45
- package/gui/server/session-entry-wait.ts +22 -3
- package/gui/server/tool-metadata.ts +3 -1
- package/gui/server/ws-hub.ts +77 -10
- package/gui/shared/chat-events.ts +10 -1
- package/gui/shared/event-reducer.ts +1 -0
- package/gui/web/dist/assets/CatalogOverlay-DZ1niyQm.js +1 -0
- package/gui/web/dist/assets/index-D4F9AJnn.css +2 -0
- package/gui/web/dist/assets/index-DtqMMBTr.js +65 -0
- package/gui/web/dist/index.html +2 -2
- package/package.json +13 -15
- package/src/analysts/orchestrator.ts +35 -56
- package/src/cli-main.ts +253 -13
- package/src/config.ts +12 -20
- package/src/doctor/report.ts +1 -1
- package/src/infra/cache.ts +24 -11
- package/src/infra/freshness.ts +165 -0
- package/src/infra/market-calendar.ts +193 -0
- package/src/infra/rate-limiter.ts +1 -0
- package/src/market-state/daily-report.ts +6 -16
- package/src/market-state/service.ts +0 -136
- package/src/market-state/summaries.ts +75 -0
- package/src/memory/sqlite.ts +23 -19
- package/src/onboarding/providers.ts +29 -1
- package/src/pi/opencandle-extension.ts +67 -270
- package/src/pi/session-action-dedupe.ts +155 -0
- package/src/pi/session-writer-lock.ts +290 -20
- package/src/pi/setup.ts +6 -6
- package/src/pi/tui-session-coordinator.ts +351 -0
- package/src/prompts/context-builder.ts +1 -1
- package/src/prompts/policy-cards.ts +1 -1
- package/src/providers/alpha-vantage.ts +6 -0
- package/src/providers/coingecko.ts +2 -0
- package/src/providers/polymarket.ts +214 -0
- package/src/providers/wrap-provider.ts +5 -3
- package/src/providers/yahoo-finance.ts +339 -27
- package/src/routing/classify-intent.ts +4 -5
- package/src/routing/entity-extractor.ts +17 -0
- package/src/routing/route-manifest.ts +1 -2
- package/src/routing/router-llm-client.ts +31 -19
- package/src/routing/router-prompt.ts +1 -1
- package/src/routing/router.ts +379 -8
- package/src/routing/slot-resolver.ts +1 -1
- package/src/runtime/evidence.ts +1 -0
- package/src/runtime/numeric-claims.ts +108 -0
- package/src/runtime/planning-evidence.ts +6 -91
- package/src/runtime/prompt-step.ts +127 -2
- package/src/runtime/session-coordinator.ts +464 -74
- package/src/runtime/validation.ts +1 -1
- package/src/runtime/workflow-events.ts +2 -0
- package/src/runtime/workflow-types.ts +3 -0
- package/src/system-prompt.ts +1 -1
- package/src/tools/AGENTS.md +1 -1
- package/src/tools/fundamentals/comps.ts +25 -14
- package/src/tools/fundamentals/dcf.ts +217 -87
- package/src/tools/index.ts +3 -3
- package/src/tools/macro/event-probabilities.ts +141 -0
- package/src/tools/market/crypto-price.ts +14 -2
- package/src/tools/market/screen-stocks.ts +14 -3
- package/src/tools/market/stock-quote.ts +49 -43
- package/src/tools/options/option-chain.ts +16 -2
- package/src/tools/portfolio/daily-report.ts +10 -4
- package/src/tools/portfolio/risk-analysis.ts +1 -1
- package/src/tools/technical/backtest.ts +167 -108
- package/src/tools/technical/indicators.ts +1 -1
- package/src/types/fundamentals.ts +1 -0
- package/src/types/index.ts +1 -0
- package/src/types/market.ts +2 -0
- package/src/types/options.ts +1 -0
- package/src/types/prediction-markets.ts +13 -0
- package/dist/tools/portfolio/predictions.d.ts +0 -55
- package/dist/tools/portfolio/predictions.js +0 -422
- package/dist/tools/portfolio/predictions.js.map +0 -1
- package/gui/server/chat-run-session.ts +0 -16
- package/gui/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
- package/gui/web/dist/assets/index-B7QAjY5g.js +0 -65
- package/gui/web/dist/assets/index-D5dbWPfM.css +0 -2
- package/src/tools/portfolio/predictions.ts +0 -553
package/gui/web/dist/index.html
CHANGED
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
9
9
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
10
10
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet" />
|
|
11
|
-
<script type="module" crossorigin src="/assets/index-
|
|
12
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<script type="module" crossorigin src="/assets/index-DtqMMBTr.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-D4F9AJnn.css">
|
|
13
13
|
</head>
|
|
14
14
|
<body>
|
|
15
15
|
<div id="root"></div>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencandle",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.11.1",
|
|
4
4
|
"description": "Financial trading & investing agent",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"homepage": "https://opencandle.app",
|
|
@@ -14,7 +14,9 @@
|
|
|
14
14
|
"type": "module",
|
|
15
15
|
"workspaces": [
|
|
16
16
|
"gui/server",
|
|
17
|
-
"gui/web"
|
|
17
|
+
"gui/web",
|
|
18
|
+
"packages/ui",
|
|
19
|
+
"website"
|
|
18
20
|
],
|
|
19
21
|
"bin": {
|
|
20
22
|
"opencandle": "dist/cli.js"
|
|
@@ -86,7 +88,7 @@
|
|
|
86
88
|
"prestart": "npm run check:node",
|
|
87
89
|
"start": "tsx src/cli.ts",
|
|
88
90
|
"gui:web:build": "npm --workspace @opencandle/gui-web run build",
|
|
89
|
-
"docs:site:build": "
|
|
91
|
+
"docs:site:build": "npm --workspace @opencandle/website run build",
|
|
90
92
|
"docs:site:serve": "node website/serve.mjs",
|
|
91
93
|
"docs:links:check": "node scripts/check-public-doc-links.mjs",
|
|
92
94
|
"gui": "tsx gui/server/server.ts",
|
|
@@ -103,15 +105,12 @@
|
|
|
103
105
|
"test:e2e:credential-soft-fallback": "tsx tests/e2e/credential-soft-fallback.test.ts",
|
|
104
106
|
"test:e2e:credential-per-workflow-cap": "tsx tests/e2e/credential-per-workflow-cap.test.ts",
|
|
105
107
|
"test:gui:browser": "OPENCANDLE_GUI_BROWSER=1 vitest run --config vitest.config.gui.ts",
|
|
108
|
+
"test:gui:release-smoke": "npm run gui:web:build && OPENCANDLE_GUI_RELEASE_SMOKE=1 vitest run --config vitest.config.gui-release.ts",
|
|
106
109
|
"test:e2e:providers": "tsx tests/e2e/providers.test.ts",
|
|
107
|
-
"test:
|
|
108
|
-
"eval
|
|
109
|
-
"test:evals:usually": "EVAL_TIER=usually vitest run --config vitest.config.evals.ts",
|
|
110
|
-
"test:evals:product": "tsx tests/scripts/run-product-evals.ts",
|
|
111
|
-
"test:evals:competitive": "tsx tests/scripts/run-competitive-finance-eval.ts",
|
|
110
|
+
"test:e2e:harness-dcf": "tsx tests/e2e/harness-dcf.test.ts",
|
|
111
|
+
"eval": "tsx tests/scripts/run-evals.ts",
|
|
112
112
|
"test:packed-install": "node scripts/packed-install-smoke.mjs",
|
|
113
|
-
"
|
|
114
|
-
"review:pr": ".agents/skills/autoreview/scripts/autoreview --mode branch --prompt-file .agents/skills/autoreview/references/opencandle-review.md --parallel-tests \"npx tsc --noEmit && npx vitest run\"",
|
|
113
|
+
"review:pr": ".agents/skills/autoreview/scripts/autoreview --mode branch --prompt-file .agents/skills/autoreview/references/opencandle-review.md --parallel-tests \"npx tsc --noEmit && npx biome ci . && npx vitest run\"",
|
|
115
114
|
"package:contents:check": "node scripts/check-package-contents.mjs",
|
|
116
115
|
"version:patch": "npm version patch --no-git-tag-version",
|
|
117
116
|
"version:minor": "npm version minor --no-git-tag-version",
|
|
@@ -119,7 +118,7 @@
|
|
|
119
118
|
"version:set": "npm version",
|
|
120
119
|
"prepublishOnly": "npm run release:check",
|
|
121
120
|
"publish:dry": "npm run release:check && npm publish --access public --dry-run",
|
|
122
|
-
"release:check": "npx tsc --noEmit && npx biome ci . && npm test && npm run docs:site:build && npm run package:contents:check && npm run test:packed-install && npm run docs:links:check",
|
|
121
|
+
"release:check": "npx tsc --noEmit && npx biome ci . && npm test && npm run test:gui:release-smoke && npm run docs:site:build && npm run package:contents:check && npm run test:packed-install && npm run docs:links:check",
|
|
123
122
|
"release:patch": "node scripts/release.mjs patch",
|
|
124
123
|
"release:minor": "node scripts/release.mjs minor",
|
|
125
124
|
"release:major": "node scripts/release.mjs major"
|
|
@@ -140,17 +139,16 @@
|
|
|
140
139
|
"@sinclair/typebox": "*"
|
|
141
140
|
},
|
|
142
141
|
"devDependencies": {
|
|
143
|
-
"@agentclientprotocol/claude-agent-acp": "^0.
|
|
142
|
+
"@agentclientprotocol/claude-agent-acp": "^0.55.0",
|
|
144
143
|
"@agentclientprotocol/codex-acp": "^1.0.1",
|
|
145
144
|
"@biomejs/biome": "^2.5.1",
|
|
146
145
|
"@sinclair/typebox": "^0.34.49",
|
|
147
146
|
"@types/better-sqlite3": "^7.6.13",
|
|
148
147
|
"@types/node": "^26.0.1",
|
|
149
|
-
"acpx": "^0.
|
|
148
|
+
"acpx": "^0.12.0",
|
|
150
149
|
"playwright-core": "^1.61.1",
|
|
151
150
|
"typescript": "^6.0.3",
|
|
152
151
|
"vite": "^8.1.0",
|
|
153
|
-
"vitest": "^4.1.9"
|
|
154
|
-
"vitest-evals": "^0.14.0"
|
|
152
|
+
"vitest": "^4.1.9"
|
|
155
153
|
}
|
|
156
154
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { WorkflowDefinition } from "../runtime/prompt-step.js";
|
|
2
2
|
import { promptStep } from "../runtime/prompt-step.js";
|
|
3
|
+
import type { AnalystOutput } from "../runtime/workflow-types.js";
|
|
4
|
+
import { isAnalystSplit, tallyVotes } from "./contracts.js";
|
|
3
5
|
|
|
4
6
|
export type AnalystRole = "valuation" | "momentum" | "options" | "contrarian" | "risk";
|
|
5
7
|
|
|
@@ -115,7 +117,24 @@ CONCESSIONS: [bullet list of points you concede]
|
|
|
115
117
|
REMAINING CONVICTION: [1-10, where 10 = fully confident despite bear case]`;
|
|
116
118
|
}
|
|
117
119
|
|
|
120
|
+
export function buildAnalystVoteTallyBlock(outputs: AnalystOutput[]): string | null {
|
|
121
|
+
if (outputs.length < 2) return null;
|
|
122
|
+
const tally = tallyVotes(outputs);
|
|
123
|
+
const split = isAnalystSplit(outputs);
|
|
124
|
+
return `## Deterministic Analyst Vote Tally
|
|
125
|
+
- Parsed analyst outputs: ${outputs.length}
|
|
126
|
+
- BUY: ${tally.buy}
|
|
127
|
+
- HOLD: ${tally.hold}
|
|
128
|
+
- SELL: ${tally.sell}
|
|
129
|
+
- Weighted average conviction: ${tally.weightedConviction}
|
|
130
|
+
- Computed verdict: ${tally.verdict}
|
|
131
|
+
- BUY+SELL split: ${split ? "yes" : "no"}`;
|
|
132
|
+
}
|
|
133
|
+
|
|
118
134
|
export function buildSynthesisPrompt(symbol: string): string {
|
|
135
|
+
// The runtime tally block is injected at dispatch time by
|
|
136
|
+
// SessionCoordinator.prepareWorkflowPrompt, where parsed analyst outputs
|
|
137
|
+
// exist; this builder runs at workflow-definition time and stays static.
|
|
119
138
|
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.
|
|
120
139
|
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.
|
|
121
140
|
|
|
@@ -148,34 +167,11 @@ Additionally check:
|
|
|
148
167
|
|
|
149
168
|
Output: VALIDATED if all checks pass, or list specific corrections needed.`;
|
|
150
169
|
|
|
151
|
-
const SYNTHESIS_PROMPT_NO_DEBATE = (symbol: string) =>
|
|
152
|
-
`**[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:
|
|
153
|
-
1. **Vote Tally**: X BUY, Y HOLD, Z SELL — weighted average conviction
|
|
154
|
-
2. **Verdict**: Buy, Hold, or Sell — based on the signal consensus
|
|
155
|
-
3. **Key thesis** in 2-3 sentences
|
|
156
|
-
4. **Bull case** — what could go right
|
|
157
|
-
5. **Bear case** — what could go wrong
|
|
158
|
-
6. **Key levels** — entry, stop-loss, and target prices
|
|
159
|
-
7. **Position sizing recommendation** based on risk profile
|
|
160
|
-
|
|
161
|
-
Be direct and actionable. This is your final word on ${symbol}.`;
|
|
162
|
-
|
|
163
|
-
const VALIDATION_PROMPT_NO_DEBATE = (symbol: string) =>
|
|
164
|
-
`**[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.`;
|
|
165
|
-
|
|
166
170
|
export function getInitialAnalysisPrompt(symbol: string): string {
|
|
167
171
|
return `Begin comprehensive analysis of ${symbol}. Start by getting the current stock quote.`;
|
|
168
172
|
}
|
|
169
173
|
|
|
170
|
-
export
|
|
171
|
-
debate?: boolean;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
export function buildComprehensiveAnalysisDefinition(
|
|
175
|
-
symbol: string,
|
|
176
|
-
options?: ComprehensiveAnalysisOptions,
|
|
177
|
-
): WorkflowDefinition {
|
|
178
|
-
const debate = options?.debate ?? true;
|
|
174
|
+
export function buildComprehensiveAnalysisDefinition(symbol: string): WorkflowDefinition {
|
|
179
175
|
const roles: AnalystRole[] = ["valuation", "momentum", "options", "contrarian", "risk"];
|
|
180
176
|
|
|
181
177
|
const analystOutputs = roles.map((r) => `${r}_signal`);
|
|
@@ -193,45 +189,28 @@ export function buildComprehensiveAnalysisDefinition(
|
|
|
193
189
|
),
|
|
194
190
|
];
|
|
195
191
|
|
|
196
|
-
if (debate) {
|
|
197
|
-
return {
|
|
198
|
-
workflowType: "comprehensive_analysis",
|
|
199
|
-
steps: [
|
|
200
|
-
...analystSteps,
|
|
201
|
-
promptStep("debate_bull", "Bull researcher case", buildBullPrompt(symbol), {
|
|
202
|
-
requiredInputs: analystOutputs,
|
|
203
|
-
expectedOutputs: ["bull_thesis"],
|
|
204
|
-
}),
|
|
205
|
-
promptStep("debate_bear", "Bear researcher case", buildBearPrompt(symbol), {
|
|
206
|
-
requiredInputs: [...analystOutputs, "bull_thesis"],
|
|
207
|
-
expectedOutputs: ["bear_thesis"],
|
|
208
|
-
}),
|
|
209
|
-
promptStep("debate_rebuttal", "Bull rebuttal (self-gating)", buildRebuttalPrompt(symbol), {
|
|
210
|
-
requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis"],
|
|
211
|
-
expectedOutputs: ["rebuttal"],
|
|
212
|
-
}),
|
|
213
|
-
promptStep("synthesis", "Resolve the debate", buildSynthesisPrompt(symbol), {
|
|
214
|
-
requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis", "rebuttal"],
|
|
215
|
-
expectedOutputs: ["verdict"],
|
|
216
|
-
}),
|
|
217
|
-
promptStep("validation", "Validate cited numbers", VALIDATION_PROMPT_DEBATE(symbol), {
|
|
218
|
-
skippable: true,
|
|
219
|
-
requiredInputs: ["verdict"],
|
|
220
|
-
expectedOutputs: ["validation_result"],
|
|
221
|
-
}),
|
|
222
|
-
],
|
|
223
|
-
};
|
|
224
|
-
}
|
|
225
|
-
|
|
226
192
|
return {
|
|
227
193
|
workflowType: "comprehensive_analysis",
|
|
228
194
|
steps: [
|
|
229
195
|
...analystSteps,
|
|
230
|
-
promptStep("
|
|
196
|
+
promptStep("debate_bull", "Bull researcher case", buildBullPrompt(symbol), {
|
|
231
197
|
requiredInputs: analystOutputs,
|
|
198
|
+
expectedOutputs: ["bull_thesis"],
|
|
199
|
+
}),
|
|
200
|
+
promptStep("debate_bear", "Bear researcher case", buildBearPrompt(symbol), {
|
|
201
|
+
requiredInputs: [...analystOutputs, "bull_thesis"],
|
|
202
|
+
expectedOutputs: ["bear_thesis"],
|
|
203
|
+
}),
|
|
204
|
+
promptStep("debate_rebuttal", "Bull rebuttal (self-gating)", buildRebuttalPrompt(symbol), {
|
|
205
|
+
skippable: true,
|
|
206
|
+
requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis"],
|
|
207
|
+
expectedOutputs: ["rebuttal"],
|
|
208
|
+
}),
|
|
209
|
+
promptStep("synthesis", "Resolve the debate", buildSynthesisPrompt(symbol), {
|
|
210
|
+
requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis", "rebuttal"],
|
|
232
211
|
expectedOutputs: ["verdict"],
|
|
233
212
|
}),
|
|
234
|
-
promptStep("validation", "Validate cited numbers",
|
|
213
|
+
promptStep("validation", "Validate cited numbers", VALIDATION_PROMPT_DEBATE(symbol), {
|
|
235
214
|
skippable: true,
|
|
236
215
|
requiredInputs: ["verdict"],
|
|
237
216
|
expectedOutputs: ["validation_result"],
|
package/src/cli-main.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
2
3
|
import { createRequire } from "node:module";
|
|
3
4
|
import { dirname, resolve } from "node:path";
|
|
5
|
+
import { createInterface } from "node:readline/promises";
|
|
4
6
|
import { fileURLToPath } from "node:url";
|
|
5
7
|
import {
|
|
6
8
|
AuthStorage,
|
|
@@ -11,6 +13,7 @@ import {
|
|
|
11
13
|
InteractiveMode,
|
|
12
14
|
initTheme,
|
|
13
15
|
ModelRegistry,
|
|
16
|
+
SessionManager,
|
|
14
17
|
SettingsManager,
|
|
15
18
|
} from "@earendil-works/pi-coding-agent";
|
|
16
19
|
import { loadEnv } from "./config.js";
|
|
@@ -19,10 +22,13 @@ import { createOpenCandleSession } from "./pi/session.js";
|
|
|
19
22
|
import { continueOpenCandleSession } from "./pi/session-storage.js";
|
|
20
23
|
import {
|
|
21
24
|
acquireSessionWriterLock,
|
|
25
|
+
migrateWriterLockScope,
|
|
22
26
|
refreshSessionWriterLock,
|
|
23
27
|
releaseSessionWriterLock,
|
|
28
|
+
type WriterLock,
|
|
24
29
|
writerLockScopeForSession,
|
|
25
30
|
} from "./pi/session-writer-lock.js";
|
|
31
|
+
import { startTuiSessionCoordinatorServer } from "./pi/tui-session-coordinator.js";
|
|
26
32
|
|
|
27
33
|
const require = createRequire(import.meta.url);
|
|
28
34
|
const packageRoot = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -196,22 +202,62 @@ async function main(): Promise<void> {
|
|
|
196
202
|
initTheme(settingsManager.getTheme(), true);
|
|
197
203
|
|
|
198
204
|
const sessionManager = continueOpenCandleSession(cwd);
|
|
205
|
+
let activeSessionManager = sessionManager;
|
|
199
206
|
const sessionWriterLockScope = writerLockScopeForSession(sessionManager);
|
|
200
|
-
|
|
207
|
+
let runtime: Awaited<ReturnType<typeof createAgentSessionRuntime>> | undefined;
|
|
208
|
+
const tuiCoordinator = await startTuiSessionCoordinatorServer({
|
|
209
|
+
getSession: () => {
|
|
210
|
+
if (!runtime) throw new Error("OpenCandle is still starting this session.");
|
|
211
|
+
return runtime.session;
|
|
212
|
+
},
|
|
213
|
+
getSessionManager: () => activeSessionManager,
|
|
214
|
+
getModelUnavailableMessage: () => {
|
|
215
|
+
if (!runtime) return "OpenCandle is still starting this session.";
|
|
216
|
+
const session = runtime.session;
|
|
217
|
+
const model = session.model;
|
|
218
|
+
if (!model) {
|
|
219
|
+
return "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
|
|
220
|
+
}
|
|
221
|
+
if (!session.modelRegistry.hasConfiguredAuth(model)) {
|
|
222
|
+
return "Connect an AI model before chat can run. Paste a Google Gemini, OpenAI, or Anthropic API key in the setup panel.";
|
|
223
|
+
}
|
|
224
|
+
return null;
|
|
225
|
+
},
|
|
226
|
+
syncWriterLockScope: () => syncActiveSessionWriterLockScope(),
|
|
227
|
+
});
|
|
228
|
+
const sessionWriterLock = await acquireSessionWriterLock(sessionWriterLockScope, "tui", {
|
|
229
|
+
coordinatorEndpoint: tuiCoordinator.endpoint,
|
|
230
|
+
coordinatorSecret: tuiCoordinator.secret,
|
|
231
|
+
});
|
|
201
232
|
if (sessionWriterLock.role !== "writer") {
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
);
|
|
233
|
+
await tuiCoordinator.close();
|
|
234
|
+
if (await runFollowerTuiProxy(sessionWriterLock.lock, sessionManager, cwd)) return;
|
|
235
|
+
console.error("OpenCandle is syncing this session in another window. Try again shortly.");
|
|
205
236
|
process.exitCode = 1;
|
|
206
237
|
return;
|
|
207
238
|
}
|
|
208
239
|
let activeSessionWriterLockScope = sessionWriterLockScope;
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
240
|
+
let activeSessionWriterLockLost = false;
|
|
241
|
+
function syncActiveSessionWriterLockScope(): void {
|
|
242
|
+
if (activeSessionWriterLockLost) throw new Error("OpenCandle is reconnecting to this session.");
|
|
243
|
+
const nextScope = writerLockScopeForSession(activeSessionManager);
|
|
244
|
+
if (nextScope === activeSessionWriterLockScope) return;
|
|
245
|
+
if (migrateWriterLockScope(activeSessionWriterLockScope, nextScope)) {
|
|
246
|
+
activeSessionWriterLockScope = nextScope;
|
|
247
|
+
} else {
|
|
248
|
+
activeSessionWriterLockLost = true;
|
|
249
|
+
throw new Error("OpenCandle is reconnecting to this session.");
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
const writerLockHeartbeat = setInterval(() => {
|
|
253
|
+
try {
|
|
254
|
+
syncActiveSessionWriterLockScope();
|
|
255
|
+
refreshSessionWriterLock(activeSessionWriterLockScope);
|
|
256
|
+
} catch {
|
|
257
|
+
clearInterval(writerLockHeartbeat);
|
|
258
|
+
}
|
|
259
|
+
}, 5000);
|
|
213
260
|
|
|
214
|
-
let runtime: Awaited<ReturnType<typeof createAgentSessionRuntime>> | undefined;
|
|
215
261
|
try {
|
|
216
262
|
runtime = await createAgentSessionRuntime(
|
|
217
263
|
async (opts) => {
|
|
@@ -239,20 +285,28 @@ async function main(): Promise<void> {
|
|
|
239
285
|
},
|
|
240
286
|
{ cwd, agentDir, sessionManager },
|
|
241
287
|
);
|
|
288
|
+
syncActiveSessionWriterLockScope();
|
|
242
289
|
runtime.setRebindSession(async (nextSession) => {
|
|
243
290
|
const nextSessionWriterLockScope = writerLockScopeForSession(nextSession.sessionManager);
|
|
244
|
-
if (nextSessionWriterLockScope === activeSessionWriterLockScope)
|
|
291
|
+
if (nextSessionWriterLockScope === activeSessionWriterLockScope) {
|
|
292
|
+
activeSessionManager = nextSession.sessionManager;
|
|
293
|
+
syncActiveSessionWriterLockScope();
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
245
296
|
const nextSessionWriterLock = await acquireSessionWriterLock(
|
|
246
297
|
nextSessionWriterLockScope,
|
|
247
298
|
"tui",
|
|
299
|
+
{
|
|
300
|
+
coordinatorEndpoint: tuiCoordinator.endpoint,
|
|
301
|
+
coordinatorSecret: tuiCoordinator.secret,
|
|
302
|
+
},
|
|
248
303
|
);
|
|
249
304
|
if (nextSessionWriterLock.role !== "writer") {
|
|
250
|
-
throw new Error(
|
|
251
|
-
`Session is currently being written by ${nextSessionWriterLock.lock.processKind} (pid ${nextSessionWriterLock.lock.pid}).`,
|
|
252
|
-
);
|
|
305
|
+
throw new Error("OpenCandle is syncing this session in another window. Try again shortly.");
|
|
253
306
|
}
|
|
254
307
|
releaseSessionWriterLock(activeSessionWriterLockScope);
|
|
255
308
|
activeSessionWriterLockScope = nextSessionWriterLockScope;
|
|
309
|
+
activeSessionManager = nextSession.sessionManager;
|
|
256
310
|
});
|
|
257
311
|
const interactiveMode = new InteractiveMode(runtime, {
|
|
258
312
|
modelFallbackMessage: shouldSuppressFallbackMessage
|
|
@@ -263,8 +317,194 @@ async function main(): Promise<void> {
|
|
|
263
317
|
} finally {
|
|
264
318
|
clearInterval(writerLockHeartbeat);
|
|
265
319
|
releaseSessionWriterLock(activeSessionWriterLockScope);
|
|
320
|
+
await tuiCoordinator.close();
|
|
266
321
|
await runtime?.dispose();
|
|
267
322
|
}
|
|
268
323
|
}
|
|
269
324
|
|
|
325
|
+
async function runFollowerTuiProxy(
|
|
326
|
+
lock: WriterLock,
|
|
327
|
+
sessionManager: ReturnType<typeof continueOpenCandleSession>,
|
|
328
|
+
cwd: string,
|
|
329
|
+
): Promise<boolean> {
|
|
330
|
+
if (!lock.coordinatorEndpoint || !lock.coordinatorSecret) return false;
|
|
331
|
+
if (!process.stdin.isTTY) return false;
|
|
332
|
+
|
|
333
|
+
const input = createInterface({ input: process.stdin, output: process.stdout });
|
|
334
|
+
const follower = startFollowerTranscriptPrinter(sessionManager, cwd);
|
|
335
|
+
try {
|
|
336
|
+
console.log("Connected to the active OpenCandle session. Type /exit to close.");
|
|
337
|
+
while (true) {
|
|
338
|
+
const prompt = (await input.question("> ")).trim();
|
|
339
|
+
if (!prompt) continue;
|
|
340
|
+
if (prompt === "/exit" || prompt === "/quit") break;
|
|
341
|
+
await forwardTuiPrompt(lock, sessionManager.getSessionId(), prompt);
|
|
342
|
+
follower.markSeen();
|
|
343
|
+
}
|
|
344
|
+
} finally {
|
|
345
|
+
follower.stop();
|
|
346
|
+
input.close();
|
|
347
|
+
}
|
|
348
|
+
return true;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function startFollowerTranscriptPrinter(
|
|
352
|
+
sessionManager: ReturnType<typeof continueOpenCandleSession>,
|
|
353
|
+
cwd: string,
|
|
354
|
+
): { stop: () => void; markSeen: () => void } {
|
|
355
|
+
const sessionFile = sessionManager.getSessionFile();
|
|
356
|
+
if (!sessionFile) return { stop: () => {}, markSeen: () => {} };
|
|
357
|
+
const seenEntryIds = new Set(
|
|
358
|
+
sessionManager
|
|
359
|
+
.getEntries()
|
|
360
|
+
.map((entry) => entryId(entry))
|
|
361
|
+
.filter((id): id is string => Boolean(id)),
|
|
362
|
+
);
|
|
363
|
+
const openFreshSession = () =>
|
|
364
|
+
SessionManager.open(sessionFile, sessionManager.getSessionDir(), cwd);
|
|
365
|
+
const markSeen = () => {
|
|
366
|
+
try {
|
|
367
|
+
for (const entry of openFreshSession().getEntries()) {
|
|
368
|
+
const id = entryId(entry);
|
|
369
|
+
if (id) seenEntryIds.add(id);
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
// The owner may be rotating the session file; the next poll will catch up.
|
|
373
|
+
}
|
|
374
|
+
};
|
|
375
|
+
let polling = false;
|
|
376
|
+
const poll = () => {
|
|
377
|
+
if (polling) return;
|
|
378
|
+
polling = true;
|
|
379
|
+
try {
|
|
380
|
+
for (const entry of openFreshSession().getEntries()) {
|
|
381
|
+
const id = entryId(entry);
|
|
382
|
+
if (!id || seenEntryIds.has(id)) continue;
|
|
383
|
+
seenEntryIds.add(id);
|
|
384
|
+
const text = followerEntryText(entry);
|
|
385
|
+
if (text) process.stdout.write(`\n${text}\n`);
|
|
386
|
+
}
|
|
387
|
+
} catch {
|
|
388
|
+
// The owner may be rotating the session file; the next poll will catch up.
|
|
389
|
+
} finally {
|
|
390
|
+
polling = false;
|
|
391
|
+
}
|
|
392
|
+
};
|
|
393
|
+
const interval = setInterval(poll, 1000);
|
|
394
|
+
return { stop: () => clearInterval(interval), markSeen };
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
function entryId(entry: unknown): string | null {
|
|
398
|
+
const record = asRecord(entry);
|
|
399
|
+
return typeof record.id === "string" ? record.id : null;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
function followerEntryText(entry: unknown): string {
|
|
403
|
+
const record = asRecord(entry);
|
|
404
|
+
if (record.type === "message") {
|
|
405
|
+
const message = asRecord(record.message);
|
|
406
|
+
const role = typeof message.role === "string" ? message.role : "message";
|
|
407
|
+
const text = contentText(message.content);
|
|
408
|
+
return text ? `${role}: ${text}` : "";
|
|
409
|
+
}
|
|
410
|
+
if (record.type === "custom") {
|
|
411
|
+
const text = contentText(record.message ?? record.text ?? record.content);
|
|
412
|
+
return text ? `system: ${text}` : "";
|
|
413
|
+
}
|
|
414
|
+
return "";
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
async function forwardTuiPrompt(
|
|
418
|
+
lock: WriterLock,
|
|
419
|
+
sessionId: string,
|
|
420
|
+
prompt: string,
|
|
421
|
+
): Promise<void> {
|
|
422
|
+
if (!lock.coordinatorEndpoint || !lock.coordinatorSecret) return;
|
|
423
|
+
const response = await fetch(
|
|
424
|
+
new URL("/api/local-coordinator/chat-run", lock.coordinatorEndpoint),
|
|
425
|
+
{
|
|
426
|
+
method: "POST",
|
|
427
|
+
headers: {
|
|
428
|
+
"content-type": "application/json",
|
|
429
|
+
"x-opencandle-coordinator-secret": lock.coordinatorSecret,
|
|
430
|
+
},
|
|
431
|
+
body: JSON.stringify({
|
|
432
|
+
prompt,
|
|
433
|
+
sessionId,
|
|
434
|
+
actionId: `tui-proxy-${randomUUID()}`,
|
|
435
|
+
}),
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
if (!response.ok) {
|
|
439
|
+
const body = (await response.json().catch(() => ({}))) as { error?: string };
|
|
440
|
+
console.error(body.error || response.statusText);
|
|
441
|
+
return;
|
|
442
|
+
}
|
|
443
|
+
await printSseResponse(response);
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
async function printSseResponse(response: Response): Promise<void> {
|
|
447
|
+
const reader = response.body?.getReader();
|
|
448
|
+
if (!reader) return;
|
|
449
|
+
const decoder = new TextDecoder();
|
|
450
|
+
let buffer = "";
|
|
451
|
+
while (true) {
|
|
452
|
+
const { value, done } = await reader.read();
|
|
453
|
+
if (done) break;
|
|
454
|
+
buffer += decoder.decode(value, { stream: true });
|
|
455
|
+
let index = buffer.indexOf("\n\n");
|
|
456
|
+
while (index !== -1) {
|
|
457
|
+
const block = buffer.slice(0, index);
|
|
458
|
+
buffer = buffer.slice(index + 2);
|
|
459
|
+
printSseBlock(block);
|
|
460
|
+
index = buffer.indexOf("\n\n");
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function printSseBlock(block: string): void {
|
|
466
|
+
const data = block
|
|
467
|
+
.split("\n")
|
|
468
|
+
.filter((line) => line.startsWith("data:"))
|
|
469
|
+
.map((line) => line.slice(5).trim())
|
|
470
|
+
.join("\n");
|
|
471
|
+
if (!data) return;
|
|
472
|
+
const event = JSON.parse(data) as {
|
|
473
|
+
type?: string;
|
|
474
|
+
role?: string;
|
|
475
|
+
text?: string;
|
|
476
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
477
|
+
error?: { message?: string };
|
|
478
|
+
};
|
|
479
|
+
const text = sseEventText(event);
|
|
480
|
+
if (event.type === "message.completed" && text) {
|
|
481
|
+
process.stdout.write(`${text}\n`);
|
|
482
|
+
} else if (event.type === "run.failed") {
|
|
483
|
+
process.stderr.write(`${event.error?.message || "Run failed"}\n`);
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
function sseEventText(event: {
|
|
488
|
+
text?: string;
|
|
489
|
+
content?: Array<{ type?: string; text?: string }>;
|
|
490
|
+
}): string {
|
|
491
|
+
if (event.text) return event.text;
|
|
492
|
+
return contentText(event.content);
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
function contentText(content: unknown): string {
|
|
496
|
+
if (typeof content === "string") return content;
|
|
497
|
+
if (!Array.isArray(content)) return "";
|
|
498
|
+
return content
|
|
499
|
+
.filter((part) => part.type === "text" && part.text)
|
|
500
|
+
.map((part) => part.text)
|
|
501
|
+
.join("");
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function asRecord(value: unknown): Record<string, unknown> {
|
|
505
|
+
return typeof value === "object" && value !== null && !Array.isArray(value)
|
|
506
|
+
? (value as Record<string, unknown>)
|
|
507
|
+
: {};
|
|
508
|
+
}
|
|
509
|
+
|
|
270
510
|
await main();
|
package/src/config.ts
CHANGED
|
@@ -14,7 +14,7 @@ export interface SentimentConfig {
|
|
|
14
14
|
maxNotableClaims?: number;
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
export type RouterMode = "
|
|
17
|
+
export type RouterMode = "llm";
|
|
18
18
|
export type ToolScopeMode = "observe" | "enforce";
|
|
19
19
|
export type PlanningMigrationStatuses = Partial<Record<TaskFamily, PlanningBehaviorMode>>;
|
|
20
20
|
|
|
@@ -24,13 +24,10 @@ export interface Config {
|
|
|
24
24
|
braveApiKey?: string;
|
|
25
25
|
exaApiKey?: string;
|
|
26
26
|
finnhubApiKey?: string;
|
|
27
|
-
/** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
|
|
28
|
-
debate?: boolean;
|
|
29
27
|
/**
|
|
30
|
-
* Intent-router mode.
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
* `OPENCANDLE_ROUTER_MODE`.
|
|
28
|
+
* Intent-router mode. The LLM router is the only production routing path;
|
|
29
|
+
* `OPENCANDLE_ROUTER_MODE` accepts only `"llm"` (or unset). The removed
|
|
30
|
+
* `"rules"` value fails startup with migration guidance.
|
|
34
31
|
*/
|
|
35
32
|
routerMode: RouterMode;
|
|
36
33
|
/**
|
|
@@ -66,8 +63,6 @@ export interface OpenCandleFileConfig {
|
|
|
66
63
|
apiKey?: string;
|
|
67
64
|
};
|
|
68
65
|
};
|
|
69
|
-
/** Enable adversarial bull/bear debate in comprehensive analysis. Default: true. */
|
|
70
|
-
debate?: boolean;
|
|
71
66
|
sentiment?: {
|
|
72
67
|
retentionDays?: number;
|
|
73
68
|
defaultSubreddits?: string[];
|
|
@@ -95,7 +90,7 @@ export function loadEnv(path = ".env"): void {
|
|
|
95
90
|
if (eqIndex === -1) continue;
|
|
96
91
|
const key = trimmed.slice(0, eqIndex).trim();
|
|
97
92
|
const value = trimmed.slice(eqIndex + 1).trim();
|
|
98
|
-
if (key && value) {
|
|
93
|
+
if (key && value && process.env[key] === undefined) {
|
|
99
94
|
process.env[key] = value;
|
|
100
95
|
}
|
|
101
96
|
}
|
|
@@ -141,11 +136,13 @@ const PLANNING_BEHAVIOR_MODES = [
|
|
|
141
136
|
|
|
142
137
|
function resolveRouterMode(): RouterMode {
|
|
143
138
|
const raw = process.env.OPENCANDLE_ROUTER_MODE;
|
|
144
|
-
if (raw === undefined || raw === "") return "
|
|
145
|
-
if (raw === "rules"
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
139
|
+
if (raw === undefined || raw === "" || raw === "llm") return "llm";
|
|
140
|
+
if (raw === "rules") {
|
|
141
|
+
throw new Error(
|
|
142
|
+
'OPENCANDLE_ROUTER_MODE="rules" was removed: the deterministic rules router is no longer a production routing path. Unset OPENCANDLE_ROUTER_MODE to use the LLM router.',
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
throw new Error(`Invalid OPENCANDLE_ROUTER_MODE="${raw}". Allowed value: "llm" (default).`);
|
|
149
146
|
}
|
|
150
147
|
|
|
151
148
|
function resolveToolScopeMode(): ToolScopeMode {
|
|
@@ -191,7 +188,6 @@ function isPlanningBehaviorMode(value: string | undefined): value is PlanningBeh
|
|
|
191
188
|
}
|
|
192
189
|
|
|
193
190
|
function resolveConfig(fileConfig: OpenCandleFileConfig): Config {
|
|
194
|
-
const debateEnv = process.env.OPENCANDLE_DEBATE;
|
|
195
191
|
const fileSentiment = fileConfig.sentiment;
|
|
196
192
|
return {
|
|
197
193
|
alphaVantageApiKey:
|
|
@@ -200,10 +196,6 @@ function resolveConfig(fileConfig: OpenCandleFileConfig): Config {
|
|
|
200
196
|
braveApiKey: process.env.BRAVE_API_KEY ?? fileConfig.providers?.brave?.apiKey,
|
|
201
197
|
exaApiKey: process.env.EXA_API_KEY ?? fileConfig.providers?.exa?.apiKey,
|
|
202
198
|
finnhubApiKey: process.env.FINNHUB_API_KEY ?? fileConfig.providers?.finnhub?.apiKey,
|
|
203
|
-
debate:
|
|
204
|
-
debateEnv !== undefined
|
|
205
|
-
? debateEnv !== "false" && debateEnv !== "0"
|
|
206
|
-
: (fileConfig.debate ?? true),
|
|
207
199
|
routerMode: resolveRouterMode(),
|
|
208
200
|
toolScopeMode: resolveToolScopeMode(),
|
|
209
201
|
planningMigrationStatuses: resolvePlanningMigrationStatuses(),
|
package/src/doctor/report.ts
CHANGED
|
@@ -450,7 +450,7 @@ function providerStatusCheck(status: ProviderStatus, modeOverride?: "session"):
|
|
|
450
450
|
summary: ok ? "Reachable" : `Not reachable (${status.state})`,
|
|
451
451
|
remediation: ok
|
|
452
452
|
? undefined
|
|
453
|
-
:
|
|
453
|
+
: `Check network connectivity and retry. OpenCandle uses ${provider.displayName} through a public HTTP endpoint.`,
|
|
454
454
|
metadata: { ...metadata, statusCode: status.statusCode ?? null },
|
|
455
455
|
};
|
|
456
456
|
}
|