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.
- package/README.md +48 -180
- package/dist/cli-main.js +9 -8
- package/dist/config.d.ts +4 -0
- package/dist/config.js +1 -0
- package/dist/doctor/cli-command.js +7 -4
- package/dist/doctor/report.js +13 -0
- package/dist/gui/server/chat-event-adapter.js +145 -20
- package/dist/gui/server/history-snapshot-store.d.ts +8 -0
- package/dist/gui/server/history-snapshot-store.js +43 -0
- package/dist/gui/server/http-routes.d.ts +2 -0
- package/dist/gui/server/http-routes.js +32 -7
- package/dist/gui/server/live-chat-event-adapter.d.ts +2 -0
- package/dist/gui/server/live-chat-event-adapter.js +33 -15
- package/dist/gui/server/market-indices-api.d.ts +21 -0
- package/dist/gui/server/market-indices-api.js +26 -0
- package/dist/gui/server/market-indices-snapshot-store.d.ts +12 -0
- package/dist/gui/server/market-indices-snapshot-store.js +56 -0
- package/dist/gui/server/market-state-api.d.ts +112 -0
- package/dist/gui/server/market-state-api.js +246 -4
- package/dist/gui/server/model-setup.d.ts +2 -11
- package/dist/gui/server/model-setup.js +10 -6
- package/dist/gui/server/server.js +12 -9
- package/dist/gui/server/tool-metadata.d.ts +14 -14
- package/dist/gui/server/ws-hub.js +1 -1
- package/dist/infra/cache.d.ts +4 -0
- package/dist/infra/cache.js +4 -0
- package/dist/infra/lse-byte-budget.d.ts +10 -0
- package/dist/infra/lse-byte-budget.js +47 -0
- package/dist/infra/rate-limiter.js +1 -0
- package/dist/onboarding/providers.d.ts +16 -1
- package/dist/onboarding/providers.js +20 -0
- package/dist/onboarding/validate-model-key.js +33 -1
- package/dist/onboarding/validation.js +8 -0
- package/dist/pi/opencandle-extension.d.ts +2 -1
- package/dist/pi/opencandle-extension.js +2 -2
- package/dist/pi/session.d.ts +2 -3
- package/dist/pi/session.js +7 -5
- package/dist/pi/setup.d.ts +3 -3
- package/dist/pi/setup.js +51 -59
- package/dist/prompts/workflow-prompts.js +16 -14
- package/dist/providers/index.d.ts +1 -0
- package/dist/providers/index.js +1 -0
- package/dist/providers/lse.d.ts +35 -0
- package/dist/providers/lse.js +284 -0
- package/dist/providers/polymarket.js +22 -5
- package/dist/providers/wrap-provider.js +1 -0
- package/dist/providers/yahoo-finance.js +1 -0
- package/dist/routing/route-manifest.js +1 -0
- package/dist/routing/router.js +8 -1
- package/dist/runtime/evidence.d.ts +1 -0
- package/dist/runtime/evidence.js +1 -1
- package/dist/runtime/session-coordinator.d.ts +2 -2
- package/dist/runtime/session-coordinator.js +2 -2
- package/dist/tools/fundamentals/dcf.js +37 -8
- package/dist/tools/fundamentals/financials.js +88 -10
- package/dist/tools/index.d.ts +10 -5
- package/dist/tools/index.js +3 -0
- package/dist/tools/market/price-comparison.d.ts +30 -0
- package/dist/tools/market/price-comparison.js +202 -0
- package/dist/tools/market/stock-history.d.ts +18 -3
- package/dist/tools/market/stock-history.js +132 -17
- package/dist/tools/portfolio/correlation.d.ts +1 -1
- package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
- package/dist/types/market.d.ts +2 -0
- package/gui/web/dist/assets/CatalogOverlay-Cy8Fq0fn.js +1 -0
- package/gui/web/dist/assets/allocation-donut-DgBRIKCk.js +52 -0
- package/gui/web/dist/assets/index-DIlFyIOO.js +66 -0
- package/gui/web/dist/assets/index-DS4jESOB.css +2 -0
- package/gui/web/dist/assets/market-chart-DammaCjH.js +1 -0
- package/gui/web/dist/assets/utils-CnADgYgh.js +1 -0
- package/gui/web/dist/index.html +3 -2
- package/package.json +15 -17
- package/gui/web/dist/assets/CatalogOverlay-CAc7e3Pf.js +0 -1
- package/gui/web/dist/assets/index-BOEKd9wT.css +0 -2
- package/gui/web/dist/assets/index-C-H05cQ2.js +0 -65
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export type ApiKeyProviderId = "alpha_vantage" | "fred" | "finnhub" | "brave" | "exa";
|
|
1
|
+
export type ApiKeyProviderId = "alpha_vantage" | "fred" | "finnhub" | "brave" | "exa" | "lse";
|
|
2
2
|
export type ExternalToolProviderId = "twitter" | "reddit";
|
|
3
3
|
export type PublicHttpProviderId = "polymarket" | "tradingview" | "yahoo";
|
|
4
4
|
export type ProviderId = ApiKeyProviderId | ExternalToolProviderId | PublicHttpProviderId;
|
|
@@ -127,6 +127,21 @@ export declare const PROVIDERS: readonly [{
|
|
|
127
127
|
readonly fallbackDescription: "Exa search continues to work via the keyless Exa MCP endpoint, which has lower rate limits but similar quality";
|
|
128
128
|
readonly snoozeDurationDays: 7;
|
|
129
129
|
readonly instructionsHint: "Paid with free tier, signup opens in your browser";
|
|
130
|
+
}, {
|
|
131
|
+
readonly id: "lse";
|
|
132
|
+
readonly kind: "api-key";
|
|
133
|
+
readonly displayName: "London Strategic Edge";
|
|
134
|
+
readonly category: "market";
|
|
135
|
+
readonly tier: "soft";
|
|
136
|
+
readonly aliases: readonly ["lse", "london strategic edge", "londonstrategicedge"];
|
|
137
|
+
readonly signupUrl: "https://londonstrategicedge.com/databank";
|
|
138
|
+
readonly freeTier: true;
|
|
139
|
+
readonly envVar: "LSE_API_KEY";
|
|
140
|
+
readonly configPath: readonly ["providers", "lse", "apiKey"];
|
|
141
|
+
readonly unlocks: readonly ["financial statements (income, balance sheet, and cash flow)", "deep split-adjusted intraday history back to 2003"];
|
|
142
|
+
readonly fallbackDescription: "Market data continues through Yahoo Finance and Alpha Vantage when London Strategic Edge is unavailable";
|
|
143
|
+
readonly snoozeDurationDays: 7;
|
|
144
|
+
readonly instructionsHint: "Free, about 30 seconds, signup opens in your browser";
|
|
130
145
|
}, {
|
|
131
146
|
readonly id: "yahoo";
|
|
132
147
|
readonly kind: "public-http";
|
|
@@ -112,6 +112,25 @@ export const PROVIDERS = [
|
|
|
112
112
|
snoozeDurationDays: 7,
|
|
113
113
|
instructionsHint: "Paid with free tier, signup opens in your browser",
|
|
114
114
|
},
|
|
115
|
+
{
|
|
116
|
+
id: "lse",
|
|
117
|
+
kind: "api-key",
|
|
118
|
+
displayName: "London Strategic Edge",
|
|
119
|
+
category: "market",
|
|
120
|
+
tier: "soft",
|
|
121
|
+
aliases: ["lse", "london strategic edge", "londonstrategicedge"],
|
|
122
|
+
signupUrl: "https://londonstrategicedge.com/databank",
|
|
123
|
+
freeTier: true,
|
|
124
|
+
envVar: "LSE_API_KEY",
|
|
125
|
+
configPath: ["providers", "lse", "apiKey"],
|
|
126
|
+
unlocks: [
|
|
127
|
+
"financial statements (income, balance sheet, and cash flow)",
|
|
128
|
+
"deep split-adjusted intraday history back to 2003",
|
|
129
|
+
],
|
|
130
|
+
fallbackDescription: "Market data continues through Yahoo Finance and Alpha Vantage when London Strategic Edge is unavailable",
|
|
131
|
+
snoozeDurationDays: 7,
|
|
132
|
+
instructionsHint: "Free, about 30 seconds, signup opens in your browser",
|
|
133
|
+
},
|
|
115
134
|
{
|
|
116
135
|
id: "yahoo",
|
|
117
136
|
kind: "public-http",
|
|
@@ -255,6 +274,7 @@ const CONFIG_FIELD_BY_ID = {
|
|
|
255
274
|
finnhub: "finnhubApiKey",
|
|
256
275
|
brave: "braveApiKey",
|
|
257
276
|
exa: "exaApiKey",
|
|
277
|
+
lse: "lseApiKey",
|
|
258
278
|
};
|
|
259
279
|
export function hasCredential(id) {
|
|
260
280
|
const descriptor = getProvider(id);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const VALIDATION_TIMEOUT_MS = 5_000;
|
|
2
|
+
const MODEL_KEY_PROBE_BASE_URL_ENV = "OPENCANDLE_MODEL_KEY_PROBE_BASE_URL";
|
|
2
3
|
const MODEL_KEY_PROBES = {
|
|
3
4
|
google: {
|
|
4
5
|
label: "Google Gemini",
|
|
@@ -23,10 +24,15 @@ const MODEL_KEY_PROBES = {
|
|
|
23
24
|
export async function validateModelKey(providerId, key) {
|
|
24
25
|
const probe = MODEL_KEY_PROBES[providerId];
|
|
25
26
|
try {
|
|
26
|
-
const response = await fetch(probe.url, {
|
|
27
|
+
const response = await fetch(resolveProbeUrl(probe.url), {
|
|
27
28
|
method: "GET",
|
|
28
29
|
headers: probe.headers(key),
|
|
29
30
|
signal: AbortSignal.timeout(VALIDATION_TIMEOUT_MS),
|
|
31
|
+
// Probes carry the typed key in headers that survive cross-origin
|
|
32
|
+
// redirects (x-goog-api-key, x-api-key); provider probe endpoints do
|
|
33
|
+
// not redirect, so any redirect is treated as a transient failure
|
|
34
|
+
// rather than followed.
|
|
35
|
+
redirect: "error",
|
|
30
36
|
});
|
|
31
37
|
if (response.status === 401 || response.status === 403) {
|
|
32
38
|
return { status: "invalid", providerLabel: probe.label };
|
|
@@ -52,3 +58,29 @@ export async function validateModelKey(providerId, key) {
|
|
|
52
58
|
};
|
|
53
59
|
}
|
|
54
60
|
}
|
|
61
|
+
function resolveProbeUrl(probeUrl) {
|
|
62
|
+
const override = process.env[MODEL_KEY_PROBE_BASE_URL_ENV]?.trim();
|
|
63
|
+
if (!override)
|
|
64
|
+
return probeUrl;
|
|
65
|
+
// Test-support hook only. Overrides are limited to loopback hosts because
|
|
66
|
+
// the probe request carries the typed API key in its headers; honoring an
|
|
67
|
+
// arbitrary origin (for example from a poisoned .env) would forward that
|
|
68
|
+
// key to a foreign host.
|
|
69
|
+
let baseUrl;
|
|
70
|
+
try {
|
|
71
|
+
baseUrl = new URL(override);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
return probeUrl;
|
|
75
|
+
}
|
|
76
|
+
if (!isLoopbackHostname(baseUrl.hostname))
|
|
77
|
+
return probeUrl;
|
|
78
|
+
const url = new URL(probeUrl);
|
|
79
|
+
url.protocol = baseUrl.protocol;
|
|
80
|
+
url.host = baseUrl.host;
|
|
81
|
+
return url.toString();
|
|
82
|
+
}
|
|
83
|
+
function isLoopbackHostname(hostname) {
|
|
84
|
+
const host = hostname.toLowerCase();
|
|
85
|
+
return (host === "localhost" || host === "::1" || host === "[::1]" || /^127(\.\d{1,3}){3}$/.test(host));
|
|
86
|
+
}
|
|
@@ -112,5 +112,13 @@ export async function validateCredential(providerId, key) {
|
|
|
112
112
|
},
|
|
113
113
|
body: JSON.stringify({ query: "test", numResults: 1 }),
|
|
114
114
|
});
|
|
115
|
+
case "lse":
|
|
116
|
+
return validateWithFetch("https://api.londonstrategicedge.com/vault/candles?symbol=AAPL&timeframe=1d&limit=1", {
|
|
117
|
+
method: "GET",
|
|
118
|
+
headers: {
|
|
119
|
+
"x-api-key": key,
|
|
120
|
+
Accept: "application/json",
|
|
121
|
+
},
|
|
122
|
+
});
|
|
115
123
|
}
|
|
116
124
|
}
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import type { ExtensionAPI, ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { InstrumentCandidate } from "../market-state/resolve.js";
|
|
3
3
|
import type { RouterLlmClient } from "../routing/router-types.js";
|
|
4
4
|
import type { AskUserHandler } from "../types/index.js";
|
|
5
5
|
export interface OpenCandleExtensionOptions {
|
|
6
|
+
modelRuntime?: ModelRuntime;
|
|
6
7
|
askUserHandler?: AskUserHandler;
|
|
7
8
|
/**
|
|
8
9
|
* Optional router LLM client. When provided, this instance is used instead
|
|
@@ -78,7 +78,7 @@ export default function openCandleExtension(pi, options) {
|
|
|
78
78
|
pi.registerCommand("setup", {
|
|
79
79
|
description: "Reconfigure the OpenCandle AI model (sign-in or API key)",
|
|
80
80
|
handler: async (_args, ctx) => {
|
|
81
|
-
const result = await coordinator.runSetup(pi, ctx, { mode: "manual" });
|
|
81
|
+
const result = await coordinator.runSetup(pi, ctx, { mode: "manual" }, options?.modelRuntime);
|
|
82
82
|
if (result === "ready") {
|
|
83
83
|
ctx.ui.notify("OpenCandle setup complete.", "info");
|
|
84
84
|
}
|
|
@@ -173,7 +173,7 @@ export default function openCandleExtension(pi, options) {
|
|
|
173
173
|
// entering the LLM's conversation context (unlike `sendMessage`, which Pi
|
|
174
174
|
// reinjects as a `role:"user"` message every turn).
|
|
175
175
|
ctx.ui.setStatus("opencandle-disclaimer", DISCLAIMER_TEXT);
|
|
176
|
-
const result = await coordinator.runSetup(pi, ctx, { mode: "startup" });
|
|
176
|
+
const result = await coordinator.runSetup(pi, ctx, { mode: "startup" }, options?.modelRuntime);
|
|
177
177
|
if (result === "shutdown") {
|
|
178
178
|
return;
|
|
179
179
|
}
|
package/dist/pi/session.d.ts
CHANGED
|
@@ -1,10 +1,9 @@
|
|
|
1
|
-
import { type
|
|
1
|
+
import { type CreateAgentSessionResult, type ModelRuntime, type SessionManager, type SettingsManager } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import type { AskUserHandler } from "../types/index.js";
|
|
3
3
|
export interface CreateOpenCandleSessionOptions {
|
|
4
4
|
cwd?: string;
|
|
5
5
|
agentDir?: string;
|
|
6
|
-
|
|
7
|
-
modelRegistry?: ModelRegistry;
|
|
6
|
+
modelRuntime?: ModelRuntime;
|
|
8
7
|
settingsManager?: SettingsManager;
|
|
9
8
|
sessionManager?: SessionManager;
|
|
10
9
|
useInlineExtension?: boolean;
|
package/dist/pi/session.js
CHANGED
|
@@ -14,7 +14,10 @@ export async function createOpenCandleSession(options = {}) {
|
|
|
14
14
|
agentDir,
|
|
15
15
|
settingsManager: options.settingsManager,
|
|
16
16
|
extensionFactories: [
|
|
17
|
-
(pi) => openCandleExtension(pi, {
|
|
17
|
+
(pi) => openCandleExtension(pi, {
|
|
18
|
+
askUserHandler: options.askUserHandler,
|
|
19
|
+
modelRuntime: options.modelRuntime,
|
|
20
|
+
}),
|
|
18
21
|
],
|
|
19
22
|
})
|
|
20
23
|
: undefined;
|
|
@@ -24,8 +27,7 @@ export async function createOpenCandleSession(options = {}) {
|
|
|
24
27
|
const result = await createAgentSession({
|
|
25
28
|
cwd,
|
|
26
29
|
agentDir,
|
|
27
|
-
|
|
28
|
-
modelRegistry: options.modelRegistry,
|
|
30
|
+
modelRuntime: options.modelRuntime,
|
|
29
31
|
sessionManager: options.sessionManager,
|
|
30
32
|
settingsManager: options.settingsManager,
|
|
31
33
|
resourceLoader,
|
|
@@ -42,8 +44,8 @@ async function applySavedDefaultModel(result) {
|
|
|
42
44
|
const modelId = result.session.settingsManager.getDefaultModel();
|
|
43
45
|
if (!provider || !modelId)
|
|
44
46
|
return;
|
|
45
|
-
const savedDefault = result.session.
|
|
46
|
-
if (!savedDefault || !result.session.
|
|
47
|
+
const savedDefault = result.session.modelRuntime.getModel(provider, modelId);
|
|
48
|
+
if (!savedDefault || !result.session.modelRuntime.hasConfiguredAuth(savedDefault.provider))
|
|
47
49
|
return;
|
|
48
50
|
const current = result.session.model;
|
|
49
51
|
if (current?.provider === savedDefault.provider && current.id === savedDefault.id)
|
package/dist/pi/setup.d.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
|
-
import { type ExtensionAPI, type ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
1
|
+
import { type ExtensionAPI, type ExtensionContext, type ModelRuntime } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
type SetupMode = "startup" | "manual";
|
|
3
3
|
type SetupRequirement = "ready" | "select_model" | "connect_auth";
|
|
4
4
|
type ApiKeyProviderId = "google" | "openai" | "anthropic";
|
|
5
5
|
type SetupResult = "ready" | "shutdown" | "cancelled";
|
|
6
6
|
export declare function getLlmSetupRequirement(ctx: Pick<ExtensionContext, "model" | "modelRegistry">): SetupRequirement;
|
|
7
|
-
export declare function runApiKeySetup(ctx: ExtensionContext, provider: ApiKeyProviderId): Promise<boolean>;
|
|
7
|
+
export declare function runApiKeySetup(ctx: ExtensionContext, provider: ApiKeyProviderId, modelRuntime: ModelRuntime): Promise<boolean>;
|
|
8
8
|
export declare function runOpenCandleSetup(api: ExtensionAPI, ctx: ExtensionContext, options?: {
|
|
9
9
|
mode: SetupMode;
|
|
10
|
-
}): Promise<SetupResult>;
|
|
10
|
+
}, modelRuntime?: ModelRuntime): Promise<SetupResult>;
|
|
11
11
|
export {};
|
package/dist/pi/setup.js
CHANGED
|
@@ -83,8 +83,8 @@ async function selectProviderForLogin(ctx) {
|
|
|
83
83
|
return undefined;
|
|
84
84
|
}
|
|
85
85
|
}
|
|
86
|
-
async function selectAdvancedOAuthProvider(ctx) {
|
|
87
|
-
const providers =
|
|
86
|
+
async function selectAdvancedOAuthProvider(ctx, modelRuntime) {
|
|
87
|
+
const providers = modelRuntime.getProviders().filter((provider) => provider.auth.oauth?.login);
|
|
88
88
|
if (providers.length === 0) {
|
|
89
89
|
ctx.ui.notify("No sign-in providers are available.", "warning");
|
|
90
90
|
return undefined;
|
|
@@ -93,12 +93,9 @@ async function selectAdvancedOAuthProvider(ctx) {
|
|
|
93
93
|
const choice = await ctx.ui.select("Choose a provider", labels);
|
|
94
94
|
return providers.find((provider) => provider.name === choice)?.id;
|
|
95
95
|
}
|
|
96
|
-
async function runLoginDialog(ctx, providerId) {
|
|
97
|
-
const provider =
|
|
98
|
-
.getOAuthProviders()
|
|
99
|
-
.find((item) => item.id === providerId);
|
|
96
|
+
async function runLoginDialog(ctx, providerId, modelRuntime) {
|
|
97
|
+
const provider = modelRuntime.getProvider(providerId);
|
|
100
98
|
const providerName = provider?.name ?? providerId;
|
|
101
|
-
const usesCallbackServer = provider?.usesCallbackServer ?? false;
|
|
102
99
|
const success = await ctx.ui.custom((tui, _theme, _keybindings, done) => {
|
|
103
100
|
let finished = false;
|
|
104
101
|
const finish = (value) => {
|
|
@@ -110,52 +107,34 @@ async function runLoginDialog(ctx, providerId) {
|
|
|
110
107
|
const dialog = new LoginDialogComponent(tui, providerId, (completed) => {
|
|
111
108
|
finish(completed);
|
|
112
109
|
});
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
manualCodeResolve = resolve;
|
|
117
|
-
manualCodeReject = reject;
|
|
118
|
-
});
|
|
119
|
-
// Cast required: advanced providers return dynamic IDs outside the SDK's static union type
|
|
120
|
-
void ctx.modelRegistry.authStorage
|
|
121
|
-
.login(providerId, {
|
|
122
|
-
onAuth: (info) => {
|
|
123
|
-
dialog.showAuth(info.url, info.instructions);
|
|
124
|
-
if (usesCallbackServer) {
|
|
125
|
-
void dialog
|
|
126
|
-
.showManualInput("Paste redirect URL below, or complete login in your browser:")
|
|
127
|
-
.then((value) => {
|
|
128
|
-
if (value && manualCodeResolve) {
|
|
129
|
-
manualCodeResolve(value);
|
|
130
|
-
manualCodeResolve = undefined;
|
|
131
|
-
}
|
|
132
|
-
})
|
|
133
|
-
.catch(() => {
|
|
134
|
-
if (manualCodeReject) {
|
|
135
|
-
manualCodeReject(new Error("Login cancelled"));
|
|
136
|
-
manualCodeReject = undefined;
|
|
137
|
-
}
|
|
138
|
-
});
|
|
139
|
-
}
|
|
140
|
-
else if (providerId === "github-copilot") {
|
|
141
|
-
dialog.showWaiting("Waiting for browser authentication...");
|
|
142
|
-
}
|
|
143
|
-
},
|
|
144
|
-
onDeviceCode: (info) => {
|
|
145
|
-
dialog.showDeviceCode(info);
|
|
146
|
-
dialog.showWaiting("Waiting for authentication...");
|
|
147
|
-
},
|
|
148
|
-
onPrompt: async (prompt) => dialog.showPrompt(prompt.message, prompt.placeholder),
|
|
149
|
-
onProgress: (message) => dialog.showProgress(message),
|
|
150
|
-
onSelect: async (prompt) => {
|
|
151
|
-
const options = prompt.options
|
|
110
|
+
const prompt = async (request) => {
|
|
111
|
+
if (request.type === "select") {
|
|
112
|
+
const options = request.options
|
|
152
113
|
.map((option, index) => `${index + 1}. ${option.label}`)
|
|
153
114
|
.join("\n");
|
|
154
|
-
const answer = await dialog.showPrompt(`${
|
|
115
|
+
const answer = await dialog.showPrompt(`${request.message}\n\n${options}`, "Enter a number");
|
|
155
116
|
const selectedIndex = Number.parseInt(answer.trim(), 10) - 1;
|
|
156
|
-
return
|
|
157
|
-
}
|
|
158
|
-
|
|
117
|
+
return request.options[selectedIndex]?.id ?? "";
|
|
118
|
+
}
|
|
119
|
+
return dialog.showPrompt(request.message, request.placeholder);
|
|
120
|
+
};
|
|
121
|
+
const notify = (event) => {
|
|
122
|
+
if (event.type === "auth_url")
|
|
123
|
+
dialog.showAuth(event.url, event.instructions);
|
|
124
|
+
else if (event.type === "device_code") {
|
|
125
|
+
dialog.showDeviceCode({
|
|
126
|
+
userCode: event.userCode,
|
|
127
|
+
verificationUri: event.verificationUri,
|
|
128
|
+
});
|
|
129
|
+
dialog.showWaiting("Waiting for authentication...");
|
|
130
|
+
}
|
|
131
|
+
else if (event.type === "progress" || event.type === "info")
|
|
132
|
+
dialog.showProgress(event.message);
|
|
133
|
+
};
|
|
134
|
+
void modelRuntime
|
|
135
|
+
.login(providerId, "oauth", {
|
|
136
|
+
prompt,
|
|
137
|
+
notify,
|
|
159
138
|
signal: dialog.signal,
|
|
160
139
|
})
|
|
161
140
|
.then(() => finish(true))
|
|
@@ -175,7 +154,7 @@ async function runLoginDialog(ctx, providerId) {
|
|
|
175
154
|
}
|
|
176
155
|
return false;
|
|
177
156
|
}
|
|
178
|
-
export async function runApiKeySetup(ctx, provider) {
|
|
157
|
+
export async function runApiKeySetup(ctx, provider, modelRuntime) {
|
|
179
158
|
const label = API_KEY_PROVIDER_LABELS[provider];
|
|
180
159
|
ctx.ui.notify(`OpenCandle stores your ${label} API key locally. After saving it, OpenCandle will select a fast default model when one is available.`, "info");
|
|
181
160
|
const key = await ctx.ui.input(`Paste your ${label} API key`, "API key");
|
|
@@ -189,8 +168,11 @@ export async function runApiKeySetup(ctx, provider) {
|
|
|
189
168
|
ctx.ui.notify(`Key was rejected by ${validation.providerLabel}. Paste a different key.`, "error");
|
|
190
169
|
return false;
|
|
191
170
|
}
|
|
192
|
-
|
|
193
|
-
|
|
171
|
+
await modelRuntime.login(provider, "api_key", {
|
|
172
|
+
prompt: async () => trimmed,
|
|
173
|
+
notify: () => { },
|
|
174
|
+
});
|
|
175
|
+
await ctx.modelRegistry.refresh();
|
|
194
176
|
ctx.ui.notify(validation.status === "transient"
|
|
195
177
|
? `Saved — couldn't verify (network issue). ${label} API key saved to OpenCandle.`
|
|
196
178
|
: `${label} API key saved to OpenCandle.`, "info");
|
|
@@ -244,7 +226,7 @@ async function selectModel(api, ctx, preferredProvider) {
|
|
|
244
226
|
ctx.ui.notify(`Model selected: ${model.provider}/${model.id}`, "info");
|
|
245
227
|
return true;
|
|
246
228
|
}
|
|
247
|
-
async function runLlmSetup(api, ctx, mode) {
|
|
229
|
+
async function runLlmSetup(api, ctx, mode, modelRuntime) {
|
|
248
230
|
while (true) {
|
|
249
231
|
const requirement = getLlmSetupRequirement(ctx);
|
|
250
232
|
if (requirement === "ready") {
|
|
@@ -276,15 +258,21 @@ async function runLlmSetup(api, ctx, mode) {
|
|
|
276
258
|
return "cancelled";
|
|
277
259
|
}
|
|
278
260
|
if (choice === "Sign in with browser") {
|
|
261
|
+
if (!modelRuntime) {
|
|
262
|
+
ctx.ui.notify("Model authentication is unavailable in this session.", "error");
|
|
263
|
+
return "cancelled";
|
|
264
|
+
}
|
|
279
265
|
const providerChoice = await selectProviderForLogin(ctx);
|
|
280
266
|
if (!providerChoice) {
|
|
281
267
|
continue;
|
|
282
268
|
}
|
|
283
|
-
const providerId = providerChoice === "advanced"
|
|
269
|
+
const providerId = providerChoice === "advanced"
|
|
270
|
+
? await selectAdvancedOAuthProvider(ctx, modelRuntime)
|
|
271
|
+
: providerChoice;
|
|
284
272
|
if (!providerId) {
|
|
285
273
|
continue;
|
|
286
274
|
}
|
|
287
|
-
const loggedIn = await runLoginDialog(ctx, providerId);
|
|
275
|
+
const loggedIn = await runLoginDialog(ctx, providerId, modelRuntime);
|
|
288
276
|
if (!loggedIn) {
|
|
289
277
|
continue;
|
|
290
278
|
}
|
|
@@ -303,7 +291,11 @@ async function runLlmSetup(api, ctx, mode) {
|
|
|
303
291
|
if (!provider) {
|
|
304
292
|
continue;
|
|
305
293
|
}
|
|
306
|
-
|
|
294
|
+
if (!modelRuntime) {
|
|
295
|
+
ctx.ui.notify("Model authentication is unavailable in this session.", "error");
|
|
296
|
+
return "cancelled";
|
|
297
|
+
}
|
|
298
|
+
const saved = await runApiKeySetup(ctx, provider, modelRuntime);
|
|
307
299
|
if (!saved) {
|
|
308
300
|
continue;
|
|
309
301
|
}
|
|
@@ -317,10 +309,10 @@ async function runLlmSetup(api, ctx, mode) {
|
|
|
317
309
|
}
|
|
318
310
|
}
|
|
319
311
|
}
|
|
320
|
-
export async function runOpenCandleSetup(api, ctx, options = { mode: "startup" }) {
|
|
312
|
+
export async function runOpenCandleSetup(api, ctx, options = { mode: "startup" }, modelRuntime) {
|
|
321
313
|
const initialRequirement = getLlmSetupRequirement(ctx);
|
|
322
314
|
if (initialRequirement !== "ready" || options.mode === "manual") {
|
|
323
|
-
return runLlmSetup(api, ctx, options.mode);
|
|
315
|
+
return runLlmSetup(api, ctx, options.mode, modelRuntime);
|
|
324
316
|
}
|
|
325
317
|
return "ready";
|
|
326
318
|
}
|
|
@@ -322,11 +322,12 @@ export function buildCompareAssetsPrompt(resolution) {
|
|
|
322
322
|
const isOverlapComparison = resolution.resolved.metrics?.includes("overlap") ?? false;
|
|
323
323
|
const hasFundContext = isFundOrIndexAssetScope(resolution.resolved.assetScope) || areLikelyFundOrIndexSymbols(symbols);
|
|
324
324
|
const shouldProbeFundOverlap = !isOverlapComparison && isLongInvestmentHorizon(timeHorizon) && hasFundContext;
|
|
325
|
+
const technicalEndStep = shouldProbeFundOverlap ? 7 : 6;
|
|
325
326
|
const sentimentStep = includeSentiment
|
|
326
|
-
? `\
|
|
327
|
+
? `\n${technicalEndStep + 1}. Use get_sentiment_summary for each of: ${symbolList} to compare retail/news sentiment and note source availability.`
|
|
327
328
|
: "";
|
|
328
329
|
const interestRateStep = isInterestRateSensitive
|
|
329
|
-
? `\n${includeSentiment ?
|
|
330
|
+
? `\n${technicalEndStep + (includeSentiment ? 2 : 1)}. Use get_economic_data for the current Fed funds backdrop. Treat this as historical/current context unless you also have explicit futures or forecast evidence.`
|
|
330
331
|
: "";
|
|
331
332
|
const sentimentMetric = includeSentiment ? ", sentiment score/summary" : "";
|
|
332
333
|
const interestRateGuidance = isInterestRateSensitive
|
|
@@ -376,23 +377,23 @@ macro hedge decision guidance:
|
|
|
376
377
|
: `- Present a comparison table with key metrics: price, P/E, revenue growth, profit margin, RSI, Sharpe, max drawdown${sentimentMetric}.
|
|
377
378
|
- Highlight which asset is stronger on each metric.`;
|
|
378
379
|
const technicalRiskSteps = isOverlapComparison
|
|
379
|
-
? `
|
|
380
|
-
|
|
381
|
-
|
|
380
|
+
? `4. Use analyze_holdings_overlap with symbols [${symbolList}] to fetch provider top holdings and compute pairwise overlap by weight.
|
|
381
|
+
5. Use analyze_correlation across [${symbolList}] only as supporting diversification evidence; do not substitute correlation for holdings overlap.
|
|
382
|
+
6. Skip momentum/risk tool calls unless the user asks about timing or trade setup; the core question is top holdings and sector overlap.`
|
|
382
383
|
: shouldProbeFundOverlap
|
|
383
|
-
? `
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
: `
|
|
388
|
-
|
|
389
|
-
|
|
384
|
+
? `4. Use analyze_holdings_overlap with symbols [${symbolList}] to fetch provider top holdings and compute pairwise overlap by weight.
|
|
385
|
+
5. Use analyze_correlation across [${symbolList}] as supporting diversification evidence.
|
|
386
|
+
6. Use analyze_risk for each to compare long-horizon risk context.
|
|
387
|
+
7. Use get_technical_indicators only as secondary timing context; do not let RSI or short-term momentum dominate the long-horizon fund decision.`
|
|
388
|
+
: `4. Use get_technical_indicators for each to compare momentum and trend.
|
|
389
|
+
5. Use analyze_risk for each to compare risk metrics.
|
|
390
|
+
6. Use analyze_correlation across [${symbolList}] to check diversification.`;
|
|
390
391
|
const horizonLine = timeHorizon ? `\nTime horizon: ${timeHorizon}` : "";
|
|
391
392
|
const budgetLine = budget !== undefined ? `\nBudget: ${formatBudget(budget)}` : "";
|
|
392
393
|
const horizonSteps = timeHorizon
|
|
393
394
|
? `
|
|
394
|
-
|
|
395
|
-
|
|
395
|
+
${technicalEndStep + (includeSentiment ? 1 : 0) + (isInterestRateSensitive ? 1 : 0) + 1}. Adapt the comparison to the ${timeHorizon} horizon: prioritize near-term catalysts, earnings/guidance, estimate revisions, sentiment, and forward-looking valuation evidence over long-term historical averages.
|
|
396
|
+
${technicalEndStep + (includeSentiment ? 1 : 0) + (isInterestRateSensitive ? 1 : 0) + 2}. Use historical risk and technical metrics as context, but explain what they do and do not imply over ${timeHorizon}.`
|
|
396
397
|
: "";
|
|
397
398
|
const horizonResponse = timeHorizon
|
|
398
399
|
? `
|
|
@@ -427,6 +428,7 @@ Compare these assets side by side: ${symbolList}${horizonLine}${budgetLine}
|
|
|
427
428
|
Steps:
|
|
428
429
|
1. Use get_stock_quote for each of: ${symbolList}.
|
|
429
430
|
2. Use compare_companies with symbols [${symbols.map((s) => `"${s}"`).join(", ")}] for peer metrics. If some fundamentals are unavailable, continue the comparison with the available symbols and mark missing metrics as unavailable.
|
|
431
|
+
3. Use get_price_comparison with symbols [${symbolList}] and a range that fits the resolved time horizon; use the default comparison range when no time horizon is resolved so the answer includes an indexed price comparison.
|
|
430
432
|
${technicalRiskSteps}${sentimentStep}${interestRateStep}${horizonSteps}
|
|
431
433
|
${macroHedgeSteps}
|
|
432
434
|
${interestRateGuidance}
|
|
@@ -2,6 +2,7 @@ export { getEarnings, getFinancials, getOverview } from "./alpha-vantage.js";
|
|
|
2
2
|
export { getCryptoHistory, getCryptoPrice } from "./coingecko.js";
|
|
3
3
|
export { getFearGreedIndex } from "./fear-greed.js";
|
|
4
4
|
export { getSeries } from "./fred.js";
|
|
5
|
+
export { getLseCandles, getLseFinancialReports, getLseFinancials, type LseCandle, type LseFinancialReportRow, LseHttpError, type LseTimeframe, toLseTimeframe, } from "./lse.js";
|
|
5
6
|
export { getSubredditPosts, scoreSentiment } from "./reddit.js";
|
|
6
7
|
export { type SECFiling, searchFilings } from "./sec-edgar.js";
|
|
7
8
|
export { getQuotes, screenStocks } from "./tradingview.js";
|
package/dist/providers/index.js
CHANGED
|
@@ -2,6 +2,7 @@ export { getEarnings, getFinancials, getOverview } from "./alpha-vantage.js";
|
|
|
2
2
|
export { getCryptoHistory, getCryptoPrice } from "./coingecko.js";
|
|
3
3
|
export { getFearGreedIndex } from "./fear-greed.js";
|
|
4
4
|
export { getSeries } from "./fred.js";
|
|
5
|
+
export { getLseCandles, getLseFinancialReports, getLseFinancials, LseHttpError, toLseTimeframe, } from "./lse.js";
|
|
5
6
|
export { getSubredditPosts, scoreSentiment } from "./reddit.js";
|
|
6
7
|
export { searchFilings } from "./sec-edgar.js";
|
|
7
8
|
export { getQuotes, screenStocks } from "./tradingview.js";
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { FinancialStatement } from "../types/fundamentals.js";
|
|
2
|
+
export type LseTimeframe = "1m" | "5m" | "15m" | "1h" | "1d" | "1w" | "1mo";
|
|
3
|
+
export interface LseCandle {
|
|
4
|
+
ts: string;
|
|
5
|
+
symbol: string;
|
|
6
|
+
open: number;
|
|
7
|
+
high: number;
|
|
8
|
+
low: number;
|
|
9
|
+
close: number;
|
|
10
|
+
volume: number;
|
|
11
|
+
}
|
|
12
|
+
export type LseFinancialReportRow = Record<string, unknown>;
|
|
13
|
+
interface LseCandleOptions {
|
|
14
|
+
start?: string;
|
|
15
|
+
end?: string;
|
|
16
|
+
order?: "asc" | "desc";
|
|
17
|
+
limit?: number;
|
|
18
|
+
}
|
|
19
|
+
interface LseFinancialReportOptions {
|
|
20
|
+
period?: "FY" | "Q1" | "Q2" | "Q3" | "Q4";
|
|
21
|
+
start?: string;
|
|
22
|
+
end?: string;
|
|
23
|
+
order?: "asc" | "desc";
|
|
24
|
+
limit?: number;
|
|
25
|
+
}
|
|
26
|
+
export declare class LseHttpError extends Error {
|
|
27
|
+
readonly status: number;
|
|
28
|
+
readonly detail: string;
|
|
29
|
+
constructor(status: number, detail: string);
|
|
30
|
+
}
|
|
31
|
+
export declare function getLseCandles(symbol: string, timeframe: LseTimeframe, opts?: LseCandleOptions): Promise<LseCandle[]>;
|
|
32
|
+
export declare function getLseFinancialReports(symbol: string, reportType: "income" | "balance" | "cashflow", opts?: LseFinancialReportOptions): Promise<LseFinancialReportRow[]>;
|
|
33
|
+
export declare function getLseFinancials(symbol: string): Promise<FinancialStatement[]>;
|
|
34
|
+
export declare function toLseTimeframe(interval: string): LseTimeframe | undefined;
|
|
35
|
+
export {};
|