horizon-code 0.1.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 (54) hide show
  1. package/assets/python/highlights.scm +137 -0
  2. package/assets/python/tree-sitter-python.wasm +0 -0
  3. package/bin/horizon.js +2 -0
  4. package/package.json +40 -0
  5. package/src/ai/client.ts +369 -0
  6. package/src/ai/system-prompt.ts +86 -0
  7. package/src/app.ts +1454 -0
  8. package/src/chat/messages.ts +48 -0
  9. package/src/chat/renderer.ts +243 -0
  10. package/src/chat/types.ts +18 -0
  11. package/src/components/code-panel.ts +329 -0
  12. package/src/components/footer.ts +72 -0
  13. package/src/components/hooks-panel.ts +224 -0
  14. package/src/components/input-bar.ts +193 -0
  15. package/src/components/mode-bar.ts +245 -0
  16. package/src/components/session-panel.ts +294 -0
  17. package/src/components/settings-panel.ts +372 -0
  18. package/src/components/splash.ts +156 -0
  19. package/src/components/strategy-panel.ts +489 -0
  20. package/src/components/tab-bar.ts +112 -0
  21. package/src/components/tutorial-panel.ts +680 -0
  22. package/src/components/widgets/progress-bar.ts +38 -0
  23. package/src/components/widgets/sparkline.ts +57 -0
  24. package/src/hooks/executor.ts +109 -0
  25. package/src/index.ts +22 -0
  26. package/src/keys/handler.ts +198 -0
  27. package/src/platform/auth.ts +36 -0
  28. package/src/platform/client.ts +159 -0
  29. package/src/platform/config.ts +121 -0
  30. package/src/platform/session-sync.ts +158 -0
  31. package/src/platform/supabase.ts +376 -0
  32. package/src/platform/sync.ts +149 -0
  33. package/src/platform/tiers.ts +103 -0
  34. package/src/platform/tools.ts +163 -0
  35. package/src/platform/types.ts +86 -0
  36. package/src/platform/usage.ts +224 -0
  37. package/src/research/apis.ts +367 -0
  38. package/src/research/tools.ts +205 -0
  39. package/src/research/widgets.ts +523 -0
  40. package/src/state/store.ts +256 -0
  41. package/src/state/types.ts +109 -0
  42. package/src/strategy/ascii-chart.ts +74 -0
  43. package/src/strategy/code-stream.ts +146 -0
  44. package/src/strategy/dashboard.ts +140 -0
  45. package/src/strategy/persistence.ts +82 -0
  46. package/src/strategy/prompts.ts +626 -0
  47. package/src/strategy/sandbox.ts +137 -0
  48. package/src/strategy/tools.ts +764 -0
  49. package/src/strategy/validator.ts +216 -0
  50. package/src/strategy/widgets.ts +270 -0
  51. package/src/syntax/setup.ts +54 -0
  52. package/src/theme/colors.ts +107 -0
  53. package/src/theme/icons.ts +27 -0
  54. package/src/util/hyperlink.ts +21 -0
@@ -0,0 +1,57 @@
1
+ import { TextRenderable, type CliRenderer } from "@opentui/core";
2
+ import { COLORS } from "../../theme/colors.ts";
3
+ import { ICONS } from "../../theme/icons.ts";
4
+
5
+ const BLOCKS = ICONS.spark;
6
+
7
+ export class Sparkline {
8
+ private text: TextRenderable;
9
+
10
+ constructor(
11
+ renderer: CliRenderer,
12
+ id: string,
13
+ private width: number = 16,
14
+ ) {
15
+ this.text = new TextRenderable(renderer, {
16
+ id,
17
+ content: "",
18
+ fg: COLORS.textMuted,
19
+ });
20
+ }
21
+
22
+ get renderable(): TextRenderable {
23
+ return this.text;
24
+ }
25
+
26
+ setData(values: number[], color?: string): void {
27
+ if (values.length === 0) {
28
+ this.text.content = "";
29
+ return;
30
+ }
31
+
32
+ const min = Math.min(...values);
33
+ const max = Math.max(...values);
34
+ const range = max - min || 1;
35
+
36
+ const sampled: number[] = [];
37
+ for (let i = 0; i < this.width; i++) {
38
+ const idx = Math.floor((i / this.width) * values.length);
39
+ sampled.push(values[idx] ?? min);
40
+ }
41
+
42
+ const chars = sampled.map((v) => {
43
+ const normalized = (v - min) / range;
44
+ const blockIdx = Math.min(Math.floor(normalized * BLOCKS.length), BLOCKS.length - 1);
45
+ return BLOCKS[blockIdx];
46
+ });
47
+
48
+ this.text.content = chars.join("");
49
+ if (color) {
50
+ this.text.fg = color;
51
+ } else {
52
+ const last = values[values.length - 1]!;
53
+ const first = values[0]!;
54
+ this.text.fg = last >= first ? COLORS.success : COLORS.error;
55
+ }
56
+ }
57
+ }
@@ -0,0 +1,109 @@
1
+ // Hooks — user-defined bash commands at strategy lifecycle points
2
+ // Each hook requires user approval before executing
3
+
4
+ import { loadConfig, saveConfig } from "../platform/config.ts";
5
+
6
+ export type HookEvent =
7
+ | "before_generate" | "after_generate"
8
+ | "before_validate" | "after_validate"
9
+ | "before_backtest" | "after_backtest"
10
+ | "before_run" | "after_run"
11
+ | "before_deploy" | "after_deploy"
12
+ | "before_stop" | "after_stop";
13
+
14
+ export const HOOK_EVENTS: HookEvent[] = [
15
+ "before_generate", "after_generate",
16
+ "before_validate", "after_validate",
17
+ "before_backtest", "after_backtest",
18
+ "before_run", "after_run",
19
+ "before_deploy", "after_deploy",
20
+ "before_stop", "after_stop",
21
+ ];
22
+
23
+ export interface HookConfig {
24
+ event: HookEvent;
25
+ command: string;
26
+ enabled: boolean;
27
+ label?: string;
28
+ }
29
+
30
+ export interface HookResult {
31
+ event: HookEvent;
32
+ command: string;
33
+ approved: boolean;
34
+ exitCode?: number;
35
+ stdout?: string;
36
+ stderr?: string;
37
+ }
38
+
39
+ // Tool name → before/after hook events
40
+ export const TOOL_BEFORE_HOOK: Record<string, HookEvent> = {
41
+ edit_strategy: "before_generate",
42
+ validate_strategy: "before_validate",
43
+ backtest_strategy: "before_backtest",
44
+ run_strategy: "before_run",
45
+ deploy_strategy: "before_deploy",
46
+ stop_strategy: "before_stop",
47
+ };
48
+
49
+ export const TOOL_AFTER_HOOK: Record<string, HookEvent> = {
50
+ edit_strategy: "after_generate",
51
+ validate_strategy: "after_validate",
52
+ backtest_strategy: "after_backtest",
53
+ run_strategy: "after_run",
54
+ deploy_strategy: "after_deploy",
55
+ stop_strategy: "after_stop",
56
+ };
57
+
58
+ export function loadHooks(): HookConfig[] {
59
+ return (loadConfig().hooks as HookConfig[] | undefined) ?? [];
60
+ }
61
+
62
+ export function saveHooks(hooks: HookConfig[]): void {
63
+ const config = loadConfig();
64
+ config.hooks = hooks as any;
65
+ saveConfig(config);
66
+ }
67
+
68
+ export function getHooksForEvent(event: HookEvent): HookConfig[] {
69
+ return loadHooks().filter((h) => h.event === event && h.enabled);
70
+ }
71
+
72
+ export async function executeHook(hook: HookConfig): Promise<HookResult> {
73
+ try {
74
+ // Restricted env — no secrets (hooks are user-defined shell commands)
75
+ const hookEnv: Record<string, string> = {};
76
+ for (const k of ["PATH", "HOME", "LANG", "LC_ALL", "TERM", "SHELL", "USER"]) {
77
+ if (process.env[k]) hookEnv[k] = process.env[k]!;
78
+ }
79
+ const proc = Bun.spawn(["bash", "-c", hook.command], {
80
+ stdout: "pipe",
81
+ stderr: "pipe",
82
+ env: hookEnv,
83
+ });
84
+
85
+ const stdout = await Promise.race([
86
+ new Response(proc.stdout).text(),
87
+ new Promise<string>((_, reject) => setTimeout(() => { proc.kill(); reject(new Error("timeout")); }, 30000)),
88
+ ]);
89
+ const stderr = await new Response(proc.stderr).text();
90
+ await proc.exited;
91
+
92
+ return {
93
+ event: hook.event,
94
+ command: hook.command,
95
+ approved: true,
96
+ exitCode: proc.exitCode ?? 1,
97
+ stdout: stdout.slice(0, 1000),
98
+ stderr: stderr.slice(0, 500),
99
+ };
100
+ } catch (err) {
101
+ return {
102
+ event: hook.event,
103
+ command: hook.command,
104
+ approved: true,
105
+ exitCode: 1,
106
+ stderr: err instanceof Error ? err.message : "Hook execution failed",
107
+ };
108
+ }
109
+ }
package/src/index.ts ADDED
@@ -0,0 +1,22 @@
1
+ import { createCliRenderer } from "@opentui/core";
2
+ import { App } from "./app.ts";
3
+ import { initMCP, closeMCP } from "./ai/client.ts";
4
+ import { saveActiveSession, stopAutoSave } from "./platform/session-sync.ts";
5
+
6
+ // Connect to Horizon MCP server (non-blocking)
7
+ initMCP().catch(() => {});
8
+
9
+ const renderer = await createCliRenderer({
10
+ exitOnCtrlC: false,
11
+ targetFps: 10,
12
+ useAlternateScreen: true,
13
+ });
14
+
15
+ // App handles auth restore, session loading, and sync startup internally
16
+ new App(renderer);
17
+
18
+ process.on("beforeExit", () => {
19
+ saveActiveSession().catch(() => {});
20
+ stopAutoSave();
21
+ closeMCP();
22
+ });
@@ -0,0 +1,198 @@
1
+ import type { CliRenderer, KeyEvent } from "@opentui/core";
2
+
3
+ export class KeyHandler {
4
+ private quitCallback: (() => void) | null = null;
5
+ private clearCallback: (() => void) | null = null;
6
+ private sessionsCallback: (() => void) | null = null;
7
+ private deploymentsCallback: (() => void) | null = null;
8
+ private scrollCallback: ((delta: number) => void) | null = null;
9
+ private cancelCallback: (() => void) | null = null;
10
+ private anyKeyCallback: (() => void) | null = null;
11
+ private modeCycleCallback: (() => void) | null = null;
12
+ private privacyCallback: (() => void) | null = null;
13
+ private metricsCallback: (() => void) | null = null;
14
+ private codePanelCallback: (() => void) | null = null;
15
+ private codeTabCallback: ((tab: number) => void) | null = null;
16
+ private tutorialNavCallback: ((dir: "left" | "right") => void) | null = null;
17
+ private settingsNavCallback: ((delta: number) => void) | null = null;
18
+ private tabNextCallback: (() => void) | null = null;
19
+ private tabPrevCallback: (() => void) | null = null;
20
+ private tabCloseCallback: (() => void) | null = null;
21
+ private tabNewCallback: (() => void) | null = null;
22
+ private acNavCallback: ((dir: "up" | "down" | "accept" | "dismiss") => void) | null = null;
23
+ acActive = false; // set by app when autocomplete is visible
24
+
25
+ // Panel navigation (shared by session & strategy panels)
26
+ private panelNavCallback: ((delta: number) => void) | null = null;
27
+ private panelSelectCallback: (() => void) | null = null;
28
+ private panelNewCallback: (() => void) | null = null;
29
+ private panelCloseCallback: (() => void) | null = null;
30
+ private panelActionCallback: ((key: string) => void) | null = null;
31
+
32
+ panelActive: "sessions" | "deployments" | null = null;
33
+ panelRenaming = false; // when true, let keys flow to the inline rename input
34
+ codePanelVisible = false; // set by app when code panel is visible
35
+
36
+ constructor(private renderer: CliRenderer) {
37
+ this.renderer.keyInput.on("keypress", (key: KeyEvent) => this.handle(key));
38
+ }
39
+
40
+ onQuit(cb: () => void): void { this.quitCallback = cb; }
41
+ onClear(cb: () => void): void { this.clearCallback = cb; }
42
+ onSessions(cb: () => void): void { this.sessionsCallback = cb; }
43
+ onDeployments(cb: () => void): void { this.deploymentsCallback = cb; }
44
+ onScroll(cb: (delta: number) => void): void { this.scrollCallback = cb; }
45
+ onCancel(cb: () => void): void { this.cancelCallback = cb; }
46
+ onAnyKey(cb: () => void): void { this.anyKeyCallback = cb; }
47
+ onModeCycle(cb: () => void): void { this.modeCycleCallback = cb; }
48
+ onPrivacy(cb: () => void): void { this.privacyCallback = cb; }
49
+ onMetrics(cb: () => void): void { this.metricsCallback = cb; }
50
+ onCodePanel(cb: () => void): void { this.codePanelCallback = cb; }
51
+ onCodeTab(cb: (tab: number) => void): void { this.codeTabCallback = cb; }
52
+ onTutorialNav(cb: (dir: "left" | "right") => void): void { this.tutorialNavCallback = cb; }
53
+ onSettingsNav(cb: (delta: number) => void): void { this.settingsNavCallback = cb; }
54
+ onTabNext(cb: () => void): void { this.tabNextCallback = cb; }
55
+ onTabPrev(cb: () => void): void { this.tabPrevCallback = cb; }
56
+ onTabClose(cb: () => void): void { this.tabCloseCallback = cb; }
57
+ onTabNew(cb: () => void): void { this.tabNewCallback = cb; }
58
+ onAcNav(cb: (dir: "up" | "down" | "accept" | "dismiss") => void): void { this.acNavCallback = cb; }
59
+
60
+ onPanelNav(cb: (delta: number) => void): void { this.panelNavCallback = cb; }
61
+ onPanelSelect(cb: () => void): void { this.panelSelectCallback = cb; }
62
+ onPanelNew(cb: () => void): void { this.panelNewCallback = cb; }
63
+ onPanelClose(cb: () => void): void { this.panelCloseCallback = cb; }
64
+ onPanelAction(cb: (key: string) => void): void { this.panelActionCallback = cb; }
65
+
66
+ private handle(key: KeyEvent): void {
67
+ this.anyKeyCallback?.();
68
+
69
+ // Ctrl+C — quit or cancel
70
+ if (key.ctrl && key.name === "c") {
71
+ if (this.panelActive) { this.panelCloseCallback?.(); return; }
72
+ this.cancelCallback?.();
73
+ return;
74
+ }
75
+
76
+ // Ctrl+E — sessions
77
+ if (key.ctrl && key.name === "e") {
78
+ this.sessionsCallback?.();
79
+ return;
80
+ }
81
+
82
+ // Ctrl+P — privacy toggle
83
+ if (key.ctrl && key.name === "p") {
84
+ this.privacyCallback?.();
85
+ return;
86
+ }
87
+
88
+ // Ctrl+T — metrics toggle
89
+ if (key.ctrl && key.name === "t") {
90
+ this.metricsCallback?.();
91
+ return;
92
+ }
93
+
94
+ // Tab navigation — Ctrl+J next, Ctrl+K prev
95
+ if (key.ctrl && key.name === "j") { this.tabNextCallback?.(); return; }
96
+ if (key.ctrl && key.name === "k") { this.tabPrevCallback?.(); return; }
97
+ // Ctrl+W — close tab
98
+ if (key.ctrl && key.name === "w") { this.tabCloseCallback?.(); return; }
99
+ // Ctrl+N — new tab
100
+ if (key.ctrl && key.name === "n") { this.tabNewCallback?.(); return; }
101
+
102
+ // Ctrl+G — code panel toggle
103
+ if (key.ctrl && key.name === "g") {
104
+ this.codePanelCallback?.();
105
+ return;
106
+ }
107
+
108
+ // Ctrl+1/2/3 — code panel tab switching (some terminals support this)
109
+ if (key.ctrl && (key.name === "1" || key.name === "2" || key.name === "3")) {
110
+ this.codeTabCallback?.(parseInt(key.name));
111
+ return;
112
+ }
113
+
114
+ // Tab key — cycle code panel tabs when code panel is visible
115
+ if (key.name === "tab" && !this.acActive && !this.panelActive && this.codePanelVisible) {
116
+ this.codeTabCallback?.(0); // 0 = cycle to next tab
117
+ return;
118
+ }
119
+
120
+ // Ctrl+D — deployments
121
+ if (key.ctrl && key.name === "d") {
122
+ this.deploymentsCallback?.();
123
+ return;
124
+ }
125
+
126
+ // Ctrl+L — next tab (was clear, now tab navigation)
127
+ if (key.ctrl && key.name === "l") {
128
+ if (this.panelActive === "sessions") { this.panelNewCallback?.(); return; }
129
+ this.tabNextCallback?.();
130
+ return;
131
+ }
132
+
133
+ // Ctrl+H — prev tab
134
+ if (key.ctrl && key.name === "h") {
135
+ this.tabPrevCallback?.();
136
+ return;
137
+ }
138
+
139
+ // Renaming mode — Escape cancels rename, everything else goes to the rename input
140
+ if (this.panelActive && this.panelRenaming) {
141
+ if (key.name === "escape") {
142
+ this.panelRenaming = false;
143
+ // Cancel rename — panelAction handler will sync state
144
+ this.panelActionCallback?.("rename-cancel");
145
+ }
146
+ return;
147
+ }
148
+
149
+ // Panel mode — route nav keys
150
+ if (this.panelActive) {
151
+ switch (key.name) {
152
+ case "j": case "down":
153
+ this.panelNavCallback?.(1); return;
154
+ case "k": case "up":
155
+ this.panelNavCallback?.(-1); return;
156
+ case "return":
157
+ this.panelSelectCallback?.(); return;
158
+ case "escape":
159
+ this.panelCloseCallback?.(); return;
160
+ case "n":
161
+ if (this.panelActive === "sessions") this.panelNewCallback?.();
162
+ return;
163
+ case "d": case "p": case "r": case "o":
164
+ if (this.panelActive === "sessions") this.panelActionCallback?.(key.name!);
165
+ if (this.panelActive === "deployments") this.panelActionCallback?.(key.name!);
166
+ return;
167
+ case "k":
168
+ if (this.panelActive === "deployments") this.panelActionCallback?.(key.name!);
169
+ return;
170
+ }
171
+ return;
172
+ }
173
+
174
+ // Autocomplete navigation (when dropdown is visible)
175
+ if (this.acActive) {
176
+ if (key.name === "up") { this.acNavCallback?.("up"); return; }
177
+ if (key.name === "down") { this.acNavCallback?.("down"); return; }
178
+ if (key.name === "tab") { this.acNavCallback?.("accept"); return; }
179
+ if (key.name === "escape") { this.acNavCallback?.("dismiss"); return; }
180
+ }
181
+
182
+ // Ctrl+R — cycle mode
183
+ if (key.ctrl && key.name === "r") {
184
+ this.modeCycleCallback?.();
185
+ return;
186
+ }
187
+
188
+ // Arrow keys for tutorial/settings panel navigation
189
+ if (key.name === "left") { this.tutorialNavCallback?.("left"); return; }
190
+ if (key.name === "right") { this.tutorialNavCallback?.("right"); return; }
191
+ if (key.name === "up") { this.settingsNavCallback?.(-1); return; }
192
+ if (key.name === "down") { this.settingsNavCallback?.(1); return; }
193
+
194
+ if (key.name === "pageup") { this.scrollCallback?.(-10); return; }
195
+ if (key.name === "pagedown") { this.scrollCallback?.(10); return; }
196
+ if (key.name === "escape") { this.cancelCallback?.(); return; }
197
+ }
198
+ }
@@ -0,0 +1,36 @@
1
+ import { saveConfig, loadConfig, getApiKey } from "./config.ts";
2
+ import { platform } from "./client.ts";
3
+
4
+ export type AuthStatus = "authenticated" | "no_key" | "invalid_key";
5
+
6
+ export async function checkAuth(): Promise<AuthStatus> {
7
+ const key = getApiKey();
8
+ if (!key) return "no_key";
9
+
10
+ try {
11
+ // Validate the key by making a lightweight request
12
+ await platform.listStrategies();
13
+ return "authenticated";
14
+ } catch (err: any) {
15
+ if (err?.status === 401 || err?.status === 403) return "invalid_key";
16
+ // Network error or server down — assume key is fine
17
+ return "authenticated";
18
+ }
19
+ }
20
+
21
+ export function login(apiKey: string): void {
22
+ const config = loadConfig();
23
+ config.api_key = apiKey;
24
+ saveConfig(config);
25
+ platform.setApiKey(apiKey);
26
+ }
27
+
28
+ export function logout(): void {
29
+ const config = loadConfig();
30
+ delete config.api_key;
31
+ saveConfig(config);
32
+ }
33
+
34
+ export function isAuthenticated(): boolean {
35
+ return getApiKey() !== null;
36
+ }
@@ -0,0 +1,159 @@
1
+ import { getApiKey, getPlatformUrl } from "./config.ts";
2
+ import type {
3
+ PlatformStrategy,
4
+ PlatformDeployment,
5
+ PlatformMetrics,
6
+ PlatformLog,
7
+ } from "./types.ts";
8
+
9
+ class PlatformError extends Error {
10
+ constructor(public status: number, message: string) {
11
+ super(message);
12
+ this.name = "PlatformError";
13
+ }
14
+ }
15
+
16
+ class PlatformClient {
17
+ private baseUrl: string;
18
+ private apiKey: string | null;
19
+
20
+ constructor() {
21
+ this.baseUrl = getPlatformUrl();
22
+ this.apiKey = getApiKey();
23
+ }
24
+
25
+ get authenticated(): boolean {
26
+ return this.apiKey !== null;
27
+ }
28
+
29
+ setApiKey(key: string): void {
30
+ this.apiKey = key;
31
+ }
32
+
33
+ private async request<T>(path: string, options: RequestInit = {}): Promise<T> {
34
+ // Re-read API key on every request (it may have been set after construction via /login)
35
+ const apiKey = this.apiKey || getApiKey();
36
+ if (!apiKey) throw new PlatformError(401, "Not authenticated. Set HORIZON_API_KEY or run horizon auth login.");
37
+
38
+ const url = `${this.baseUrl}${path}`;
39
+ const res = await fetch(url, {
40
+ ...options,
41
+ signal: AbortSignal.timeout(15000),
42
+ headers: {
43
+ "Authorization": `Bearer ${apiKey}`,
44
+ "Content-Type": "application/json",
45
+ ...options.headers,
46
+ },
47
+ });
48
+
49
+ if (!res.ok) {
50
+ const body = await res.text().catch(() => "");
51
+ throw new PlatformError(res.status, `${res.status} ${res.statusText}: ${body}`);
52
+ }
53
+
54
+ return res.json() as Promise<T>;
55
+ }
56
+
57
+ // ── Strategies ──
58
+
59
+ async listStrategies(): Promise<PlatformStrategy[]> {
60
+ const res = await this.request<{ data: PlatformStrategy[] } | PlatformStrategy[]>("/api/v1/strategies");
61
+ return Array.isArray(res) ? res : (res as any).data ?? [];
62
+ }
63
+
64
+ async getStrategy(id: string): Promise<PlatformStrategy> {
65
+ return this.request<PlatformStrategy>(`/api/v1/strategies/${id}`);
66
+ }
67
+
68
+ async createStrategy(opts: {
69
+ name: string;
70
+ code: string;
71
+ params?: Record<string, unknown>;
72
+ risk_config?: Record<string, unknown>;
73
+ }): Promise<PlatformStrategy> {
74
+ return this.request<PlatformStrategy>("/api/v1/strategies", {
75
+ method: "POST",
76
+ body: JSON.stringify(opts),
77
+ });
78
+ }
79
+
80
+ // ── Deployments ──
81
+
82
+ async listDeployments(strategyId: string): Promise<PlatformDeployment[]> {
83
+ const res = await this.request<{ data: PlatformDeployment[] } | PlatformDeployment[]>(`/api/v1/strategies/${strategyId}/deployments`);
84
+ return Array.isArray(res) ? res : (res as any).data ?? [];
85
+ }
86
+
87
+ async deploy(strategyId: string, opts: {
88
+ credentialId: string;
89
+ dryRun: boolean;
90
+ targetMarkets?: Array<{ slug: string; tokenId: string }>;
91
+ deploymentMode?: "manual" | "scanner";
92
+ }): Promise<{ deployment: PlatformDeployment }> {
93
+ return this.request(`/api/v1/strategies/${strategyId}/deploy`, {
94
+ method: "POST",
95
+ body: JSON.stringify({
96
+ credential_id: opts.credentialId,
97
+ mode: opts.dryRun ? "paper" : "live",
98
+ markets: opts.targetMarkets ?? [],
99
+ deployment_mode: opts.deploymentMode ?? "manual",
100
+ }),
101
+ });
102
+ }
103
+
104
+ async stop(strategyId: string): Promise<{ success: boolean }> {
105
+ return this.request(`/api/v1/strategies/${strategyId}/stop`, {
106
+ method: "POST",
107
+ });
108
+ }
109
+
110
+ // ── Metrics ──
111
+
112
+ async getMetrics(strategyId: string, limit = 50): Promise<{
113
+ latest: PlatformMetrics | null;
114
+ history: PlatformMetrics[];
115
+ }> {
116
+ const res = await this.request<{ data: { latest: PlatformMetrics | null; history: PlatformMetrics[] } }>(
117
+ `/api/v1/strategies/${strategyId}/metrics?limit=${limit}`
118
+ );
119
+ return res.data;
120
+ }
121
+
122
+ // ── Logs ──
123
+
124
+ async getLogs(strategyId: string, limit = 100): Promise<PlatformLog[]> {
125
+ const res = await this.request<{ data: PlatformLog[] }>(
126
+ `/api/v1/strategies/${strategyId}/logs?limit=${limit}`
127
+ );
128
+ return res.data;
129
+ }
130
+
131
+ // ── Credentials ──
132
+
133
+ async listCredentials(): Promise<Array<{ id: string; label: string; exchange: string; wallet_address: string | null; created_at: string }>> {
134
+ const res = await this.request<{ data: any[] }>("/api/v1/credentials");
135
+ return res.data ?? [];
136
+ }
137
+
138
+ async createCredential(opts: { label: string; exchange: string; private_key: string; wallet_address?: string }): Promise<{ id: string; label: string; exchange: string }> {
139
+ const res = await this.request<{ data: any }>("/api/v1/credentials", {
140
+ method: "POST",
141
+ body: JSON.stringify(opts),
142
+ });
143
+ return res.data;
144
+ }
145
+
146
+ // ── Validate ──
147
+
148
+ async validateCode(code: string, strategyId?: string): Promise<{
149
+ valid: boolean;
150
+ errors: string[];
151
+ }> {
152
+ return this.request("/api/strategies/validate", {
153
+ method: "POST",
154
+ body: JSON.stringify({ code, strategyId }),
155
+ });
156
+ }
157
+ }
158
+
159
+ export const platform = new PlatformClient();