horizon-code 0.3.0 → 0.3.2
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/package.json +1 -1
- package/src/app.ts +5 -1
- package/src/chat/renderer.ts +1 -1
- package/src/components/code-panel.ts +1 -1
- package/src/components/hooks-panel.ts +1 -1
- package/src/components/mode-bar.ts +1 -1
- package/src/components/settings-panel.ts +1 -1
- package/src/components/splash.ts +0 -1
- package/src/components/tab-bar.ts +10 -7
- package/src/components/tutorial-panel.ts +1 -1
- package/src/platform/supabase.ts +30 -12
- package/src/research/apis.ts +109 -28
- package/src/research/widgets.ts +12 -10
- package/src/theme/colors.ts +12 -12
package/package.json
CHANGED
package/src/app.ts
CHANGED
|
@@ -638,6 +638,11 @@ export class App {
|
|
|
638
638
|
if (loginResult.success) {
|
|
639
639
|
this.authenticated = true;
|
|
640
640
|
this.showSystemMsg(`Logged in as ${loginResult.email}`);
|
|
641
|
+
// Start session sync + platform sync now that we have a live session
|
|
642
|
+
import("./platform/session-sync.ts").then(({ loadSessions, startAutoSave }) => {
|
|
643
|
+
loadSessions().catch(() => {});
|
|
644
|
+
startAutoSave();
|
|
645
|
+
}).catch(() => {});
|
|
641
646
|
import("./platform/sync.ts").then(({ platformSync }) => {
|
|
642
647
|
platformSync.start(30000).catch(() => {});
|
|
643
648
|
});
|
|
@@ -934,7 +939,6 @@ export class App {
|
|
|
934
939
|
|
|
935
940
|
if (!live) {
|
|
936
941
|
this.splash.setAuthStatus(true, email);
|
|
937
|
-
this.showSystemMsg("Your session has expired. Type /login to sign in again.");
|
|
938
942
|
} else {
|
|
939
943
|
this.splash.setAuthStatus(true, email, firstTime);
|
|
940
944
|
}
|
package/src/chat/renderer.ts
CHANGED
|
@@ -386,7 +386,7 @@ export class CodePanel {
|
|
|
386
386
|
: id === "dashboard" ? "Metrics"
|
|
387
387
|
: id.charAt(0).toUpperCase() + id.slice(1);
|
|
388
388
|
if (id === this._activeTab) {
|
|
389
|
-
text.fg =
|
|
389
|
+
text.fg = COLORS.bg;
|
|
390
390
|
text.bg = COLORS.accent;
|
|
391
391
|
text.content = ` ${label} `;
|
|
392
392
|
} else {
|
|
@@ -35,7 +35,7 @@ export class HooksPanel {
|
|
|
35
35
|
backgroundColor: COLORS.bgDarker, flexShrink: 0,
|
|
36
36
|
});
|
|
37
37
|
header.add(new TextRenderable(renderer, {
|
|
38
|
-
id: "hooks-title", content: " Hooks ", fg:
|
|
38
|
+
id: "hooks-title", content: " Hooks ", fg: COLORS.bg, bg: COLORS.accent,
|
|
39
39
|
}));
|
|
40
40
|
this.container.add(header);
|
|
41
41
|
|
|
@@ -181,7 +181,7 @@ export class ModeBar {
|
|
|
181
181
|
private updateDisplay(): void {
|
|
182
182
|
const m = MODE_CONFIG[this._current];
|
|
183
183
|
this.modeText.content = ` ${m.icon} ${m.label} `;
|
|
184
|
-
this.modeText.fg =
|
|
184
|
+
this.modeText.fg = COLORS.bg;
|
|
185
185
|
this.modeText.bg = m.color;
|
|
186
186
|
this.updateStatus();
|
|
187
187
|
this.updateMetricsDisplay();
|
package/src/components/splash.ts
CHANGED
|
@@ -11,11 +11,14 @@ const MODE_LABEL: Record<string, string> = {
|
|
|
11
11
|
portfolio: "(p)",
|
|
12
12
|
};
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
function getModeColor(mode: string): string {
|
|
15
|
+
switch (mode) {
|
|
16
|
+
case "research": return COLORS.secondary;
|
|
17
|
+
case "strategy": return COLORS.accent;
|
|
18
|
+
case "portfolio": return COLORS.success;
|
|
19
|
+
default: return COLORS.secondary;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
19
22
|
|
|
20
23
|
const BRAILLE = ["\u2801", "\u2803", "\u2807", "\u280f", "\u281f", "\u283f", "\u287f", "\u28ff", "\u28fe", "\u28fc", "\u28f8", "\u28f0", "\u28e0", "\u28c0", "\u2880", "\u2800"];
|
|
21
24
|
|
|
@@ -53,7 +56,7 @@ export class TabBar {
|
|
|
53
56
|
|
|
54
57
|
const isActive = tabId === activeSessionId;
|
|
55
58
|
const modeTag = MODE_LABEL[session.mode] ?? "(r)";
|
|
56
|
-
const modeColor =
|
|
59
|
+
const modeColor = getModeColor(session.mode);
|
|
57
60
|
|
|
58
61
|
// Truncate name
|
|
59
62
|
let name = session.name.length > 12 ? session.name.slice(0, 11) + "." : session.name;
|
|
@@ -69,7 +72,7 @@ export class TabBar {
|
|
|
69
72
|
const tab = new TextRenderable(this.renderer, {
|
|
70
73
|
id: `tab-${i}`,
|
|
71
74
|
content: label,
|
|
72
|
-
fg: isActive ?
|
|
75
|
+
fg: isActive ? COLORS.bg : COLORS.textMuted,
|
|
73
76
|
bg: isActive ? modeColor : undefined,
|
|
74
77
|
});
|
|
75
78
|
this.container.add(tab);
|
|
@@ -667,7 +667,7 @@ export class TutorialPanel {
|
|
|
667
667
|
private updateTabBar(): void {
|
|
668
668
|
for (const [id, text] of this.tabTexts) {
|
|
669
669
|
if (id === this._activeTab) {
|
|
670
|
-
text.fg =
|
|
670
|
+
text.fg = COLORS.bg;
|
|
671
671
|
text.bg = COLORS.secondary;
|
|
672
672
|
text.content = ` ${TAB_LIST.find((t) => t.id === id)!.label} `;
|
|
673
673
|
} else {
|
package/src/platform/supabase.ts
CHANGED
|
@@ -114,20 +114,38 @@ export async function restoreSession(): Promise<boolean> {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
117
|
+
// Try setSession first (works if access token still valid)
|
|
118
|
+
try {
|
|
119
|
+
const { data, error } = await sb.auth.setSession({
|
|
120
|
+
access_token: accessToken,
|
|
121
|
+
refresh_token: refreshToken,
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
if (!error && data.session) {
|
|
125
|
+
if (config.api_key) platform.setApiKey(config.api_key);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
} catch {}
|
|
121
129
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
130
|
+
// Access token expired — try refreshing with just the refresh token
|
|
131
|
+
try {
|
|
132
|
+
const { data: refreshData, error: refreshError } = await sb.auth.refreshSession({
|
|
133
|
+
refresh_token: refreshToken,
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
if (!refreshError && refreshData.session) {
|
|
137
|
+
// Save the new tokens
|
|
138
|
+
saveSessionTokens(config, refreshData.session.access_token, refreshData.session.refresh_token);
|
|
139
|
+
config.user_email = refreshData.session.user.email ?? config.user_email;
|
|
140
|
+
config.user_id = refreshData.session.user.id;
|
|
141
|
+
saveConfig(config);
|
|
142
|
+
if (config.api_key) platform.setApiKey(config.api_key);
|
|
143
|
+
return true;
|
|
144
|
+
}
|
|
145
|
+
} catch {}
|
|
127
146
|
|
|
128
|
-
//
|
|
129
|
-
|
|
130
|
-
saveConfig(config);
|
|
147
|
+
// Both failed — don't delete the session, it might work next time
|
|
148
|
+
// (network issue, Supabase outage, etc.). The API key still works for chat.
|
|
131
149
|
}
|
|
132
150
|
|
|
133
151
|
// API key still works for the LLM proxy even without a Supabase session
|
package/src/research/apis.ts
CHANGED
|
@@ -312,53 +312,134 @@ function parseKalshiMarket(m: any): any {
|
|
|
312
312
|
let _kalshiCache: { data: any[]; ts: number } = { data: [], ts: 0 };
|
|
313
313
|
const KALSHI_CACHE_TTL = 60_000;
|
|
314
314
|
|
|
315
|
-
|
|
315
|
+
/** Fetch events from Kalshi — uses cursor pagination to get more results */
|
|
316
|
+
async function fetchKalshiEvents(limit = 200): Promise<any[]> {
|
|
316
317
|
if (_kalshiCache.data.length > 0 && Date.now() - _kalshiCache.ts < KALSHI_CACHE_TTL) {
|
|
317
318
|
return _kalshiCache.data;
|
|
318
319
|
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
320
|
+
|
|
321
|
+
const all: any[] = [];
|
|
322
|
+
let cursor = "";
|
|
323
|
+
const batchSize = 100;
|
|
324
|
+
|
|
325
|
+
// Fetch up to `limit` events across pages
|
|
326
|
+
while (all.length < limit) {
|
|
327
|
+
const params = new URLSearchParams({
|
|
328
|
+
status: "open", with_nested_markets: "true",
|
|
329
|
+
limit: String(Math.min(batchSize, limit - all.length)),
|
|
330
|
+
});
|
|
331
|
+
if (cursor) params.set("cursor", cursor);
|
|
332
|
+
|
|
333
|
+
const data = await get(`${KALSHI}/events?${params}`).catch(() => ({ events: [], cursor: "" }));
|
|
334
|
+
const events = data?.events ?? [];
|
|
335
|
+
if (events.length === 0) break;
|
|
336
|
+
|
|
337
|
+
all.push(...events);
|
|
338
|
+
cursor = data?.cursor ?? "";
|
|
339
|
+
if (!cursor) break;
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
_kalshiCache = { data: all, ts: Date.now() };
|
|
343
|
+
return all;
|
|
327
344
|
}
|
|
328
345
|
|
|
346
|
+
/** Build searchable text from a Kalshi event (including nested market titles) */
|
|
347
|
+
function kalshiSearchText(e: any): string {
|
|
348
|
+
const parts = [e.title, e.sub_title, e.category, e.series_ticker, e.event_ticker];
|
|
349
|
+
// Include nested market titles for better search
|
|
350
|
+
for (const m of (e.markets ?? [])) {
|
|
351
|
+
parts.push(m.title, m.subtitle, m.yes_sub_title, m.no_sub_title, m.ticker);
|
|
352
|
+
}
|
|
353
|
+
return parts.filter(Boolean).join(" ").toLowerCase();
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
// Common series tickers for keyword-to-series mapping
|
|
357
|
+
const KALSHI_SERIES_MAP: Record<string, string> = {
|
|
358
|
+
bitcoin: "KXBTC", btc: "KXBTC", crypto: "KXBTC",
|
|
359
|
+
ethereum: "KXETH", eth: "KXETH",
|
|
360
|
+
nasdaq: "KXNASDAQ", spy: "KXSPY", sp500: "KXSPY", "s&p": "KXSPY",
|
|
361
|
+
fed: "KXFED", "interest rate": "KXFED", fomc: "KXFED",
|
|
362
|
+
cpi: "KXCPI", inflation: "KXCPI",
|
|
363
|
+
gdp: "KXGDP", unemployment: "KXUNEMP", jobs: "KXUNEMP",
|
|
364
|
+
oil: "KXOIL", gold: "KXGOLD",
|
|
365
|
+
};
|
|
366
|
+
|
|
329
367
|
export async function kalshiEvents(opts: { query?: string; limit?: number } = {}): Promise<any[]> {
|
|
330
368
|
const limit = Math.min(opts.limit ?? 10, 20);
|
|
331
369
|
const query = (opts.query ?? "").toLowerCase().trim();
|
|
332
370
|
|
|
333
|
-
const allEvents = await fetchAllKalshiEvents();
|
|
334
|
-
let events: any[];
|
|
335
|
-
|
|
336
371
|
if (!query) {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
372
|
+
const allEvents = await fetchKalshiEvents();
|
|
373
|
+
return allEvents.slice(0, limit).map(formatKalshiEvent);
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// 1. Try series_ticker mapping first (crypto, fed, etc.)
|
|
377
|
+
const seriesTicker = KALSHI_SERIES_MAP[query] ??
|
|
378
|
+
Object.entries(KALSHI_SERIES_MAP).find(([k]) => query.includes(k))?.[1];
|
|
379
|
+
|
|
380
|
+
if (seriesTicker) {
|
|
381
|
+
try {
|
|
382
|
+
const params = new URLSearchParams({
|
|
383
|
+
status: "open", with_nested_markets: "true",
|
|
384
|
+
series_ticker: seriesTicker, limit: String(limit),
|
|
385
|
+
});
|
|
386
|
+
const data = await get(`${KALSHI}/events?${params}`);
|
|
387
|
+
const events = data?.events ?? [];
|
|
388
|
+
if (events.length > 0) return events.slice(0, limit).map(formatKalshiEvent);
|
|
389
|
+
} catch {}
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// 2. Search cached events (title + market titles)
|
|
393
|
+
const allEvents = await fetchKalshiEvents();
|
|
394
|
+
const words = query.split(/\s+/).filter(w => w.length > 1);
|
|
395
|
+
|
|
396
|
+
let events = allEvents.filter((e: any) => {
|
|
397
|
+
const text = kalshiSearchText(e);
|
|
398
|
+
return words.every(w => text.includes(w));
|
|
399
|
+
});
|
|
400
|
+
|
|
401
|
+
// Fallback: any word match
|
|
402
|
+
if (events.length === 0 && words.length > 1) {
|
|
341
403
|
events = allEvents.filter((e: any) => {
|
|
342
|
-
const text = (
|
|
343
|
-
|
|
344
|
-
return words.every(w => text.includes(w));
|
|
404
|
+
const text = kalshiSearchText(e);
|
|
405
|
+
return words.some(w => text.includes(w));
|
|
345
406
|
});
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
// 3. If still empty, try series_ticker as-is (user might pass a ticker directly)
|
|
410
|
+
if (events.length === 0) {
|
|
411
|
+
try {
|
|
412
|
+
const params = new URLSearchParams({
|
|
413
|
+
status: "open", with_nested_markets: "true",
|
|
414
|
+
series_ticker: query.toUpperCase(), limit: String(limit),
|
|
351
415
|
});
|
|
352
|
-
|
|
353
|
-
|
|
416
|
+
const data = await get(`${KALSHI}/events?${params}`);
|
|
417
|
+
events = data?.events ?? [];
|
|
418
|
+
} catch {}
|
|
354
419
|
}
|
|
355
420
|
|
|
356
|
-
|
|
421
|
+
// Sort by relevance
|
|
422
|
+
events.sort((a: any, b: any) => {
|
|
423
|
+
const aTitle = (a.title ?? "").toLowerCase();
|
|
424
|
+
const bTitle = (b.title ?? "").toLowerCase();
|
|
425
|
+
const aInTitle = words.some(w => aTitle.includes(w)) ? 1 : 0;
|
|
426
|
+
const bInTitle = words.some(w => bTitle.includes(w)) ? 1 : 0;
|
|
427
|
+
if (aInTitle !== bInTitle) return bInTitle - aInTitle;
|
|
428
|
+
const aVol = (a.markets ?? []).reduce((s: number, m: any) => s + kVol(m.volume_fp ?? m.volume), 0);
|
|
429
|
+
const bVol = (b.markets ?? []).reduce((s: number, m: any) => s + kVol(m.volume_fp ?? m.volume), 0);
|
|
430
|
+
return bVol - aVol;
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
return events.slice(0, limit).map(formatKalshiEvent);
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
function formatKalshiEvent(e: any): any {
|
|
437
|
+
return {
|
|
357
438
|
ticker: e.event_ticker, title: e.title,
|
|
358
439
|
category: e.category, subtitle: e.sub_title,
|
|
359
440
|
url: `https://kalshi.com/markets/${(e.event_ticker ?? "").toLowerCase()}`,
|
|
360
441
|
markets: (e.markets ?? []).slice(0, 5).map(parseKalshiMarket),
|
|
361
|
-
}
|
|
442
|
+
};
|
|
362
443
|
}
|
|
363
444
|
|
|
364
445
|
export async function kalshiEventDetail(ticker: string): Promise<any> {
|
package/src/research/widgets.ts
CHANGED
|
@@ -7,15 +7,17 @@ import { COLORS } from "../theme/colors.ts";
|
|
|
7
7
|
const SPARK = ["\u2581", "\u2582", "\u2583", "\u2584", "\u2585", "\u2586", "\u2587", "\u2588"];
|
|
8
8
|
const BAR = "\u2588";
|
|
9
9
|
|
|
10
|
-
// Exchange brand colors
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
10
|
+
// Exchange brand colors — uses theme palette so they adapt to all themes
|
|
11
|
+
function getExchangeColor(exchange: string): string {
|
|
12
|
+
switch (exchange) {
|
|
13
|
+
case "polymarket": return COLORS.secondary; // blue
|
|
14
|
+
case "kalshi": return COLORS.success; // green
|
|
15
|
+
case "stock": case "yahoo": return COLORS.accent; // purple
|
|
16
|
+
case "cross": return COLORS.warning; // amber
|
|
17
|
+
case "analysis": return COLORS.info; // teal/blue
|
|
18
|
+
default: return COLORS.textMuted;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
19
21
|
|
|
20
22
|
/** Format a price for chart Y-axis labels */
|
|
21
23
|
function fmtChartLabel(v: number): string {
|
|
@@ -210,7 +212,7 @@ function renderChart(box: BoxRenderable, renderer: CliRenderer, chart: ChartResu
|
|
|
210
212
|
}
|
|
211
213
|
|
|
212
214
|
function exchangeBadge(parent: BoxRenderable, renderer: CliRenderer, exchange: string, title: string): void {
|
|
213
|
-
const color =
|
|
215
|
+
const color = getExchangeColor(exchange);
|
|
214
216
|
const label = exchange.toUpperCase();
|
|
215
217
|
const row = new BoxRenderable(renderer, { id: uid(), flexDirection: "row" });
|
|
216
218
|
row.add(new TextRenderable(renderer, { id: uid(), content: `[${label}]`, fg: color, attributes: 1 }));
|
package/src/theme/colors.ts
CHANGED
|
@@ -19,18 +19,18 @@ interface Colors {
|
|
|
19
19
|
|
|
20
20
|
const THEMES: Record<ThemeName, Colors> = {
|
|
21
21
|
dark: {
|
|
22
|
-
bg: "#
|
|
23
|
-
text: "#
|
|
24
|
-
border: "#4b4c5c", borderDim: "#
|
|
25
|
-
primary: "#fab283", primaryDim: "#c48a62", secondary: "#
|
|
26
|
-
success: "#7fd88f", error: "#e06c75", warning: "#f5a742", info: "#
|
|
22
|
+
bg: "#1a1a1a", bgSecondary: "#1e1e1e", bgDarker: "#141414", selection: "#252525",
|
|
23
|
+
text: "#d4d4d4", textMuted: "#808080", textEmphasized: "#ffffff",
|
|
24
|
+
border: "#4b4c5c", borderDim: "#3a3a3a", borderFocus: "#fab283",
|
|
25
|
+
primary: "#fab283", primaryDim: "#c48a62", secondary: "#4d8ef7", accent: "#9d7cd8",
|
|
26
|
+
success: "#7fd88f", error: "#e06c75", warning: "#f5a742", info: "#5bb8d0",
|
|
27
27
|
},
|
|
28
28
|
midnight: {
|
|
29
|
-
bg: "#0d1117", bgSecondary: "#
|
|
30
|
-
text: "#c9d1d9", textMuted: "#
|
|
31
|
-
border: "#30363d", borderDim: "#
|
|
32
|
-
primary: "#ff79c6", primaryDim: "#cc5f9e", secondary: "#
|
|
33
|
-
success: "#50fa7b", error: "#ff5555", warning: "#f1fa8c", info: "#
|
|
29
|
+
bg: "#0d1117", bgSecondary: "#131920", bgDarker: "#080c12", selection: "#182030",
|
|
30
|
+
text: "#c9d1d9", textMuted: "#636e7b", textEmphasized: "#f0f6fc",
|
|
31
|
+
border: "#30363d", borderDim: "#2d333b", borderFocus: "#8be9fd",
|
|
32
|
+
primary: "#ff79c6", primaryDim: "#cc5f9e", secondary: "#79b8ff", accent: "#bd93f9",
|
|
33
|
+
success: "#50fa7b", error: "#ff5555", warning: "#f1fa8c", info: "#79b8ff",
|
|
34
34
|
},
|
|
35
35
|
nord: {
|
|
36
36
|
bg: "#2e3440", bgSecondary: "#3b4252", bgDarker: "#272c36", selection: "#434c5e",
|
|
@@ -68,8 +68,8 @@ const THEMES: Record<ThemeName, Colors> = {
|
|
|
68
68
|
// ── Ultra: Poly Dark (black and blue) ──
|
|
69
69
|
"poly-dark": {
|
|
70
70
|
bg: "#0f1120", bgSecondary: "#151830", bgDarker: "#0a0c18", selection: "#1e2240",
|
|
71
|
-
text: "#c8cee0", textMuted: "#
|
|
72
|
-
border: "#2a2f50", borderDim: "#
|
|
71
|
+
text: "#c8cee0", textMuted: "#7278a0", textEmphasized: "#f0f2ff",
|
|
72
|
+
border: "#2a2f50", borderDim: "#252a48", borderFocus: "#3b82f6",
|
|
73
73
|
primary: "#3b82f6", primaryDim: "#2563eb", secondary: "#6366f1", accent: "#818cf8",
|
|
74
74
|
success: "#34d399", error: "#f87171", warning: "#fbbf24", info: "#60a5fa",
|
|
75
75
|
},
|