opencandle 0.10.0 → 0.11.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.
- package/README.md +7 -5
- package/dist/analysts/orchestrator.d.ts +1 -4
- package/dist/analysts/orchestrator.js +15 -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/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/memory/sqlite.js +20 -19
- package/dist/memory/sqlite.js.map +1 -1
- package/dist/pi/opencandle-extension.js +13 -221
- 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/routing/classify-intent.js +4 -5
- package/dist/routing/classify-intent.js.map +1 -1
- package/dist/routing/route-manifest.js +0 -1
- package/dist/routing/route-manifest.js.map +1 -1
- package/dist/routing/router-prompt.js +1 -1
- package/dist/routing/router.js +35 -4
- package/dist/routing/router.js.map +1 -1
- package/dist/runtime/session-coordinator.js +2 -13
- package/dist/runtime/session-coordinator.js.map +1 -1
- package/dist/system-prompt.js +1 -1
- package/dist/tools/fundamentals/dcf.d.ts +1 -1
- package/dist/tools/fundamentals/dcf.js +49 -6
- package/dist/tools/fundamentals/dcf.js.map +1 -1
- package/dist/tools/index.d.ts +0 -1
- package/dist/tools/index.js +0 -3
- package/dist/tools/index.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/technical/backtest.d.ts +23 -5
- package/dist/tools/technical/backtest.js +131 -94
- package/dist/tools/technical/backtest.js.map +1 -1
- package/gui/server/http-routes.ts +395 -19
- package/gui/server/invoke-tool.ts +164 -16
- package/gui/server/local-session-coordinator.ts +97 -0
- package/gui/server/market-state-api.ts +1 -43
- package/gui/server/server.ts +51 -6
- package/gui/server/session-actions.ts +117 -8
- package/gui/server/tool-metadata.ts +3 -1
- package/gui/server/ws-hub.ts +61 -6
- package/gui/web/dist/assets/CatalogOverlay-ChRTKNlf.js +1 -0
- package/gui/web/dist/assets/index-BzyqyVnd.css +2 -0
- package/gui/web/dist/assets/{index-B7QAjY5g.js → index-DvMjkOP9.js} +9 -9
- package/gui/web/dist/index.html +2 -2
- package/package.json +6 -3
- package/src/analysts/orchestrator.ts +15 -56
- package/src/cli-main.ts +253 -13
- package/src/config.ts +12 -20
- package/src/market-state/daily-report.ts +6 -16
- package/src/market-state/service.ts +0 -136
- package/src/memory/sqlite.ts +23 -19
- package/src/pi/opencandle-extension.ts +12 -265
- 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/routing/classify-intent.ts +4 -5
- package/src/routing/route-manifest.ts +0 -1
- package/src/routing/router-prompt.ts +1 -1
- package/src/routing/router.ts +39 -3
- package/src/runtime/session-coordinator.ts +2 -17
- package/src/system-prompt.ts +1 -1
- package/src/tools/AGENTS.md +1 -1
- package/src/tools/fundamentals/dcf.ts +57 -7
- package/src/tools/index.ts +0 -3
- package/src/tools/portfolio/daily-report.ts +10 -4
- package/src/tools/technical/backtest.ts +167 -108
- 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/web/dist/assets/CatalogOverlay-CYptsda-.js +0 -1
- 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-DvMjkOP9.js"></script>
|
|
12
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BzyqyVnd.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.0",
|
|
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",
|
|
@@ -104,6 +106,7 @@
|
|
|
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",
|
|
106
108
|
"test:e2e:providers": "tsx tests/e2e/providers.test.ts",
|
|
109
|
+
"test:e2e:harness-dcf": "tsx tests/e2e/harness-dcf.test.ts",
|
|
107
110
|
"test:evals": "vitest run --config vitest.config.evals.ts",
|
|
108
111
|
"eval:router-live": "tsx tests/scripts/run-live-router-eval.ts",
|
|
109
112
|
"test:evals:usually": "EVAL_TIER=usually vitest run --config vitest.config.evals.ts",
|
|
@@ -148,34 +148,11 @@ Additionally check:
|
|
|
148
148
|
|
|
149
149
|
Output: VALIDATED if all checks pass, or list specific corrections needed.`;
|
|
150
150
|
|
|
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
151
|
export function getInitialAnalysisPrompt(symbol: string): string {
|
|
167
152
|
return `Begin comprehensive analysis of ${symbol}. Start by getting the current stock quote.`;
|
|
168
153
|
}
|
|
169
154
|
|
|
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;
|
|
155
|
+
export function buildComprehensiveAnalysisDefinition(symbol: string): WorkflowDefinition {
|
|
179
156
|
const roles: AnalystRole[] = ["valuation", "momentum", "options", "contrarian", "risk"];
|
|
180
157
|
|
|
181
158
|
const analystOutputs = roles.map((r) => `${r}_signal`);
|
|
@@ -193,45 +170,27 @@ export function buildComprehensiveAnalysisDefinition(
|
|
|
193
170
|
),
|
|
194
171
|
];
|
|
195
172
|
|
|
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
173
|
return {
|
|
227
174
|
workflowType: "comprehensive_analysis",
|
|
228
175
|
steps: [
|
|
229
176
|
...analystSteps,
|
|
230
|
-
promptStep("
|
|
177
|
+
promptStep("debate_bull", "Bull researcher case", buildBullPrompt(symbol), {
|
|
231
178
|
requiredInputs: analystOutputs,
|
|
179
|
+
expectedOutputs: ["bull_thesis"],
|
|
180
|
+
}),
|
|
181
|
+
promptStep("debate_bear", "Bear researcher case", buildBearPrompt(symbol), {
|
|
182
|
+
requiredInputs: [...analystOutputs, "bull_thesis"],
|
|
183
|
+
expectedOutputs: ["bear_thesis"],
|
|
184
|
+
}),
|
|
185
|
+
promptStep("debate_rebuttal", "Bull rebuttal (self-gating)", buildRebuttalPrompt(symbol), {
|
|
186
|
+
requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis"],
|
|
187
|
+
expectedOutputs: ["rebuttal"],
|
|
188
|
+
}),
|
|
189
|
+
promptStep("synthesis", "Resolve the debate", buildSynthesisPrompt(symbol), {
|
|
190
|
+
requiredInputs: [...analystOutputs, "bull_thesis", "bear_thesis", "rebuttal"],
|
|
232
191
|
expectedOutputs: ["verdict"],
|
|
233
192
|
}),
|
|
234
|
-
promptStep("validation", "Validate cited numbers",
|
|
193
|
+
promptStep("validation", "Validate cited numbers", VALIDATION_PROMPT_DEBATE(symbol), {
|
|
235
194
|
skippable: true,
|
|
236
195
|
requiredInputs: ["verdict"],
|
|
237
196
|
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(),
|
|
@@ -17,25 +17,18 @@ export interface DailyWatchlistReport {
|
|
|
17
17
|
dataGaps: string[];
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
// Returns the configured default-watchlist morning template if one exists.
|
|
21
|
+
// Manual report runs must never create a schedule template as a side effect;
|
|
22
|
+
// only the explicit configure flow stores schedule intent.
|
|
23
|
+
export function findDefaultWatchlistReportTemplate(
|
|
21
24
|
service: MarketStateService,
|
|
22
|
-
): ReportTemplateRecord {
|
|
23
|
-
|
|
25
|
+
): ReportTemplateRecord | undefined {
|
|
26
|
+
return service
|
|
24
27
|
.listReportTemplates()
|
|
25
28
|
.find(
|
|
26
29
|
(template) =>
|
|
27
30
|
template.reportType === "watchlist_daily" && targetsDefaultWatchlist(template.configJson),
|
|
28
31
|
);
|
|
29
|
-
if (existing) return existing;
|
|
30
|
-
return service.createReportTemplate({
|
|
31
|
-
name: "Morning watchlist",
|
|
32
|
-
reportType: "watchlist_daily",
|
|
33
|
-
cadence: "daily",
|
|
34
|
-
timezone: Intl.DateTimeFormat().resolvedOptions().timeZone,
|
|
35
|
-
localTime: "08:00",
|
|
36
|
-
config: { targets: { default_watchlist: true } },
|
|
37
|
-
enabled: true,
|
|
38
|
-
});
|
|
39
32
|
}
|
|
40
33
|
|
|
41
34
|
export async function recordDailyWatchlistReportRun(
|
|
@@ -129,9 +122,6 @@ export async function generateDailyWatchlistReport(
|
|
|
129
122
|
`Recent alerts`,
|
|
130
123
|
` ${service.listAlertEvents().length} recorded alert event(s).`,
|
|
131
124
|
``,
|
|
132
|
-
`Technical snapshot`,
|
|
133
|
-
` Deferred unless quote/history data is available through a later section builder.`,
|
|
134
|
-
``,
|
|
135
125
|
`Data gaps`,
|
|
136
126
|
...dataGapLines,
|
|
137
127
|
];
|