horizon-code 0.3.0 → 0.3.1
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 +10 -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/tab-bar.ts +10 -7
- package/src/components/tutorial-panel.ts +1 -1
- package/src/platform/supabase.ts +17 -2
- package/src/research/apis.ts +109 -28
- package/src/research/widgets.ts +12 -10
- package/src/theme/colors.ts +6 -6
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,11 @@ export class App {
|
|
|
934
939
|
|
|
935
940
|
if (!live) {
|
|
936
941
|
this.splash.setAuthStatus(true, email);
|
|
937
|
-
|
|
942
|
+
// Only warn about expired session if they actually had one before
|
|
943
|
+
// Don't warn if they just have an api_key (chat still works fine)
|
|
944
|
+
if (config.supabase_session) {
|
|
945
|
+
this.showSystemMsg("Supabase session expired — chat sync disabled. Type /login to reconnect.");
|
|
946
|
+
}
|
|
938
947
|
} else {
|
|
939
948
|
this.splash.setAuthStatus(true, email, firstTime);
|
|
940
949
|
}
|
|
@@ -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();
|
|
@@ -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,18 +114,33 @@ export async function restoreSession(): Promise<boolean> {
|
|
|
114
114
|
}
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
// Try setSession first (works if access token still valid)
|
|
117
118
|
const { data, error } = await sb.auth.setSession({
|
|
118
119
|
access_token: accessToken,
|
|
119
120
|
refresh_token: refreshToken,
|
|
120
121
|
});
|
|
121
122
|
|
|
122
123
|
if (!error && data.session) {
|
|
123
|
-
// Session restored — auto-refresh will keep it alive
|
|
124
124
|
if (config.api_key) platform.setApiKey(config.api_key);
|
|
125
125
|
return true;
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
-
//
|
|
128
|
+
// Access token expired — try refreshing with just the refresh token
|
|
129
|
+
const { data: refreshData, error: refreshError } = await sb.auth.refreshSession({
|
|
130
|
+
refresh_token: refreshToken,
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
if (!refreshError && refreshData.session) {
|
|
134
|
+
// Save the new tokens
|
|
135
|
+
saveSessionTokens(config, refreshData.session.access_token, refreshData.session.refresh_token);
|
|
136
|
+
config.user_email = refreshData.session.user.email ?? config.user_email;
|
|
137
|
+
config.user_id = refreshData.session.user.id;
|
|
138
|
+
saveConfig(config);
|
|
139
|
+
if (config.api_key) platform.setApiKey(config.api_key);
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Both failed — refresh token fully expired
|
|
129
144
|
delete config.supabase_session;
|
|
130
145
|
saveConfig(config);
|
|
131
146
|
}
|
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
|
@@ -20,15 +20,15 @@ interface Colors {
|
|
|
20
20
|
const THEMES: Record<ThemeName, Colors> = {
|
|
21
21
|
dark: {
|
|
22
22
|
bg: "#212121", bgSecondary: "#252525", bgDarker: "#1a1a1a", selection: "#303030",
|
|
23
|
-
text: "#e0e0e0", textMuted: "#
|
|
24
|
-
border: "#4b4c5c", borderDim: "#
|
|
23
|
+
text: "#e0e0e0", textMuted: "#808080", textEmphasized: "#ffffff",
|
|
24
|
+
border: "#4b4c5c", borderDim: "#444444", borderFocus: "#fab283",
|
|
25
25
|
primary: "#fab283", primaryDim: "#c48a62", secondary: "#5c9cf5", accent: "#9d7cd8",
|
|
26
26
|
success: "#7fd88f", error: "#e06c75", warning: "#f5a742", info: "#56b6c2",
|
|
27
27
|
},
|
|
28
28
|
midnight: {
|
|
29
29
|
bg: "#0d1117", bgSecondary: "#161b22", bgDarker: "#080c12", selection: "#1c2333",
|
|
30
|
-
text: "#c9d1d9", textMuted: "#
|
|
31
|
-
border: "#30363d", borderDim: "#
|
|
30
|
+
text: "#c9d1d9", textMuted: "#636e7b", textEmphasized: "#f0f6fc",
|
|
31
|
+
border: "#30363d", borderDim: "#2d333b", borderFocus: "#8be9fd",
|
|
32
32
|
primary: "#ff79c6", primaryDim: "#cc5f9e", secondary: "#8be9fd", accent: "#bd93f9",
|
|
33
33
|
success: "#50fa7b", error: "#ff5555", warning: "#f1fa8c", info: "#8be9fd",
|
|
34
34
|
},
|
|
@@ -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
|
},
|