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
@@ -2,8 +2,135 @@ import { buildFreshnessStamp } from "../../infra/freshness.js";
2
2
  import { isZeroFilledQuote, searchYahooInstruments } from "../../market-state/resolve.js";
3
3
  import { MarketStateService } from "../../market-state/service.js";
4
4
  import { initDefaultDatabase } from "../../memory/sqlite.js";
5
+ import { getProvider } from "../../onboarding/providers.js";
5
6
  import { wrapProvider } from "../../providers/wrap-provider.js";
6
- import { getQuote } from "../../providers/yahoo-finance.js";
7
+ import { getHistory, getQuote, getYahooCompanyOverview, } from "../../providers/yahoo-finance.js";
8
+ import { fetchHistoryWithFallback, HISTORY_INTERVALS, } from "../../tools/market/stock-history.js";
9
+ import { HistorySnapshotStore } from "./history-snapshot-store.js";
10
+ const instrumentHistorySnapshotStore = new HistorySnapshotStore();
11
+ const DEFAULT_HISTORY_RANGE_MAP = {
12
+ "1D": { range: "1d", interval: "5m" },
13
+ "5D": { range: "5d", interval: "15m" },
14
+ "1M": { range: "1mo", interval: "1h" },
15
+ "6M": { range: "6mo", interval: "1d" },
16
+ YTD: { range: "ytd", interval: "1d" },
17
+ "1Y": { range: "1y", interval: "1d" },
18
+ "5Y": { range: "5y", interval: "1wk" },
19
+ MAX: { range: "max", interval: "1mo" },
20
+ };
21
+ const HISTORY_RANGE_SPAN_DAYS = {
22
+ "1D": 1,
23
+ "5D": 5,
24
+ "1M": 31,
25
+ "6M": 183,
26
+ YTD: 366,
27
+ "1Y": 366,
28
+ "5Y": 1_827,
29
+ MAX: Number.POSITIVE_INFINITY,
30
+ };
31
+ const INTRADAY_DEPTH_CAP_DAYS = {
32
+ "1m": 7,
33
+ "5m": 60,
34
+ "15m": 60,
35
+ "1h": 730,
36
+ };
37
+ const DAILY_OR_LONGER_INTERVALS = new Set(["1d", "1wk", "1mo"]);
38
+ const HISTORY_PROVIDER_REGISTRY_IDS = {
39
+ yahoo: "yahoo",
40
+ alphavantage: "alpha_vantage",
41
+ lse: "lse",
42
+ };
43
+ function historyProviderDisplayName(provider) {
44
+ if (!(provider && provider in HISTORY_PROVIDER_REGISTRY_IDS))
45
+ return null;
46
+ const providerId = HISTORY_PROVIDER_REGISTRY_IDS[provider];
47
+ return getProvider(providerId).displayName;
48
+ }
49
+ export function resolveHistoryRange(rangeLabel, intervalOverride) {
50
+ const resolved = DEFAULT_HISTORY_RANGE_MAP[rangeLabel];
51
+ if (!resolved) {
52
+ return { status: "invalid_request", reason: `Unknown history range: ${rangeLabel}` };
53
+ }
54
+ if (intervalOverride === undefined)
55
+ return resolved;
56
+ if (!isHistoryInterval(intervalOverride)) {
57
+ return { status: "invalid_request", reason: `Unknown history interval: ${intervalOverride}` };
58
+ }
59
+ const depthCapDays = INTRADAY_DEPTH_CAP_DAYS[intervalOverride];
60
+ if (depthCapDays !== undefined &&
61
+ HISTORY_RANGE_SPAN_DAYS[rangeLabel] > depthCapDays) {
62
+ return {
63
+ status: "invalid_request",
64
+ reason: `History range ${rangeLabel} exceeds the depth cap for interval ${intervalOverride}`,
65
+ };
66
+ }
67
+ return { range: resolved.range, interval: intervalOverride };
68
+ }
69
+ function isHistoryInterval(value) {
70
+ return HISTORY_INTERVALS.includes(value);
71
+ }
72
+ export async function getInstrumentHistorySnapshot(symbol, rangeLabel = "1D", intervalOverride) {
73
+ const resolved = resolveHistoryRange(rangeLabel, intervalOverride);
74
+ if ("status" in resolved)
75
+ return resolved;
76
+ const key = `${symbol}|${resolved.range}|${resolved.interval}`;
77
+ return instrumentHistorySnapshotStore.get(key, async () => {
78
+ const result = await fetchHistoryWithFallback(symbol, resolved.range, resolved.interval);
79
+ if (result.status === "unavailable") {
80
+ return { status: "unavailable", reason: result.reason };
81
+ }
82
+ const source = historyProviderDisplayName(result.provider);
83
+ if (source === null) {
84
+ return { status: "unavailable", reason: "History provider attribution is unavailable" };
85
+ }
86
+ const bars = result.data.filter((bar) => Number.isFinite(bar.close));
87
+ const dataAsOf = bars.at(-1)?.date;
88
+ const chartBars = [];
89
+ for (const bar of bars) {
90
+ let time = bar.timestamp;
91
+ if (typeof time !== "number" || !Number.isFinite(time)) {
92
+ if (DAILY_OR_LONGER_INTERVALS.has(resolved.interval)) {
93
+ time = Date.parse(`${bar.date}T00:00:00Z`) / 1_000;
94
+ }
95
+ if (typeof time !== "number" || !Number.isFinite(time)) {
96
+ return {
97
+ status: "unavailable",
98
+ reason: `Historical bars for ${resolved.interval} are missing finite timestamps`,
99
+ dataAsOf,
100
+ stale: result.stale === true || undefined,
101
+ };
102
+ }
103
+ }
104
+ chartBars.push({
105
+ time,
106
+ open: bar.open,
107
+ high: bar.high,
108
+ low: bar.low,
109
+ close: bar.close,
110
+ volume: bar.volume,
111
+ });
112
+ }
113
+ // Design D3: daily+ bars can honestly derive the previous close from the prior bar.
114
+ const prevClose = DAILY_OR_LONGER_INTERVALS.has(resolved.interval) && bars.length >= 2
115
+ ? bars[bars.length - 2].close
116
+ : null;
117
+ return {
118
+ status: "ok",
119
+ symbol,
120
+ range: rangeLabel,
121
+ interval: resolved.interval,
122
+ source,
123
+ fetchedAt: result.timestamp,
124
+ dataAsOf,
125
+ stale: result.stale === true,
126
+ prevClose,
127
+ bars: chartBars,
128
+ };
129
+ });
130
+ }
131
+ export function resetInstrumentHistoryMemoForTests() {
132
+ instrumentHistorySnapshotStore.clear();
133
+ }
7
134
  export function createSavedSymbolsMemo(load, options = {}) {
8
135
  const ttlMs = options.ttlMs ?? 30_000;
9
136
  const now = options.now ?? Date.now;
@@ -96,8 +223,13 @@ export async function buildMarketStateQuoteSnapshot(db) {
96
223
  ]),
97
224
  ];
98
225
  const quoteMap = new Map();
226
+ const sparklineMap = new Map();
99
227
  for (const symbol of symbols) {
100
- quoteMap.set(symbol, await fetchQuoteSnapshot(symbol));
228
+ const quote = await fetchQuoteSnapshot(symbol);
229
+ quoteMap.set(symbol, quote);
230
+ sparklineMap.set(symbol, quote.status === "ok"
231
+ ? await fetchSparklineSnapshot(symbol)
232
+ : unavailableSparkline(quote.reason));
101
233
  }
102
234
  const generatedAt = new Date().toISOString();
103
235
  const watchlistQuotes = watchlist.map((item) => {
@@ -109,6 +241,7 @@ export async function buildMarketStateQuoteSnapshot(db) {
109
241
  symbol: item.symbol,
110
242
  status: "unavailable",
111
243
  reason: quote?.reason ?? "quote unavailable",
244
+ sparkline: sparklineMap.get(item.symbol) ?? unavailableSparkline("History unavailable"),
112
245
  };
113
246
  }
114
247
  return {
@@ -134,12 +267,14 @@ export async function buildMarketStateQuoteSnapshot(db) {
134
267
  extendedChangePercent: quote.extendedChangePercent,
135
268
  extendedAsOf: quote.extendedAsOf,
136
269
  stale: quote.stale,
270
+ sparkline: sparklineMap.get(item.symbol) ?? unavailableSparkline("History unavailable"),
137
271
  };
138
272
  });
139
273
  const portfolioResults = portfolios.map((portfolio) => buildPortfolioQuoteResult({
140
274
  portfolio,
141
275
  lots: portfolioLots.filter((lot) => lot.portfolioId === portfolio.id),
142
276
  quoteMap,
277
+ sparklineMap,
143
278
  }));
144
279
  const defaultPortfolio = portfolios.find((portfolio) => portfolio.isDefault) ?? portfolios[0];
145
280
  const defaultResult = portfolioResults.find((result) => result.summary.portfolioId === defaultPortfolio.id) ??
@@ -157,7 +292,7 @@ export async function buildMarketStateQuoteSnapshot(db) {
157
292
  ownedDb.close();
158
293
  }
159
294
  }
160
- function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, }) {
295
+ function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, sparklineMap, }) {
161
296
  const baseCurrency = portfolio.baseCurrency ?? "USD";
162
297
  const portfolioQuotes = lots.map((lot) => {
163
298
  const quote = quoteMap.get(lot.symbol);
@@ -177,6 +312,7 @@ function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, }) {
177
312
  currency: lotCurrency,
178
313
  includedInTotals: false,
179
314
  reason: quote?.reason ?? "quote unavailable",
315
+ sparkline: sparklineMap.get(lot.symbol) ?? unavailableSparkline("History unavailable"),
180
316
  };
181
317
  }
182
318
  const resolvedQuoteCurrency = quote.currency ?? quoteCurrency;
@@ -203,6 +339,7 @@ function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, }) {
203
339
  extendedChangePercent: quote.extendedChangePercent,
204
340
  extendedAsOf: quote.extendedAsOf,
205
341
  stale: quote.stale,
342
+ sparkline: sparklineMap.get(lot.symbol) ?? unavailableSparkline("History unavailable"),
206
343
  };
207
344
  }
208
345
  const marketValue = quote.price * lot.quantity;
@@ -229,6 +366,7 @@ function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, }) {
229
366
  extendedChangePercent: quote.extendedChangePercent,
230
367
  extendedAsOf: quote.extendedAsOf,
231
368
  stale: quote.stale,
369
+ sparkline: sparklineMap.get(lot.symbol) ?? unavailableSparkline("History unavailable"),
232
370
  reason: includedInTotals
233
371
  ? undefined
234
372
  : `No FX conversion from ${lotCurrency === baseCurrency ? quoteCurrency : lotCurrency} to ${baseCurrency}`,
@@ -265,6 +403,36 @@ function buildPortfolioQuoteResult({ portfolio, lots, quoteMap, }) {
265
403
  },
266
404
  };
267
405
  }
406
+ export async function fetchSparklineSnapshot(symbol) {
407
+ const result = await wrapProvider("yahoo", () => getHistory(symbol, "1d", "5m"));
408
+ if (result.status === "unavailable")
409
+ return unavailableSparkline(result.reason);
410
+ const bars = result.data.filter((bar) => Number.isFinite(bar.close));
411
+ const dataAsOf = bars.at(-1)?.date;
412
+ if (result.stale) {
413
+ return unavailableSparkline("Historical data is stale", dataAsOf, true);
414
+ }
415
+ if (bars.length < 2) {
416
+ return unavailableSparkline("Not enough intraday history", dataAsOf);
417
+ }
418
+ return {
419
+ status: "ok",
420
+ source: "Yahoo Finance",
421
+ points: bars.map((bar) => bar.close),
422
+ fetchedAt: result.timestamp,
423
+ dataAsOf,
424
+ stale: false,
425
+ };
426
+ }
427
+ function unavailableSparkline(reason, dataAsOf, stale) {
428
+ return {
429
+ status: "unavailable",
430
+ source: "Yahoo Finance",
431
+ reason,
432
+ dataAsOf,
433
+ stale,
434
+ };
435
+ }
268
436
  export async function searchInstrumentCandidates(query) {
269
437
  const trimmed = query.trim();
270
438
  if (!trimmed)
@@ -302,6 +470,7 @@ export async function getInstrumentQuoteSnapshot(symbol) {
302
470
  low: quote.low,
303
471
  week52High: quote.week52High,
304
472
  week52Low: quote.week52Low,
473
+ marketCap: quote.marketCap,
305
474
  fetchedAt: quote.fetchedAt,
306
475
  dataAsOf: quote.dataAsOf,
307
476
  marketState: quote.marketState,
@@ -313,7 +482,79 @@ export async function getInstrumentQuoteSnapshot(symbol) {
313
482
  currency: quote.currency,
314
483
  };
315
484
  }
316
- async function fetchQuoteSnapshot(symbol) {
485
+ const INSTRUMENT_OVERVIEW_TTL_MS = 5 * 60_000;
486
+ const instrumentOverviewMemo = new Map();
487
+ export async function getInstrumentOverviewSnapshot(symbol) {
488
+ const normalized = symbol.trim().toUpperCase();
489
+ if (!normalized)
490
+ return { symbol: "", status: "unavailable", reason: "symbol is required" };
491
+ const existing = instrumentOverviewMemo.get(normalized);
492
+ if (existing?.snapshot && Date.now() - existing.fetchedAtMs < INSTRUMENT_OVERVIEW_TTL_MS) {
493
+ return existing.snapshot;
494
+ }
495
+ if (existing?.snapshot) {
496
+ if (!existing.inFlight)
497
+ refreshInstrumentOverview(normalized, existing).catch(() => { });
498
+ return existing.snapshot.status === "ok"
499
+ ? { ...existing.snapshot, stale: true }
500
+ : existing.snapshot;
501
+ }
502
+ if (existing?.inFlight)
503
+ return existing.inFlight;
504
+ const entry = existing ?? { snapshot: null, fetchedAtMs: 0, inFlight: null };
505
+ return refreshInstrumentOverview(normalized, entry);
506
+ }
507
+ function refreshInstrumentOverview(normalized, entry) {
508
+ if (entry.inFlight)
509
+ return entry.inFlight;
510
+ const inFlight = loadInstrumentOverviewSnapshot(normalized)
511
+ .then((snapshot) => {
512
+ if (entry.snapshot?.status === "ok" && snapshot.status === "unavailable") {
513
+ return entry.snapshot;
514
+ }
515
+ entry.snapshot = snapshot;
516
+ entry.fetchedAtMs = Date.now();
517
+ return snapshot;
518
+ })
519
+ .finally(() => {
520
+ if (entry.inFlight === inFlight)
521
+ entry.inFlight = null;
522
+ });
523
+ entry.inFlight = inFlight;
524
+ instrumentOverviewMemo.set(normalized, entry);
525
+ return inFlight;
526
+ }
527
+ async function loadInstrumentOverviewSnapshot(normalized) {
528
+ const result = await wrapProvider("yahoo", () => getYahooCompanyOverview(normalized));
529
+ if (result.status === "unavailable") {
530
+ return { symbol: normalized, status: "unavailable", reason: result.reason };
531
+ }
532
+ return {
533
+ symbol: normalized,
534
+ status: "ok",
535
+ name: result.data.name,
536
+ description: result.data.description,
537
+ exchange: result.data.exchange,
538
+ sector: result.data.sector,
539
+ industry: result.data.industry,
540
+ marketCap: result.data.marketCap,
541
+ pe: result.data.pe,
542
+ forwardPe: result.data.forwardPe,
543
+ eps: result.data.eps,
544
+ dividendYield: result.data.dividendYield,
545
+ beta: result.data.beta,
546
+ avgVolume: result.data.avgVolume,
547
+ profitMargin: result.data.profitMargin,
548
+ revenueGrowth: result.data.revenueGrowth,
549
+ week52High: result.data.week52High,
550
+ week52Low: result.data.week52Low,
551
+ stale: result.stale === true,
552
+ };
553
+ }
554
+ export function resetInstrumentOverviewMemoForTests() {
555
+ instrumentOverviewMemo.clear();
556
+ }
557
+ export async function fetchQuoteSnapshot(symbol) {
317
558
  const result = await wrapProvider("yahoo", () => getQuote(symbol));
318
559
  if (result.status === "unavailable")
319
560
  return { status: "unavailable", reason: result.reason };
@@ -342,6 +583,7 @@ async function fetchQuoteSnapshot(symbol) {
342
583
  low: result.data.low,
343
584
  week52High: result.data.week52High,
344
585
  week52Low: result.data.week52Low,
586
+ marketCap: result.data.marketCap,
345
587
  fetchedAt: result.timestamp,
346
588
  dataAsOf: freshness.providerDataAt,
347
589
  marketState: result.data.marketState,
@@ -1,4 +1,5 @@
1
1
  import type { Api, Model } from "@earendil-works/pi-ai";
2
+ import { type ModelRuntime } from "@earendil-works/pi-coding-agent";
2
3
  export type ModelSetupRequirement = "ready" | "select_model" | "connect_auth";
3
4
  export interface ModelSetupProvider {
4
5
  id: string;
@@ -23,18 +24,8 @@ export interface ModelSetupRegistry {
23
24
  getAvailable(): Model<Api>[];
24
25
  hasConfiguredAuth(model: Model<Api>): boolean;
25
26
  }
26
- interface ModelSetupAuthStorage {
27
- set(provider: string, credential: {
28
- type: "api_key";
29
- key: string;
30
- }): void;
31
- }
32
- interface MutableModelSetupRegistry extends ModelSetupRegistry {
33
- authStorage: ModelSetupAuthStorage;
34
- find(provider: string, modelId: string): Model<Api> | undefined;
35
- }
36
27
  interface ModelSetupSession {
37
- modelRegistry: MutableModelSetupRegistry;
28
+ modelRuntime: ModelRuntime;
38
29
  model?: Model<Api>;
39
30
  setModel(model: Model<Api>): Promise<void>;
40
31
  settingsManager: {
@@ -1,3 +1,4 @@
1
+ import { ModelRegistry } from "@earendil-works/pi-coding-agent";
1
2
  import { persistProviderCredential } from "../../onboarding/connect.js";
2
3
  import { getCredentialSource, isApiKeyProvider, PROVIDERS, } from "../../onboarding/providers.js";
3
4
  import { validateModelKey, } from "../../onboarding/validate-model-key.js";
@@ -72,7 +73,7 @@ export function createModelSetupController({ role, getSession, getSessionManager
72
73
  }
73
74
  function buildCurrentModelSetupState() {
74
75
  const session = getSession();
75
- return buildModelSetupState(session.modelRegistry, session.model);
76
+ return buildModelSetupState(new ModelRegistry(session.modelRuntime), session.model);
76
77
  }
77
78
  async function handleSaveModelApiKey(providerId, apiKey) {
78
79
  ensureWriter();
@@ -87,9 +88,12 @@ export function createModelSetupController({ role, getSession, getSessionManager
87
88
  throw new Error(`Key was rejected by ${validation.providerLabel}. The existing configuration was not changed.`);
88
89
  }
89
90
  const session = getSession();
90
- session.modelRegistry.authStorage.set(provider.id, { type: "api_key", key: trimmed });
91
- session.modelRegistry.refresh();
92
- const model = findPreferredModel(session.modelRegistry, provider);
91
+ await session.modelRuntime.login(provider.id, "api_key", {
92
+ prompt: async () => trimmed,
93
+ notify: () => { },
94
+ });
95
+ const modelRegistry = new ModelRegistry(session.modelRuntime);
96
+ const model = findPreferredModel(modelRegistry, provider);
93
97
  if (!model) {
94
98
  throw new Error(`Saved the ${provider.label} key, but no ${provider.label} models are available yet.`);
95
99
  }
@@ -134,8 +138,8 @@ export function createModelSetupController({ role, getSession, getSessionManager
134
138
  async function handleSelectModel(provider, modelId) {
135
139
  ensureWriter();
136
140
  const session = getSession();
137
- session.modelRegistry.refresh();
138
- const model = session.modelRegistry.find(provider, modelId);
141
+ await session.modelRuntime.refresh();
142
+ const model = session.modelRuntime.getModel(provider, modelId);
139
143
  if (!model)
140
144
  throw new Error(`Unknown model: ${provider}/${modelId}`);
141
145
  await session.setModel(model);
@@ -3,7 +3,7 @@ import { existsSync } from "node:fs";
3
3
  import { createServer } from "node:http";
4
4
  import { resolve } from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
- import { AuthStorage, createAgentSessionRuntime, createAgentSessionServices, getAgentDir, ModelRegistry, SettingsManager, } from "@earendil-works/pi-coding-agent";
6
+ import { createAgentSessionRuntime, createAgentSessionServices, getAgentDir, ModelRuntime, SettingsManager, } from "@earendil-works/pi-coding-agent";
7
7
  import { createOpenCandleSession } from "../../index.js";
8
8
  import { assertSupportedNodeVersion } from "../../infra/node-version.js";
9
9
  import { createAskUserBridge } from "./ask-user-bridge.js";
@@ -13,6 +13,8 @@ import { createInitialGuiSessionManager } from "./gui-session-manager.js";
13
13
  import { createHttpRequestHandler, resolveSessionManagerById } from "./http-routes.js";
14
14
  import { createToolInvokeController } from "./invoke-tool.js";
15
15
  import { createLocalSessionCoordinator } from "./local-session-coordinator.js";
16
+ import { buildMarketIndicesSnapshot } from "./market-indices-api.js";
17
+ import { MarketIndicesSnapshotStore } from "./market-indices-snapshot-store.js";
16
18
  import { buildMarketStateQuoteSnapshot } from "./market-state-api.js";
17
19
  import { createModelSetupController } from "./model-setup.js";
18
20
  import { isTrustedPrivateApiRequest } from "./private-api-access.js";
@@ -33,8 +35,10 @@ const localCoordinatorEndpoint = `http://${coordinatorEndpointHost(host)}:${port
33
35
  const __dirname = fileURLToPath(new URL(".", import.meta.url));
34
36
  const webDist = resolveGuiWebDist(__dirname);
35
37
  const agentDir = getAgentDir();
36
- const authStorage = AuthStorage.create();
37
- const modelRegistry = ModelRegistry.create(authStorage);
38
+ const modelRuntime = await ModelRuntime.create({
39
+ authPath: resolve(agentDir, "auth.json"),
40
+ modelsPath: resolve(agentDir, "models.json"),
41
+ });
38
42
  const settingsManager = SettingsManager.create(cwd, agentDir);
39
43
  const initialSessionManager = createInitialGuiSessionManager(cwd);
40
44
  let sessionManager = initialSessionManager;
@@ -56,15 +60,13 @@ const runtime = await createAgentSessionRuntime(async (opts) => {
56
60
  const services = await createAgentSessionServices({
57
61
  cwd: opts.cwd,
58
62
  agentDir: opts.agentDir,
59
- authStorage,
60
63
  settingsManager,
61
- modelRegistry,
64
+ modelRuntime,
62
65
  });
63
66
  const result = await createOpenCandleSession({
64
67
  cwd: opts.cwd,
65
68
  agentDir: opts.agentDir,
66
- authStorage,
67
- modelRegistry,
69
+ modelRuntime,
68
70
  settingsManager,
69
71
  sessionManager: opts.sessionManager,
70
72
  askUserHandler: askUserBridge.ask,
@@ -100,6 +102,7 @@ const heartbeat = setInterval(() => {
100
102
  const backgroundQuoteRefreshes = new BackgroundQuoteRefreshes();
101
103
  const localSessionCoordinator = createLocalSessionCoordinator();
102
104
  const quoteSnapshotStore = new QuoteSnapshotStore(() => buildMarketStateQuoteSnapshot());
105
+ const indicesSnapshotStore = new MarketIndicesSnapshotStore(() => buildMarketIndicesSnapshot());
103
106
  quotePoller = createBackgroundQuotePoller({
104
107
  getClientCount: () => wsHub.getClientCount(),
105
108
  getSessionManager: () => sessionManager,
@@ -200,8 +203,7 @@ const httpRequestHandler = createHttpRequestHandler({
200
203
  createSessionForManager: async (targetSessionManager) => createOpenCandleSession({
201
204
  cwd,
202
205
  agentDir,
203
- authStorage,
204
- modelRegistry,
206
+ modelRuntime,
205
207
  settingsManager,
206
208
  sessionManager: targetSessionManager,
207
209
  askUserHandler: askUserBridge.askForSession(targetSessionManager.getSessionId()),
@@ -211,6 +213,7 @@ const httpRequestHandler = createHttpRequestHandler({
211
213
  sessionActionsController,
212
214
  toolInvokeController,
213
215
  quoteSnapshotStore,
216
+ indicesSnapshotStore,
214
217
  localSessionCoordinator,
215
218
  });
216
219
  const server = createServer((req, res) => {
@@ -20,13 +20,23 @@ export declare function buildCatalog(): {
20
20
  }[];
21
21
  }[];
22
22
  providers: ({
23
- source: "env" | "file" | "absent";
23
+ id: import("../../onboarding/providers.js").ApiKeyProviderId | import("../../onboarding/providers.js").ExternalToolProviderId | import("../../onboarding/providers.js").PublicHttpProviderId;
24
+ kind: "api-key" | "external-tool" | "public-http";
25
+ displayName: string;
26
+ category: import("../../onboarding/providers.js").ProviderCategory;
27
+ tier: import("../../onboarding/providers.js").ProviderTier;
28
+ aliases: readonly string[];
29
+ unlocks: readonly string[];
30
+ fallbackDescription: string | null;
31
+ instructionsHint: string;
32
+ source: "absent" | "env" | "file";
24
33
  configured: boolean;
25
34
  maskedKeyHint: string | undefined;
26
- status: "env" | "file" | "absent";
35
+ status: "absent" | "env" | "file";
27
36
  signupUrl: string;
28
37
  freeTier: boolean;
29
38
  envVar: string;
39
+ } | {
30
40
  id: import("../../onboarding/providers.js").ApiKeyProviderId | import("../../onboarding/providers.js").ExternalToolProviderId | import("../../onboarding/providers.js").PublicHttpProviderId;
31
41
  kind: "api-key" | "external-tool" | "public-http";
32
42
  displayName: string;
@@ -36,7 +46,6 @@ export declare function buildCatalog(): {
36
46
  unlocks: readonly string[];
37
47
  fallbackDescription: string | null;
38
48
  instructionsHint: string;
39
- } | {
40
49
  status: string;
41
50
  statusDetail: {
42
51
  providerId: import("../../onboarding/providers.js").ExternalToolProviderId;
@@ -52,6 +61,7 @@ export declare function buildCatalog(): {
52
61
  installCmd: string;
53
62
  sessionSource: "browser-cookies";
54
63
  supportedBrowsers: readonly string[] | undefined;
64
+ } | {
55
65
  id: import("../../onboarding/providers.js").ApiKeyProviderId | import("../../onboarding/providers.js").ExternalToolProviderId | import("../../onboarding/providers.js").PublicHttpProviderId;
56
66
  kind: "api-key" | "external-tool" | "public-http";
57
67
  displayName: string;
@@ -61,12 +71,12 @@ export declare function buildCatalog(): {
61
71
  unlocks: readonly string[];
62
72
  fallbackDescription: string | null;
63
73
  instructionsHint: string;
64
- } | {
65
74
  status: string;
66
75
  binary: string;
67
76
  installCmd: string;
68
77
  sessionSource: "browser-cookies";
69
78
  supportedBrowsers: readonly string[] | undefined;
79
+ } | {
70
80
  id: import("../../onboarding/providers.js").ApiKeyProviderId | import("../../onboarding/providers.js").ExternalToolProviderId | import("../../onboarding/providers.js").PublicHttpProviderId;
71
81
  kind: "api-key" | "external-tool" | "public-http";
72
82
  displayName: string;
@@ -76,18 +86,8 @@ export declare function buildCatalog(): {
76
86
  unlocks: readonly string[];
77
87
  fallbackDescription: string | null;
78
88
  instructionsHint: string;
79
- } | {
80
89
  status: string;
81
90
  probeUrl: string;
82
- id: import("../../onboarding/providers.js").ApiKeyProviderId | import("../../onboarding/providers.js").ExternalToolProviderId | import("../../onboarding/providers.js").PublicHttpProviderId;
83
- kind: "api-key" | "external-tool" | "public-http";
84
- displayName: string;
85
- category: import("../../onboarding/providers.js").ProviderCategory;
86
- tier: import("../../onboarding/providers.js").ProviderTier;
87
- aliases: readonly string[];
88
- unlocks: readonly string[];
89
- fallbackDescription: string | null;
90
- instructionsHint: string;
91
91
  })[];
92
92
  };
93
93
  export declare function setToolEnabled(toolName: string, enabled: boolean): void;
@@ -64,7 +64,7 @@ export function createWsHub({ role, lock, cwd, sessionDir, getSession, getSessio
64
64
  client.send({ type: "catalog", catalog: buildCatalog() });
65
65
  break;
66
66
  case "model.setup.refresh":
67
- getSession().modelRegistry.refresh();
67
+ await getSession().modelRuntime.refresh();
68
68
  broadcastModelSetup();
69
69
  break;
70
70
  case "model.setup.save_api_key":
@@ -31,7 +31,9 @@ export declare const cache: Cache;
31
31
  export declare const TTL: {
32
32
  readonly QUOTE: 60000;
33
33
  readonly HISTORY: 3600000;
34
+ readonly CANDLES: 3600000;
34
35
  readonly FUNDAMENTALS: 86400000;
36
+ readonly FINANCIAL_REPORTS: 86400000;
35
37
  readonly MACRO: 3600000;
36
38
  readonly SENTIMENT: 300000;
37
39
  readonly OPTIONS_CHAIN: 120000;
@@ -44,7 +46,9 @@ export declare const TTL: {
44
46
  export declare const STALE_LIMIT: {
45
47
  readonly QUOTE: number;
46
48
  readonly HISTORY: number;
49
+ readonly CANDLES: number;
47
50
  readonly FUNDAMENTALS: number;
51
+ readonly FINANCIAL_REPORTS: number;
48
52
  readonly MACRO: number;
49
53
  readonly SENTIMENT: 3600000;
50
54
  readonly OPTIONS_CHAIN: number;
@@ -69,7 +69,9 @@ export const cache = new Cache();
69
69
  export const TTL = {
70
70
  QUOTE: 60_000, // 1 minute
71
71
  HISTORY: 3_600_000, // 1 hour
72
+ CANDLES: 3_600_000, // 1 hour
72
73
  FUNDAMENTALS: 86_400_000, // 24 hours
74
+ FINANCIAL_REPORTS: 86_400_000, // 24 hours
73
75
  MACRO: 3_600_000, // 1 hour
74
76
  SENTIMENT: 300_000, // 5 minutes
75
77
  OPTIONS_CHAIN: 120_000, // 2 minutes
@@ -83,7 +85,9 @@ export const TTL = {
83
85
  export const STALE_LIMIT = {
84
86
  QUOTE: 15 * 60_000, // 15 minutes
85
87
  HISTORY: 24 * 3_600_000, // 24 hours
88
+ CANDLES: 24 * 3_600_000, // 24 hours
86
89
  FUNDAMENTALS: 7 * 86_400_000, // 7 days
90
+ FINANCIAL_REPORTS: 7 * 86_400_000, // 7 days
87
91
  MACRO: 24 * 3_600_000, // 24 hours
88
92
  SENTIMENT: 3_600_000, // 1 hour
89
93
  OPTIONS_CHAIN: 30 * 60_000, // 30 minutes
@@ -0,0 +1,10 @@
1
+ export declare const LSE_MONTHLY_BYTE_CAP = 53687091200;
2
+ export declare const LSE_SOFT_BYTE_THRESHOLD = 42949672960;
3
+ export interface LseByteBudgetUsage {
4
+ month: string;
5
+ bytesUsed: number;
6
+ }
7
+ export declare function recordBytes(n: number): void;
8
+ export declare function getUsage(): LseByteBudgetUsage;
9
+ export declare function isOverSoftThreshold(): boolean;
10
+ export declare function markExhausted(): void;
@@ -0,0 +1,47 @@
1
+ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
+ import { ensureParentDir, resolveOpenCandlePath } from "./opencandle-paths.js";
3
+ export const LSE_MONTHLY_BYTE_CAP = 53_687_091_200;
4
+ export const LSE_SOFT_BYTE_THRESHOLD = 42_949_672_960;
5
+ function currentMonth() {
6
+ return new Date().toISOString().slice(0, 7);
7
+ }
8
+ function budgetPath() {
9
+ return resolveOpenCandlePath("lse-byte-budget.json");
10
+ }
11
+ function readUsage() {
12
+ const month = currentMonth();
13
+ const path = budgetPath();
14
+ if (!existsSync(path))
15
+ return { month, bytesUsed: 0 };
16
+ try {
17
+ const parsed = JSON.parse(readFileSync(path, "utf-8"));
18
+ const bytesUsed = parsed.bytesUsed;
19
+ if (parsed.month !== month || typeof bytesUsed !== "number" || !Number.isFinite(bytesUsed)) {
20
+ return { month, bytesUsed: 0 };
21
+ }
22
+ return { month, bytesUsed: bytesUsed < 0 ? 0 : bytesUsed };
23
+ }
24
+ catch {
25
+ return { month, bytesUsed: 0 };
26
+ }
27
+ }
28
+ function writeUsage(usage) {
29
+ const path = budgetPath();
30
+ ensureParentDir(path);
31
+ writeFileSync(path, `${JSON.stringify(usage, null, 2)}\n`, "utf-8");
32
+ }
33
+ export function recordBytes(n) {
34
+ if (!Number.isFinite(n) || n < 0)
35
+ return;
36
+ const usage = readUsage();
37
+ writeUsage({ ...usage, bytesUsed: usage.bytesUsed + n });
38
+ }
39
+ export function getUsage() {
40
+ return readUsage();
41
+ }
42
+ export function isOverSoftThreshold() {
43
+ return readUsage().bytesUsed >= LSE_SOFT_BYTE_THRESHOLD;
44
+ }
45
+ export function markExhausted() {
46
+ writeUsage({ month: currentMonth(), bytesUsed: LSE_MONTHLY_BYTE_CAP });
47
+ }
@@ -50,3 +50,4 @@ rateLimiter.configure("tradingview", 5, 1); // 5 burst, 1 req/s sustained
50
50
  rateLimiter.configure("polymarket", 10, 5); // 10 burst, 5 req/s sustained
51
51
  // SEC EDGAR fair-access guideline is 10 req/s; stay below it.
52
52
  rateLimiter.configure("sec_edgar", 5, 5); // 5 burst, 5 req/s sustained
53
+ rateLimiter.configure("lse", 100, 1.66); // 100 req/min (free tier)