opencandle 0.12.0 → 0.13.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/README.md +48 -180
  2. package/dist/cli-main.js +9 -8
  3. package/dist/config.d.ts +4 -0
  4. package/dist/config.js +1 -0
  5. package/dist/doctor/cli-command.js +7 -4
  6. package/dist/doctor/report.js +13 -0
  7. package/dist/gui/server/chat-event-adapter.js +145 -20
  8. package/dist/gui/server/history-snapshot-store.d.ts +8 -0
  9. package/dist/gui/server/history-snapshot-store.js +43 -0
  10. package/dist/gui/server/http-routes.d.ts +2 -0
  11. package/dist/gui/server/http-routes.js +32 -7
  12. package/dist/gui/server/live-chat-event-adapter.d.ts +2 -0
  13. package/dist/gui/server/live-chat-event-adapter.js +33 -15
  14. package/dist/gui/server/market-indices-api.d.ts +21 -0
  15. package/dist/gui/server/market-indices-api.js +26 -0
  16. package/dist/gui/server/market-indices-snapshot-store.d.ts +12 -0
  17. package/dist/gui/server/market-indices-snapshot-store.js +56 -0
  18. package/dist/gui/server/market-state-api.d.ts +112 -0
  19. package/dist/gui/server/market-state-api.js +246 -4
  20. package/dist/gui/server/model-setup.d.ts +2 -11
  21. package/dist/gui/server/model-setup.js +10 -6
  22. package/dist/gui/server/server.js +12 -9
  23. package/dist/gui/server/tool-metadata.d.ts +14 -14
  24. package/dist/gui/server/ws-hub.js +1 -1
  25. package/dist/infra/cache.d.ts +4 -0
  26. package/dist/infra/cache.js +4 -0
  27. package/dist/infra/lse-byte-budget.d.ts +10 -0
  28. package/dist/infra/lse-byte-budget.js +47 -0
  29. package/dist/infra/rate-limiter.js +1 -0
  30. package/dist/onboarding/providers.d.ts +16 -1
  31. package/dist/onboarding/providers.js +20 -0
  32. package/dist/onboarding/validate-model-key.js +33 -1
  33. package/dist/onboarding/validation.js +8 -0
  34. package/dist/pi/opencandle-extension.d.ts +2 -1
  35. package/dist/pi/opencandle-extension.js +2 -2
  36. package/dist/pi/session.d.ts +2 -3
  37. package/dist/pi/session.js +7 -5
  38. package/dist/pi/setup.d.ts +3 -3
  39. package/dist/pi/setup.js +51 -59
  40. package/dist/prompts/workflow-prompts.js +16 -14
  41. package/dist/providers/index.d.ts +1 -0
  42. package/dist/providers/index.js +1 -0
  43. package/dist/providers/lse.d.ts +35 -0
  44. package/dist/providers/lse.js +284 -0
  45. package/dist/providers/polymarket.js +22 -5
  46. package/dist/providers/wrap-provider.js +1 -0
  47. package/dist/providers/yahoo-finance.js +1 -0
  48. package/dist/routing/route-manifest.js +1 -0
  49. package/dist/routing/router.js +8 -1
  50. package/dist/runtime/evidence.d.ts +1 -0
  51. package/dist/runtime/evidence.js +1 -1
  52. package/dist/runtime/session-coordinator.d.ts +2 -2
  53. package/dist/runtime/session-coordinator.js +2 -2
  54. package/dist/tools/fundamentals/dcf.js +37 -8
  55. package/dist/tools/fundamentals/financials.js +88 -10
  56. package/dist/tools/index.d.ts +10 -5
  57. package/dist/tools/index.js +3 -0
  58. package/dist/tools/market/price-comparison.d.ts +30 -0
  59. package/dist/tools/market/price-comparison.js +202 -0
  60. package/dist/tools/market/stock-history.d.ts +18 -3
  61. package/dist/tools/market/stock-history.js +132 -17
  62. package/dist/tools/portfolio/correlation.d.ts +1 -1
  63. package/dist/tools/portfolio/risk-analysis.d.ts +1 -1
  64. package/dist/types/market.d.ts +2 -0
  65. package/gui/web/dist/assets/CatalogOverlay-Cy8Fq0fn.js +1 -0
  66. package/gui/web/dist/assets/allocation-donut-DgBRIKCk.js +52 -0
  67. package/gui/web/dist/assets/index-DIlFyIOO.js +66 -0
  68. package/gui/web/dist/assets/index-DS4jESOB.css +2 -0
  69. package/gui/web/dist/assets/market-chart-DammaCjH.js +1 -0
  70. package/gui/web/dist/assets/utils-CnADgYgh.js +1 -0
  71. package/gui/web/dist/index.html +3 -2
  72. package/package.json +15 -17
  73. package/gui/web/dist/assets/CatalogOverlay-CAc7e3Pf.js +0 -1
  74. package/gui/web/dist/assets/index-BOEKd9wT.css +0 -2
  75. package/gui/web/dist/assets/index-C-H05cQ2.js +0 -65
@@ -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
  }
@@ -1,10 +1,9 @@
1
- import { type AuthStorage, type CreateAgentSessionResult, type ModelRegistry, type SessionManager, type SettingsManager } from "@earendil-works/pi-coding-agent";
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
- authStorage?: AuthStorage;
7
- modelRegistry?: ModelRegistry;
6
+ modelRuntime?: ModelRuntime;
8
7
  settingsManager?: SettingsManager;
9
8
  sessionManager?: SessionManager;
10
9
  useInlineExtension?: boolean;
@@ -14,7 +14,10 @@ export async function createOpenCandleSession(options = {}) {
14
14
  agentDir,
15
15
  settingsManager: options.settingsManager,
16
16
  extensionFactories: [
17
- (pi) => openCandleExtension(pi, { askUserHandler: options.askUserHandler }),
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
- authStorage: options.authStorage,
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.modelRegistry.find(provider, modelId);
46
- if (!savedDefault || !result.session.modelRegistry.hasConfiguredAuth(savedDefault))
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)
@@ -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 = ctx.modelRegistry.authStorage.getOAuthProviders();
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 = ctx.modelRegistry.authStorage
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
- let manualCodeResolve;
114
- let manualCodeReject;
115
- const manualCodePromise = new Promise((resolve, reject) => {
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(`${prompt.message}\n\n${options}`, "Enter a number");
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 prompt.options[selectedIndex]?.id;
157
- },
158
- onManualCodeInput: () => manualCodePromise,
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
- ctx.modelRegistry.authStorage.set(provider, { type: "api_key", key: trimmed });
193
- ctx.modelRegistry.refresh();
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" ? await selectAdvancedOAuthProvider(ctx) : providerChoice;
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
- const saved = await runApiKeySetup(ctx, provider);
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
- ? `\n6. Use get_sentiment_summary for each of: ${symbolList} to compare retail/news sentiment and note source availability.`
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 ? "7" : "6"}. 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
+ ? `\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
- ? `3. Use analyze_holdings_overlap with symbols [${symbolList}] to fetch provider top holdings and compute pairwise overlap by weight.
380
- 4. Use analyze_correlation across [${symbolList}] only as supporting diversification evidence; do not substitute correlation for holdings overlap.
381
- 5. Skip momentum/risk tool calls unless the user asks about timing or trade setup; the core question is top holdings and sector overlap.`
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
- ? `3. Use analyze_holdings_overlap with symbols [${symbolList}] to fetch provider top holdings and compute pairwise overlap by weight.
384
- 4. Use analyze_correlation across [${symbolList}] as supporting diversification evidence.
385
- 5. Use analyze_risk for each to compare long-horizon risk context.
386
- 6. Use get_technical_indicators only as secondary timing context; do not let RSI or short-term momentum dominate the long-horizon fund decision.`
387
- : `3. Use get_technical_indicators for each to compare momentum and trend.
388
- 4. Use analyze_risk for each to compare risk metrics.
389
- 5. Use analyze_correlation across [${symbolList}] to check diversification.`;
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
- 6. 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.
395
- 7. Use historical risk and technical metrics as context, but explain what they do and do not imply over ${timeHorizon}.`
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";
@@ -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 {};