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,137 @@
1
+ ; Identifier naming conventions
2
+
3
+ (identifier) @variable
4
+
5
+ ((identifier) @constructor
6
+ (#match? @constructor "^[A-Z]"))
7
+
8
+ ((identifier) @constant
9
+ (#match? @constant "^[A-Z][A-Z_]*$"))
10
+
11
+ ; Function calls
12
+
13
+ (decorator) @function
14
+ (decorator
15
+ (identifier) @function)
16
+
17
+ (call
18
+ function: (attribute attribute: (identifier) @function.method))
19
+ (call
20
+ function: (identifier) @function)
21
+
22
+ ; Builtin functions
23
+
24
+ ((call
25
+ function: (identifier) @function.builtin)
26
+ (#match?
27
+ @function.builtin
28
+ "^(abs|all|any|ascii|bin|bool|breakpoint|bytearray|bytes|callable|chr|classmethod|compile|complex|delattr|dict|dir|divmod|enumerate|eval|exec|filter|float|format|frozenset|getattr|globals|hasattr|hash|help|hex|id|input|int|isinstance|issubclass|iter|len|list|locals|map|max|memoryview|min|next|object|oct|open|ord|pow|print|property|range|repr|reversed|round|set|setattr|slice|sorted|staticmethod|str|sum|super|tuple|type|vars|zip|__import__)$"))
29
+
30
+ ; Function definitions
31
+
32
+ (function_definition
33
+ name: (identifier) @function)
34
+
35
+ (attribute attribute: (identifier) @property)
36
+ (type (identifier) @type)
37
+
38
+ ; Literals
39
+
40
+ [
41
+ (none)
42
+ (true)
43
+ (false)
44
+ ] @constant.builtin
45
+
46
+ [
47
+ (integer)
48
+ (float)
49
+ ] @number
50
+
51
+ (comment) @comment
52
+ (string) @string
53
+ (escape_sequence) @escape
54
+
55
+ (interpolation
56
+ "{" @punctuation.special
57
+ "}" @punctuation.special) @embedded
58
+
59
+ [
60
+ "-"
61
+ "-="
62
+ "!="
63
+ "*"
64
+ "**"
65
+ "**="
66
+ "*="
67
+ "/"
68
+ "//"
69
+ "//="
70
+ "/="
71
+ "&"
72
+ "&="
73
+ "%"
74
+ "%="
75
+ "^"
76
+ "^="
77
+ "+"
78
+ "->"
79
+ "+="
80
+ "<"
81
+ "<<"
82
+ "<<="
83
+ "<="
84
+ "<>"
85
+ "="
86
+ ":="
87
+ "=="
88
+ ">"
89
+ ">="
90
+ ">>"
91
+ ">>="
92
+ "|"
93
+ "|="
94
+ "~"
95
+ "@="
96
+ "and"
97
+ "in"
98
+ "is"
99
+ "not"
100
+ "or"
101
+ "is not"
102
+ "not in"
103
+ ] @operator
104
+
105
+ [
106
+ "as"
107
+ "assert"
108
+ "async"
109
+ "await"
110
+ "break"
111
+ "class"
112
+ "continue"
113
+ "def"
114
+ "del"
115
+ "elif"
116
+ "else"
117
+ "except"
118
+ "exec"
119
+ "finally"
120
+ "for"
121
+ "from"
122
+ "global"
123
+ "if"
124
+ "import"
125
+ "lambda"
126
+ "nonlocal"
127
+ "pass"
128
+ "print"
129
+ "raise"
130
+ "return"
131
+ "try"
132
+ "while"
133
+ "with"
134
+ "yield"
135
+ "match"
136
+ "case"
137
+ ] @keyword
package/bin/horizon.js ADDED
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env bun
2
+ import "../src/index.ts";
package/package.json ADDED
@@ -0,0 +1,40 @@
1
+ {
2
+ "name": "horizon-code",
3
+ "version": "0.1.0",
4
+ "description": "AI-powered trading strategy terminal for Polymarket",
5
+ "type": "module",
6
+ "bin": {
7
+ "horizon": "./bin/horizon.js"
8
+ },
9
+ "files": [
10
+ "src",
11
+ "bin",
12
+ "assets"
13
+ ],
14
+ "scripts": {
15
+ "start": "bun run src/index.ts",
16
+ "dev": "bun run --watch src/index.ts"
17
+ },
18
+ "keywords": ["polymarket", "trading", "ai", "terminal", "strategy", "horizon"],
19
+ "author": "Oppenheimer Labs",
20
+ "license": "UNLICENSED",
21
+ "devDependencies": {
22
+ "@types/bun": "latest"
23
+ },
24
+ "peerDependencies": {
25
+ "typescript": "^5"
26
+ },
27
+ "dependencies": {
28
+ "@ai-sdk/mcp": "^1.0.25",
29
+ "@ai-sdk/provider-utils": "^3.0.0",
30
+ "@modelcontextprotocol/sdk": "^1.27.1",
31
+ "@openrouter/ai-sdk-provider": "^2.2.3",
32
+ "@opentui/core": "^0.1.87",
33
+ "@supabase/supabase-js": "^2.99.1",
34
+ "ai": "^6.0.78",
35
+ "zod": "^4.3.6"
36
+ },
37
+ "engines": {
38
+ "bun": ">=1.0.0"
39
+ }
40
+ }
@@ -0,0 +1,369 @@
1
+ // AI client — all LLM calls go through the API server (OpenRouter key stays server-side)
2
+ // Tools execute LOCALLY (Python sandbox, file ops, local APIs).
3
+ // Architecture: TUI sends messages → server calls OpenRouter → streams SSE back
4
+ // If LLM calls a tool → TUI executes it → sends result back → server continues
5
+
6
+ import { z } from "zod";
7
+ import { createMCPClient } from "@ai-sdk/mcp";
8
+ import { getSystemPrompt } from "./system-prompt.ts";
9
+ import { researchTools } from "../research/tools.ts";
10
+ import { portfolioTools } from "../platform/tools.ts";
11
+ import { strategyTools } from "../strategy/tools.ts";
12
+ import { getAuthUrl, loadConfig } from "../platform/config.ts";
13
+ import type { Mode } from "../components/mode-bar.ts";
14
+ import type { ModelPower } from "../platform/tiers.ts";
15
+ import { MODEL_IDS, PREMIUM_TOOLS, type PlanTier } from "../platform/tiers.ts";
16
+
17
+ let mcpTools: Record<string, any> = {};
18
+ let mcpClient: Awaited<ReturnType<typeof createMCPClient>> | null = null;
19
+
20
+ export async function initMCP(): Promise<{ toolCount: number }> {
21
+ const command = process.env.HORIZON_MCP_COMMAND;
22
+ if (!command) return { toolCount: 0 };
23
+ const args = (process.env.HORIZON_MCP_ARGS ?? "mcp serve").split(" ");
24
+ try {
25
+ // Only pass safe env vars to MCP — no exchange keys, API secrets, or private keys
26
+ const mcpEnv: Record<string, string> = {};
27
+ const mcpAllow = ["PATH", "HOME", "LANG", "LC_ALL", "TERM", "PYTHONPATH", "VIRTUAL_ENV", "CONDA_PREFIX", "SHELL", "USER"];
28
+ for (const k of mcpAllow) { if (process.env[k]) mcpEnv[k] = process.env[k]!; }
29
+
30
+ mcpClient = await createMCPClient({
31
+ transport: { type: "stdio" as any, command, args, env: mcpEnv } as any,
32
+ });
33
+ mcpTools = await mcpClient.tools();
34
+ return { toolCount: Object.keys(mcpTools).length };
35
+ } catch { return { toolCount: 0 }; }
36
+ }
37
+
38
+ export async function closeMCP(): Promise<void> {
39
+ if (mcpClient) { await mcpClient.close(); mcpClient = null; }
40
+ }
41
+
42
+ interface ChatMessage { role: "user" | "assistant"; content: string; }
43
+
44
+ export type ChatEvent =
45
+ | { type: "thinking" }
46
+ | { type: "text-delta"; textDelta: string }
47
+ | { type: "tool-call"; toolName: string; args: Record<string, unknown> }
48
+ | { type: "tool-result"; toolName: string; result: unknown }
49
+ | { type: "strategy-code"; code: string }
50
+ | { type: "usage"; inputTokens: number; outputTokens: number; weightedTokens: number; model: string }
51
+ | { type: "meta"; tier: string; model: string; effectivePower: string; budgetUsed: number; budgetTotal: number; overflowing: boolean }
52
+ | { type: "error"; message: string }
53
+ | { type: "finish" };
54
+
55
+ // ── Structured output schema for strategy mode ──
56
+
57
+ const strategySchema = z.object({
58
+ response: z.string().describe("Your response to the user in markdown"),
59
+ code: z.string().nullable().describe("Complete Python strategy code (imports + pipeline + hz.run), or null if no code change"),
60
+ });
61
+
62
+ // ── Partial JSON field extractor ──
63
+
64
+ function extractJsonStringField(json: string, field: string): string | undefined {
65
+ const pattern = new RegExp(`"${field}"\\s*:\\s*"`);
66
+ const match = json.match(pattern);
67
+ if (!match || match.index === undefined) return undefined;
68
+
69
+ const start = match.index + match[0].length;
70
+ let result = "";
71
+ let i = start;
72
+
73
+ while (i < json.length) {
74
+ const ch = json[i]!;
75
+ if (ch === "\\") {
76
+ if (i + 1 >= json.length) break;
77
+ const next = json[i + 1]!;
78
+ switch (next) {
79
+ case "n": result += "\n"; break;
80
+ case "t": result += "\t"; break;
81
+ case "r": result += "\r"; break;
82
+ case '"': result += '"'; break;
83
+ case "\\": result += "\\"; break;
84
+ case "/": result += "/"; break;
85
+ case "u": {
86
+ if (i + 5 < json.length) {
87
+ const hex = json.slice(i + 2, i + 6);
88
+ const cp = parseInt(hex, 16);
89
+ if (!isNaN(cp)) result += String.fromCharCode(cp);
90
+ i += 4;
91
+ }
92
+ break;
93
+ }
94
+ default: result += next; break;
95
+ }
96
+ i += 2;
97
+ } else if (ch === '"') {
98
+ break;
99
+ } else {
100
+ result += ch;
101
+ i++;
102
+ }
103
+ }
104
+ return result;
105
+ }
106
+
107
+ function extractCodeField(json: string): string | null | undefined {
108
+ if (json.match(/"code"\s*:\s*null/)) return null;
109
+ return extractJsonStringField(json, "code");
110
+ }
111
+
112
+ // ── Build tools for a given mode ──
113
+
114
+ function buildTools(mode: Mode): Record<string, any> {
115
+ // All tools available — server handles tier gating via model selection
116
+ return {
117
+ ...(mode === "research" ? researchTools : {}),
118
+ ...(mode === "strategy" ? { ...strategyTools, ...researchTools, deploy_strategy: portfolioTools.deploy_strategy, stop_strategy: portfolioTools.stop_strategy, list_strategies: portfolioTools.list_strategies, list_credentials: portfolioTools.list_credentials, add_credential: portfolioTools.add_credential } : {}),
119
+ ...(mode === "portfolio" ? portfolioTools : {}),
120
+ ...mcpTools,
121
+ };
122
+ }
123
+
124
+ // ── SSE stream consumer ──
125
+
126
+ async function* consumeSSE(res: Response): AsyncGenerator<Record<string, any>> {
127
+ if (!res.body) return;
128
+ const reader = res.body.getReader();
129
+ const decoder = new TextDecoder();
130
+ let buf = "";
131
+
132
+ while (true) {
133
+ const { done, value } = await reader.read();
134
+ if (done) break;
135
+ buf += decoder.decode(value, { stream: true });
136
+
137
+ const lines = buf.split("\n");
138
+ buf = lines.pop() ?? "";
139
+
140
+ for (const line of lines) {
141
+ if (!line.startsWith("data: ")) continue;
142
+ const data = line.slice(6);
143
+ if (data === "[DONE]") return;
144
+ try { yield JSON.parse(data); } catch {}
145
+ }
146
+ }
147
+ }
148
+
149
+ // ── Convert tools to JSON-serializable format for the server ──
150
+
151
+ import { zodSchema } from "@ai-sdk/provider-utils";
152
+
153
+ function toolsToServerFormat(tools: Record<string, any>): Record<string, any> {
154
+ const result: Record<string, any> = {};
155
+ for (const [name, tool] of Object.entries(tools)) {
156
+ if (!tool) continue;
157
+ let params: any = { type: "object", properties: {} };
158
+ try {
159
+ if (tool.parameters) {
160
+ // Use AI SDK's zodSchema which correctly converts Zod v4 to JSON Schema
161
+ const converted = zodSchema(tool.parameters);
162
+ params = converted.jsonSchema ?? params;
163
+ }
164
+ } catch {}
165
+ result[name] = { description: tool.description ?? "", parameters: params };
166
+ }
167
+ return result;
168
+ }
169
+
170
+ // ── Server call — sends messages, receives SSE stream ──
171
+
172
+ // Shared abort controller — cancel from outside to abort the current stream
173
+ let currentAbort: AbortController | null = null;
174
+ export function abortChat(): void { currentAbort?.abort(); }
175
+
176
+ async function callServer(opts: {
177
+ messages: { role: string; content: string }[];
178
+ system: string;
179
+ modelPower: ModelPower;
180
+ tools: Record<string, any>;
181
+ responseFormat?: any;
182
+ signal?: AbortSignal;
183
+ }): Promise<Response> {
184
+ const config = loadConfig();
185
+ if (!config.api_key) throw new Error("Not authenticated. Type /login.");
186
+
187
+ const res = await fetch(`${getAuthUrl()}/api/v1/chat`, {
188
+ method: "POST",
189
+ signal: opts.signal ?? AbortSignal.timeout(120000), // 2 min max
190
+ headers: { "Authorization": `Bearer ${config.api_key}`, "Content-Type": "application/json" },
191
+ body: JSON.stringify({
192
+ messages: opts.messages,
193
+ system: opts.system,
194
+ model_power: opts.modelPower,
195
+ tools: toolsToServerFormat(opts.tools),
196
+ response_format: opts.responseFormat,
197
+ }),
198
+ });
199
+
200
+ if (!res.ok) {
201
+ const err = await res.json().catch(() => ({ error: `Server error ${res.status}` })) as any;
202
+ throw new Error(err.message ?? err.error ?? `Error ${res.status}`);
203
+ }
204
+
205
+ return res;
206
+ }
207
+
208
+ // ── Main chat function ──
209
+
210
+ export async function* chat(
211
+ messages: ChatMessage[],
212
+ mode: Mode = "research",
213
+ modelPower?: ModelPower,
214
+ verbosity: string = "normal",
215
+ ): AsyncGenerator<ChatEvent> {
216
+ const power = modelPower ?? "standard";
217
+ const config = loadConfig();
218
+ if (!config.api_key) {
219
+ yield { type: "error", message: "Not authenticated. Type /login." };
220
+ return;
221
+ }
222
+
223
+ const tools = buildTools(mode);
224
+ const system = getSystemPrompt(mode, verbosity);
225
+ const useStructuredOutput = mode === "strategy";
226
+
227
+ // Abort controller — allows cancellation from the UI
228
+ currentAbort = new AbortController();
229
+ const signal = currentAbort.signal;
230
+
231
+ // Build the conversation for the server
232
+ const serverMessages = messages.map((m) => ({ role: m.role, content: m.content }));
233
+
234
+ let step = 0;
235
+ const MAX_STEPS = 5;
236
+
237
+ while (step < MAX_STEPS) {
238
+ step++;
239
+
240
+ let res: Response;
241
+ try {
242
+ res = await callServer({
243
+ messages: serverMessages,
244
+ system,
245
+ modelPower: power,
246
+ tools,
247
+ responseFormat: useStructuredOutput ? { type: "json_schema", schema: { response: "string", code: "string|null" } } : undefined,
248
+ signal,
249
+ });
250
+ } catch (err: any) {
251
+ yield { type: "error", message: err.message };
252
+ return;
253
+ }
254
+
255
+ // Process SSE events
256
+ let hasToolCalls = false;
257
+ let jsonBuf = "";
258
+ let emittedResponseLen = 0;
259
+ let emittedCode: string | null = null;
260
+ let structuredActive = useStructuredOutput;
261
+ const pendingToolCalls: { toolName: string; args: Record<string, unknown> }[] = [];
262
+ let eventCount = 0;
263
+
264
+ for await (const event of consumeSSE(res)) {
265
+ eventCount++;
266
+ if (event.type === "meta") {
267
+ yield {
268
+ type: "meta", tier: event.tier ?? "", model: event.model ?? "",
269
+ effectivePower: event.effective_power ?? power,
270
+ budgetUsed: event.budget_used ?? 0, budgetTotal: event.budget_total ?? 0,
271
+ overflowing: event.overflowing ?? false,
272
+ };
273
+ } else if (event.type === "text-delta") {
274
+ const delta = event.text ?? event.textDelta ?? "";
275
+ if (!delta) continue;
276
+
277
+ if (structuredActive) {
278
+ jsonBuf += delta;
279
+
280
+ const trimmed = jsonBuf.trimStart();
281
+ if (trimmed.length > 3 && !trimmed.startsWith("{") && !trimmed.startsWith("[")) {
282
+ structuredActive = false;
283
+ yield { type: "text-delta", textDelta: jsonBuf };
284
+ jsonBuf = "";
285
+ continue;
286
+ }
287
+
288
+ const response = extractJsonStringField(jsonBuf, "response");
289
+ if (response && response.length > emittedResponseLen) {
290
+ yield { type: "text-delta", textDelta: response.slice(emittedResponseLen) };
291
+ emittedResponseLen = response.length;
292
+ }
293
+
294
+ const code = extractCodeField(jsonBuf);
295
+ if (code !== undefined && code !== emittedCode) {
296
+ if (code !== null) yield { type: "strategy-code", code };
297
+ emittedCode = code;
298
+ }
299
+ } else {
300
+ yield { type: "text-delta", textDelta: delta };
301
+ }
302
+ } else if (event.type === "tool-call") {
303
+ hasToolCalls = true;
304
+ const toolName = event.toolName ?? "";
305
+ const args = event.args ?? {};
306
+ pendingToolCalls.push({ toolName, args });
307
+ yield { type: "tool-call", toolName, args };
308
+ } else if (event.type === "usage") {
309
+ yield {
310
+ type: "usage",
311
+ inputTokens: event.input_tokens ?? 0,
312
+ outputTokens: event.output_tokens ?? 0,
313
+ weightedTokens: event.weighted_tokens ?? 0,
314
+ model: event.model ?? "",
315
+ };
316
+ } else if (event.type === "error") {
317
+ yield { type: "error", message: event.message ?? "Server error" };
318
+ return;
319
+ }
320
+ }
321
+
322
+ // If no tool calls, we're done
323
+ if (!hasToolCalls || pendingToolCalls.length === 0) {
324
+ if (eventCount === 0) {
325
+ yield { type: "error", message: "No response from server. Check your connection or try again." };
326
+ }
327
+ yield { type: "finish" };
328
+ return;
329
+ }
330
+
331
+ // Execute tools locally and build the assistant's "I called these tools" message
332
+ const assistantParts: string[] = [];
333
+ for (const tc of pendingToolCalls) {
334
+ const tool = tools[tc.toolName];
335
+ if (!tool) {
336
+ const errorResult = { error: `Unknown tool: ${tc.toolName}` };
337
+ yield { type: "tool-result", toolName: tc.toolName, result: errorResult };
338
+ assistantParts.push(`I called ${tc.toolName} but it was not found.`);
339
+ continue;
340
+ }
341
+
342
+ try {
343
+ const result = await tool.execute(tc.args, { toolCallId: `call-${Date.now()}`, messages: [] });
344
+ yield { type: "tool-result", toolName: tc.toolName, result };
345
+ const summary = typeof result === "object" ? JSON.stringify(result).slice(0, 2000) : String(result);
346
+ assistantParts.push(`I called ${tc.toolName}(${JSON.stringify(tc.args).slice(0, 100)}) and got:\n${summary}`);
347
+ } catch (err: any) {
348
+ const errorResult = { error: err.message ?? String(err) };
349
+ yield { type: "tool-result", toolName: tc.toolName, result: errorResult };
350
+ assistantParts.push(`I called ${tc.toolName} but it failed: ${err.message}`);
351
+ }
352
+ }
353
+
354
+ // Add as assistant message — the LLM sees it already ran the tools
355
+ serverMessages.push({ role: "assistant", content: assistantParts.join("\n\n") });
356
+ // Prompt the LLM to continue with its response based on the tool results
357
+ serverMessages.push({ role: "user", content: "Now respond to the user based on the tool results above. Do NOT call the same tools again." });
358
+
359
+ // Reset structured output state for next turn
360
+ if (structuredActive) {
361
+ jsonBuf = "";
362
+ emittedResponseLen = 0;
363
+ }
364
+
365
+ // Loop back — server will call OpenRouter again with the full conversation including tool results
366
+ }
367
+
368
+ yield { type: "finish" };
369
+ }
@@ -0,0 +1,86 @@
1
+ import type { Mode } from "../components/mode-bar.ts";
2
+ import { buildGeneratePrompt } from "../strategy/prompts.ts";
3
+
4
+ const BASE = `You are Horizon, an AI trading research assistant running in a CLI terminal.
5
+
6
+ Rules:
7
+ - Concise and direct. You're a terminal, not a chatbot.
8
+ - Tool results render as rich CLI widgets automatically — do NOT reformat the data as tables. Just add 1-2 sentences of insight after the widget.
9
+ - NEVER suggest switching modes.
10
+ - Format: $102,450 not 102450.
11
+ - NEVER answer questions about markets, events, prices, or predictions from memory. ALWAYS call tools to get real-time data. Your training data is stale — the tools have live data.`;
12
+
13
+ const MODE_PROMPTS: Record<Mode, string> = {
14
+ research: `${BASE}
15
+
16
+ ## Mode: Research
17
+
18
+ You have 13 research tools that fetch LIVE data from Polymarket, Kalshi, and the web. Use them.
19
+
20
+ ### CRITICAL RULE
21
+ When the user mentions ANY topic, event, market, or asks about what's happening — ALWAYS call a tool to search for real data. Never answer from your training data alone. Prediction markets change by the minute.
22
+
23
+ - "What's happening with Iran?" → polymarketEvents("iran")
24
+ - "Show me crypto markets" → polymarketEvents("crypto")
25
+ - "Tell me about the fed rate market" → polymarketEvents("fed rate"), then polymarketEventDetail with the slug
26
+ - "What's the sentiment on bitcoin?" → newsSentiment with the slug from a prior search
27
+ - "How much should I bet?" → evCalculator with the slug, side, and edge
28
+
29
+ ### Available Tools
30
+
31
+ **Search & discover:**
32
+ - polymarketEvents(query, limit) — search Polymarket by topic. ALWAYS call this first when the user asks about any topic.
33
+ - kalshiEvents(query) — search Kalshi markets
34
+ - webSearch(query) — search the internet for news, facts, context
35
+ - calaKnowledge(query) — deep research on companies, people, industries
36
+
37
+ **Analyze a specific market (requires a slug from polymarketEvents):**
38
+ - polymarketEventDetail(slug) — full details, sub-markets, price history, spreads
39
+ - polymarketPriceHistory(slug, interval) — price chart (1h/6h/1d/1w/1m/max)
40
+ - polymarketOrderBook(slug) — bid/ask depth
41
+ - whaleTracker(slug) — large trades, smart money flow
42
+ - newsSentiment(slug) — news sentiment score + articles
43
+ - historicalVolatility(slug, interval) — realized vol, regime detection
44
+
45
+ **Quantitative analysis:**
46
+ - evCalculator(slug, side, estimatedEdge, bankroll) — expected value + Kelly sizing
47
+ - probabilityCalculator(slugA, slugB) — joint/conditional probability
48
+ - compareMarkets(topic) — cross-platform price comparison (Polymarket vs Kalshi)
49
+
50
+ ### Tool Chaining
51
+ When the user refers to a market from a previous result, reuse the SLUG from that result. Don't call polymarketEvents again — go straight to the detail tool. Look at conversation history for slugs.
52
+
53
+ Use your intelligence to understand what the user wants. If they say "dig deeper into the first one" — that means polymarketEventDetail with the first slug. If they say "what do whales think" — that means whaleTracker. You're smart enough to figure this out without a flowchart.`,
54
+
55
+ strategy: "", // dynamically generated
56
+
57
+ portfolio: `${BASE}
58
+
59
+ ## Mode: Portfolio Management
60
+
61
+ You have these tools to access the user's REAL account data. Always call tools — never guess.
62
+
63
+ - **list_strategies** — all strategies with status. Call this first.
64
+ - **get_strategy(strategy_id)** — full detail (code, params, risk config)
65
+ - **list_deployments(strategy_id)** — all deployments (active, stopped, errored)
66
+ - **get_metrics(strategy_id)** — live P&L, positions, orders, win rate, drawdown
67
+ - **get_logs(strategy_id)** — recent stdout/stderr from running bot
68
+ - **deploy_strategy(strategy_id, credential_id, dry_run)** — deploy (paper or live)
69
+ - **stop_strategy(strategy_id)** — stop active deployment
70
+ - **validate_code(code)** — validate strategy code
71
+
72
+ When the user asks about their portfolio, strategies, or deployments — call the tools immediately. The data is real and live. Format P&L, exposure, and drawdown clearly. Flag high drawdown or errors.`,
73
+ };
74
+
75
+ const VERBOSITY_PREFIX: Record<string, string> = {
76
+ short: "\n\nIMPORTANT: Be extremely terse. Maximum 1-2 sentences per response. No elaboration unless asked. Bullet points over paragraphs.",
77
+ normal: "",
78
+ verbose: "\n\nBe thorough and detailed. Explain your reasoning, provide context, and be comprehensive. Use examples when helpful.",
79
+ };
80
+
81
+ export function getSystemPrompt(mode: Mode, verbosity: string = "normal"): string {
82
+ // Strategy mode has its own verbosity rules — don't override
83
+ if (mode === "strategy") return buildGeneratePrompt();
84
+ const prefix = VERBOSITY_PREFIX[verbosity] ?? "";
85
+ return MODE_PROMPTS[mode] + prefix;
86
+ }