@spfunctions/cli 1.7.19 → 1.7.20
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/dist/101.index.js +1 -0
- package/dist/12.index.js +1 -0
- package/dist/160.index.js +1 -0
- package/dist/174.index.js +1 -0
- package/dist/278.index.js +6 -0
- package/dist/582.index.js +1 -0
- package/dist/641.index.js +324 -0
- package/dist/669.index.js +1 -0
- package/dist/722.index.js +1 -0
- package/dist/788.index.js +1 -0
- package/dist/816.index.js +12 -0
- package/dist/830.index.js +1 -0
- package/dist/921.index.js +1 -0
- package/dist/index.js +1 -833
- package/package.json +5 -2
- package/dist/cache.d.ts +0 -6
- package/dist/cache.js +0 -31
- package/dist/cache.test.d.ts +0 -1
- package/dist/cache.test.js +0 -73
- package/dist/client.d.ts +0 -56
- package/dist/client.js +0 -205
- package/dist/client.test.d.ts +0 -1
- package/dist/client.test.js +0 -89
- package/dist/commands/agent.d.ts +0 -20
- package/dist/commands/agent.js +0 -4119
- package/dist/commands/announcements.d.ts +0 -3
- package/dist/commands/announcements.js +0 -28
- package/dist/commands/augment.d.ts +0 -12
- package/dist/commands/augment.js +0 -56
- package/dist/commands/balance.d.ts +0 -3
- package/dist/commands/balance.js +0 -17
- package/dist/commands/book.d.ts +0 -17
- package/dist/commands/book.js +0 -220
- package/dist/commands/cancel.d.ts +0 -5
- package/dist/commands/cancel.js +0 -41
- package/dist/commands/context.d.ts +0 -6
- package/dist/commands/context.js +0 -208
- package/dist/commands/create.d.ts +0 -7
- package/dist/commands/create.js +0 -42
- package/dist/commands/dashboard.d.ts +0 -14
- package/dist/commands/dashboard.js +0 -215
- package/dist/commands/delta.d.ts +0 -16
- package/dist/commands/delta.js +0 -115
- package/dist/commands/edges.d.ts +0 -26
- package/dist/commands/edges.js +0 -246
- package/dist/commands/evaluate.d.ts +0 -4
- package/dist/commands/evaluate.js +0 -30
- package/dist/commands/explore.d.ts +0 -14
- package/dist/commands/explore.js +0 -116
- package/dist/commands/feed.d.ts +0 -13
- package/dist/commands/feed.js +0 -73
- package/dist/commands/fills.d.ts +0 -4
- package/dist/commands/fills.js +0 -29
- package/dist/commands/forecast.d.ts +0 -4
- package/dist/commands/forecast.js +0 -53
- package/dist/commands/get.d.ts +0 -5
- package/dist/commands/get.js +0 -98
- package/dist/commands/heartbeat.d.ts +0 -20
- package/dist/commands/heartbeat.js +0 -73
- package/dist/commands/history.d.ts +0 -3
- package/dist/commands/history.js +0 -38
- package/dist/commands/liquidity.d.ts +0 -14
- package/dist/commands/liquidity.js +0 -378
- package/dist/commands/list.d.ts +0 -5
- package/dist/commands/list.js +0 -38
- package/dist/commands/login.d.ts +0 -10
- package/dist/commands/login.js +0 -98
- package/dist/commands/markets.d.ts +0 -10
- package/dist/commands/markets.js +0 -39
- package/dist/commands/milestones.d.ts +0 -8
- package/dist/commands/milestones.js +0 -56
- package/dist/commands/orders.d.ts +0 -4
- package/dist/commands/orders.js +0 -28
- package/dist/commands/performance.d.ts +0 -11
- package/dist/commands/performance.js +0 -250
- package/dist/commands/positions.d.ts +0 -19
- package/dist/commands/positions.js +0 -294
- package/dist/commands/prompt.d.ts +0 -13
- package/dist/commands/prompt.js +0 -35
- package/dist/commands/publish.d.ts +0 -15
- package/dist/commands/publish.js +0 -39
- package/dist/commands/query.d.ts +0 -15
- package/dist/commands/query.js +0 -132
- package/dist/commands/rfq.d.ts +0 -5
- package/dist/commands/rfq.js +0 -35
- package/dist/commands/scan.d.ts +0 -11
- package/dist/commands/scan.js +0 -230
- package/dist/commands/schedule.d.ts +0 -3
- package/dist/commands/schedule.js +0 -38
- package/dist/commands/settlements.d.ts +0 -6
- package/dist/commands/settlements.js +0 -50
- package/dist/commands/setup.d.ts +0 -24
- package/dist/commands/setup.js +0 -700
- package/dist/commands/signal.d.ts +0 -6
- package/dist/commands/signal.js +0 -32
- package/dist/commands/strategies.d.ts +0 -11
- package/dist/commands/strategies.js +0 -130
- package/dist/commands/telegram.d.ts +0 -15
- package/dist/commands/telegram.js +0 -125
- package/dist/commands/trade.d.ts +0 -12
- package/dist/commands/trade.js +0 -112
- package/dist/commands/watch.d.ts +0 -19
- package/dist/commands/watch.js +0 -157
- package/dist/commands/whatif.d.ts +0 -17
- package/dist/commands/whatif.js +0 -209
- package/dist/commands/x.d.ts +0 -28
- package/dist/commands/x.js +0 -167
- package/dist/config.d.ts +0 -55
- package/dist/config.js +0 -139
- package/dist/config.test.d.ts +0 -1
- package/dist/config.test.js +0 -138
- package/dist/index.d.ts +0 -20
- package/dist/kalshi.d.ts +0 -144
- package/dist/kalshi.js +0 -498
- package/dist/polymarket.d.ts +0 -237
- package/dist/polymarket.js +0 -353
- package/dist/polymarket.test.d.ts +0 -1
- package/dist/polymarket.test.js +0 -424
- package/dist/share.d.ts +0 -4
- package/dist/share.js +0 -27
- package/dist/skills/loader.d.ts +0 -19
- package/dist/skills/loader.js +0 -86
- package/dist/telegram/agent-bridge.d.ts +0 -15
- package/dist/telegram/agent-bridge.js +0 -573
- package/dist/telegram/bot.d.ts +0 -10
- package/dist/telegram/bot.js +0 -297
- package/dist/telegram/commands.d.ts +0 -11
- package/dist/telegram/commands.js +0 -120
- package/dist/telegram/format.d.ts +0 -11
- package/dist/telegram/format.js +0 -51
- package/dist/telegram/format.test.d.ts +0 -1
- package/dist/telegram/format.test.js +0 -73
- package/dist/telegram/poller.d.ts +0 -6
- package/dist/telegram/poller.js +0 -32
- package/dist/topics.d.ts +0 -17
- package/dist/topics.js +0 -102
- package/dist/topics.test.d.ts +0 -1
- package/dist/topics.test.js +0 -131
- package/dist/tui/border.d.ts +0 -33
- package/dist/tui/border.js +0 -87
- package/dist/tui/chart.d.ts +0 -19
- package/dist/tui/chart.js +0 -117
- package/dist/tui/dashboard.d.ts +0 -9
- package/dist/tui/dashboard.js +0 -814
- package/dist/tui/layout.d.ts +0 -16
- package/dist/tui/layout.js +0 -41
- package/dist/tui/screen.d.ts +0 -33
- package/dist/tui/screen.js +0 -102
- package/dist/tui/state.d.ts +0 -40
- package/dist/tui/state.js +0 -36
- package/dist/tui/widgets/commandbar.d.ts +0 -8
- package/dist/tui/widgets/commandbar.js +0 -82
- package/dist/tui/widgets/detail.d.ts +0 -9
- package/dist/tui/widgets/detail.js +0 -151
- package/dist/tui/widgets/edges.d.ts +0 -4
- package/dist/tui/widgets/edges.js +0 -34
- package/dist/tui/widgets/liquidity.d.ts +0 -9
- package/dist/tui/widgets/liquidity.js +0 -142
- package/dist/tui/widgets/orders.d.ts +0 -4
- package/dist/tui/widgets/orders.js +0 -37
- package/dist/tui/widgets/portfolio.d.ts +0 -4
- package/dist/tui/widgets/portfolio.js +0 -59
- package/dist/tui/widgets/signals.d.ts +0 -4
- package/dist/tui/widgets/signals.js +0 -31
- package/dist/tui/widgets/statusbar.d.ts +0 -8
- package/dist/tui/widgets/statusbar.js +0 -72
- package/dist/tui/widgets/thesis.d.ts +0 -4
- package/dist/tui/widgets/thesis.js +0 -66
- package/dist/tui/widgets/trade.d.ts +0 -9
- package/dist/tui/widgets/trade.js +0 -117
- package/dist/tui/widgets/upcoming.d.ts +0 -4
- package/dist/tui/widgets/upcoming.js +0 -41
- package/dist/tui/widgets/whatif.d.ts +0 -7
- package/dist/tui/widgets/whatif.js +0 -113
- package/dist/types/output.d.ts +0 -412
- package/dist/types/output.js +0 -9
- package/dist/utils.d.ts +0 -52
- package/dist/utils.js +0 -146
- package/dist/utils.test.d.ts +0 -1
- package/dist/utils.test.js +0 -111
package/dist/commands/agent.js
DELETED
|
@@ -1,4119 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
/**
|
|
3
|
-
* sf agent — Interactive TUI agent powered by pi-tui + pi-agent-core.
|
|
4
|
-
*
|
|
5
|
-
* Layout:
|
|
6
|
-
* [Header overlay] — thesis id, confidence, model
|
|
7
|
-
* [Spacer] — room for header
|
|
8
|
-
* [Chat container] — messages (user, assistant, tool, system)
|
|
9
|
-
* [Editor] — multi-line input with slash command autocomplete
|
|
10
|
-
* [Spacer] — room for footer
|
|
11
|
-
* [Footer overlay] — tokens, cost, tool count, /help hint
|
|
12
|
-
*
|
|
13
|
-
* Slash commands (bypass LLM):
|
|
14
|
-
* /help /tree /edges /pos /eval /model /clear /exit
|
|
15
|
-
*/
|
|
16
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
17
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
18
|
-
};
|
|
19
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
20
|
-
exports.agentCommand = agentCommand;
|
|
21
|
-
const fs_1 = __importDefault(require("fs"));
|
|
22
|
-
const path_1 = __importDefault(require("path"));
|
|
23
|
-
const os_1 = __importDefault(require("os"));
|
|
24
|
-
const client_js_1 = require("../client.js");
|
|
25
|
-
const kalshi_js_1 = require("../kalshi.js");
|
|
26
|
-
const polymarket_js_1 = require("../polymarket.js");
|
|
27
|
-
const topics_js_1 = require("../topics.js");
|
|
28
|
-
const config_js_1 = require("../config.js");
|
|
29
|
-
const loader_js_1 = require("../skills/loader.js");
|
|
30
|
-
// ─── Session persistence ─────────────────────────────────────────────────────
|
|
31
|
-
function getSessionDir() {
|
|
32
|
-
return path_1.default.join(os_1.default.homedir(), '.sf', 'sessions');
|
|
33
|
-
}
|
|
34
|
-
function getSessionPath(thesisId) {
|
|
35
|
-
return path_1.default.join(getSessionDir(), `${thesisId}.json`);
|
|
36
|
-
}
|
|
37
|
-
function loadSession(thesisId) {
|
|
38
|
-
const p = getSessionPath(thesisId);
|
|
39
|
-
try {
|
|
40
|
-
if (fs_1.default.existsSync(p)) {
|
|
41
|
-
return JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
catch { /* corrupt file, ignore */ }
|
|
45
|
-
return null;
|
|
46
|
-
}
|
|
47
|
-
function saveSession(thesisId, model, messages) {
|
|
48
|
-
const dir = getSessionDir();
|
|
49
|
-
fs_1.default.mkdirSync(dir, { recursive: true });
|
|
50
|
-
fs_1.default.writeFileSync(getSessionPath(thesisId), JSON.stringify({
|
|
51
|
-
thesisId,
|
|
52
|
-
model,
|
|
53
|
-
updatedAt: new Date().toISOString(),
|
|
54
|
-
messages,
|
|
55
|
-
}, null, 2));
|
|
56
|
-
}
|
|
57
|
-
// ─── ANSI 24-bit color helpers (no chalk dependency) ─────────────────────────
|
|
58
|
-
const rgb = (r, g, b) => (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
|
|
59
|
-
const bgRgb = (r, g, b) => (s) => `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`;
|
|
60
|
-
const bold = (s) => `\x1b[1m${s}\x1b[22m`;
|
|
61
|
-
const dim = (s) => `\x1b[2m${s}\x1b[22m`;
|
|
62
|
-
const italic = (s) => `\x1b[3m${s}\x1b[23m`;
|
|
63
|
-
const underline = (s) => `\x1b[4m${s}\x1b[24m`;
|
|
64
|
-
const strikethrough = (s) => `\x1b[9m${s}\x1b[29m`;
|
|
65
|
-
const C = {
|
|
66
|
-
emerald: rgb(16, 185, 129), // #10b981
|
|
67
|
-
zinc200: rgb(228, 228, 231), // #e4e4e7
|
|
68
|
-
zinc400: rgb(161, 161, 170), // #a1a1aa
|
|
69
|
-
zinc600: rgb(82, 82, 91), // #52525b
|
|
70
|
-
zinc800: rgb(39, 39, 42), // #27272a
|
|
71
|
-
red: rgb(239, 68, 68), // #ef4444
|
|
72
|
-
amber: rgb(245, 158, 11), // #f59e0b
|
|
73
|
-
white: rgb(255, 255, 255), // #ffffff
|
|
74
|
-
bgZinc900: bgRgb(24, 24, 27), // #18181b
|
|
75
|
-
bgZinc800: bgRgb(39, 39, 42), // #27272a
|
|
76
|
-
};
|
|
77
|
-
// ─── Custom components ───────────────────────────────────────────────────────
|
|
78
|
-
/** Mutable single-line component (TruncatedText is immutable) */
|
|
79
|
-
function createMutableLine(piTui) {
|
|
80
|
-
const { truncateToWidth, visibleWidth } = piTui;
|
|
81
|
-
return class MutableLine {
|
|
82
|
-
text;
|
|
83
|
-
cachedWidth;
|
|
84
|
-
cachedLines;
|
|
85
|
-
constructor(text) {
|
|
86
|
-
this.text = text;
|
|
87
|
-
}
|
|
88
|
-
setText(text) {
|
|
89
|
-
this.text = text;
|
|
90
|
-
this.cachedWidth = undefined;
|
|
91
|
-
this.cachedLines = undefined;
|
|
92
|
-
}
|
|
93
|
-
invalidate() {
|
|
94
|
-
this.cachedWidth = undefined;
|
|
95
|
-
this.cachedLines = undefined;
|
|
96
|
-
}
|
|
97
|
-
render(width) {
|
|
98
|
-
if (this.cachedLines && this.cachedWidth === width)
|
|
99
|
-
return this.cachedLines;
|
|
100
|
-
this.cachedWidth = width;
|
|
101
|
-
this.cachedLines = [truncateToWidth(this.text, width)];
|
|
102
|
-
return this.cachedLines;
|
|
103
|
-
}
|
|
104
|
-
};
|
|
105
|
-
}
|
|
106
|
-
/**
|
|
107
|
-
* Header bar — trading terminal style.
|
|
108
|
-
* Shows: thesis ID, confidence+delta, positions P&L, edge count, top edge
|
|
109
|
-
*/
|
|
110
|
-
function createHeaderBar(piTui) {
|
|
111
|
-
const { truncateToWidth, visibleWidth } = piTui;
|
|
112
|
-
return class HeaderBar {
|
|
113
|
-
thesisId = '';
|
|
114
|
-
confidence = 0;
|
|
115
|
-
confidenceDelta = 0;
|
|
116
|
-
pnlDollars = 0;
|
|
117
|
-
positionCount = 0;
|
|
118
|
-
edgeCount = 0;
|
|
119
|
-
topEdge = ''; // e.g. "RECESSION +21¢"
|
|
120
|
-
cachedWidth;
|
|
121
|
-
cachedLines;
|
|
122
|
-
setFromContext(ctx, positions) {
|
|
123
|
-
this.thesisId = (ctx.thesisId || '').slice(0, 8);
|
|
124
|
-
this.confidence = typeof ctx.confidence === 'number'
|
|
125
|
-
? Math.round(ctx.confidence * 100)
|
|
126
|
-
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
127
|
-
this.confidenceDelta = ctx.lastEvaluation?.confidenceDelta
|
|
128
|
-
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
129
|
-
: 0;
|
|
130
|
-
this.edgeCount = (ctx.edges || []).length;
|
|
131
|
-
// Top edge by absolute size
|
|
132
|
-
const edges = ctx.edges || [];
|
|
133
|
-
if (edges.length > 0) {
|
|
134
|
-
const top = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0))[0];
|
|
135
|
-
const name = (top.market || top.marketTitle || top.marketId || '').slice(0, 20);
|
|
136
|
-
const edge = top.edge || top.edgeSize || 0;
|
|
137
|
-
this.topEdge = `${name} ${edge > 0 ? '+' : ''}${Math.round(edge)}\u00A2`;
|
|
138
|
-
}
|
|
139
|
-
// P&L from positions
|
|
140
|
-
if (positions && positions.length > 0) {
|
|
141
|
-
this.positionCount = positions.length;
|
|
142
|
-
this.pnlDollars = positions.reduce((sum, p) => {
|
|
143
|
-
const pnl = p.unrealized_pnl || 0;
|
|
144
|
-
return sum + pnl;
|
|
145
|
-
}, 0) / 100; // cents → dollars
|
|
146
|
-
}
|
|
147
|
-
this.cachedWidth = undefined;
|
|
148
|
-
this.cachedLines = undefined;
|
|
149
|
-
}
|
|
150
|
-
updateConfidence(newConf, delta) {
|
|
151
|
-
this.confidence = Math.round(newConf * 100);
|
|
152
|
-
this.confidenceDelta = Math.round(delta * 100);
|
|
153
|
-
this.cachedWidth = undefined;
|
|
154
|
-
this.cachedLines = undefined;
|
|
155
|
-
}
|
|
156
|
-
invalidate() {
|
|
157
|
-
this.cachedWidth = undefined;
|
|
158
|
-
this.cachedLines = undefined;
|
|
159
|
-
}
|
|
160
|
-
// Keep legacy update() for compatibility with /switch etc.
|
|
161
|
-
update(left, center, right) {
|
|
162
|
-
this.cachedWidth = undefined;
|
|
163
|
-
this.cachedLines = undefined;
|
|
164
|
-
}
|
|
165
|
-
render(width) {
|
|
166
|
-
if (this.cachedLines && this.cachedWidth === width)
|
|
167
|
-
return this.cachedLines;
|
|
168
|
-
this.cachedWidth = width;
|
|
169
|
-
// Build segments
|
|
170
|
-
const id = C.emerald(bold(this.thesisId));
|
|
171
|
-
// Confidence with arrow
|
|
172
|
-
const arrow = this.confidenceDelta > 0 ? '\u25B2' : this.confidenceDelta < 0 ? '\u25BC' : '\u2500';
|
|
173
|
-
const arrowColor = this.confidenceDelta > 0 ? C.emerald : this.confidenceDelta < 0 ? C.red : C.zinc600;
|
|
174
|
-
const deltaStr = this.confidenceDelta !== 0 ? ` (${this.confidenceDelta > 0 ? '+' : ''}${this.confidenceDelta})` : '';
|
|
175
|
-
const conf = arrowColor(`${arrow} ${this.confidence}%${deltaStr}`);
|
|
176
|
-
// P&L
|
|
177
|
-
let pnl = '';
|
|
178
|
-
if (this.positionCount > 0) {
|
|
179
|
-
const pnlStr = this.pnlDollars >= 0
|
|
180
|
-
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
181
|
-
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
182
|
-
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
183
|
-
}
|
|
184
|
-
// Edges
|
|
185
|
-
const edges = C.zinc600(`${this.edgeCount} edges`);
|
|
186
|
-
// Top edge
|
|
187
|
-
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
188
|
-
// Assemble with separators
|
|
189
|
-
const sep = C.zinc600(' \u2502 ');
|
|
190
|
-
const parts = [id, conf, pnl, edges, top].filter(Boolean);
|
|
191
|
-
const content = parts.join(sep);
|
|
192
|
-
let line = C.bgZinc900(' ' + truncateToWidth(content, width - 2, '') + ' ');
|
|
193
|
-
const lineVw = visibleWidth(line);
|
|
194
|
-
if (lineVw < width) {
|
|
195
|
-
line = line + C.bgZinc900(' '.repeat(width - lineVw));
|
|
196
|
-
}
|
|
197
|
-
this.cachedLines = [line];
|
|
198
|
-
return this.cachedLines;
|
|
199
|
-
}
|
|
200
|
-
};
|
|
201
|
-
}
|
|
202
|
-
/** Combined footer bar: thesis info (line 1) + model/exchange (line 2) */
|
|
203
|
-
function createFooterBar(piTui) {
|
|
204
|
-
const { truncateToWidth, visibleWidth } = piTui;
|
|
205
|
-
return class FooterBar {
|
|
206
|
-
// Thesis info (was HeaderBar)
|
|
207
|
-
thesisId = '';
|
|
208
|
-
confidence = 0;
|
|
209
|
-
confidenceDelta = 0;
|
|
210
|
-
pnlDollars = 0;
|
|
211
|
-
positionCount = 0;
|
|
212
|
-
edgeCount = 0;
|
|
213
|
-
topEdge = '';
|
|
214
|
-
// Model info
|
|
215
|
-
tokens = 0;
|
|
216
|
-
cost = 0;
|
|
217
|
-
toolCount = 0;
|
|
218
|
-
modelName = '';
|
|
219
|
-
tradingEnabled = false;
|
|
220
|
-
exchangeOpen = null;
|
|
221
|
-
cachedWidth;
|
|
222
|
-
cachedLines;
|
|
223
|
-
isExplorer = false;
|
|
224
|
-
setFromContext(ctx, positions) {
|
|
225
|
-
if (ctx._explorerMode) {
|
|
226
|
-
this.isExplorer = true;
|
|
227
|
-
this.thesisId = 'Explorer';
|
|
228
|
-
this.confidence = 0;
|
|
229
|
-
this.confidenceDelta = 0;
|
|
230
|
-
this.edgeCount = (ctx.edges || []).length;
|
|
231
|
-
const edges = ctx.edges || [];
|
|
232
|
-
if (edges.length > 0) {
|
|
233
|
-
const top = [...edges].sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))[0];
|
|
234
|
-
this.topEdge = `${(top.title || '').slice(0, 20)} +${Math.round(top.edge)}¢`;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
else {
|
|
238
|
-
this.isExplorer = false;
|
|
239
|
-
this.thesisId = (ctx.thesisId || '').slice(0, 8);
|
|
240
|
-
this.confidence = typeof ctx.confidence === 'number'
|
|
241
|
-
? Math.round(ctx.confidence * 100)
|
|
242
|
-
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
243
|
-
this.confidenceDelta = ctx.lastEvaluation?.confidenceDelta
|
|
244
|
-
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
245
|
-
: 0;
|
|
246
|
-
this.edgeCount = (ctx.edges || []).length;
|
|
247
|
-
const edges = ctx.edges || [];
|
|
248
|
-
if (edges.length > 0) {
|
|
249
|
-
const top = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0))[0];
|
|
250
|
-
const name = (top.market || top.marketTitle || top.marketId || '').slice(0, 20);
|
|
251
|
-
const edge = top.edge || top.edgeSize || 0;
|
|
252
|
-
this.topEdge = `${name} ${edge > 0 ? '+' : ''}${Math.round(edge)}\u00A2`;
|
|
253
|
-
}
|
|
254
|
-
}
|
|
255
|
-
if (positions && positions.length > 0) {
|
|
256
|
-
this.positionCount = positions.length;
|
|
257
|
-
this.pnlDollars = positions.reduce((sum, p) => sum + (p.unrealized_pnl || 0), 0) / 100;
|
|
258
|
-
}
|
|
259
|
-
this.cachedWidth = undefined;
|
|
260
|
-
this.cachedLines = undefined;
|
|
261
|
-
}
|
|
262
|
-
updateConfidence(newConf, delta) {
|
|
263
|
-
this.confidence = Math.round(newConf * 100);
|
|
264
|
-
this.confidenceDelta = Math.round(delta * 100);
|
|
265
|
-
this.cachedWidth = undefined;
|
|
266
|
-
this.cachedLines = undefined;
|
|
267
|
-
}
|
|
268
|
-
invalidate() {
|
|
269
|
-
this.cachedWidth = undefined;
|
|
270
|
-
this.cachedLines = undefined;
|
|
271
|
-
}
|
|
272
|
-
update() {
|
|
273
|
-
this.cachedWidth = undefined;
|
|
274
|
-
this.cachedLines = undefined;
|
|
275
|
-
}
|
|
276
|
-
render(width) {
|
|
277
|
-
if (this.cachedLines && this.cachedWidth === width)
|
|
278
|
-
return this.cachedLines;
|
|
279
|
-
this.cachedWidth = width;
|
|
280
|
-
// Line 1: thesis info (or explorer mode)
|
|
281
|
-
const sep = C.zinc600(' \u2502 ');
|
|
282
|
-
let line1Parts;
|
|
283
|
-
if (this.isExplorer) {
|
|
284
|
-
const id = C.emerald(bold('Explorer'));
|
|
285
|
-
const edges = C.zinc600(`${this.edgeCount} public edges`);
|
|
286
|
-
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
287
|
-
let pnl = '';
|
|
288
|
-
if (this.positionCount > 0) {
|
|
289
|
-
const pnlStr = this.pnlDollars >= 0
|
|
290
|
-
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
291
|
-
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
292
|
-
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
293
|
-
}
|
|
294
|
-
line1Parts = [id, pnl, edges, top].filter(Boolean);
|
|
295
|
-
}
|
|
296
|
-
else {
|
|
297
|
-
const id = C.emerald(this.thesisId);
|
|
298
|
-
const arrow = this.confidenceDelta > 0 ? '\u25B2' : this.confidenceDelta < 0 ? '\u25BC' : '\u2500';
|
|
299
|
-
const arrowColor = this.confidenceDelta > 0 ? C.emerald : this.confidenceDelta < 0 ? C.red : C.zinc600;
|
|
300
|
-
const deltaStr = this.confidenceDelta !== 0 ? ` (${this.confidenceDelta > 0 ? '+' : ''}${this.confidenceDelta})` : '';
|
|
301
|
-
const conf = arrowColor(`${arrow} ${this.confidence}%${deltaStr}`);
|
|
302
|
-
let pnl = '';
|
|
303
|
-
if (this.positionCount > 0) {
|
|
304
|
-
const pnlStr = this.pnlDollars >= 0
|
|
305
|
-
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
306
|
-
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
307
|
-
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
308
|
-
}
|
|
309
|
-
const edges = C.zinc600(`${this.edgeCount} edges`);
|
|
310
|
-
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
311
|
-
line1Parts = [id, conf, pnl, edges, top].filter(Boolean);
|
|
312
|
-
}
|
|
313
|
-
let line1 = C.bgZinc800(' ' + truncateToWidth(line1Parts.join(sep), width - 2, '') + ' ');
|
|
314
|
-
const l1vw = visibleWidth(line1);
|
|
315
|
-
if (l1vw < width)
|
|
316
|
-
line1 += C.bgZinc800(' '.repeat(width - l1vw));
|
|
317
|
-
// Line 2: model + exchange
|
|
318
|
-
const model = C.zinc600(this.modelName.split('/').pop() || this.modelName);
|
|
319
|
-
const tokStr = this.tokens >= 1000 ? `${(this.tokens / 1000).toFixed(1)}k` : `${this.tokens}`;
|
|
320
|
-
const tokens = C.zinc600(`${tokStr} tok`);
|
|
321
|
-
const exchange = this.exchangeOpen === true ? C.emerald('OPEN') : this.exchangeOpen === false ? C.red('CLOSED') : C.zinc600('...');
|
|
322
|
-
const trading = this.tradingEnabled ? C.amber('trading') : C.zinc600('read-only');
|
|
323
|
-
const help = C.zinc600('/help');
|
|
324
|
-
const leftText = [model, tokens, exchange, trading].join(sep);
|
|
325
|
-
const lw = visibleWidth(leftText);
|
|
326
|
-
const rw = visibleWidth(help);
|
|
327
|
-
const gap = Math.max(1, width - lw - rw - 2);
|
|
328
|
-
let line2 = C.bgZinc900(' ' + leftText + ' '.repeat(gap) + help + ' ');
|
|
329
|
-
const l2vw = visibleWidth(line2);
|
|
330
|
-
if (l2vw < width)
|
|
331
|
-
line2 += C.bgZinc900(' '.repeat(width - l2vw));
|
|
332
|
-
this.cachedLines = [line1, line2];
|
|
333
|
-
return this.cachedLines;
|
|
334
|
-
}
|
|
335
|
-
};
|
|
336
|
-
}
|
|
337
|
-
// ─── Formatted renderers ─────────────────────────────────────────────────────
|
|
338
|
-
function renderCausalTree(context, piTui) {
|
|
339
|
-
const tree = context.causalTree;
|
|
340
|
-
if (!tree?.nodes?.length)
|
|
341
|
-
return C.zinc600(' No causal tree data');
|
|
342
|
-
const lines = [];
|
|
343
|
-
for (const node of tree.nodes) {
|
|
344
|
-
const id = node.id || '';
|
|
345
|
-
const label = node.label || node.description || '';
|
|
346
|
-
const prob = typeof node.probability === 'number'
|
|
347
|
-
? Math.round(node.probability * 100)
|
|
348
|
-
: (typeof node.impliedProbability === 'number' ? Math.round(node.impliedProbability * 100) : null);
|
|
349
|
-
const depth = (id.match(/\./g) || []).length;
|
|
350
|
-
const indent = ' '.repeat(depth + 1);
|
|
351
|
-
if (prob !== null) {
|
|
352
|
-
// Progress bar: 10 chars
|
|
353
|
-
const filled = Math.round(prob / 10);
|
|
354
|
-
const bar = '\u2588'.repeat(filled) + '\u2591'.repeat(10 - filled);
|
|
355
|
-
const probColor = prob >= 70 ? C.emerald : prob >= 40 ? C.amber : C.red;
|
|
356
|
-
// Dots to pad between label and percentage
|
|
357
|
-
const labelPart = `${indent}${C.zinc600(id)} ${C.zinc400(label)} `;
|
|
358
|
-
const probPart = ` ${probColor(`${prob}%`)} ${probColor(bar)}`;
|
|
359
|
-
lines.push(labelPart + probPart);
|
|
360
|
-
}
|
|
361
|
-
else {
|
|
362
|
-
lines.push(`${indent}${C.zinc600(id)} ${C.zinc400(label)}`);
|
|
363
|
-
}
|
|
364
|
-
}
|
|
365
|
-
return lines.join('\n');
|
|
366
|
-
}
|
|
367
|
-
function renderEdges(context, piTui) {
|
|
368
|
-
const edges = context.edges;
|
|
369
|
-
if (!edges?.length)
|
|
370
|
-
return C.zinc600(' No edge data');
|
|
371
|
-
const positions = context._positions || [];
|
|
372
|
-
const lines = [];
|
|
373
|
-
for (const e of edges) {
|
|
374
|
-
// Context API field names: market, marketId, thesisPrice, edge, orderbook.spread, orderbook.liquidityScore
|
|
375
|
-
const name = (e.market || e.marketId || '').slice(0, 18).padEnd(18);
|
|
376
|
-
const marketStr = typeof e.marketPrice === 'number' ? `${e.marketPrice}\u00A2` : '?';
|
|
377
|
-
const thesisStr = typeof e.thesisPrice === 'number' ? `${e.thesisPrice}\u00A2` : '?';
|
|
378
|
-
const edgeVal = typeof e.edge === 'number' ? (e.edge > 0 ? `+${e.edge}` : `${e.edge}`) : '?';
|
|
379
|
-
const ob = e.orderbook || {};
|
|
380
|
-
const spreadStr = typeof ob.spread === 'number' ? `${ob.spread}\u00A2` : '?';
|
|
381
|
-
const liq = ob.liquidityScore || 'low';
|
|
382
|
-
const liqBars = liq === 'high' ? '\u25A0\u25A0\u25A0' : liq === 'medium' ? '\u25A0\u25A0 ' : '\u25A0 ';
|
|
383
|
-
const liqColor = liq === 'high' ? C.emerald : liq === 'medium' ? C.amber : C.red;
|
|
384
|
-
// Check if we have a position on this edge (match by marketId prefix in ticker)
|
|
385
|
-
const pos = positions.find((p) => p.ticker === e.marketId ||
|
|
386
|
-
(e.marketId && p.ticker?.includes(e.marketId)));
|
|
387
|
-
let posStr = C.zinc600('\u2014');
|
|
388
|
-
if (pos) {
|
|
389
|
-
const side = pos.side?.toUpperCase() || 'YES';
|
|
390
|
-
const pnl = typeof pos.unrealized_pnl === 'number'
|
|
391
|
-
? (pos.unrealized_pnl >= 0 ? C.emerald(`+$${(pos.unrealized_pnl / 100).toFixed(0)}`) : C.red(`-$${(Math.abs(pos.unrealized_pnl) / 100).toFixed(0)}`))
|
|
392
|
-
: '';
|
|
393
|
-
posStr = C.emerald(`${side} (${pos.quantity}@${pos.average_price_paid}\u00A2 ${pnl})`);
|
|
394
|
-
}
|
|
395
|
-
lines.push(` ${C.zinc200(name)} ${C.zinc400(marketStr)} \u2192 ${C.zinc400(thesisStr)} edge ${edgeVal.includes('+') ? C.emerald(edgeVal) : C.red(edgeVal)} spread ${C.zinc600(spreadStr)} ${liqColor(liqBars)} ${liqColor(liq.padEnd(4))} ${posStr}`);
|
|
396
|
-
}
|
|
397
|
-
return lines.join('\n');
|
|
398
|
-
}
|
|
399
|
-
function renderPositions(positions) {
|
|
400
|
-
if (!positions?.length)
|
|
401
|
-
return C.zinc600(' No positions');
|
|
402
|
-
const lines = [];
|
|
403
|
-
let totalPnl = 0;
|
|
404
|
-
for (const p of positions) {
|
|
405
|
-
const ticker = (p.ticker || '').slice(0, 18).padEnd(18);
|
|
406
|
-
const side = (p.side || 'yes').toUpperCase().padEnd(3);
|
|
407
|
-
const qty = String(p.quantity || 0);
|
|
408
|
-
const avg = `${p.average_price_paid || 0}\u00A2`;
|
|
409
|
-
const now = typeof p.current_value === 'number' && p.current_value > 0
|
|
410
|
-
? `${p.current_value}\u00A2`
|
|
411
|
-
: '?\u00A2';
|
|
412
|
-
const pnlCents = p.unrealized_pnl || 0;
|
|
413
|
-
totalPnl += pnlCents;
|
|
414
|
-
const pnlDollars = (pnlCents / 100).toFixed(2);
|
|
415
|
-
const pnlStr = pnlCents >= 0
|
|
416
|
-
? C.emerald(`+$${pnlDollars}`)
|
|
417
|
-
: C.red(`-$${Math.abs(parseFloat(pnlDollars)).toFixed(2)}`);
|
|
418
|
-
const arrow = pnlCents >= 0 ? C.emerald('\u25B2') : C.red('\u25BC');
|
|
419
|
-
lines.push(` ${C.zinc200(ticker)} ${C.zinc400(side)} ${C.zinc400(qty)} @ ${C.zinc400(avg)} now ${C.zinc200(now)} ${pnlStr} ${arrow}`);
|
|
420
|
-
}
|
|
421
|
-
const totalDollars = (totalPnl / 100).toFixed(2);
|
|
422
|
-
lines.push(C.zinc600(' ' + '\u2500'.repeat(40)));
|
|
423
|
-
lines.push(totalPnl >= 0
|
|
424
|
-
? ` Total P&L: ${C.emerald(bold(`+$${totalDollars}`))}`
|
|
425
|
-
: ` Total P&L: ${C.red(bold(`-$${Math.abs(parseFloat(totalDollars)).toFixed(2)}`))}`);
|
|
426
|
-
return lines.join('\n');
|
|
427
|
-
}
|
|
428
|
-
// ─── Thesis selector (arrow keys + enter, like Claude Code) ─────────────────
|
|
429
|
-
async function selectThesis(theses, includeExplorer = false) {
|
|
430
|
-
return new Promise((resolve) => {
|
|
431
|
-
let selected = 0;
|
|
432
|
-
const items = [];
|
|
433
|
-
if (includeExplorer) {
|
|
434
|
-
items.push({ id: '_explorer', conf: -1, title: 'Explorer mode — no thesis, full market access' });
|
|
435
|
-
}
|
|
436
|
-
for (const t of theses) {
|
|
437
|
-
const conf = typeof t.confidence === 'number' ? Math.round(t.confidence * 100) : 0;
|
|
438
|
-
const title = (t.rawThesis || t.thesis || t.title || '').slice(0, 55);
|
|
439
|
-
items.push({ id: t.id, conf, title });
|
|
440
|
-
}
|
|
441
|
-
const write = process.stdout.write.bind(process.stdout);
|
|
442
|
-
// Use alternate screen buffer for clean rendering (like Claude Code)
|
|
443
|
-
write('\x1b[?1049h'); // enter alternate screen
|
|
444
|
-
write('\x1b[?25l'); // hide cursor
|
|
445
|
-
function render() {
|
|
446
|
-
write('\x1b[H\x1b[2J'); // cursor home + clear screen
|
|
447
|
-
write('\n \x1b[2mSelect thesis\x1b[22m\n\n');
|
|
448
|
-
for (let i = 0; i < items.length; i++) {
|
|
449
|
-
const item = items[i];
|
|
450
|
-
const sel = i === selected;
|
|
451
|
-
const cursor = sel ? '\x1b[38;2;16;185;129m › \x1b[39m' : ' ';
|
|
452
|
-
if (item.id === '_explorer') {
|
|
453
|
-
const title = sel ? `\x1b[38;2;16;185;129m${item.title}\x1b[39m` : `\x1b[38;2;80;80;88m${item.title}\x1b[39m`;
|
|
454
|
-
write(`${cursor}${title}\n`);
|
|
455
|
-
}
|
|
456
|
-
else {
|
|
457
|
-
const id = sel ? `\x1b[38;2;16;185;129m${item.id.slice(0, 8)}\x1b[39m` : `\x1b[38;2;55;55;60m${item.id.slice(0, 8)}\x1b[39m`;
|
|
458
|
-
const conf = `\x1b[38;2;55;55;60m${item.conf}%\x1b[39m`;
|
|
459
|
-
const title = sel ? `\x1b[38;2;160;160;165m${item.title}\x1b[39m` : `\x1b[38;2;80;80;88m${item.title}\x1b[39m`;
|
|
460
|
-
write(`${cursor}${id} ${conf} ${title}\n`);
|
|
461
|
-
}
|
|
462
|
-
}
|
|
463
|
-
write(`\n \x1b[38;2;55;55;60m↑↓ navigate · enter select\x1b[39m`);
|
|
464
|
-
}
|
|
465
|
-
render();
|
|
466
|
-
if (process.stdin.isTTY)
|
|
467
|
-
process.stdin.setRawMode(true);
|
|
468
|
-
process.stdin.resume();
|
|
469
|
-
process.stdin.setEncoding('utf8');
|
|
470
|
-
const onKey = (key) => {
|
|
471
|
-
const buf = Buffer.from(key);
|
|
472
|
-
if (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x41) {
|
|
473
|
-
selected = (selected - 1 + items.length) % items.length;
|
|
474
|
-
render();
|
|
475
|
-
}
|
|
476
|
-
else if (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x42) {
|
|
477
|
-
selected = (selected + 1) % items.length;
|
|
478
|
-
render();
|
|
479
|
-
}
|
|
480
|
-
else if (key === 'k') {
|
|
481
|
-
selected = (selected - 1 + items.length) % items.length;
|
|
482
|
-
render();
|
|
483
|
-
}
|
|
484
|
-
else if (key === 'j') {
|
|
485
|
-
selected = (selected + 1) % items.length;
|
|
486
|
-
render();
|
|
487
|
-
}
|
|
488
|
-
else if (key === '\r' || key === '\n') {
|
|
489
|
-
cleanup();
|
|
490
|
-
resolve(items[selected].id);
|
|
491
|
-
}
|
|
492
|
-
else if (buf[0] === 0x03) {
|
|
493
|
-
cleanup();
|
|
494
|
-
process.exit(0);
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
function cleanup() {
|
|
498
|
-
process.stdin.removeListener('data', onKey);
|
|
499
|
-
if (process.stdin.isTTY)
|
|
500
|
-
process.stdin.setRawMode(false);
|
|
501
|
-
process.stdin.pause();
|
|
502
|
-
write('\x1b[?25h'); // show cursor
|
|
503
|
-
write('\x1b[?1049l'); // exit alternate screen — restores original content
|
|
504
|
-
}
|
|
505
|
-
process.stdin.on('data', onKey);
|
|
506
|
-
});
|
|
507
|
-
}
|
|
508
|
-
// ─── Main command ────────────────────────────────────────────────────────────
|
|
509
|
-
async function agentCommand(thesisId, opts) {
|
|
510
|
-
// ── Validate API keys ──────────────────────────────────────────────────────
|
|
511
|
-
const directOrKey = opts?.modelKey || process.env.OPENROUTER_API_KEY;
|
|
512
|
-
const sfApiKey = process.env.SF_API_KEY;
|
|
513
|
-
const sfApiUrl = process.env.SF_API_URL || 'https://simplefunctions.dev';
|
|
514
|
-
// Proxy mode: no local OpenRouter key, but have SF API key → route through server
|
|
515
|
-
const useProxy = !directOrKey && !!sfApiKey;
|
|
516
|
-
const openrouterKey = directOrKey || sfApiKey; // SF key used as Bearer for proxy
|
|
517
|
-
const llmBaseUrl = useProxy ? `${sfApiUrl}/api/proxy` : 'https://openrouter.ai/api/v1';
|
|
518
|
-
if (!openrouterKey) {
|
|
519
|
-
console.error('Need an API key to power the agent LLM.');
|
|
520
|
-
console.error('');
|
|
521
|
-
console.error(' Option 1 (recommended): sf login');
|
|
522
|
-
console.error(' Option 2: Get an OpenRouter key at https://openrouter.ai/keys');
|
|
523
|
-
console.error(' export OPENROUTER_API_KEY=sk-or-v1-...');
|
|
524
|
-
console.error(' sf agent --model-key sk-or-v1-...');
|
|
525
|
-
console.error(' sf setup (saves to ~/.sf/config.json)');
|
|
526
|
-
process.exit(1);
|
|
527
|
-
}
|
|
528
|
-
// Pre-flight: validate key (skip for proxy mode — server validates)
|
|
529
|
-
if (!useProxy) {
|
|
530
|
-
try {
|
|
531
|
-
const checkRes = await fetch('https://openrouter.ai/api/v1/auth/key', {
|
|
532
|
-
headers: { 'Authorization': `Bearer ${openrouterKey}` },
|
|
533
|
-
signal: AbortSignal.timeout(8000),
|
|
534
|
-
});
|
|
535
|
-
if (!checkRes.ok) {
|
|
536
|
-
console.error('OpenRouter API key is invalid or expired.');
|
|
537
|
-
console.error('Get a new key at https://openrouter.ai/keys');
|
|
538
|
-
process.exit(1);
|
|
539
|
-
}
|
|
540
|
-
}
|
|
541
|
-
catch (err) {
|
|
542
|
-
const msg = err instanceof Error ? err.message : String(err);
|
|
543
|
-
if (!msg.includes('timeout')) {
|
|
544
|
-
console.warn(`Warning: Could not verify OpenRouter key (${msg}). Continuing anyway.`);
|
|
545
|
-
}
|
|
546
|
-
}
|
|
547
|
-
}
|
|
548
|
-
else {
|
|
549
|
-
console.log(' \x1b[2mUsing SimpleFunctions LLM proxy (no OpenRouter key needed)\x1b[22m');
|
|
550
|
-
}
|
|
551
|
-
const sfClient = new client_js_1.SFClient();
|
|
552
|
-
// ── Resolve thesis ID (interactive selection if needed) ─────────────────────
|
|
553
|
-
let resolvedThesisId = thesisId || null;
|
|
554
|
-
let explorerMode = false;
|
|
555
|
-
if (!resolvedThesisId) {
|
|
556
|
-
let active = [];
|
|
557
|
-
try {
|
|
558
|
-
const data = await sfClient.listTheses();
|
|
559
|
-
const theses = (data.theses || data);
|
|
560
|
-
active = theses.filter((t) => t.status === 'active');
|
|
561
|
-
}
|
|
562
|
-
catch {
|
|
563
|
-
// No API key or network error — explorer mode
|
|
564
|
-
active = [];
|
|
565
|
-
}
|
|
566
|
-
if (active.length === 0) {
|
|
567
|
-
// No theses — go straight to explorer mode
|
|
568
|
-
explorerMode = true;
|
|
569
|
-
}
|
|
570
|
-
else if (active.length === 1) {
|
|
571
|
-
resolvedThesisId = active[0].id;
|
|
572
|
-
}
|
|
573
|
-
else if (process.stdin.isTTY && !opts?.noTui) {
|
|
574
|
-
// Multiple theses — interactive selector with explorer option at top
|
|
575
|
-
const selected = await selectThesis(active, true);
|
|
576
|
-
if (selected === '_explorer') {
|
|
577
|
-
explorerMode = true;
|
|
578
|
-
}
|
|
579
|
-
else {
|
|
580
|
-
resolvedThesisId = selected;
|
|
581
|
-
}
|
|
582
|
-
}
|
|
583
|
-
else {
|
|
584
|
-
// Non-interactive (--plain, telegram, piped) — use first active
|
|
585
|
-
resolvedThesisId = active[0].id;
|
|
586
|
-
}
|
|
587
|
-
}
|
|
588
|
-
// ── Fetch initial context ──────────────────────────────────────────────────
|
|
589
|
-
let latestContext;
|
|
590
|
-
if (explorerMode) {
|
|
591
|
-
const { fetchGlobalContext } = await import('../client.js');
|
|
592
|
-
latestContext = await fetchGlobalContext();
|
|
593
|
-
latestContext._explorerMode = true;
|
|
594
|
-
}
|
|
595
|
-
else {
|
|
596
|
-
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
597
|
-
}
|
|
598
|
-
// ── Branch: plain-text mode ────────────────────────────────────────────────
|
|
599
|
-
if (opts?.noTui) {
|
|
600
|
-
return runPlainTextAgent({ openrouterKey, sfClient, resolvedThesisId: resolvedThesisId || '_explorer', latestContext, useProxy, llmBaseUrl, sfApiKey, sfApiUrl, opts });
|
|
601
|
-
}
|
|
602
|
-
// ── Dynamic imports (all ESM-only packages) ────────────────────────────────
|
|
603
|
-
const piTui = await import('@mariozechner/pi-tui');
|
|
604
|
-
const piAi = await import('@mariozechner/pi-ai');
|
|
605
|
-
const piAgent = await import('@mariozechner/pi-agent-core');
|
|
606
|
-
const { TUI, ProcessTerminal, Container, Text, Markdown, Editor, Loader, Spacer, CombinedAutocompleteProvider, truncateToWidth, visibleWidth, } = piTui;
|
|
607
|
-
const { getModel, streamSimple, Type } = piAi;
|
|
608
|
-
const { Agent } = piAgent;
|
|
609
|
-
// ── Component class factories (need piTui ref) ─────────────────────────────
|
|
610
|
-
const MutableLine = createMutableLine(piTui);
|
|
611
|
-
const FooterBar = createFooterBar(piTui);
|
|
612
|
-
// ── Model setup ────────────────────────────────────────────────────────────
|
|
613
|
-
const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
|
|
614
|
-
let currentModelName = rawModelName.replace(/^openrouter\//, '');
|
|
615
|
-
function resolveModel(name) {
|
|
616
|
-
let m;
|
|
617
|
-
try {
|
|
618
|
-
m = getModel('openrouter', name);
|
|
619
|
-
}
|
|
620
|
-
catch {
|
|
621
|
-
m = {
|
|
622
|
-
modelId: name,
|
|
623
|
-
provider: 'openrouter',
|
|
624
|
-
api: 'openai-completions',
|
|
625
|
-
baseUrl: 'https://openrouter.ai/api/v1',
|
|
626
|
-
id: name,
|
|
627
|
-
name: name,
|
|
628
|
-
inputPrice: 0,
|
|
629
|
-
outputPrice: 0,
|
|
630
|
-
contextWindow: 200000,
|
|
631
|
-
supportsImages: true,
|
|
632
|
-
supportsTools: true,
|
|
633
|
-
};
|
|
634
|
-
}
|
|
635
|
-
// Override baseUrl in proxy mode
|
|
636
|
-
if (useProxy)
|
|
637
|
-
m.baseUrl = llmBaseUrl;
|
|
638
|
-
return m;
|
|
639
|
-
}
|
|
640
|
-
let model = resolveModel(currentModelName);
|
|
641
|
-
// ── Tracking state ─────────────────────────────────────────────────────────
|
|
642
|
-
let totalTokens = 0;
|
|
643
|
-
let totalCost = 0;
|
|
644
|
-
let totalToolCalls = 0;
|
|
645
|
-
let isProcessing = false;
|
|
646
|
-
// Cache for positions (fetched by /pos or get_positions tool)
|
|
647
|
-
let cachedPositions = null;
|
|
648
|
-
// ── Heartbeat polling state ───────────────────────────────────────────────
|
|
649
|
-
// Background poll delta endpoint every 60s.
|
|
650
|
-
// If confidence changed ≥ 3%, auto-trigger agent analysis.
|
|
651
|
-
// If agent is busy (isProcessing), queue and deliver after agent finishes.
|
|
652
|
-
let lastPollTimestamp = new Date().toISOString();
|
|
653
|
-
let pendingHeartbeatDelta = null; // queued delta when agent is busy
|
|
654
|
-
let heartbeatPollTimer = null;
|
|
655
|
-
// ── Inline confirmation mechanism ─────────────────────────────────────────
|
|
656
|
-
// Tools can call promptUser() during execution to ask the user a question.
|
|
657
|
-
// This temporarily unlocks the editor, waits for input, then resumes.
|
|
658
|
-
let pendingPrompt = null;
|
|
659
|
-
// ── Setup TUI ──────────────────────────────────────────────────────────────
|
|
660
|
-
const terminal = new ProcessTerminal();
|
|
661
|
-
const tui = new TUI(terminal);
|
|
662
|
-
// Markdown theme for assistant messages
|
|
663
|
-
const mdTheme = {
|
|
664
|
-
heading: (s) => C.zinc200(bold(s)),
|
|
665
|
-
link: (s) => C.emerald(s),
|
|
666
|
-
linkUrl: (s) => C.zinc600(s),
|
|
667
|
-
code: (s) => C.zinc200(s),
|
|
668
|
-
codeBlock: (s) => C.zinc400(s),
|
|
669
|
-
codeBlockBorder: (s) => C.zinc600(s),
|
|
670
|
-
quote: (s) => C.zinc400(s),
|
|
671
|
-
quoteBorder: (s) => C.zinc600(s),
|
|
672
|
-
hr: (s) => C.zinc600(s),
|
|
673
|
-
listBullet: (s) => C.emerald(s),
|
|
674
|
-
bold: (s) => bold(s),
|
|
675
|
-
italic: (s) => italic(s),
|
|
676
|
-
strikethrough: (s) => strikethrough(s),
|
|
677
|
-
underline: (s) => underline(s),
|
|
678
|
-
};
|
|
679
|
-
const mdDefaultStyle = {
|
|
680
|
-
color: (s) => C.zinc400(s),
|
|
681
|
-
};
|
|
682
|
-
// Editor theme — use dim zinc borders instead of default green
|
|
683
|
-
const editorTheme = {
|
|
684
|
-
borderColor: (s) => `\x1b[38;2;50;50;55m${s}\x1b[39m`,
|
|
685
|
-
selectList: {
|
|
686
|
-
selectedPrefix: (s) => C.emerald(s),
|
|
687
|
-
selectedText: (s) => C.zinc200(s),
|
|
688
|
-
description: (s) => C.zinc600(s),
|
|
689
|
-
scrollInfo: (s) => C.zinc600(s),
|
|
690
|
-
noMatch: (s) => C.zinc600(s),
|
|
691
|
-
},
|
|
692
|
-
};
|
|
693
|
-
// ── Build components ───────────────────────────────────────────────────────
|
|
694
|
-
// No header bar — all info in footer (2 lines)
|
|
695
|
-
const footerBar = new FooterBar();
|
|
696
|
-
footerBar.modelName = currentModelName;
|
|
697
|
-
footerBar.tradingEnabled = (0, config_js_1.loadConfig)().tradingEnabled || false;
|
|
698
|
-
// Fetch positions for footer P&L (non-blocking, best-effort)
|
|
699
|
-
let initialPositions = null;
|
|
700
|
-
try {
|
|
701
|
-
initialPositions = await (0, kalshi_js_1.getPositions)();
|
|
702
|
-
if (initialPositions) {
|
|
703
|
-
for (const pos of initialPositions) {
|
|
704
|
-
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
705
|
-
if (livePrice !== null) {
|
|
706
|
-
pos.current_value = livePrice;
|
|
707
|
-
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
708
|
-
}
|
|
709
|
-
}
|
|
710
|
-
}
|
|
711
|
-
}
|
|
712
|
-
catch { /* positions not available, fine */ }
|
|
713
|
-
footerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
714
|
-
// Fetch exchange status for footer (non-blocking)
|
|
715
|
-
fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } })
|
|
716
|
-
.then(r => r.json())
|
|
717
|
-
.then(d => { footerBar.exchangeOpen = !!d.exchange_active; footerBar.update(); tui.requestRender(); })
|
|
718
|
-
.catch(() => { });
|
|
719
|
-
const topSpacer = new Spacer(1);
|
|
720
|
-
const bottomSpacer = new Spacer(1);
|
|
721
|
-
const chatContainer = new Container();
|
|
722
|
-
const editor = new Editor(tui, editorTheme, { paddingX: 1 });
|
|
723
|
-
// Slash command autocomplete
|
|
724
|
-
const slashCommands = [
|
|
725
|
-
{ name: 'help', description: 'Show available commands' },
|
|
726
|
-
{ name: 'tree', description: 'Display causal tree' },
|
|
727
|
-
{ name: 'edges', description: 'Display edge/spread table' },
|
|
728
|
-
{ name: 'pos', description: 'Display Kalshi positions' },
|
|
729
|
-
{ name: 'eval', description: 'Trigger deep evaluation' },
|
|
730
|
-
{ name: 'switch', description: 'Switch thesis (e.g. /switch f582bf76)' },
|
|
731
|
-
{ name: 'compact', description: 'Compress conversation history' },
|
|
732
|
-
{ name: 'new', description: 'Start fresh session' },
|
|
733
|
-
{ name: 'model', description: 'Switch model (e.g. /model anthropic/claude-sonnet-4)' },
|
|
734
|
-
{ name: 'env', description: 'Show environment variable status' },
|
|
735
|
-
{ name: 'clear', description: 'Clear screen (keeps history)' },
|
|
736
|
-
{ name: 'exit', description: 'Exit agent (auto-saves)' },
|
|
737
|
-
];
|
|
738
|
-
// Add trading commands if enabled
|
|
739
|
-
if ((0, config_js_1.loadConfig)().tradingEnabled) {
|
|
740
|
-
slashCommands.splice(-2, 0, // insert before /clear and /exit
|
|
741
|
-
{ name: 'buy', description: 'TICKER QTY PRICE — quick buy' }, { name: 'sell', description: 'TICKER QTY PRICE — quick sell' }, { name: 'cancel', description: 'ORDER_ID — cancel order' });
|
|
742
|
-
}
|
|
743
|
-
// Load skills and register as slash commands
|
|
744
|
-
const skills = (0, loader_js_1.loadSkills)();
|
|
745
|
-
for (const skill of skills) {
|
|
746
|
-
const trigger = skill.trigger.replace(/^\//, ''); // remove leading /
|
|
747
|
-
slashCommands.splice(-2, 0, { name: trigger, description: `[skill] ${skill.description.slice(0, 50)}` });
|
|
748
|
-
}
|
|
749
|
-
const autocompleteProvider = new CombinedAutocompleteProvider(slashCommands, process.cwd());
|
|
750
|
-
editor.setAutocompleteProvider(autocompleteProvider);
|
|
751
|
-
// Assemble TUI tree
|
|
752
|
-
tui.addChild(topSpacer);
|
|
753
|
-
tui.addChild(chatContainer);
|
|
754
|
-
tui.addChild(editor);
|
|
755
|
-
tui.addChild(bottomSpacer);
|
|
756
|
-
// Focus on editor
|
|
757
|
-
tui.setFocus(editor);
|
|
758
|
-
// ── Footer overlay (2-line: thesis info + model/exchange) ──────────────────
|
|
759
|
-
const footerOverlay = tui.showOverlay(footerBar, {
|
|
760
|
-
anchor: 'bottom-left',
|
|
761
|
-
width: '100%',
|
|
762
|
-
nonCapturing: true,
|
|
763
|
-
});
|
|
764
|
-
// ── Helper: add system text to chat ────────────────────────────────────────
|
|
765
|
-
function addSystemText(content) {
|
|
766
|
-
const text = new Text(content, 1, 0);
|
|
767
|
-
chatContainer.addChild(text);
|
|
768
|
-
tui.requestRender();
|
|
769
|
-
}
|
|
770
|
-
function addSpacer() {
|
|
771
|
-
chatContainer.addChild(new Spacer(1));
|
|
772
|
-
}
|
|
773
|
-
/**
|
|
774
|
-
* Ask the user a question during tool execution.
|
|
775
|
-
* Temporarily unlocks the editor, waits for input, then resumes.
|
|
776
|
-
* Used for order confirmations and other dangerous operations.
|
|
777
|
-
*/
|
|
778
|
-
function promptUser(question) {
|
|
779
|
-
return new Promise(resolve => {
|
|
780
|
-
addSystemText(C.amber(bold('\u26A0 ')) + C.zinc200(question));
|
|
781
|
-
addSpacer();
|
|
782
|
-
tui.requestRender();
|
|
783
|
-
pendingPrompt = { resolve };
|
|
784
|
-
});
|
|
785
|
-
}
|
|
786
|
-
// ── Define agent tools (same as before) ────────────────────────────────────
|
|
787
|
-
const thesisIdParam = Type.Object({
|
|
788
|
-
thesisId: Type.String({ description: 'Thesis ID (short or full UUID)' }),
|
|
789
|
-
});
|
|
790
|
-
const signalParams = Type.Object({
|
|
791
|
-
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
792
|
-
content: Type.String({ description: 'Signal content' }),
|
|
793
|
-
type: Type.Optional(Type.String({ description: 'Signal type: news, user_note, external. Default: user_note' })),
|
|
794
|
-
});
|
|
795
|
-
const scanParams = Type.Object({
|
|
796
|
-
query: Type.Optional(Type.String({ description: 'Keyword search for Kalshi markets' })),
|
|
797
|
-
series: Type.Optional(Type.String({ description: 'Kalshi series ticker (e.g. KXWTIMAX)' })),
|
|
798
|
-
market: Type.Optional(Type.String({ description: 'Specific market ticker' })),
|
|
799
|
-
});
|
|
800
|
-
const webSearchParams = Type.Object({
|
|
801
|
-
query: Type.String({ description: 'Search keywords' }),
|
|
802
|
-
});
|
|
803
|
-
const emptyParams = Type.Object({});
|
|
804
|
-
const tools = [
|
|
805
|
-
{
|
|
806
|
-
name: 'get_context',
|
|
807
|
-
label: 'Get Context',
|
|
808
|
-
description: 'Get thesis snapshot: causal tree, edge prices, last evaluation, confidence',
|
|
809
|
-
parameters: thesisIdParam,
|
|
810
|
-
execute: async (_toolCallId, params) => {
|
|
811
|
-
const ctx = await sfClient.getContext(params.thesisId);
|
|
812
|
-
latestContext = ctx;
|
|
813
|
-
footerBar.setFromContext(ctx, initialPositions || undefined);
|
|
814
|
-
tui.requestRender();
|
|
815
|
-
return {
|
|
816
|
-
content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
|
|
817
|
-
details: {},
|
|
818
|
-
};
|
|
819
|
-
},
|
|
820
|
-
},
|
|
821
|
-
{
|
|
822
|
-
name: 'global_context',
|
|
823
|
-
label: 'Market Snapshot',
|
|
824
|
-
description: 'Global market snapshot — top movers, expiring contracts, milestones, liquidity, signals. No thesis needed.',
|
|
825
|
-
parameters: emptyParams,
|
|
826
|
-
execute: async () => {
|
|
827
|
-
const { fetchGlobalContext } = await import('../client.js');
|
|
828
|
-
const data = await fetchGlobalContext();
|
|
829
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
830
|
-
},
|
|
831
|
-
},
|
|
832
|
-
{
|
|
833
|
-
name: 'query',
|
|
834
|
-
label: 'Query',
|
|
835
|
-
description: 'LLM-enhanced prediction market knowledge search. Ask any question about prediction markets, macro, geopolitics. Returns structured answer with live market prices, thesis data, and key factors.',
|
|
836
|
-
parameters: Type.Object({
|
|
837
|
-
q: Type.String({ description: 'Natural language query (e.g. "iran oil prices", "fed rate cut 2026")' }),
|
|
838
|
-
}),
|
|
839
|
-
execute: async (_toolCallId, params) => {
|
|
840
|
-
const { fetchQuery } = await import('../client.js');
|
|
841
|
-
const data = await fetchQuery(params.q);
|
|
842
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
843
|
-
},
|
|
844
|
-
},
|
|
845
|
-
{
|
|
846
|
-
name: 'get_markets',
|
|
847
|
-
label: 'Traditional Markets',
|
|
848
|
-
description: 'Get traditional market prices via Databento: S&P 500 (SPY), VIX (VIXY), 20Y Treasury (TLT), Gold (GLD), Oil (USO). Daily close + 1-day change.',
|
|
849
|
-
parameters: emptyParams,
|
|
850
|
-
execute: async () => {
|
|
851
|
-
const { fetchTraditionalMarkets } = await import('../client.js');
|
|
852
|
-
const data = await fetchTraditionalMarkets();
|
|
853
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
854
|
-
},
|
|
855
|
-
},
|
|
856
|
-
{
|
|
857
|
-
name: 'inject_signal',
|
|
858
|
-
label: 'Inject Signal',
|
|
859
|
-
description: 'Inject a signal into the thesis (news, note, external event)',
|
|
860
|
-
parameters: signalParams,
|
|
861
|
-
execute: async (_toolCallId, params) => {
|
|
862
|
-
const result = await sfClient.injectSignal(params.thesisId, params.type || 'user_note', params.content);
|
|
863
|
-
return {
|
|
864
|
-
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
865
|
-
details: {},
|
|
866
|
-
};
|
|
867
|
-
},
|
|
868
|
-
},
|
|
869
|
-
{
|
|
870
|
-
name: 'trigger_evaluation',
|
|
871
|
-
label: 'Evaluate',
|
|
872
|
-
description: 'Trigger a deep evaluation cycle (heavy model, takes longer)',
|
|
873
|
-
parameters: thesisIdParam,
|
|
874
|
-
execute: async (_toolCallId, params) => {
|
|
875
|
-
const result = await sfClient.evaluate(params.thesisId);
|
|
876
|
-
// Show confidence change prominently
|
|
877
|
-
if (result.evaluation?.confidenceDelta && Math.abs(result.evaluation.confidenceDelta) >= 0.01) {
|
|
878
|
-
const delta = result.evaluation.confidenceDelta;
|
|
879
|
-
const prev = Math.round((result.evaluation.previousConfidence || 0) * 100);
|
|
880
|
-
const now = Math.round((result.evaluation.newConfidence || 0) * 100);
|
|
881
|
-
const arrow = delta > 0 ? '\u25B2' : '\u25BC';
|
|
882
|
-
const color = delta > 0 ? C.emerald : C.red;
|
|
883
|
-
addSystemText(color(` ${arrow} Confidence ${prev}% \u2192 ${now}% (${delta > 0 ? '+' : ''}${Math.round(delta * 100)})`));
|
|
884
|
-
addSpacer();
|
|
885
|
-
// Update header
|
|
886
|
-
footerBar.updateConfidence(result.evaluation.newConfidence, delta);
|
|
887
|
-
tui.requestRender();
|
|
888
|
-
}
|
|
889
|
-
// Refresh context after eval
|
|
890
|
-
try {
|
|
891
|
-
latestContext = await sfClient.getContext(params.thesisId);
|
|
892
|
-
footerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
893
|
-
tui.requestRender();
|
|
894
|
-
}
|
|
895
|
-
catch { }
|
|
896
|
-
return {
|
|
897
|
-
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
898
|
-
details: {},
|
|
899
|
-
};
|
|
900
|
-
},
|
|
901
|
-
},
|
|
902
|
-
{
|
|
903
|
-
name: 'scan_markets',
|
|
904
|
-
label: 'Scan Markets',
|
|
905
|
-
description: 'Search Kalshi + Polymarket prediction markets. Provide exactly one of: query (keyword search), series (Kalshi series ticker), or market (specific Kalshi ticker). Keyword search returns results from BOTH venues.',
|
|
906
|
-
parameters: scanParams,
|
|
907
|
-
execute: async (_toolCallId, params) => {
|
|
908
|
-
if (params.market) {
|
|
909
|
-
const result = await (0, client_js_1.kalshiFetchMarket)(params.market);
|
|
910
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
911
|
-
}
|
|
912
|
-
if (params.series) {
|
|
913
|
-
const result = await (0, client_js_1.kalshiFetchMarketsBySeries)(params.series);
|
|
914
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
915
|
-
}
|
|
916
|
-
if (params.query) {
|
|
917
|
-
// Kalshi: keyword grep on series
|
|
918
|
-
const series = await (0, client_js_1.kalshiFetchAllSeries)();
|
|
919
|
-
const keywords = params.query.toLowerCase().split(/\s+/);
|
|
920
|
-
const kalshiMatched = series
|
|
921
|
-
.filter((s) => keywords.some((kw) => (s.title || '').toLowerCase().includes(kw) ||
|
|
922
|
-
(s.ticker || '').toLowerCase().includes(kw)))
|
|
923
|
-
.filter((s) => parseFloat(s.volume_24h_fp || s.volume_fp || '0') > 0)
|
|
924
|
-
.sort((a, b) => parseFloat(b.volume_24h_fp || b.volume_fp || '0') - parseFloat(a.volume_24h_fp || a.volume_fp || '0'))
|
|
925
|
-
.slice(0, 10)
|
|
926
|
-
.map((s) => ({ venue: 'kalshi', ticker: s.ticker, title: s.title, volume: s.volume_fp }));
|
|
927
|
-
// Polymarket: Gamma API search
|
|
928
|
-
let polyMatched = [];
|
|
929
|
-
try {
|
|
930
|
-
const events = await (0, polymarket_js_1.polymarketSearch)(params.query, 10);
|
|
931
|
-
for (const event of events) {
|
|
932
|
-
for (const m of (event.markets || []).slice(0, 3)) {
|
|
933
|
-
if (!m.active || m.closed)
|
|
934
|
-
continue;
|
|
935
|
-
const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
|
|
936
|
-
polyMatched.push({
|
|
937
|
-
venue: 'polymarket',
|
|
938
|
-
id: m.conditionId || m.id,
|
|
939
|
-
title: m.groupItemTitle ? `${event.title}: ${m.groupItemTitle}` : m.question || event.title,
|
|
940
|
-
price: prices[0] ? Math.round(prices[0] * 100) : null,
|
|
941
|
-
volume24h: m.volume24hr,
|
|
942
|
-
liquidity: m.liquidityNum,
|
|
943
|
-
});
|
|
944
|
-
}
|
|
945
|
-
}
|
|
946
|
-
}
|
|
947
|
-
catch { /* Polymarket search optional */ }
|
|
948
|
-
return {
|
|
949
|
-
content: [{ type: 'text', text: JSON.stringify({ kalshi: kalshiMatched, polymarket: polyMatched }, null, 2) }],
|
|
950
|
-
details: {},
|
|
951
|
-
};
|
|
952
|
-
}
|
|
953
|
-
return { content: [{ type: 'text', text: '{"error":"Provide query, series, or market parameter"}' }], details: {} };
|
|
954
|
-
},
|
|
955
|
-
},
|
|
956
|
-
{
|
|
957
|
-
name: 'list_theses',
|
|
958
|
-
label: 'List Theses',
|
|
959
|
-
description: 'List all theses for the current user',
|
|
960
|
-
parameters: emptyParams,
|
|
961
|
-
execute: async () => {
|
|
962
|
-
const theses = await sfClient.listTheses();
|
|
963
|
-
return {
|
|
964
|
-
content: [{ type: 'text', text: JSON.stringify(theses, null, 2) }],
|
|
965
|
-
details: {},
|
|
966
|
-
};
|
|
967
|
-
},
|
|
968
|
-
},
|
|
969
|
-
{
|
|
970
|
-
name: 'get_positions',
|
|
971
|
-
label: 'Get Positions',
|
|
972
|
-
description: 'Get positions across Kalshi + Polymarket with live prices and PnL',
|
|
973
|
-
parameters: emptyParams,
|
|
974
|
-
execute: async () => {
|
|
975
|
-
const result = { kalshi: [], polymarket: [] };
|
|
976
|
-
// Kalshi positions
|
|
977
|
-
const positions = await (0, kalshi_js_1.getPositions)();
|
|
978
|
-
if (positions) {
|
|
979
|
-
for (const pos of positions) {
|
|
980
|
-
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
981
|
-
if (livePrice !== null) {
|
|
982
|
-
pos.current_value = livePrice;
|
|
983
|
-
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
984
|
-
}
|
|
985
|
-
}
|
|
986
|
-
cachedPositions = positions;
|
|
987
|
-
result.kalshi = positions.map((p) => ({
|
|
988
|
-
venue: 'kalshi',
|
|
989
|
-
ticker: p.ticker,
|
|
990
|
-
side: p.side,
|
|
991
|
-
quantity: p.quantity,
|
|
992
|
-
avg_price: `${p.average_price_paid}¢`,
|
|
993
|
-
current_price: `${p.current_value}¢`,
|
|
994
|
-
unrealized_pnl: `$${(p.unrealized_pnl / 100).toFixed(2)}`,
|
|
995
|
-
total_cost: `$${(p.total_cost / 100).toFixed(2)}`,
|
|
996
|
-
}));
|
|
997
|
-
}
|
|
998
|
-
// Polymarket positions
|
|
999
|
-
const config = (0, config_js_1.loadConfig)();
|
|
1000
|
-
if (config.polymarketWalletAddress) {
|
|
1001
|
-
try {
|
|
1002
|
-
const polyPos = await (0, polymarket_js_1.polymarketGetPositions)(config.polymarketWalletAddress);
|
|
1003
|
-
result.polymarket = polyPos.map((p) => ({
|
|
1004
|
-
venue: 'polymarket',
|
|
1005
|
-
market: p.title || p.slug || p.asset,
|
|
1006
|
-
side: p.outcome || 'Yes',
|
|
1007
|
-
size: p.size,
|
|
1008
|
-
avg_price: `${Math.round((p.avgPrice || 0) * 100)}¢`,
|
|
1009
|
-
current_price: `${Math.round((p.curPrice || p.currentPrice || 0) * 100)}¢`,
|
|
1010
|
-
pnl: `$${(p.cashPnl || 0).toFixed(2)}`,
|
|
1011
|
-
}));
|
|
1012
|
-
}
|
|
1013
|
-
catch { /* skip */ }
|
|
1014
|
-
}
|
|
1015
|
-
if (result.kalshi.length === 0 && result.polymarket.length === 0) {
|
|
1016
|
-
return {
|
|
1017
|
-
content: [{ type: 'text', text: 'No positions found. Configure Kalshi (KALSHI_API_KEY_ID) or Polymarket (sf setup --polymarket) to see positions.' }],
|
|
1018
|
-
details: {},
|
|
1019
|
-
};
|
|
1020
|
-
}
|
|
1021
|
-
return {
|
|
1022
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
1023
|
-
details: {},
|
|
1024
|
-
};
|
|
1025
|
-
},
|
|
1026
|
-
},
|
|
1027
|
-
{
|
|
1028
|
-
name: 'web_search',
|
|
1029
|
-
label: 'Web Search',
|
|
1030
|
-
description: 'Search latest news and information. Use for real-time info not yet covered by the causal tree or heartbeat engine.',
|
|
1031
|
-
parameters: webSearchParams,
|
|
1032
|
-
execute: async (_toolCallId, params) => {
|
|
1033
|
-
const tavilyKey = process.env.TAVILY_API_KEY;
|
|
1034
|
-
const canProxy = !tavilyKey && sfApiKey;
|
|
1035
|
-
if (!tavilyKey && !canProxy) {
|
|
1036
|
-
return {
|
|
1037
|
-
content: [{ type: 'text', text: 'Web search not available. Run sf login (proxied search) or set TAVILY_API_KEY.' }],
|
|
1038
|
-
details: {},
|
|
1039
|
-
};
|
|
1040
|
-
}
|
|
1041
|
-
let res;
|
|
1042
|
-
if (tavilyKey) {
|
|
1043
|
-
// Direct mode
|
|
1044
|
-
res = await fetch('https://api.tavily.com/search', {
|
|
1045
|
-
method: 'POST',
|
|
1046
|
-
headers: { 'Content-Type': 'application/json' },
|
|
1047
|
-
body: JSON.stringify({
|
|
1048
|
-
api_key: tavilyKey,
|
|
1049
|
-
query: params.query,
|
|
1050
|
-
max_results: 5,
|
|
1051
|
-
search_depth: 'basic',
|
|
1052
|
-
include_answer: true,
|
|
1053
|
-
}),
|
|
1054
|
-
});
|
|
1055
|
-
}
|
|
1056
|
-
else {
|
|
1057
|
-
// Proxy mode
|
|
1058
|
-
res = await fetch(`${sfApiUrl}/api/proxy/search`, {
|
|
1059
|
-
method: 'POST',
|
|
1060
|
-
headers: {
|
|
1061
|
-
'Content-Type': 'application/json',
|
|
1062
|
-
'Authorization': `Bearer ${sfApiKey}`,
|
|
1063
|
-
},
|
|
1064
|
-
body: JSON.stringify({
|
|
1065
|
-
query: params.query,
|
|
1066
|
-
max_results: 5,
|
|
1067
|
-
search_depth: 'basic',
|
|
1068
|
-
include_answer: true,
|
|
1069
|
-
}),
|
|
1070
|
-
});
|
|
1071
|
-
}
|
|
1072
|
-
if (!res.ok) {
|
|
1073
|
-
return {
|
|
1074
|
-
content: [{ type: 'text', text: `Search failed: ${res.status}` }],
|
|
1075
|
-
details: {},
|
|
1076
|
-
};
|
|
1077
|
-
}
|
|
1078
|
-
const data = await res.json();
|
|
1079
|
-
const results = (data.results || []).map((r) => `[${r.title}](${r.url})\n${r.content?.slice(0, 200)}`).join('\n\n');
|
|
1080
|
-
const answer = data.answer ? `Summary: ${data.answer}\n\n---\n\n` : '';
|
|
1081
|
-
return {
|
|
1082
|
-
content: [{ type: 'text', text: `${answer}${results}` }],
|
|
1083
|
-
details: {},
|
|
1084
|
-
};
|
|
1085
|
-
},
|
|
1086
|
-
},
|
|
1087
|
-
{
|
|
1088
|
-
name: 'explore_public',
|
|
1089
|
-
label: 'Explore Public Theses',
|
|
1090
|
-
description: 'Browse public theses from other users. No auth required. Pass a slug to get details, or omit to list all.',
|
|
1091
|
-
parameters: Type.Object({
|
|
1092
|
-
slug: Type.Optional(Type.String({ description: 'Specific thesis slug, or empty to list all' })),
|
|
1093
|
-
}),
|
|
1094
|
-
execute: async (_toolCallId, params) => {
|
|
1095
|
-
const base = 'https://simplefunctions.dev';
|
|
1096
|
-
if (params.slug) {
|
|
1097
|
-
const res = await fetch(`${base}/api/public/thesis/${params.slug}`);
|
|
1098
|
-
if (!res.ok)
|
|
1099
|
-
return { content: [{ type: 'text', text: `Not found: ${params.slug}` }], details: {} };
|
|
1100
|
-
const data = await res.json();
|
|
1101
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1102
|
-
}
|
|
1103
|
-
const res = await fetch(`${base}/api/public/theses`);
|
|
1104
|
-
if (!res.ok)
|
|
1105
|
-
return { content: [{ type: 'text', text: 'Failed to fetch public theses' }], details: {} };
|
|
1106
|
-
const data = await res.json();
|
|
1107
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1108
|
-
},
|
|
1109
|
-
},
|
|
1110
|
-
{
|
|
1111
|
-
name: 'create_strategy',
|
|
1112
|
-
label: 'Create Strategy',
|
|
1113
|
-
description: 'Create a trading strategy for a thesis. Extract hard conditions (entryBelow/stopLoss/takeProfit as cents) and soft conditions from conversation. Called when user mentions specific trade ideas.',
|
|
1114
|
-
parameters: Type.Object({
|
|
1115
|
-
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
1116
|
-
marketId: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T150' }),
|
|
1117
|
-
market: Type.String({ description: 'Human-readable market name' }),
|
|
1118
|
-
direction: Type.String({ description: 'yes or no' }),
|
|
1119
|
-
horizon: Type.Optional(Type.String({ description: 'short, medium, or long. Default: medium' })),
|
|
1120
|
-
entryBelow: Type.Optional(Type.Number({ description: 'Entry trigger: ask <= this value (cents)' })),
|
|
1121
|
-
entryAbove: Type.Optional(Type.Number({ description: 'Entry trigger: ask >= this value (cents, for NO direction)' })),
|
|
1122
|
-
stopLoss: Type.Optional(Type.Number({ description: 'Stop loss: bid <= this value (cents)' })),
|
|
1123
|
-
takeProfit: Type.Optional(Type.Number({ description: 'Take profit: bid >= this value (cents)' })),
|
|
1124
|
-
maxQuantity: Type.Optional(Type.Number({ description: 'Max total contracts. Default: 500' })),
|
|
1125
|
-
perOrderQuantity: Type.Optional(Type.Number({ description: 'Contracts per order. Default: 50' })),
|
|
1126
|
-
softConditions: Type.Optional(Type.String({ description: 'LLM-evaluated conditions e.g. "only enter when n3 > 60%"' })),
|
|
1127
|
-
rationale: Type.Optional(Type.String({ description: 'Full logic description' })),
|
|
1128
|
-
}),
|
|
1129
|
-
execute: async (_toolCallId, params) => {
|
|
1130
|
-
const result = await sfClient.createStrategyAPI(params.thesisId, {
|
|
1131
|
-
marketId: params.marketId,
|
|
1132
|
-
market: params.market,
|
|
1133
|
-
direction: params.direction,
|
|
1134
|
-
horizon: params.horizon,
|
|
1135
|
-
entryBelow: params.entryBelow,
|
|
1136
|
-
entryAbove: params.entryAbove,
|
|
1137
|
-
stopLoss: params.stopLoss,
|
|
1138
|
-
takeProfit: params.takeProfit,
|
|
1139
|
-
maxQuantity: params.maxQuantity,
|
|
1140
|
-
perOrderQuantity: params.perOrderQuantity,
|
|
1141
|
-
softConditions: params.softConditions,
|
|
1142
|
-
rationale: params.rationale,
|
|
1143
|
-
createdBy: 'agent',
|
|
1144
|
-
});
|
|
1145
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
1146
|
-
},
|
|
1147
|
-
},
|
|
1148
|
-
{
|
|
1149
|
-
name: 'list_strategies',
|
|
1150
|
-
label: 'List Strategies',
|
|
1151
|
-
description: 'List strategies for a thesis. Filter by status (active/watching/executed/cancelled/review) or omit for all.',
|
|
1152
|
-
parameters: Type.Object({
|
|
1153
|
-
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
1154
|
-
status: Type.Optional(Type.String({ description: 'Filter by status. Omit for all.' })),
|
|
1155
|
-
}),
|
|
1156
|
-
execute: async (_toolCallId, params) => {
|
|
1157
|
-
const result = await sfClient.getStrategies(params.thesisId, params.status);
|
|
1158
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
1159
|
-
},
|
|
1160
|
-
},
|
|
1161
|
-
{
|
|
1162
|
-
name: 'update_strategy',
|
|
1163
|
-
label: 'Update Strategy',
|
|
1164
|
-
description: 'Update a strategy (change stop loss, take profit, status, priority, etc.)',
|
|
1165
|
-
parameters: Type.Object({
|
|
1166
|
-
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
1167
|
-
strategyId: Type.String({ description: 'Strategy ID (UUID)' }),
|
|
1168
|
-
stopLoss: Type.Optional(Type.Number({ description: 'New stop loss (cents)' })),
|
|
1169
|
-
takeProfit: Type.Optional(Type.Number({ description: 'New take profit (cents)' })),
|
|
1170
|
-
entryBelow: Type.Optional(Type.Number({ description: 'New entry below trigger (cents)' })),
|
|
1171
|
-
entryAbove: Type.Optional(Type.Number({ description: 'New entry above trigger (cents)' })),
|
|
1172
|
-
status: Type.Optional(Type.String({ description: 'New status: active|watching|executed|cancelled|review' })),
|
|
1173
|
-
priority: Type.Optional(Type.Number({ description: 'New priority' })),
|
|
1174
|
-
softConditions: Type.Optional(Type.String({ description: 'Updated soft conditions' })),
|
|
1175
|
-
rationale: Type.Optional(Type.String({ description: 'Updated rationale' })),
|
|
1176
|
-
}),
|
|
1177
|
-
execute: async (_toolCallId, params) => {
|
|
1178
|
-
const { thesisId, strategyId, ...updates } = params;
|
|
1179
|
-
const result = await sfClient.updateStrategyAPI(thesisId, strategyId, updates);
|
|
1180
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
1181
|
-
},
|
|
1182
|
-
},
|
|
1183
|
-
{
|
|
1184
|
-
name: 'get_milestones',
|
|
1185
|
-
label: 'Milestones',
|
|
1186
|
-
description: 'Get upcoming events from Kalshi calendar. Use to check economic releases, political events, or other catalysts coming up that might affect the thesis.',
|
|
1187
|
-
parameters: Type.Object({
|
|
1188
|
-
hours: Type.Optional(Type.Number({ description: 'Hours ahead to look (default 168 = 1 week)' })),
|
|
1189
|
-
category: Type.Optional(Type.String({ description: 'Filter by category (e.g. Economics, Politics, Sports)' })),
|
|
1190
|
-
}),
|
|
1191
|
-
execute: async (_toolCallId, params) => {
|
|
1192
|
-
const hours = params.hours || 168;
|
|
1193
|
-
const now = new Date();
|
|
1194
|
-
const url = `https://api.elections.kalshi.com/trade-api/v2/milestones?limit=200&minimum_start_date=${now.toISOString()}` +
|
|
1195
|
-
(params.category ? `&category=${params.category}` : '');
|
|
1196
|
-
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
1197
|
-
if (!res.ok)
|
|
1198
|
-
return { content: [{ type: 'text', text: `Milestones API error: ${res.status}` }], details: {} };
|
|
1199
|
-
const data = await res.json();
|
|
1200
|
-
const cutoff = now.getTime() + hours * 3600000;
|
|
1201
|
-
const filtered = (data.milestones || [])
|
|
1202
|
-
.filter((m) => new Date(m.start_date).getTime() <= cutoff)
|
|
1203
|
-
.slice(0, 30)
|
|
1204
|
-
.map((m) => ({
|
|
1205
|
-
title: m.title,
|
|
1206
|
-
category: m.category,
|
|
1207
|
-
start_date: m.start_date,
|
|
1208
|
-
related_event_tickers: m.related_event_tickers,
|
|
1209
|
-
hours_until: Math.round((new Date(m.start_date).getTime() - now.getTime()) / 3600000),
|
|
1210
|
-
}));
|
|
1211
|
-
return { content: [{ type: 'text', text: JSON.stringify(filtered, null, 2) }], details: {} };
|
|
1212
|
-
},
|
|
1213
|
-
},
|
|
1214
|
-
{
|
|
1215
|
-
name: 'get_forecast',
|
|
1216
|
-
label: 'Forecast',
|
|
1217
|
-
description: 'Get market distribution (P50/P75/P90 percentile history) for a Kalshi event. Shows how market consensus has shifted over time.',
|
|
1218
|
-
parameters: Type.Object({
|
|
1219
|
-
eventTicker: Type.String({ description: 'Kalshi event ticker (e.g. KXWTIMAX-26DEC31)' }),
|
|
1220
|
-
days: Type.Optional(Type.Number({ description: 'Days of history (default 7)' })),
|
|
1221
|
-
}),
|
|
1222
|
-
execute: async (_toolCallId, params) => {
|
|
1223
|
-
const { getForecastHistory } = await import('../kalshi.js');
|
|
1224
|
-
const days = params.days || 7;
|
|
1225
|
-
// Get series ticker from event
|
|
1226
|
-
const evtRes = await fetch(`https://api.elections.kalshi.com/trade-api/v2/events/${params.eventTicker}`, { headers: { 'Accept': 'application/json' } });
|
|
1227
|
-
if (!evtRes.ok)
|
|
1228
|
-
return { content: [{ type: 'text', text: `Event not found: ${params.eventTicker}` }], details: {} };
|
|
1229
|
-
const evtData = await evtRes.json();
|
|
1230
|
-
const seriesTicker = evtData.event?.series_ticker;
|
|
1231
|
-
if (!seriesTicker)
|
|
1232
|
-
return { content: [{ type: 'text', text: `No series_ticker for ${params.eventTicker}` }], details: {} };
|
|
1233
|
-
const history = await getForecastHistory({
|
|
1234
|
-
seriesTicker,
|
|
1235
|
-
eventTicker: params.eventTicker,
|
|
1236
|
-
percentiles: [5000, 7500, 9000],
|
|
1237
|
-
startTs: Math.floor((Date.now() - days * 86400000) / 1000),
|
|
1238
|
-
endTs: Math.floor(Date.now() / 1000),
|
|
1239
|
-
periodInterval: 1440,
|
|
1240
|
-
});
|
|
1241
|
-
if (!history || history.length === 0)
|
|
1242
|
-
return { content: [{ type: 'text', text: 'No forecast data available' }], details: {} };
|
|
1243
|
-
return { content: [{ type: 'text', text: JSON.stringify(history, null, 2) }], details: {} };
|
|
1244
|
-
},
|
|
1245
|
-
},
|
|
1246
|
-
{
|
|
1247
|
-
name: 'get_settlements',
|
|
1248
|
-
label: 'Settlements',
|
|
1249
|
-
description: 'Get settled (resolved) contracts with P&L. Shows which contracts won/lost and realized returns.',
|
|
1250
|
-
parameters: Type.Object({
|
|
1251
|
-
ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })),
|
|
1252
|
-
}),
|
|
1253
|
-
execute: async (_toolCallId, params) => {
|
|
1254
|
-
const { getSettlements } = await import('../kalshi.js');
|
|
1255
|
-
const result = await getSettlements({ limit: 100, ticker: params.ticker });
|
|
1256
|
-
if (!result)
|
|
1257
|
-
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
1258
|
-
return { content: [{ type: 'text', text: JSON.stringify(result.settlements, null, 2) }], details: {} };
|
|
1259
|
-
},
|
|
1260
|
-
},
|
|
1261
|
-
{
|
|
1262
|
-
name: 'get_balance',
|
|
1263
|
-
label: 'Balance',
|
|
1264
|
-
description: 'Get Kalshi account balance and portfolio value.',
|
|
1265
|
-
parameters: emptyParams,
|
|
1266
|
-
execute: async () => {
|
|
1267
|
-
const { getBalance } = await import('../kalshi.js');
|
|
1268
|
-
const result = await getBalance();
|
|
1269
|
-
if (!result)
|
|
1270
|
-
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
1271
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
1272
|
-
},
|
|
1273
|
-
},
|
|
1274
|
-
{
|
|
1275
|
-
name: 'get_orders',
|
|
1276
|
-
label: 'Orders',
|
|
1277
|
-
description: 'Get current resting orders on Kalshi. Stale orders (>7 days old AND >10¢ from market) are flagged.',
|
|
1278
|
-
parameters: Type.Object({
|
|
1279
|
-
status: Type.Optional(Type.String({ description: 'Filter by status: resting, canceled, executed. Default: resting' })),
|
|
1280
|
-
}),
|
|
1281
|
-
execute: async (_toolCallId, params) => {
|
|
1282
|
-
const { getOrders, getMarketPrice } = await import('../kalshi.js');
|
|
1283
|
-
const result = await getOrders({ status: params.status || 'resting', limit: 100 });
|
|
1284
|
-
if (!result)
|
|
1285
|
-
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
1286
|
-
// Enrich orders with staleness detection
|
|
1287
|
-
const enriched = await Promise.all((result.orders || []).map(async (order) => {
|
|
1288
|
-
const daysSinceCreated = order.created_time
|
|
1289
|
-
? Math.round((Date.now() - new Date(order.created_time).getTime()) / 86400000)
|
|
1290
|
-
: null;
|
|
1291
|
-
let distanceFromMarket = null;
|
|
1292
|
-
let stale = false;
|
|
1293
|
-
try {
|
|
1294
|
-
const price = await getMarketPrice(order.ticker);
|
|
1295
|
-
if (price != null && order.yes_price_dollars) {
|
|
1296
|
-
distanceFromMarket = Math.round(Math.abs(price - parseFloat(order.yes_price_dollars)) * 100);
|
|
1297
|
-
if (daysSinceCreated != null && daysSinceCreated > 7 && distanceFromMarket > 10)
|
|
1298
|
-
stale = true;
|
|
1299
|
-
}
|
|
1300
|
-
}
|
|
1301
|
-
catch { }
|
|
1302
|
-
return { ...order, daysSinceCreated, distanceFromMarket, stale };
|
|
1303
|
-
}));
|
|
1304
|
-
return { content: [{ type: 'text', text: JSON.stringify(enriched, null, 2) }], details: {} };
|
|
1305
|
-
},
|
|
1306
|
-
},
|
|
1307
|
-
{
|
|
1308
|
-
name: 'get_fills',
|
|
1309
|
-
label: 'Fills',
|
|
1310
|
-
description: 'Get recent trade fills (executed trades) on Kalshi.',
|
|
1311
|
-
parameters: Type.Object({
|
|
1312
|
-
ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })),
|
|
1313
|
-
}),
|
|
1314
|
-
execute: async (_toolCallId, params) => {
|
|
1315
|
-
const { getFills } = await import('../kalshi.js');
|
|
1316
|
-
const result = await getFills({ ticker: params.ticker, limit: 50 });
|
|
1317
|
-
if (!result)
|
|
1318
|
-
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
1319
|
-
return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
|
|
1320
|
-
},
|
|
1321
|
-
},
|
|
1322
|
-
{
|
|
1323
|
-
name: 'get_liquidity',
|
|
1324
|
-
label: 'Liquidity Scanner',
|
|
1325
|
-
description: 'Scan orderbook liquidity for a topic across Kalshi + Polymarket. Returns spread, depth, liquidity scores. Topics: ' + Object.keys(topics_js_1.TOPIC_SERIES).join(', '),
|
|
1326
|
-
parameters: Type.Object({
|
|
1327
|
-
topic: Type.String({ description: 'Topic to scan (e.g. oil, crypto, fed, geopolitics)' }),
|
|
1328
|
-
}),
|
|
1329
|
-
execute: async (_toolCallId, params) => {
|
|
1330
|
-
const topicKey = params.topic.toLowerCase();
|
|
1331
|
-
const seriesList = topics_js_1.TOPIC_SERIES[topicKey];
|
|
1332
|
-
if (!seriesList) {
|
|
1333
|
-
return { content: [{ type: 'text', text: `Unknown topic "${params.topic}". Available: ${Object.keys(topics_js_1.TOPIC_SERIES).join(', ')}` }], details: {} };
|
|
1334
|
-
}
|
|
1335
|
-
const results = [];
|
|
1336
|
-
for (const series of seriesList) {
|
|
1337
|
-
try {
|
|
1338
|
-
const url = `https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=${series}&status=open&limit=200`;
|
|
1339
|
-
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
1340
|
-
if (!res.ok)
|
|
1341
|
-
continue;
|
|
1342
|
-
const markets = (await res.json()).markets || [];
|
|
1343
|
-
const obResults = await Promise.allSettled(markets.slice(0, 20).map((m) => (0, kalshi_js_1.getPublicOrderbook)(m.ticker).then(ob => ({ ticker: m.ticker, title: m.title, ob }))));
|
|
1344
|
-
for (const r of obResults) {
|
|
1345
|
-
if (r.status !== 'fulfilled' || !r.value.ob)
|
|
1346
|
-
continue;
|
|
1347
|
-
const { ticker, title, ob } = r.value;
|
|
1348
|
-
const yes = (ob.yes_dollars || []).map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), qty: parseFloat(q) })).filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
1349
|
-
const no = (ob.no_dollars || []).map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), qty: parseFloat(q) })).filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
1350
|
-
const bestBid = yes[0]?.price || 0;
|
|
1351
|
-
const bestAsk = no.length > 0 ? (100 - no[0].price) : 100;
|
|
1352
|
-
const spread = bestAsk - bestBid;
|
|
1353
|
-
const depth = yes.slice(0, 3).reduce((s, l) => s + l.qty, 0) + no.slice(0, 3).reduce((s, l) => s + l.qty, 0);
|
|
1354
|
-
const liq = spread <= 2 && depth >= 500 ? 'high' : spread <= 5 && depth >= 100 ? 'medium' : 'low';
|
|
1355
|
-
results.push({ venue: 'kalshi', ticker, title: (title || '').slice(0, 50), bestBid, bestAsk, spread, depth: Math.round(depth), liquidityScore: liq });
|
|
1356
|
-
}
|
|
1357
|
-
}
|
|
1358
|
-
catch { /* skip */ }
|
|
1359
|
-
}
|
|
1360
|
-
try {
|
|
1361
|
-
const events = await (0, polymarket_js_1.polymarketSearch)(topicKey, 5);
|
|
1362
|
-
for (const event of events) {
|
|
1363
|
-
for (const m of (event.markets || []).slice(0, 5)) {
|
|
1364
|
-
if (!m.active || m.closed || !m.clobTokenIds)
|
|
1365
|
-
continue;
|
|
1366
|
-
const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
|
|
1367
|
-
if (!ids)
|
|
1368
|
-
continue;
|
|
1369
|
-
const d = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
|
|
1370
|
-
if (!d)
|
|
1371
|
-
continue;
|
|
1372
|
-
results.push({ venue: 'polymarket', ticker: (m.question || event.title).slice(0, 50), bestBid: d.bestBid, bestAsk: d.bestAsk, spread: d.spread, depth: d.bidDepthTop3 + d.askDepthTop3, liquidityScore: d.liquidityScore });
|
|
1373
|
-
}
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
catch { /* skip */ }
|
|
1377
|
-
results.sort((a, b) => a.spread - b.spread);
|
|
1378
|
-
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
|
|
1379
|
-
},
|
|
1380
|
-
},
|
|
1381
|
-
{
|
|
1382
|
-
name: 'inspect_book',
|
|
1383
|
-
label: 'Orderbook',
|
|
1384
|
-
description: 'Get orderbook depth, spread, and liquidity. Returns a status field per market: "ok", "empty_orderbook", "market_closed", or "api_error". Supports multiple tickers in one call — use tickers array for batch position checks.',
|
|
1385
|
-
parameters: Type.Object({
|
|
1386
|
-
ticker: Type.Optional(Type.String({ description: 'Single Kalshi market ticker (e.g. KXWTIMAX-26DEC31-T135)' })),
|
|
1387
|
-
tickers: Type.Optional(Type.Array(Type.String(), { description: 'Multiple Kalshi tickers for batch check (e.g. ["T$135", "T$140", "T$150"])' })),
|
|
1388
|
-
polyQuery: Type.Optional(Type.String({ description: 'Search Polymarket by keyword (e.g. "oil price above 100")' })),
|
|
1389
|
-
}),
|
|
1390
|
-
execute: async (_toolCallId, params) => {
|
|
1391
|
-
const results = [];
|
|
1392
|
-
// Batch: expand tickers array into individual lookups
|
|
1393
|
-
const tickerList = [];
|
|
1394
|
-
if (params.tickers?.length)
|
|
1395
|
-
tickerList.push(...params.tickers);
|
|
1396
|
-
else if (params.ticker)
|
|
1397
|
-
tickerList.push(params.ticker);
|
|
1398
|
-
for (const tkr of tickerList) {
|
|
1399
|
-
try {
|
|
1400
|
-
const market = await (0, client_js_1.kalshiFetchMarket)(tkr);
|
|
1401
|
-
const mStatus = market.status || 'unknown';
|
|
1402
|
-
if (mStatus !== 'open' && mStatus !== 'active') {
|
|
1403
|
-
results.push({
|
|
1404
|
-
venue: 'kalshi', ticker: tkr, title: market.title,
|
|
1405
|
-
status: 'market_closed', reason: `Market status: ${mStatus}. Orderbook unavailable for closed/settled markets.`,
|
|
1406
|
-
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
1407
|
-
});
|
|
1408
|
-
}
|
|
1409
|
-
else {
|
|
1410
|
-
const ob = await (0, kalshi_js_1.getPublicOrderbook)(tkr);
|
|
1411
|
-
const yesBids = (ob?.yes_dollars || [])
|
|
1412
|
-
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
1413
|
-
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
1414
|
-
const noAsks = (ob?.no_dollars || [])
|
|
1415
|
-
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
1416
|
-
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
1417
|
-
if (yesBids.length === 0 && noAsks.length === 0) {
|
|
1418
|
-
results.push({
|
|
1419
|
-
venue: 'kalshi', ticker: tkr, title: market.title,
|
|
1420
|
-
status: 'empty_orderbook', reason: 'Market open but no resting orders. Normal for illiquid/OTM contracts. Use lastPrice as reference.',
|
|
1421
|
-
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
1422
|
-
volume24h: parseFloat(market.volume_24h_fp || '0'),
|
|
1423
|
-
openInterest: parseFloat(market.open_interest_fp || '0'),
|
|
1424
|
-
expiry: market.close_time || null,
|
|
1425
|
-
});
|
|
1426
|
-
}
|
|
1427
|
-
else {
|
|
1428
|
-
const bestBid = yesBids[0]?.price || 0;
|
|
1429
|
-
const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : (yesBids[0] ? yesBids[0].price + 1 : 100);
|
|
1430
|
-
const spread = bestAsk - bestBid;
|
|
1431
|
-
const top3Depth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) + noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
|
|
1432
|
-
const liq = spread <= 2 && top3Depth >= 500 ? 'high' : spread <= 5 && top3Depth >= 100 ? 'medium' : 'low';
|
|
1433
|
-
results.push({
|
|
1434
|
-
venue: 'kalshi', ticker: tkr, title: market.title, status: 'ok',
|
|
1435
|
-
bestBid, bestAsk, spread, liquidityScore: liq,
|
|
1436
|
-
bidLevels: yesBids.slice(0, 5), askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
|
|
1437
|
-
totalBidDepth: yesBids.reduce((s, l) => s + l.size, 0),
|
|
1438
|
-
totalAskDepth: noAsks.reduce((s, l) => s + l.size, 0),
|
|
1439
|
-
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
1440
|
-
volume24h: parseFloat(market.volume_24h_fp || '0'),
|
|
1441
|
-
openInterest: parseFloat(market.open_interest_fp || '0'),
|
|
1442
|
-
expiry: market.close_time || null,
|
|
1443
|
-
});
|
|
1444
|
-
}
|
|
1445
|
-
}
|
|
1446
|
-
}
|
|
1447
|
-
catch (err) {
|
|
1448
|
-
results.push({ venue: 'kalshi', ticker: tkr, status: 'api_error', reason: `Kalshi API error: ${err.message}` });
|
|
1449
|
-
}
|
|
1450
|
-
}
|
|
1451
|
-
if (params.polyQuery) {
|
|
1452
|
-
try {
|
|
1453
|
-
const events = await (0, polymarket_js_1.polymarketSearch)(params.polyQuery, 5);
|
|
1454
|
-
for (const event of events) {
|
|
1455
|
-
for (const m of (event.markets || []).slice(0, 3)) {
|
|
1456
|
-
if (!m.active || m.closed || !m.clobTokenIds)
|
|
1457
|
-
continue;
|
|
1458
|
-
const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
|
|
1459
|
-
if (!ids)
|
|
1460
|
-
continue;
|
|
1461
|
-
const depth = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
|
|
1462
|
-
if (!depth)
|
|
1463
|
-
continue;
|
|
1464
|
-
const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
|
|
1465
|
-
results.push({
|
|
1466
|
-
venue: 'polymarket', title: m.question || event.title,
|
|
1467
|
-
bestBid: depth.bestBid, bestAsk: depth.bestAsk, spread: depth.spread,
|
|
1468
|
-
liquidityScore: depth.liquidityScore,
|
|
1469
|
-
totalBidDepth: depth.totalBidDepth, totalAskDepth: depth.totalAskDepth,
|
|
1470
|
-
lastPrice: prices[0] ? Math.round(prices[0] * 100) : 0,
|
|
1471
|
-
volume24h: m.volume24hr || 0,
|
|
1472
|
-
});
|
|
1473
|
-
}
|
|
1474
|
-
}
|
|
1475
|
-
}
|
|
1476
|
-
catch { /* skip */ }
|
|
1477
|
-
}
|
|
1478
|
-
if (results.length === 0) {
|
|
1479
|
-
return { content: [{ type: 'text', text: 'No markets found. Provide ticker (Kalshi) or polyQuery (Polymarket search).' }], details: {} };
|
|
1480
|
-
}
|
|
1481
|
-
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
|
|
1482
|
-
},
|
|
1483
|
-
},
|
|
1484
|
-
{
|
|
1485
|
-
name: 'get_schedule',
|
|
1486
|
-
label: 'Schedule',
|
|
1487
|
-
description: 'Get exchange status (open/closed) and trading hours. Use to check if low liquidity is due to off-hours.',
|
|
1488
|
-
parameters: emptyParams,
|
|
1489
|
-
execute: async () => {
|
|
1490
|
-
try {
|
|
1491
|
-
const res = await fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } });
|
|
1492
|
-
if (!res.ok)
|
|
1493
|
-
return { content: [{ type: 'text', text: `Exchange API error: ${res.status}` }], details: {} };
|
|
1494
|
-
const data = await res.json();
|
|
1495
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1496
|
-
}
|
|
1497
|
-
catch (err) {
|
|
1498
|
-
return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
|
|
1499
|
-
}
|
|
1500
|
-
},
|
|
1501
|
-
},
|
|
1502
|
-
{
|
|
1503
|
-
name: 'create_thesis',
|
|
1504
|
-
label: 'Create Thesis',
|
|
1505
|
-
description: 'Create a new thesis from a raw thesis statement. Returns the thesis ID, confidence, node count, and edge count. In explorer mode, this automatically transitions to thesis mode.',
|
|
1506
|
-
parameters: Type.Object({
|
|
1507
|
-
rawThesis: Type.String({ description: 'The raw thesis statement to create' }),
|
|
1508
|
-
webhookUrl: Type.Optional(Type.String({ description: 'Optional webhook URL for notifications' })),
|
|
1509
|
-
}),
|
|
1510
|
-
execute: async (_toolCallId, params) => {
|
|
1511
|
-
const result = await sfClient.createThesis(params.rawThesis, true);
|
|
1512
|
-
const thesis = result.thesis || result;
|
|
1513
|
-
const nodeCount = thesis.causalTree?.nodes?.length || 0;
|
|
1514
|
-
const edgeCount = (thesis.edges || []).length;
|
|
1515
|
-
const confidence = typeof thesis.confidence === 'number' ? Math.round(thesis.confidence * 100) : 0;
|
|
1516
|
-
// ── Auto-transition from explorer to thesis mode ──────────────────
|
|
1517
|
-
if (explorerMode && thesis.id) {
|
|
1518
|
-
explorerMode = false;
|
|
1519
|
-
resolvedThesisId = thesis.id;
|
|
1520
|
-
try {
|
|
1521
|
-
latestContext = await sfClient.getContext(thesis.id);
|
|
1522
|
-
const newPrompt = buildSystemPrompt(latestContext);
|
|
1523
|
-
agent.setSystemPrompt(newPrompt);
|
|
1524
|
-
footerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
1525
|
-
tui.requestRender();
|
|
1526
|
-
}
|
|
1527
|
-
catch { /* context fetch failed, still switch */ }
|
|
1528
|
-
}
|
|
1529
|
-
return {
|
|
1530
|
-
content: [{ type: 'text', text: `Thesis created.\nID: ${thesis.id}\nConfidence: ${confidence}%\nNodes: ${nodeCount}\nEdges: ${edgeCount}\n\nHeartbeat engine is now monitoring this thesis 24/7. Use /switch ${thesis.id?.slice(0, 8)} to focus on it.` }],
|
|
1531
|
-
details: {},
|
|
1532
|
-
};
|
|
1533
|
-
},
|
|
1534
|
-
},
|
|
1535
|
-
{
|
|
1536
|
-
name: 'get_edges',
|
|
1537
|
-
label: 'Get Edges',
|
|
1538
|
-
description: 'Get top edges across all active theses. Returns the top 10 edges sorted by absolute edge size with ticker, market name, edge size, direction, and venue.',
|
|
1539
|
-
parameters: emptyParams,
|
|
1540
|
-
execute: async () => {
|
|
1541
|
-
const { theses } = await sfClient.listTheses();
|
|
1542
|
-
const activeTheses = (theses || []).filter((t) => t.status === 'active' || t.status === 'monitoring');
|
|
1543
|
-
const results = await Promise.allSettled(activeTheses.map(async (t) => {
|
|
1544
|
-
const ctx = await sfClient.getContext(t.id);
|
|
1545
|
-
return (ctx.edges || []).map((e) => ({ ...e, thesisId: t.id }));
|
|
1546
|
-
}));
|
|
1547
|
-
const allEdges = [];
|
|
1548
|
-
for (const r of results) {
|
|
1549
|
-
if (r.status === 'fulfilled')
|
|
1550
|
-
allEdges.push(...r.value);
|
|
1551
|
-
}
|
|
1552
|
-
allEdges.sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0));
|
|
1553
|
-
const top10 = allEdges.slice(0, 10).map((e) => ({
|
|
1554
|
-
ticker: e.marketId || e.ticker || '-',
|
|
1555
|
-
market: e.market || e.marketTitle || '-',
|
|
1556
|
-
edge: e.edge || e.edgeSize || 0,
|
|
1557
|
-
direction: e.direction || 'yes',
|
|
1558
|
-
venue: e.venue || 'kalshi',
|
|
1559
|
-
}));
|
|
1560
|
-
return {
|
|
1561
|
-
content: [{ type: 'text', text: JSON.stringify(top10, null, 2) }],
|
|
1562
|
-
details: {},
|
|
1563
|
-
};
|
|
1564
|
-
},
|
|
1565
|
-
},
|
|
1566
|
-
{
|
|
1567
|
-
name: 'get_feed',
|
|
1568
|
-
label: 'Get Feed',
|
|
1569
|
-
description: 'Get evaluation history with topSignal highlighting. The most important signal (largest confidence change or most actionable) is surfaced first so you don\'t have to scan all entries.',
|
|
1570
|
-
parameters: Type.Object({
|
|
1571
|
-
hours: Type.Optional(Type.Number({ description: 'Hours of history to fetch (default 24)' })),
|
|
1572
|
-
}),
|
|
1573
|
-
execute: async (_toolCallId, params) => {
|
|
1574
|
-
const result = await sfClient.getFeed(params.hours || 24);
|
|
1575
|
-
const items = Array.isArray(result) ? result : (result?.evaluations || result?.items || []);
|
|
1576
|
-
// Find the most important signal: largest |confidenceDelta|, or newest with actual content
|
|
1577
|
-
let topSignal = null;
|
|
1578
|
-
let topScore = 0;
|
|
1579
|
-
for (const item of items) {
|
|
1580
|
-
let score = 0;
|
|
1581
|
-
const delta = Math.abs(item.confidenceDelta || item.confidence_delta || 0);
|
|
1582
|
-
if (delta > 0)
|
|
1583
|
-
score = delta * 100; // confidence changes are most important
|
|
1584
|
-
else if (item.summary?.length > 50)
|
|
1585
|
-
score = 0.1; // has substance but no delta
|
|
1586
|
-
if (score > topScore) {
|
|
1587
|
-
topScore = score;
|
|
1588
|
-
topSignal = item;
|
|
1589
|
-
}
|
|
1590
|
-
}
|
|
1591
|
-
const output = { total: items.length };
|
|
1592
|
-
if (topSignal) {
|
|
1593
|
-
output.topSignal = {
|
|
1594
|
-
summary: topSignal.summary || topSignal.content || '',
|
|
1595
|
-
confidenceDelta: topSignal.confidenceDelta || topSignal.confidence_delta || 0,
|
|
1596
|
-
evaluatedAt: topSignal.evaluatedAt || topSignal.evaluated_at || topSignal.createdAt || '',
|
|
1597
|
-
why: topScore > 1 ? 'Largest confidence movement in this period'
|
|
1598
|
-
: topScore > 0 ? 'Most substantive evaluation (no confidence change)'
|
|
1599
|
-
: 'Most recent evaluation',
|
|
1600
|
-
};
|
|
1601
|
-
}
|
|
1602
|
-
output.items = items;
|
|
1603
|
-
return {
|
|
1604
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
1605
|
-
details: {},
|
|
1606
|
-
};
|
|
1607
|
-
},
|
|
1608
|
-
},
|
|
1609
|
-
{
|
|
1610
|
-
name: 'get_changes',
|
|
1611
|
-
label: 'Get Changes',
|
|
1612
|
-
description: 'Get recent market changes detected server-side. Returns real price moves (>5¢), new contracts, and removed/settled contracts across Kalshi, Polymarket, and traditional markets. Checked every 15 minutes. Use for situational awareness, discovering new opportunities, or checking if anything material happened recently.',
|
|
1613
|
-
parameters: Type.Object({
|
|
1614
|
-
hours: Type.Optional(Type.Number({ description: 'Hours of history (default 1)' })),
|
|
1615
|
-
}),
|
|
1616
|
-
execute: async (_toolCallId, params) => {
|
|
1617
|
-
const hours = params.hours || 1;
|
|
1618
|
-
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
1619
|
-
const apiUrl = process.env.SF_API_URL || 'https://simplefunctions.dev';
|
|
1620
|
-
const res = await fetch(`${apiUrl}/api/changes?since=${encodeURIComponent(since)}&limit=100`);
|
|
1621
|
-
if (!res.ok) {
|
|
1622
|
-
return { content: [{ type: 'text', text: JSON.stringify({ error: `API error ${res.status}` }) }], details: {} };
|
|
1623
|
-
}
|
|
1624
|
-
const data = await res.json();
|
|
1625
|
-
return {
|
|
1626
|
-
content: [{ type: 'text', text: JSON.stringify(data, null, 2) }],
|
|
1627
|
-
details: {},
|
|
1628
|
-
};
|
|
1629
|
-
},
|
|
1630
|
-
},
|
|
1631
|
-
];
|
|
1632
|
-
// ── What-if tool (always available) ────────────────────────────────────────
|
|
1633
|
-
tools.push({
|
|
1634
|
-
name: 'what_if',
|
|
1635
|
-
label: 'What-If',
|
|
1636
|
-
description: 'Run a what-if scenario: override causal tree node probabilities and see how edges and confidence change. Zero LLM cost — pure computation. Use when user asks "what if X happens?" or "what if this node drops to Y%?".',
|
|
1637
|
-
parameters: Type.Object({
|
|
1638
|
-
overrides: Type.Array(Type.Object({
|
|
1639
|
-
nodeId: Type.String({ description: 'Causal tree node ID (e.g. n1, n3.1)' }),
|
|
1640
|
-
newProbability: Type.Number({ description: 'New probability 0-1' }),
|
|
1641
|
-
}), { description: 'Node probability overrides' }),
|
|
1642
|
-
}),
|
|
1643
|
-
execute: async (_toolCallId, params) => {
|
|
1644
|
-
// Refresh context before simulation to avoid stale confidence values
|
|
1645
|
-
if (resolvedThesisId) {
|
|
1646
|
-
try {
|
|
1647
|
-
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
1648
|
-
}
|
|
1649
|
-
catch { }
|
|
1650
|
-
}
|
|
1651
|
-
const ctx = latestContext;
|
|
1652
|
-
const allNodes = [];
|
|
1653
|
-
function flatten(nodes) {
|
|
1654
|
-
for (const n of nodes) {
|
|
1655
|
-
allNodes.push(n);
|
|
1656
|
-
if (n.children?.length)
|
|
1657
|
-
flatten(n.children);
|
|
1658
|
-
}
|
|
1659
|
-
}
|
|
1660
|
-
const rawNodes = ctx.causalTree?.nodes || [];
|
|
1661
|
-
flatten(rawNodes);
|
|
1662
|
-
const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
|
|
1663
|
-
const overrideMap = new Map(params.overrides.map((o) => [o.nodeId, o.newProbability]));
|
|
1664
|
-
// Propagate child node overrides to parent nodes.
|
|
1665
|
-
// If n2.2 is overridden, recalculate n2's effective probability
|
|
1666
|
-
// as the average of its children's (possibly overridden) probabilities.
|
|
1667
|
-
function effectiveProb(node) {
|
|
1668
|
-
// Direct override on this node
|
|
1669
|
-
if (overrideMap.has(node.id))
|
|
1670
|
-
return overrideMap.get(node.id);
|
|
1671
|
-
// If node has children, aggregate from children
|
|
1672
|
-
if (node.children?.length > 0) {
|
|
1673
|
-
const childProbs = node.children.map((c) => effectiveProb(c));
|
|
1674
|
-
const childImps = node.children.map((c) => c.importance || 1);
|
|
1675
|
-
const totalImp = childImps.reduce((s, w) => s + w, 0);
|
|
1676
|
-
if (totalImp > 0) {
|
|
1677
|
-
return childProbs.reduce((s, p, i) => s + p * childImps[i], 0) / totalImp;
|
|
1678
|
-
}
|
|
1679
|
-
return childProbs.reduce((s, p) => s + p, 0) / childProbs.length;
|
|
1680
|
-
}
|
|
1681
|
-
return node.probability ?? 0;
|
|
1682
|
-
}
|
|
1683
|
-
const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
|
|
1684
|
-
const newConf = treeNodes.reduce((s, n) => {
|
|
1685
|
-
return s + effectiveProb(n) * (n.importance || 0);
|
|
1686
|
-
}, 0);
|
|
1687
|
-
const nodeScales = new Map();
|
|
1688
|
-
for (const [nid, np] of overrideMap.entries()) {
|
|
1689
|
-
const nd = allNodes.find((n) => n.id === nid);
|
|
1690
|
-
if (nd && nd.probability > 0)
|
|
1691
|
-
nodeScales.set(nid, Math.max(0, Math.min(2, np / nd.probability)));
|
|
1692
|
-
}
|
|
1693
|
-
const edges = (ctx.edges || []).map((edge) => {
|
|
1694
|
-
const relNode = edge.relatedNodeId;
|
|
1695
|
-
let scaleFactor = 1;
|
|
1696
|
-
if (relNode) {
|
|
1697
|
-
const candidates = [relNode, relNode.split('.').slice(0, -1).join('.'), relNode.split('.')[0]].filter(Boolean);
|
|
1698
|
-
for (const cid of candidates) {
|
|
1699
|
-
if (nodeScales.has(cid)) {
|
|
1700
|
-
scaleFactor = nodeScales.get(cid);
|
|
1701
|
-
break;
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
}
|
|
1705
|
-
const mkt = edge.marketPrice || 0;
|
|
1706
|
-
const oldTP = edge.thesisPrice || edge.thesisImpliedPrice || mkt;
|
|
1707
|
-
const oldEdge = edge.edge || edge.edgeSize || 0;
|
|
1708
|
-
const newTP = Math.round((mkt + (oldTP - mkt) * scaleFactor) * 100) / 100;
|
|
1709
|
-
const dir = edge.direction || 'yes';
|
|
1710
|
-
const newEdge = Math.round((dir === 'yes' ? newTP - mkt : mkt - newTP) * 100) / 100;
|
|
1711
|
-
return {
|
|
1712
|
-
market: edge.market || edge.marketTitle || edge.marketId,
|
|
1713
|
-
marketPrice: mkt,
|
|
1714
|
-
oldEdge,
|
|
1715
|
-
newEdge,
|
|
1716
|
-
delta: newEdge - oldEdge,
|
|
1717
|
-
signal: Math.abs(newEdge - oldEdge) < 1 ? 'unchanged' : (oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0) ? 'REVERSED' : Math.abs(newEdge) < 2 ? 'GONE' : 'reduced',
|
|
1718
|
-
};
|
|
1719
|
-
}).filter((e) => e.signal !== 'unchanged');
|
|
1720
|
-
// Server confidence = LLM's holistic assessment (includes factors beyond the tree)
|
|
1721
|
-
// Tree confidence = weighted sum of node probabilities (pure math from causal tree)
|
|
1722
|
-
// These measure different things and will often differ.
|
|
1723
|
-
const serverConf = ctx.confidence != null ? Math.round(Number(ctx.confidence) * 100) : null;
|
|
1724
|
-
const result = {
|
|
1725
|
-
overrides: params.overrides.map((o) => {
|
|
1726
|
-
const node = allNodes.find((n) => n.id === o.nodeId);
|
|
1727
|
-
return { nodeId: o.nodeId, label: node?.label || o.nodeId, oldProb: node?.probability, newProb: o.newProbability };
|
|
1728
|
-
}),
|
|
1729
|
-
serverConfidence: serverConf,
|
|
1730
|
-
treeConfidence: { old: Math.round(oldConf * 100), new: Math.round(newConf * 100), delta: Math.round((newConf - oldConf) * 100) },
|
|
1731
|
-
note: serverConf != null && Math.abs(serverConf - Math.round(oldConf * 100)) > 5
|
|
1732
|
-
? `serverConfidence (${serverConf}%) differs from treeConfidence (${Math.round(oldConf * 100)}%) because the LLM evaluation considers factors beyond the causal tree.`
|
|
1733
|
-
: undefined,
|
|
1734
|
-
affectedEdges: edges,
|
|
1735
|
-
};
|
|
1736
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
1737
|
-
},
|
|
1738
|
-
});
|
|
1739
|
-
// ── X (Twitter) tools ─────────────────────────────────────────────────────
|
|
1740
|
-
tools.push({
|
|
1741
|
-
name: 'search_x',
|
|
1742
|
-
label: 'X Search',
|
|
1743
|
-
description: 'Search X (Twitter) for recent discussions on a topic. Returns top posts with engagement metrics, sentiment analysis, and key themes. Use for social signal research on any prediction market topic.',
|
|
1744
|
-
parameters: Type.Object({
|
|
1745
|
-
query: Type.String({ description: 'Search query (e.g. "iran oil", "fed rate cut", "$BTC")' }),
|
|
1746
|
-
mode: Type.Optional(Type.String({ description: '"summary" (default, with AI analysis) or "raw" (just posts)' })),
|
|
1747
|
-
hours: Type.Optional(Type.Number({ description: 'Hours of history (default 24)' })),
|
|
1748
|
-
}),
|
|
1749
|
-
execute: async (_toolCallId, params) => {
|
|
1750
|
-
const data = await sfClient.searchX(params.query, { mode: params.mode, hours: params.hours });
|
|
1751
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1752
|
-
},
|
|
1753
|
-
}, {
|
|
1754
|
-
name: 'x_volume',
|
|
1755
|
-
label: 'X Volume',
|
|
1756
|
-
description: 'Get X discussion volume trend for a topic — total posts, velocity change vs prior period, peak time, and hourly timeseries. Use to detect social momentum shifts.',
|
|
1757
|
-
parameters: Type.Object({
|
|
1758
|
-
query: Type.String({ description: 'Search query' }),
|
|
1759
|
-
hours: Type.Optional(Type.Number({ description: 'Hours of history (default 72)' })),
|
|
1760
|
-
}),
|
|
1761
|
-
execute: async (_toolCallId, params) => {
|
|
1762
|
-
const data = await sfClient.getXVolume(params.query, { hours: params.hours });
|
|
1763
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1764
|
-
},
|
|
1765
|
-
}, {
|
|
1766
|
-
name: 'x_news',
|
|
1767
|
-
label: 'X News',
|
|
1768
|
-
description: 'Get news stories trending on X — titles, summaries, categories, and ticker mentions. Use for breaking news and narrative tracking.',
|
|
1769
|
-
parameters: Type.Object({
|
|
1770
|
-
query: Type.String({ description: 'Search query' }),
|
|
1771
|
-
limit: Type.Optional(Type.Number({ description: 'Max stories (default 10)' })),
|
|
1772
|
-
}),
|
|
1773
|
-
execute: async (_toolCallId, params) => {
|
|
1774
|
-
const data = await sfClient.searchXNews(params.query, { limit: params.limit });
|
|
1775
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1776
|
-
},
|
|
1777
|
-
}, {
|
|
1778
|
-
name: 'x_account',
|
|
1779
|
-
label: 'X Account',
|
|
1780
|
-
description: 'Get recent posts from a specific X account. Use to track key opinion leaders, officials, or analysts.',
|
|
1781
|
-
parameters: Type.Object({
|
|
1782
|
-
username: Type.String({ description: 'X username (with or without @)' }),
|
|
1783
|
-
hours: Type.Optional(Type.Number({ description: 'Hours of history (default 24)' })),
|
|
1784
|
-
}),
|
|
1785
|
-
execute: async (_toolCallId, params) => {
|
|
1786
|
-
const data = await sfClient.getXAccount(params.username, { hours: params.hours });
|
|
1787
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1788
|
-
},
|
|
1789
|
-
}, {
|
|
1790
|
-
name: 'heartbeat_config',
|
|
1791
|
-
label: 'Heartbeat Config',
|
|
1792
|
-
description: 'View or update heartbeat settings for a thesis: scan intervals, model tier, budget cap, pause/resume. Also shows this month\'s cost breakdown.',
|
|
1793
|
-
parameters: Type.Object({
|
|
1794
|
-
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
1795
|
-
newsIntervalMin: Type.Optional(Type.Number({ description: 'News scan interval in minutes (15-1440)' })),
|
|
1796
|
-
xIntervalMin: Type.Optional(Type.Number({ description: 'X scan interval in minutes (60-1440)' })),
|
|
1797
|
-
evalModelTier: Type.Optional(Type.String({ description: 'Eval model: cheap, medium, or heavy' })),
|
|
1798
|
-
monthlyBudgetUsd: Type.Optional(Type.Number({ description: 'Monthly budget cap in USD (0 = unlimited)' })),
|
|
1799
|
-
paused: Type.Optional(Type.Boolean({ description: 'Pause (true) or resume (false) heartbeat' })),
|
|
1800
|
-
}),
|
|
1801
|
-
execute: async (_toolCallId, params) => {
|
|
1802
|
-
const hasUpdates = params.newsIntervalMin || params.xIntervalMin || params.evalModelTier || params.monthlyBudgetUsd !== undefined || params.paused !== undefined;
|
|
1803
|
-
if (hasUpdates) {
|
|
1804
|
-
const updates = {};
|
|
1805
|
-
if (params.newsIntervalMin)
|
|
1806
|
-
updates.newsIntervalMin = params.newsIntervalMin;
|
|
1807
|
-
if (params.xIntervalMin)
|
|
1808
|
-
updates.xIntervalMin = params.xIntervalMin;
|
|
1809
|
-
if (params.evalModelTier)
|
|
1810
|
-
updates.evalModelTier = params.evalModelTier;
|
|
1811
|
-
if (params.monthlyBudgetUsd !== undefined)
|
|
1812
|
-
updates.monthlyBudgetUsd = params.monthlyBudgetUsd;
|
|
1813
|
-
if (params.paused !== undefined)
|
|
1814
|
-
updates.paused = params.paused;
|
|
1815
|
-
await sfClient.updateHeartbeatConfig(params.thesisId, updates);
|
|
1816
|
-
}
|
|
1817
|
-
const data = await sfClient.getHeartbeatConfig(params.thesisId);
|
|
1818
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
1819
|
-
},
|
|
1820
|
-
});
|
|
1821
|
-
// ── Trading tools (conditional on tradingEnabled) ──────────────────────────
|
|
1822
|
-
const config = (0, config_js_1.loadConfig)();
|
|
1823
|
-
if (config.tradingEnabled) {
|
|
1824
|
-
tools.push({
|
|
1825
|
-
name: 'place_order',
|
|
1826
|
-
label: 'Place Order',
|
|
1827
|
-
description: 'Place a buy or sell order on Kalshi. Shows a preview and asks for user confirmation before executing. Use for limit or market orders.',
|
|
1828
|
-
parameters: Type.Object({
|
|
1829
|
-
ticker: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T135' }),
|
|
1830
|
-
side: Type.String({ description: 'yes or no' }),
|
|
1831
|
-
action: Type.String({ description: 'buy or sell' }),
|
|
1832
|
-
type: Type.String({ description: 'limit or market' }),
|
|
1833
|
-
count: Type.Number({ description: 'Number of contracts' }),
|
|
1834
|
-
price_cents: Type.Optional(Type.Number({ description: 'Limit price in cents (1-99). Required for limit orders.' })),
|
|
1835
|
-
}),
|
|
1836
|
-
execute: async (_toolCallId, params) => {
|
|
1837
|
-
const { createOrder } = await import('../kalshi.js');
|
|
1838
|
-
const priceCents = params.price_cents ? Math.round(Number(params.price_cents)) : undefined;
|
|
1839
|
-
const maxCost = ((priceCents || 99) * params.count / 100).toFixed(2);
|
|
1840
|
-
// Show preview
|
|
1841
|
-
const preview = [
|
|
1842
|
-
C.zinc200(bold('ORDER PREVIEW')),
|
|
1843
|
-
` Ticker: ${params.ticker}`,
|
|
1844
|
-
` Side: ${params.side === 'yes' ? C.emerald('YES') : C.red('NO')}`,
|
|
1845
|
-
` Action: ${params.action.toUpperCase()}`,
|
|
1846
|
-
` Quantity: ${params.count}`,
|
|
1847
|
-
` Type: ${params.type}`,
|
|
1848
|
-
priceCents ? ` Price: ${priceCents}\u00A2` : '',
|
|
1849
|
-
` Max cost: $${maxCost}`,
|
|
1850
|
-
].filter(Boolean).join('\n');
|
|
1851
|
-
addSystemText(preview);
|
|
1852
|
-
addSpacer();
|
|
1853
|
-
tui.requestRender();
|
|
1854
|
-
// Ask for confirmation via promptUser
|
|
1855
|
-
const answer = await promptUser('Execute this order? (y/n)');
|
|
1856
|
-
if (!answer.toLowerCase().startsWith('y')) {
|
|
1857
|
-
return { content: [{ type: 'text', text: 'Order cancelled by user.' }], details: {} };
|
|
1858
|
-
}
|
|
1859
|
-
try {
|
|
1860
|
-
const result = await createOrder({
|
|
1861
|
-
ticker: params.ticker,
|
|
1862
|
-
side: params.side,
|
|
1863
|
-
action: params.action,
|
|
1864
|
-
type: params.type,
|
|
1865
|
-
count: params.count,
|
|
1866
|
-
...(priceCents ? { yes_price: priceCents } : {}),
|
|
1867
|
-
});
|
|
1868
|
-
const order = result.order || result;
|
|
1869
|
-
return {
|
|
1870
|
-
content: [{ type: 'text', text: `Order placed: ${order.order_id || 'OK'}\nStatus: ${order.status || '-'}\nFilled: ${order.fill_count_fp || 0}/${order.initial_count_fp || params.count}` }],
|
|
1871
|
-
details: {},
|
|
1872
|
-
};
|
|
1873
|
-
}
|
|
1874
|
-
catch (err) {
|
|
1875
|
-
const msg = err.message || String(err);
|
|
1876
|
-
if (msg.includes('403')) {
|
|
1877
|
-
return { content: [{ type: 'text', text: '403 Forbidden \u2014 your Kalshi key lacks write permission. Get a read+write key at kalshi.com/account/api-keys' }], details: {} };
|
|
1878
|
-
}
|
|
1879
|
-
return { content: [{ type: 'text', text: `Order failed: ${msg}` }], details: {} };
|
|
1880
|
-
}
|
|
1881
|
-
},
|
|
1882
|
-
}, {
|
|
1883
|
-
name: 'cancel_order',
|
|
1884
|
-
label: 'Cancel Order',
|
|
1885
|
-
description: 'Cancel a resting order by order ID.',
|
|
1886
|
-
parameters: Type.Object({
|
|
1887
|
-
order_id: Type.String({ description: 'Order ID to cancel' }),
|
|
1888
|
-
}),
|
|
1889
|
-
execute: async (_toolCallId, params) => {
|
|
1890
|
-
const { cancelOrder } = await import('../kalshi.js');
|
|
1891
|
-
const answer = await promptUser(`Cancel order ${params.order_id}? (y/n)`);
|
|
1892
|
-
if (!answer.toLowerCase().startsWith('y')) {
|
|
1893
|
-
return { content: [{ type: 'text', text: 'Cancel aborted by user.' }], details: {} };
|
|
1894
|
-
}
|
|
1895
|
-
try {
|
|
1896
|
-
await cancelOrder(params.order_id);
|
|
1897
|
-
return { content: [{ type: 'text', text: `Order ${params.order_id} cancelled.` }], details: {} };
|
|
1898
|
-
}
|
|
1899
|
-
catch (err) {
|
|
1900
|
-
return { content: [{ type: 'text', text: `Cancel failed: ${err.message}` }], details: {} };
|
|
1901
|
-
}
|
|
1902
|
-
},
|
|
1903
|
-
});
|
|
1904
|
-
}
|
|
1905
|
-
// ── System prompt builder ──────────────────────────────────────────────────
|
|
1906
|
-
function buildSystemPrompt(ctx) {
|
|
1907
|
-
const edgesSummary = ctx.edges
|
|
1908
|
-
?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
1909
|
-
.slice(0, 5)
|
|
1910
|
-
.map((e) => ` ${(e.market || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}\u00A2 \u2192 thesis ${e.thesisPrice}\u00A2 | edge ${e.edge > 0 ? '+' : ''}${e.edge} | ${e.orderbook?.liquidityScore || '?'}`)
|
|
1911
|
-
.join('\n') || ' (no edge data)';
|
|
1912
|
-
const nodesSummary = ctx.causalTree?.nodes
|
|
1913
|
-
?.filter((n) => n.depth === 0)
|
|
1914
|
-
.map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
|
|
1915
|
-
.join('\n') || ' (no causal tree)';
|
|
1916
|
-
const conf = typeof ctx.confidence === 'number'
|
|
1917
|
-
? Math.round(ctx.confidence * 100)
|
|
1918
|
-
: (typeof ctx.confidence === 'string' ? parseInt(ctx.confidence) : 0);
|
|
1919
|
-
return `You are a prediction market trading assistant. Your job is not to please the user \u2014 it is to help them see reality clearly and make correct trading decisions.
|
|
1920
|
-
|
|
1921
|
-
## Your analytical framework
|
|
1922
|
-
|
|
1923
|
-
Each thesis has a causal tree. Every node is a causal hypothesis with a probability. Nodes have causal relationships \u2014 when upstream nodes change, downstream nodes follow.
|
|
1924
|
-
|
|
1925
|
-
Edge = thesis-implied price - actual market price. Positive edge = market underprices. Contracts with large edges AND good liquidity are most tradeable.
|
|
1926
|
-
|
|
1927
|
-
executableEdge = real edge after subtracting bid-ask spread. Big theoretical edge with wide spread may not be worth entering.
|
|
1928
|
-
|
|
1929
|
-
### Edge diagnosis (always classify)
|
|
1930
|
-
When reporting an edge, classify it:
|
|
1931
|
-
- "consensus gap": depth >= 500, market actively disagrees — real edge, real opponents
|
|
1932
|
-
- "attention gap": depth < 100, no real pricing — edge may be illusory, needs catalyst
|
|
1933
|
-
- "timing gap": market directionally agrees but lags — may close suddenly on news
|
|
1934
|
-
- "risk premium": edge reflects settlement ambiguity or platform risk, not alpha
|
|
1935
|
-
For edges > 20 cents, state in one sentence what must be true for the market to be right and the thesis wrong.
|
|
1936
|
-
|
|
1937
|
-
### Price reliability
|
|
1938
|
-
47 cents with depth 14K = strong consensus. 47 cents with depth 50 = three people's opinion.
|
|
1939
|
-
- depth >= 500: reliable, treat as market consensus
|
|
1940
|
-
- depth 100-500: moderate confidence, caveat when citing
|
|
1941
|
-
- depth < 100: unreliable — do NOT treat as "the market thinks X"
|
|
1942
|
-
- spread > 5 cents: wide disagreement, noisy midpoint
|
|
1943
|
-
- liquidityScore = low: do NOT recommend entry
|
|
1944
|
-
|
|
1945
|
-
### Kill condition awareness
|
|
1946
|
-
Each top-level causal node has an implicit falsifier. When scanning news or evaluating events, check: "Does any event here fundamentally break a core assumption?" If yes, flag immediately — this overrides all other analysis.
|
|
1947
|
-
|
|
1948
|
-
### Catalyst and time
|
|
1949
|
-
When discussing an edge, always state contract expiry and next identifiable catalyst. No visible catalyst = flag capital lock risk.
|
|
1950
|
-
|
|
1951
|
-
### Research workflow
|
|
1952
|
-
For complex questions, chain multiple tool calls:
|
|
1953
|
-
1. get_context 2. inspect_book 3. get_liquidity 4. web_search 5. search_x 6. synthesize
|
|
1954
|
-
Don't answer a complex question with a single tool call.
|
|
1955
|
-
|
|
1956
|
-
### Social signal research
|
|
1957
|
-
Use search_x to check X/Twitter sentiment on any topic — especially useful for geopolitical events, macro shifts, and breaking news that moves prediction markets. Use x_volume to detect discussion spikes (velocity > 1 = increasing attention). Use x_account to track specific analysts or officials.
|
|
1958
|
-
|
|
1959
|
-
### Heartbeat config
|
|
1960
|
-
Use heartbeat_config to view or adjust per-thesis heartbeat settings: news/X scan intervals, evaluation model tier (cheap/medium/heavy), monthly budget cap, pause/resume. Also shows this month's cost breakdown (LLM calls, search calls, tokens, spend). Useful when the user asks about costs, wants to slow down/speed up monitoring, or if you detect budget overrun.
|
|
1961
|
-
|
|
1962
|
-
### Conditional rules
|
|
1963
|
-
- Portfolio/positions questions: flag correlated exposure — positions sharing upstream causal nodes are not independent bets.
|
|
1964
|
-
- No catalyst visible within 30 days + edge not improving: flag "stale capital risk."
|
|
1965
|
-
- Edges < 10 cents, thin liquidity, no catalyst near: say "nothing to do right now." Do not manufacture urgency.
|
|
1966
|
-
|
|
1967
|
-
Short-term markets settle into hard data that calibrates the thesis. Use them to verify node probabilities, not to bet.
|
|
1968
|
-
|
|
1969
|
-
## Your behavioral rules
|
|
1970
|
-
|
|
1971
|
-
- IMPORTANT: You do NOT know the user's current positions at conversation start. Before discussing trades, recommending entries/exits, or analyzing portfolio risk, call get_positions FIRST. Never assume the user has no positions — they may have large existing holdings you don't know about.
|
|
1972
|
-
- Think before calling tools. If the data is already in context, don't re-fetch.
|
|
1973
|
-
- If the user says "note this" or mentions a news event, inject a signal. Don't ask "should I note this?"
|
|
1974
|
-
- If the user says "evaluate" or "run it", trigger immediately. Don't confirm.
|
|
1975
|
-
- Don't end every response with "anything else?" \u2014 the user will ask when they want to.
|
|
1976
|
-
- If the user asks about latest news or real-time events, use web_search first, then answer based on results. If you find important information, suggest injecting it as a signal.
|
|
1977
|
-
- If you notice an edge narrowing or disappearing, say so proactively. Don't only report good news.
|
|
1978
|
-
- If a causal tree node probability seriously contradicts the market price, point it out.
|
|
1979
|
-
- Use Chinese if the user writes in Chinese, English if they write in English.
|
|
1980
|
-
- For any question about prices, positions, or P&L, ALWAYS call a tool to get fresh data first. Never answer price-related questions using the cached data in this system prompt.
|
|
1981
|
-
- Prices are in cents (e.g. 35¢). P&L, cost, and balance are in dollars (e.g. $90.66). Tool outputs are pre-formatted with units — do not re-convert.
|
|
1982
|
-
- Align tables. Be precise with numbers to the cent.
|
|
1983
|
-
|
|
1984
|
-
## Strategy rules
|
|
1985
|
-
|
|
1986
|
-
When the conversation produces a concrete trade idea (specific contract, direction, price conditions), use create_strategy to record it immediately. Don't wait for the user to say "record this."
|
|
1987
|
-
- Extract hard conditions (specific prices in cents) into entryBelow/stopLoss/takeProfit.
|
|
1988
|
-
- Put fuzzy conditions into softConditions (e.g. "only if n3 > 60%", "spread < 3¢").
|
|
1989
|
-
- Put the full reasoning into rationale.
|
|
1990
|
-
- After creating, confirm the strategy details.
|
|
1991
|
-
- If the user says "change the stop loss on T150 to 30", use update_strategy.
|
|
1992
|
-
|
|
1993
|
-
## Trading status
|
|
1994
|
-
|
|
1995
|
-
${config.tradingEnabled ? 'Trading is ENABLED. You have place_order and cancel_order tools.' : 'Trading is DISABLED. You cannot place or cancel orders. Tell the user to run `sf setup --enable-trading` to unlock trading.'}
|
|
1996
|
-
|
|
1997
|
-
## Current thesis state
|
|
1998
|
-
|
|
1999
|
-
Thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
|
|
2000
|
-
ID: ${ctx.thesisId || resolvedThesisId}
|
|
2001
|
-
Confidence: ${conf}%
|
|
2002
|
-
Status: ${ctx.status}
|
|
2003
|
-
|
|
2004
|
-
Top-level causal tree nodes:
|
|
2005
|
-
${nodesSummary}
|
|
2006
|
-
|
|
2007
|
-
Top 5 edges by magnitude:
|
|
2008
|
-
${edgesSummary}
|
|
2009
|
-
|
|
2010
|
-
${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
|
|
2011
|
-
}
|
|
2012
|
-
function buildExplorerPrompt(ctx) {
|
|
2013
|
-
const config = (0, config_js_1.loadConfig)();
|
|
2014
|
-
const theseCount = ctx.theses?.length || 0;
|
|
2015
|
-
const edgeCount = ctx.edges?.length || 0;
|
|
2016
|
-
const topEdges = (ctx.edges || [])
|
|
2017
|
-
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
2018
|
-
.slice(0, 5)
|
|
2019
|
-
.map((e) => ` ${(e.title || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.price}¢ | edge +${e.edge}`)
|
|
2020
|
-
.join('\n') || ' (no edges)';
|
|
2021
|
-
return `You are a prediction market research assistant with access to live data across Kalshi, Polymarket, X/Twitter, and traditional markets.
|
|
2022
|
-
|
|
2023
|
-
You are in EXPLORER MODE — not bound to any specific thesis. Help the user research, compare, and understand prediction market data across all sources.
|
|
2024
|
-
|
|
2025
|
-
## What you can do
|
|
2026
|
-
- Search and compare markets across Kalshi and Polymarket (scan_markets)
|
|
2027
|
-
- Answer questions with live market data + LLM synthesis (query)
|
|
2028
|
-
- Check traditional market prices — SPY, VIX, gold, oil, bonds (get_markets)
|
|
2029
|
-
- Browse public theses and their edges (explore_public)
|
|
2030
|
-
- Search X/Twitter for sentiment and breaking news (search_x, x_volume, x_news)
|
|
2031
|
-
- Check orderbook depth and liquidity (inspect_book, get_liquidity)
|
|
2032
|
-
- View user positions across venues (get_positions)
|
|
2033
|
-
- Create a new thesis when the user forms a view (create_thesis)
|
|
2034
|
-
|
|
2035
|
-
## CRITICAL: Thesis creation transition
|
|
2036
|
-
When the user expresses a market view worth tracking — explicitly ("create a thesis") or implicitly ("I think oil stays above $100", "the war won't end soon") — use create_thesis to create it. After creation, tell the user: "Thesis created. The heartbeat engine is now monitoring this 24/7. Use /switch <id> to focus on it."
|
|
2037
|
-
|
|
2038
|
-
## Your analytical framework
|
|
2039
|
-
Edge = thesis price - market price. Positive = market underprices.
|
|
2040
|
-
Edge types: "consensus_gap" (real disagreement), "attention_gap" (no real pricing), "timing_gap" (market lags), "risk_premium" (settlement risk).
|
|
2041
|
-
Price reliability: depth >= 500 = consensus. depth < 100 = unreliable. spread > 5¢ = noisy.
|
|
2042
|
-
Always state contract expiry and next catalyst. No catalyst = capital lock risk.
|
|
2043
|
-
|
|
2044
|
-
## Your behavioral rules
|
|
2045
|
-
- Be concise. Use tools for fresh data. Don't guess prices.
|
|
2046
|
-
- You do NOT know the user's positions at start. Call get_positions before discussing trades.
|
|
2047
|
-
- If user mentions news, offer to create a thesis or inject a signal if one exists.
|
|
2048
|
-
- Don't end with "anything else?"
|
|
2049
|
-
- Use Chinese if user writes Chinese, English if English.
|
|
2050
|
-
- Prices in cents (¢). P&L in dollars ($).
|
|
2051
|
-
|
|
2052
|
-
## Trading status
|
|
2053
|
-
${config.tradingEnabled ? 'Trading is ENABLED.' : 'Trading is DISABLED. Tell user: sf setup --enable-trading'}
|
|
2054
|
-
|
|
2055
|
-
## Current market snapshot
|
|
2056
|
-
Public theses tracked: ${theseCount}
|
|
2057
|
-
Top edges across all public theses:
|
|
2058
|
-
${topEdges}
|
|
2059
|
-
`;
|
|
2060
|
-
}
|
|
2061
|
-
const systemPrompt = explorerMode
|
|
2062
|
-
? buildExplorerPrompt(latestContext)
|
|
2063
|
-
: buildSystemPrompt(latestContext);
|
|
2064
|
-
// ── Create Agent ───────────────────────────────────────────────────────────
|
|
2065
|
-
const agent = new Agent({
|
|
2066
|
-
initialState: {
|
|
2067
|
-
systemPrompt,
|
|
2068
|
-
model,
|
|
2069
|
-
tools,
|
|
2070
|
-
thinkingLevel: 'off',
|
|
2071
|
-
},
|
|
2072
|
-
streamFn: streamSimple,
|
|
2073
|
-
getApiKey: (provider) => {
|
|
2074
|
-
if (provider === 'openrouter')
|
|
2075
|
-
return openrouterKey;
|
|
2076
|
-
return undefined;
|
|
2077
|
-
},
|
|
2078
|
-
});
|
|
2079
|
-
// ── Session restore ────────────────────────────────────────────────────────
|
|
2080
|
-
let sessionRestored = false;
|
|
2081
|
-
if (!opts?.newSession) {
|
|
2082
|
-
const saved = loadSession(resolvedThesisId || '_explorer');
|
|
2083
|
-
if (saved?.messages?.length > 0) {
|
|
2084
|
-
try {
|
|
2085
|
-
// Clean corrupted messages: empty content, missing role, broken alternation
|
|
2086
|
-
const filtered = saved.messages.filter((m) => {
|
|
2087
|
-
if (!m.role)
|
|
2088
|
-
return false;
|
|
2089
|
-
if (Array.isArray(m.content) && m.content.length === 0)
|
|
2090
|
-
return false;
|
|
2091
|
-
if (m.role === 'assistant' && !m.content && !m.tool_calls?.length)
|
|
2092
|
-
return false;
|
|
2093
|
-
return true;
|
|
2094
|
-
});
|
|
2095
|
-
// Fix alternation: ensure user → assistant → user → assistant
|
|
2096
|
-
// Drop consecutive messages of the same role (keep first)
|
|
2097
|
-
const cleaned = [];
|
|
2098
|
-
for (const m of filtered) {
|
|
2099
|
-
const lastRole = cleaned.length > 0 ? cleaned[cleaned.length - 1].role : null;
|
|
2100
|
-
if (m.role === lastRole && m.role !== 'tool') {
|
|
2101
|
-
// Skip consecutive same-role (except tool messages which can follow each other)
|
|
2102
|
-
continue;
|
|
2103
|
-
}
|
|
2104
|
-
cleaned.push(m);
|
|
2105
|
-
}
|
|
2106
|
-
// Ensure conversation doesn't end with user message (API needs assistant reply)
|
|
2107
|
-
if (cleaned.length > 0 && cleaned[cleaned.length - 1].role === 'user') {
|
|
2108
|
-
cleaned.pop();
|
|
2109
|
-
}
|
|
2110
|
-
agent.replaceMessages(cleaned);
|
|
2111
|
-
// Always update system prompt with fresh context
|
|
2112
|
-
agent.setSystemPrompt(systemPrompt);
|
|
2113
|
-
sessionRestored = true;
|
|
2114
|
-
}
|
|
2115
|
-
catch { /* corrupt session, start fresh */ }
|
|
2116
|
-
}
|
|
2117
|
-
}
|
|
2118
|
-
// Helper to persist session after each turn
|
|
2119
|
-
function persistSession() {
|
|
2120
|
-
try {
|
|
2121
|
-
const msgs = agent.state.messages;
|
|
2122
|
-
if (msgs.length > 0) {
|
|
2123
|
-
saveSession(resolvedThesisId || '_explorer', currentModelName, msgs);
|
|
2124
|
-
}
|
|
2125
|
-
}
|
|
2126
|
-
catch { /* best-effort save */ }
|
|
2127
|
-
}
|
|
2128
|
-
// ── Subscribe to agent events → update TUI ────────────────────────────────
|
|
2129
|
-
let currentAssistantMd = null;
|
|
2130
|
-
let currentAssistantText = '';
|
|
2131
|
-
let currentLoader = null;
|
|
2132
|
-
const toolStartTimes = new Map();
|
|
2133
|
-
const toolLines = new Map();
|
|
2134
|
-
// Throttle renders during streaming to prevent flicker (max ~15fps)
|
|
2135
|
-
let renderTimer = null;
|
|
2136
|
-
function throttledRender() {
|
|
2137
|
-
if (renderTimer)
|
|
2138
|
-
return;
|
|
2139
|
-
renderTimer = setTimeout(() => {
|
|
2140
|
-
renderTimer = null;
|
|
2141
|
-
tui.requestRender();
|
|
2142
|
-
}, 66);
|
|
2143
|
-
}
|
|
2144
|
-
function flushRender() {
|
|
2145
|
-
if (renderTimer) {
|
|
2146
|
-
clearTimeout(renderTimer);
|
|
2147
|
-
renderTimer = null;
|
|
2148
|
-
}
|
|
2149
|
-
tui.requestRender();
|
|
2150
|
-
}
|
|
2151
|
-
agent.subscribe((event) => {
|
|
2152
|
-
if (event.type === 'message_start') {
|
|
2153
|
-
// Show loader while waiting for first text
|
|
2154
|
-
currentAssistantText = '';
|
|
2155
|
-
currentAssistantMd = null;
|
|
2156
|
-
currentLoader = new Loader(tui, (s) => C.emerald(s), (s) => C.zinc600(s), 'thinking...');
|
|
2157
|
-
currentLoader.start();
|
|
2158
|
-
chatContainer.addChild(currentLoader);
|
|
2159
|
-
tui.requestRender();
|
|
2160
|
-
}
|
|
2161
|
-
if (event.type === 'message_update') {
|
|
2162
|
-
const e = event.assistantMessageEvent;
|
|
2163
|
-
if (e.type === 'text_delta') {
|
|
2164
|
-
// Remove loader on first text delta
|
|
2165
|
-
if (currentLoader) {
|
|
2166
|
-
currentLoader.stop();
|
|
2167
|
-
chatContainer.removeChild(currentLoader);
|
|
2168
|
-
currentLoader = null;
|
|
2169
|
-
// Create markdown component for assistant response
|
|
2170
|
-
currentAssistantMd = new Markdown('', 1, 0, mdTheme, mdDefaultStyle);
|
|
2171
|
-
chatContainer.addChild(currentAssistantMd);
|
|
2172
|
-
}
|
|
2173
|
-
currentAssistantText += e.delta;
|
|
2174
|
-
if (currentAssistantMd) {
|
|
2175
|
-
currentAssistantMd.setText(currentAssistantText);
|
|
2176
|
-
}
|
|
2177
|
-
// Throttled render to prevent flicker during fast token streaming
|
|
2178
|
-
throttledRender();
|
|
2179
|
-
}
|
|
2180
|
-
}
|
|
2181
|
-
if (event.type === 'message_end') {
|
|
2182
|
-
// Clean up loader if still present (no text was generated)
|
|
2183
|
-
if (currentLoader) {
|
|
2184
|
-
currentLoader.stop();
|
|
2185
|
-
chatContainer.removeChild(currentLoader);
|
|
2186
|
-
currentLoader = null;
|
|
2187
|
-
}
|
|
2188
|
-
// Final render of the complete message
|
|
2189
|
-
if (currentAssistantMd && currentAssistantText) {
|
|
2190
|
-
currentAssistantMd.setText(currentAssistantText);
|
|
2191
|
-
}
|
|
2192
|
-
addSpacer();
|
|
2193
|
-
currentAssistantMd = null;
|
|
2194
|
-
currentAssistantText = '';
|
|
2195
|
-
flushRender();
|
|
2196
|
-
}
|
|
2197
|
-
if (event.type === 'agent_end') {
|
|
2198
|
-
// Agent turn fully complete — safe to accept new input
|
|
2199
|
-
isProcessing = false;
|
|
2200
|
-
persistSession();
|
|
2201
|
-
flushRender();
|
|
2202
|
-
// Deliver queued heartbeat notification if any
|
|
2203
|
-
if (pendingHeartbeatDelta) {
|
|
2204
|
-
const delta = pendingHeartbeatDelta;
|
|
2205
|
-
pendingHeartbeatDelta = null;
|
|
2206
|
-
handleHeartbeatDelta(delta);
|
|
2207
|
-
}
|
|
2208
|
-
}
|
|
2209
|
-
if (event.type === 'tool_execution_start') {
|
|
2210
|
-
const toolLine = new MutableLine(C.zinc600(` \u25B8 ${event.toolName}...`));
|
|
2211
|
-
toolStartTimes.set(event.toolCallId || event.toolName, Date.now());
|
|
2212
|
-
toolLines.set(event.toolCallId || event.toolName, toolLine);
|
|
2213
|
-
chatContainer.addChild(toolLine);
|
|
2214
|
-
totalToolCalls++;
|
|
2215
|
-
footerBar.toolCount = totalToolCalls;
|
|
2216
|
-
footerBar.update();
|
|
2217
|
-
tui.requestRender();
|
|
2218
|
-
}
|
|
2219
|
-
if (event.type === 'tool_execution_end') {
|
|
2220
|
-
const key = event.toolCallId || event.toolName;
|
|
2221
|
-
const startTime = toolStartTimes.get(key);
|
|
2222
|
-
const elapsed = startTime ? ((Date.now() - startTime) / 1000).toFixed(1) : '?';
|
|
2223
|
-
const line = toolLines.get(key);
|
|
2224
|
-
if (line) {
|
|
2225
|
-
if (event.isError) {
|
|
2226
|
-
line.setText(C.red(` \u2717 ${event.toolName} (${elapsed}s) error`));
|
|
2227
|
-
}
|
|
2228
|
-
else {
|
|
2229
|
-
line.setText(C.zinc600(` \u25B8 ${event.toolName} `) + C.emerald(`\u2713`) + C.zinc600(` ${elapsed}s`));
|
|
2230
|
-
}
|
|
2231
|
-
}
|
|
2232
|
-
toolStartTimes.delete(key);
|
|
2233
|
-
toolLines.delete(key);
|
|
2234
|
-
tui.requestRender();
|
|
2235
|
-
}
|
|
2236
|
-
});
|
|
2237
|
-
// ── Slash command handlers ─────────────────────────────────────────────────
|
|
2238
|
-
async function handleSlashCommand(cmd) {
|
|
2239
|
-
const parts = cmd.trim().split(/\s+/);
|
|
2240
|
-
const command = parts[0].toLowerCase();
|
|
2241
|
-
switch (command) {
|
|
2242
|
-
case '/help': {
|
|
2243
|
-
addSpacer();
|
|
2244
|
-
addSystemText(C.zinc200(bold('Commands')) + '\n' +
|
|
2245
|
-
C.emerald('/help ') + C.zinc400('Show this help') + '\n' +
|
|
2246
|
-
C.emerald('/tree ') + C.zinc400('Display causal tree') + '\n' +
|
|
2247
|
-
C.emerald('/edges ') + C.zinc400('Display edge/spread table') + '\n' +
|
|
2248
|
-
C.emerald('/pos ') + C.zinc400('Display Kalshi positions') + '\n' +
|
|
2249
|
-
C.emerald('/eval ') + C.zinc400('Trigger deep evaluation') + '\n' +
|
|
2250
|
-
C.emerald('/switch <id>') + C.zinc400(' Switch thesis') + '\n' +
|
|
2251
|
-
C.emerald('/compact ') + C.zinc400('Compress conversation history') + '\n' +
|
|
2252
|
-
C.emerald('/new ') + C.zinc400('Start fresh session') + '\n' +
|
|
2253
|
-
C.emerald('/model <m> ') + C.zinc400('Switch model') + '\n' +
|
|
2254
|
-
C.emerald('/env ') + C.zinc400('Show environment variable status') + '\n' +
|
|
2255
|
-
(config.tradingEnabled ? (C.zinc600('\u2500'.repeat(30)) + '\n' +
|
|
2256
|
-
C.emerald('/buy ') + C.zinc400('TICKER QTY PRICE \u2014 quick buy') + '\n' +
|
|
2257
|
-
C.emerald('/sell ') + C.zinc400('TICKER QTY PRICE \u2014 quick sell') + '\n' +
|
|
2258
|
-
C.emerald('/cancel ') + C.zinc400('ORDER_ID \u2014 cancel order') + '\n' +
|
|
2259
|
-
C.zinc600('\u2500'.repeat(30)) + '\n') : '') +
|
|
2260
|
-
(skills.length > 0 ? (C.zinc600('\u2500'.repeat(30)) + '\n' +
|
|
2261
|
-
C.zinc200(bold('Skills')) + '\n' +
|
|
2262
|
-
skills.map(s => C.emerald(`/${s.name.padEnd(10)}`) + C.zinc400(s.description.slice(0, 45))).join('\n') + '\n' +
|
|
2263
|
-
C.zinc600('\u2500'.repeat(30)) + '\n') : '') +
|
|
2264
|
-
C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
|
|
2265
|
-
C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
|
|
2266
|
-
addSpacer();
|
|
2267
|
-
return true;
|
|
2268
|
-
}
|
|
2269
|
-
case '/tree': {
|
|
2270
|
-
addSpacer();
|
|
2271
|
-
if (explorerMode) {
|
|
2272
|
-
addSystemText(C.zinc400('No thesis selected. Use /switch <id> to pick one, or ask me to create one.'));
|
|
2273
|
-
}
|
|
2274
|
-
else {
|
|
2275
|
-
try {
|
|
2276
|
-
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2277
|
-
addSystemText(C.zinc200(bold('Causal Tree')) + '\n' + renderCausalTree(latestContext, piTui));
|
|
2278
|
-
}
|
|
2279
|
-
catch (err) {
|
|
2280
|
-
addSystemText(C.red(`Error: ${err.message}`));
|
|
2281
|
-
}
|
|
2282
|
-
}
|
|
2283
|
-
addSpacer();
|
|
2284
|
-
return true;
|
|
2285
|
-
}
|
|
2286
|
-
case '/edges': {
|
|
2287
|
-
addSpacer();
|
|
2288
|
-
if (explorerMode) {
|
|
2289
|
-
// Show global public edges
|
|
2290
|
-
try {
|
|
2291
|
-
const { fetchGlobalContext } = await import('../client.js');
|
|
2292
|
-
const global = await fetchGlobalContext();
|
|
2293
|
-
const edges = (global.edges || []).sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge)).slice(0, 10);
|
|
2294
|
-
if (edges.length === 0) {
|
|
2295
|
-
addSystemText(C.zinc400('No public edges available.'));
|
|
2296
|
-
}
|
|
2297
|
-
else {
|
|
2298
|
-
const lines = edges.map((e) => {
|
|
2299
|
-
const name = (e.title || '').slice(0, 35).padEnd(35);
|
|
2300
|
-
const venue = (e.venue || 'kalshi').padEnd(5);
|
|
2301
|
-
const mkt = String(Math.round(e.price || 0)).padStart(3) + '¢';
|
|
2302
|
-
const edge = '+' + Math.round(e.edge || 0);
|
|
2303
|
-
return ` ${C.zinc400(name)} ${C.zinc600(venue)} ${C.zinc400(mkt)} ${C.emerald(edge.padStart(4))}`;
|
|
2304
|
-
}).join('\n');
|
|
2305
|
-
addSystemText(C.zinc200(bold('Public Edges')) + '\n' + lines);
|
|
2306
|
-
}
|
|
2307
|
-
}
|
|
2308
|
-
catch (err) {
|
|
2309
|
-
addSystemText(C.red(`Error: ${err.message}`));
|
|
2310
|
-
}
|
|
2311
|
-
}
|
|
2312
|
-
else {
|
|
2313
|
-
try {
|
|
2314
|
-
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2315
|
-
if (cachedPositions) {
|
|
2316
|
-
latestContext._positions = cachedPositions;
|
|
2317
|
-
}
|
|
2318
|
-
addSystemText(C.zinc200(bold('Edges')) + '\n' + renderEdges(latestContext, piTui));
|
|
2319
|
-
}
|
|
2320
|
-
catch (err) {
|
|
2321
|
-
addSystemText(C.red(`Error: ${err.message}`));
|
|
2322
|
-
}
|
|
2323
|
-
}
|
|
2324
|
-
addSpacer();
|
|
2325
|
-
return true;
|
|
2326
|
-
}
|
|
2327
|
-
case '/pos': {
|
|
2328
|
-
addSpacer();
|
|
2329
|
-
try {
|
|
2330
|
-
const positions = await (0, kalshi_js_1.getPositions)();
|
|
2331
|
-
if (!positions) {
|
|
2332
|
-
addSystemText(C.zinc600('Kalshi not configured'));
|
|
2333
|
-
return true;
|
|
2334
|
-
}
|
|
2335
|
-
for (const pos of positions) {
|
|
2336
|
-
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
2337
|
-
if (livePrice !== null) {
|
|
2338
|
-
pos.current_value = livePrice;
|
|
2339
|
-
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
2340
|
-
}
|
|
2341
|
-
}
|
|
2342
|
-
cachedPositions = positions;
|
|
2343
|
-
addSystemText(C.zinc200(bold('Positions')) + '\n' + renderPositions(positions));
|
|
2344
|
-
}
|
|
2345
|
-
catch (err) {
|
|
2346
|
-
addSystemText(C.red(`Error: ${err.message}`));
|
|
2347
|
-
}
|
|
2348
|
-
addSpacer();
|
|
2349
|
-
return true;
|
|
2350
|
-
}
|
|
2351
|
-
case '/eval': {
|
|
2352
|
-
addSpacer();
|
|
2353
|
-
if (explorerMode) {
|
|
2354
|
-
addSystemText(C.zinc400('No thesis selected. Use /switch <id> to pick one, or ask me to create one.'));
|
|
2355
|
-
addSpacer();
|
|
2356
|
-
return true;
|
|
2357
|
-
}
|
|
2358
|
-
addSystemText(C.zinc600('Triggering evaluation...'));
|
|
2359
|
-
tui.requestRender();
|
|
2360
|
-
try {
|
|
2361
|
-
const result = await sfClient.evaluate(resolvedThesisId);
|
|
2362
|
-
addSystemText(C.emerald('Evaluation complete') + '\n' + C.zinc400(JSON.stringify(result, null, 2)));
|
|
2363
|
-
}
|
|
2364
|
-
catch (err) {
|
|
2365
|
-
addSystemText(C.red(`Error: ${err.message}`));
|
|
2366
|
-
}
|
|
2367
|
-
addSpacer();
|
|
2368
|
-
return true;
|
|
2369
|
-
}
|
|
2370
|
-
case '/model': {
|
|
2371
|
-
const newModel = parts.slice(1).join(' ').trim();
|
|
2372
|
-
if (!newModel) {
|
|
2373
|
-
addSystemText(C.zinc400(`Current model: ${currentModelName}`));
|
|
2374
|
-
return true;
|
|
2375
|
-
}
|
|
2376
|
-
addSpacer();
|
|
2377
|
-
currentModelName = newModel.replace(/^openrouter\//, '');
|
|
2378
|
-
model = resolveModel(currentModelName);
|
|
2379
|
-
// Update agent model
|
|
2380
|
-
agent.setModel(model);
|
|
2381
|
-
footerBar.modelName = currentModelName;
|
|
2382
|
-
footerBar.update();
|
|
2383
|
-
addSystemText(C.emerald(`Model switched to ${currentModelName}`));
|
|
2384
|
-
addSpacer();
|
|
2385
|
-
tui.requestRender();
|
|
2386
|
-
return true;
|
|
2387
|
-
}
|
|
2388
|
-
case '/switch': {
|
|
2389
|
-
const newId = parts[1]?.trim();
|
|
2390
|
-
if (!newId) {
|
|
2391
|
-
addSystemText(C.zinc400('Usage: /switch <thesisId>'));
|
|
2392
|
-
return true;
|
|
2393
|
-
}
|
|
2394
|
-
addSpacer();
|
|
2395
|
-
try {
|
|
2396
|
-
// Save current session
|
|
2397
|
-
persistSession();
|
|
2398
|
-
// Load new thesis context
|
|
2399
|
-
const newContext = await sfClient.getContext(newId);
|
|
2400
|
-
resolvedThesisId = newContext.thesisId || newId;
|
|
2401
|
-
latestContext = newContext;
|
|
2402
|
-
// Build new system prompt using the rich builder
|
|
2403
|
-
const newSysPrompt = buildSystemPrompt(newContext);
|
|
2404
|
-
const newConf = typeof newContext.confidence === 'number'
|
|
2405
|
-
? Math.round(newContext.confidence * 100) : 0;
|
|
2406
|
-
// CRITICAL: Always clearMessages() first to reset agent internal state.
|
|
2407
|
-
// replaceMessages() on a mid-conversation agent corrupts pi-agent-core's
|
|
2408
|
-
// state machine, causing the TUI to freeze.
|
|
2409
|
-
agent.clearMessages();
|
|
2410
|
-
// Load saved session or start fresh
|
|
2411
|
-
const saved = loadSession(resolvedThesisId);
|
|
2412
|
-
if (saved?.messages?.length > 0) {
|
|
2413
|
-
agent.replaceMessages(saved.messages);
|
|
2414
|
-
agent.setSystemPrompt(newSysPrompt);
|
|
2415
|
-
addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(` (resumed ${saved.messages.length} messages)`));
|
|
2416
|
-
}
|
|
2417
|
-
else {
|
|
2418
|
-
agent.setSystemPrompt(newSysPrompt);
|
|
2419
|
-
addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
|
|
2420
|
-
}
|
|
2421
|
-
// Update header
|
|
2422
|
-
footerBar.setFromContext(newContext, initialPositions || undefined);
|
|
2423
|
-
chatContainer.clear();
|
|
2424
|
-
addSystemText(buildWelcomeDashboard(newContext, initialPositions));
|
|
2425
|
-
}
|
|
2426
|
-
catch (err) {
|
|
2427
|
-
addSystemText(C.red(`Switch failed: ${err.message}`));
|
|
2428
|
-
}
|
|
2429
|
-
addSpacer();
|
|
2430
|
-
// Force re-focus editor so input stays responsive
|
|
2431
|
-
tui.setFocus(editor);
|
|
2432
|
-
tui.requestRender();
|
|
2433
|
-
return true;
|
|
2434
|
-
}
|
|
2435
|
-
case '/compact': {
|
|
2436
|
-
addSpacer();
|
|
2437
|
-
try {
|
|
2438
|
-
const msgs = agent.state.messages;
|
|
2439
|
-
if (msgs.length <= 10) {
|
|
2440
|
-
addSystemText(C.zinc400('Conversation too short to compact'));
|
|
2441
|
-
addSpacer();
|
|
2442
|
-
tui.setFocus(editor);
|
|
2443
|
-
return true;
|
|
2444
|
-
}
|
|
2445
|
-
// ── Find clean cut point ──────────────────────────────────────
|
|
2446
|
-
// Walk backwards counting user messages as turn starts.
|
|
2447
|
-
// Keep 3 complete turns. Never split a tool_call/tool_result pair.
|
|
2448
|
-
const turnsToKeep = 3;
|
|
2449
|
-
let turnsSeen = 0;
|
|
2450
|
-
let cutIndex = msgs.length;
|
|
2451
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
2452
|
-
if (msgs[i].role === 'user') {
|
|
2453
|
-
turnsSeen++;
|
|
2454
|
-
if (turnsSeen >= turnsToKeep) {
|
|
2455
|
-
cutIndex = i;
|
|
2456
|
-
break;
|
|
2457
|
-
}
|
|
2458
|
-
}
|
|
2459
|
-
}
|
|
2460
|
-
if (cutIndex <= 2) {
|
|
2461
|
-
addSystemText(C.zinc400('Not enough complete turns to compact'));
|
|
2462
|
-
addSpacer();
|
|
2463
|
-
tui.setFocus(editor);
|
|
2464
|
-
return true;
|
|
2465
|
-
}
|
|
2466
|
-
const toCompress = msgs.slice(0, cutIndex);
|
|
2467
|
-
const toKeep = msgs.slice(cutIndex);
|
|
2468
|
-
// ── Show loader ───────────────────────────────────────────────
|
|
2469
|
-
const compactLoader = new Loader(tui, (s) => C.emerald(s), (s) => C.zinc600(s), 'compacting with LLM...');
|
|
2470
|
-
compactLoader.start();
|
|
2471
|
-
chatContainer.addChild(compactLoader);
|
|
2472
|
-
tui.requestRender();
|
|
2473
|
-
// ── Serialize messages for the summarizer ─────────────────────
|
|
2474
|
-
// Strip tool results to raw text, cap total length to ~12k chars
|
|
2475
|
-
const serialized = [];
|
|
2476
|
-
let totalLen = 0;
|
|
2477
|
-
const MAX_CHARS = 12000;
|
|
2478
|
-
for (const m of toCompress) {
|
|
2479
|
-
if (totalLen >= MAX_CHARS)
|
|
2480
|
-
break;
|
|
2481
|
-
let text = '';
|
|
2482
|
-
if (typeof m.content === 'string') {
|
|
2483
|
-
text = m.content;
|
|
2484
|
-
}
|
|
2485
|
-
else if (Array.isArray(m.content)) {
|
|
2486
|
-
// OpenAI format: content blocks
|
|
2487
|
-
text = m.content
|
|
2488
|
-
.filter((b) => b.type === 'text')
|
|
2489
|
-
.map((b) => b.text)
|
|
2490
|
-
.join('\n');
|
|
2491
|
-
}
|
|
2492
|
-
if (!text)
|
|
2493
|
-
continue;
|
|
2494
|
-
const role = (m.role || 'unknown').toUpperCase();
|
|
2495
|
-
const truncated = text.slice(0, 800);
|
|
2496
|
-
const line = `[${role}]: ${truncated}`;
|
|
2497
|
-
serialized.push(line);
|
|
2498
|
-
totalLen += line.length;
|
|
2499
|
-
}
|
|
2500
|
-
const conversationDump = serialized.join('\n\n');
|
|
2501
|
-
// ── Call OpenRouter for LLM summary ───────────────────────────
|
|
2502
|
-
// Use a cheap/fast model — gemini flash
|
|
2503
|
-
const summaryModel = 'google/gemini-2.0-flash-001';
|
|
2504
|
-
const summarySystemPrompt = `You are a conversation compressor. Given a conversation between a user and a prediction-market trading assistant, produce a dense summary that preserves:
|
|
2505
|
-
1. All factual conclusions, numbers, prices, and probabilities mentioned
|
|
2506
|
-
2. Key trading decisions, positions taken or discussed
|
|
2507
|
-
3. Signals injected, evaluations triggered, and their outcomes
|
|
2508
|
-
4. Any action items or pending questions
|
|
2509
|
-
|
|
2510
|
-
Output a structured summary. Be concise but preserve every important detail — this summary replaces the original messages for continued conversation. Do NOT add commentary or meta-text. Just the summary.`;
|
|
2511
|
-
let summaryText;
|
|
2512
|
-
try {
|
|
2513
|
-
const compactUrl = useProxy
|
|
2514
|
-
? `${sfApiUrl}/api/proxy/llm`
|
|
2515
|
-
: 'https://openrouter.ai/api/v1/chat/completions';
|
|
2516
|
-
const compactBody = {
|
|
2517
|
-
model: summaryModel,
|
|
2518
|
-
messages: [
|
|
2519
|
-
{ role: 'system', content: summarySystemPrompt },
|
|
2520
|
-
{ role: 'user', content: `Summarize this conversation (${toCompress.length} messages):\n\n${conversationDump}` },
|
|
2521
|
-
],
|
|
2522
|
-
max_tokens: 2000,
|
|
2523
|
-
temperature: 0.2,
|
|
2524
|
-
};
|
|
2525
|
-
const orRes = await fetch(compactUrl, {
|
|
2526
|
-
method: 'POST',
|
|
2527
|
-
headers: {
|
|
2528
|
-
'Content-Type': 'application/json',
|
|
2529
|
-
'Authorization': `Bearer ${openrouterKey}`,
|
|
2530
|
-
'HTTP-Referer': 'https://simplefunctions.com',
|
|
2531
|
-
'X-Title': 'SF Agent Compact',
|
|
2532
|
-
},
|
|
2533
|
-
body: JSON.stringify(compactBody),
|
|
2534
|
-
});
|
|
2535
|
-
if (!orRes.ok) {
|
|
2536
|
-
const errText = await orRes.text().catch(() => '');
|
|
2537
|
-
throw new Error(`OpenRouter ${orRes.status}: ${errText.slice(0, 200)}`);
|
|
2538
|
-
}
|
|
2539
|
-
const orData = await orRes.json();
|
|
2540
|
-
summaryText = orData.choices?.[0]?.message?.content || '';
|
|
2541
|
-
if (!summaryText) {
|
|
2542
|
-
throw new Error('Empty summary from LLM');
|
|
2543
|
-
}
|
|
2544
|
-
}
|
|
2545
|
-
catch (llmErr) {
|
|
2546
|
-
// LLM failed — fall back to bullet-point extraction
|
|
2547
|
-
const bulletPoints = [];
|
|
2548
|
-
for (const m of toCompress) {
|
|
2549
|
-
const content = typeof m.content === 'string' ? m.content : '';
|
|
2550
|
-
if (m.role === 'user' && content) {
|
|
2551
|
-
bulletPoints.push(`- User: ${content.slice(0, 100)}`);
|
|
2552
|
-
}
|
|
2553
|
-
else if (m.role === 'assistant' && content) {
|
|
2554
|
-
bulletPoints.push(`- Assistant: ${content.slice(0, 150)}`);
|
|
2555
|
-
}
|
|
2556
|
-
}
|
|
2557
|
-
summaryText = `[LLM summary failed: ${llmErr.message}. Fallback bullet points:]\n\n${bulletPoints.slice(-20).join('\n')}`;
|
|
2558
|
-
}
|
|
2559
|
-
// ── Remove loader ─────────────────────────────────────────────
|
|
2560
|
-
compactLoader.stop();
|
|
2561
|
-
chatContainer.removeChild(compactLoader);
|
|
2562
|
-
// ── Build compacted message array ──────────────────────────────
|
|
2563
|
-
// user(summary) → assistant(ack) → ...toKeep
|
|
2564
|
-
// This maintains valid user→assistant alternation.
|
|
2565
|
-
// toKeep starts with a user message (guaranteed by our cut logic).
|
|
2566
|
-
const compactedMessages = [
|
|
2567
|
-
{
|
|
2568
|
-
role: 'user',
|
|
2569
|
-
content: `[Conversation summary — ${toCompress.length} messages compressed]\n\n${summaryText}`,
|
|
2570
|
-
},
|
|
2571
|
-
{
|
|
2572
|
-
role: 'assistant',
|
|
2573
|
-
content: 'Understood. I have the full conversation context from the summary above. Continuing from where we left off.',
|
|
2574
|
-
},
|
|
2575
|
-
...toKeep,
|
|
2576
|
-
];
|
|
2577
|
-
// ── Replace agent state ───────────────────────────────────────
|
|
2578
|
-
// Clear first to reset internal state, then load compacted messages
|
|
2579
|
-
agent.clearMessages();
|
|
2580
|
-
agent.replaceMessages(compactedMessages);
|
|
2581
|
-
agent.setSystemPrompt(systemPrompt);
|
|
2582
|
-
persistSession();
|
|
2583
|
-
addSystemText(C.emerald(`Compacted: ${toCompress.length} messages \u2192 summary + ${toKeep.length} recent`) +
|
|
2584
|
-
C.zinc600(` (via ${summaryModel.split('/').pop()})`));
|
|
2585
|
-
addSpacer();
|
|
2586
|
-
// Force re-focus and render so editor stays responsive
|
|
2587
|
-
tui.setFocus(editor);
|
|
2588
|
-
tui.requestRender();
|
|
2589
|
-
}
|
|
2590
|
-
catch (err) {
|
|
2591
|
-
addSystemText(C.red(`Compact failed: ${err.message || err}`));
|
|
2592
|
-
addSpacer();
|
|
2593
|
-
tui.setFocus(editor);
|
|
2594
|
-
tui.requestRender();
|
|
2595
|
-
}
|
|
2596
|
-
return true;
|
|
2597
|
-
}
|
|
2598
|
-
case '/new': {
|
|
2599
|
-
addSpacer();
|
|
2600
|
-
persistSession(); // save current before clearing
|
|
2601
|
-
agent.clearMessages();
|
|
2602
|
-
agent.setSystemPrompt(systemPrompt);
|
|
2603
|
-
chatContainer.clear();
|
|
2604
|
-
addSystemText(C.emerald('Session cleared') + C.zinc400(' \u2014 fresh start'));
|
|
2605
|
-
addSpacer();
|
|
2606
|
-
tui.requestRender();
|
|
2607
|
-
return true;
|
|
2608
|
-
}
|
|
2609
|
-
case '/env': {
|
|
2610
|
-
addSpacer();
|
|
2611
|
-
const envVars = [
|
|
2612
|
-
{ name: 'SF_API_KEY', key: 'SF_API_KEY', required: true, mask: true },
|
|
2613
|
-
{ name: 'SF_API_URL', key: 'SF_API_URL', required: false, mask: false },
|
|
2614
|
-
{ name: 'OPENROUTER_KEY', key: 'OPENROUTER_API_KEY', required: true, mask: true },
|
|
2615
|
-
{ name: 'KALSHI_KEY_ID', key: 'KALSHI_API_KEY_ID', required: false, mask: true },
|
|
2616
|
-
{ name: 'KALSHI_PEM_PATH', key: 'KALSHI_PRIVATE_KEY_PATH', required: false, mask: false },
|
|
2617
|
-
{ name: 'TAVILY_API_KEY', key: 'TAVILY_API_KEY', required: false, mask: true },
|
|
2618
|
-
];
|
|
2619
|
-
const lines = envVars.map(v => {
|
|
2620
|
-
const val = process.env[v.key];
|
|
2621
|
-
if (val) {
|
|
2622
|
-
const display = v.mask
|
|
2623
|
-
? val.slice(0, Math.min(8, val.length)) + '...' + val.slice(-4)
|
|
2624
|
-
: val;
|
|
2625
|
-
return ` ${v.name.padEnd(18)} ${C.emerald('\u2713')} ${C.zinc400(display)}`;
|
|
2626
|
-
}
|
|
2627
|
-
else {
|
|
2628
|
-
const note = v.required ? '\u5FC5\u987B' : '\u53EF\u9009';
|
|
2629
|
-
return ` ${v.name.padEnd(18)} ${C.red('\u2717')} ${C.zinc600(`\u672A\u914D\u7F6E\uFF08${note}\uFF09`)}`;
|
|
2630
|
-
}
|
|
2631
|
-
});
|
|
2632
|
-
addSystemText(C.zinc200(bold('Environment')) + '\n' + lines.join('\n'));
|
|
2633
|
-
addSpacer();
|
|
2634
|
-
return true;
|
|
2635
|
-
}
|
|
2636
|
-
case '/clear': {
|
|
2637
|
-
// Don't use chatContainer.clear() — it breaks pi-tui layout.
|
|
2638
|
-
// Instead, remove children one by one and add a fresh spacer
|
|
2639
|
-
// so the container still has content for layout calculations.
|
|
2640
|
-
const children = [...chatContainer.children || []];
|
|
2641
|
-
for (const child of children) {
|
|
2642
|
-
try {
|
|
2643
|
-
chatContainer.removeChild(child);
|
|
2644
|
-
}
|
|
2645
|
-
catch { /* ignore */ }
|
|
2646
|
-
}
|
|
2647
|
-
addSpacer();
|
|
2648
|
-
isProcessing = false;
|
|
2649
|
-
pendingPrompt = null;
|
|
2650
|
-
tui.setFocus(editor);
|
|
2651
|
-
tui.requestRender();
|
|
2652
|
-
return true;
|
|
2653
|
-
}
|
|
2654
|
-
case '/buy': {
|
|
2655
|
-
// /buy TICKER QTY PRICE — quick trade without LLM
|
|
2656
|
-
const [, ticker, qtyStr, priceStr] = parts;
|
|
2657
|
-
if (!ticker || !qtyStr || !priceStr) {
|
|
2658
|
-
addSystemText(C.zinc400('Usage: /buy TICKER QTY PRICE_CENTS (e.g. /buy KXWTIMAX-26DEC31-T135 100 50)'));
|
|
2659
|
-
return true;
|
|
2660
|
-
}
|
|
2661
|
-
if (!config.tradingEnabled) {
|
|
2662
|
-
addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
|
|
2663
|
-
return true;
|
|
2664
|
-
}
|
|
2665
|
-
addSpacer();
|
|
2666
|
-
const answer = await promptUser(`BUY ${qtyStr}x ${ticker} YES @ ${priceStr}\u00A2 — execute? (y/n)`);
|
|
2667
|
-
if (answer.toLowerCase().startsWith('y')) {
|
|
2668
|
-
try {
|
|
2669
|
-
const { createOrder } = await import('../kalshi.js');
|
|
2670
|
-
const result = await createOrder({
|
|
2671
|
-
ticker, side: 'yes', action: 'buy', type: 'limit',
|
|
2672
|
-
count: parseInt(qtyStr),
|
|
2673
|
-
yes_price: parseInt(priceStr),
|
|
2674
|
-
});
|
|
2675
|
-
addSystemText(C.emerald('\u2713 Order placed: ' + ((result.order || result).order_id || 'OK')));
|
|
2676
|
-
}
|
|
2677
|
-
catch (err) {
|
|
2678
|
-
addSystemText(C.red('\u2717 ' + err.message));
|
|
2679
|
-
}
|
|
2680
|
-
}
|
|
2681
|
-
else {
|
|
2682
|
-
addSystemText(C.zinc400('Cancelled.'));
|
|
2683
|
-
}
|
|
2684
|
-
addSpacer();
|
|
2685
|
-
return true;
|
|
2686
|
-
}
|
|
2687
|
-
case '/sell': {
|
|
2688
|
-
const [, ticker, qtyStr, priceStr] = parts;
|
|
2689
|
-
if (!ticker || !qtyStr || !priceStr) {
|
|
2690
|
-
addSystemText(C.zinc400('Usage: /sell TICKER QTY PRICE_CENTS'));
|
|
2691
|
-
return true;
|
|
2692
|
-
}
|
|
2693
|
-
if (!config.tradingEnabled) {
|
|
2694
|
-
addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
|
|
2695
|
-
return true;
|
|
2696
|
-
}
|
|
2697
|
-
addSpacer();
|
|
2698
|
-
const answer = await promptUser(`SELL ${qtyStr}x ${ticker} YES @ ${priceStr}\u00A2 — execute? (y/n)`);
|
|
2699
|
-
if (answer.toLowerCase().startsWith('y')) {
|
|
2700
|
-
try {
|
|
2701
|
-
const { createOrder } = await import('../kalshi.js');
|
|
2702
|
-
const result = await createOrder({
|
|
2703
|
-
ticker, side: 'yes', action: 'sell', type: 'limit',
|
|
2704
|
-
count: parseInt(qtyStr),
|
|
2705
|
-
yes_price: parseInt(priceStr),
|
|
2706
|
-
});
|
|
2707
|
-
addSystemText(C.emerald('\u2713 Order placed: ' + ((result.order || result).order_id || 'OK')));
|
|
2708
|
-
}
|
|
2709
|
-
catch (err) {
|
|
2710
|
-
addSystemText(C.red('\u2717 ' + err.message));
|
|
2711
|
-
}
|
|
2712
|
-
}
|
|
2713
|
-
else {
|
|
2714
|
-
addSystemText(C.zinc400('Cancelled.'));
|
|
2715
|
-
}
|
|
2716
|
-
addSpacer();
|
|
2717
|
-
return true;
|
|
2718
|
-
}
|
|
2719
|
-
case '/cancel': {
|
|
2720
|
-
const [, orderId] = parts;
|
|
2721
|
-
if (!orderId) {
|
|
2722
|
-
addSystemText(C.zinc400('Usage: /cancel ORDER_ID'));
|
|
2723
|
-
return true;
|
|
2724
|
-
}
|
|
2725
|
-
if (!config.tradingEnabled) {
|
|
2726
|
-
addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
|
|
2727
|
-
return true;
|
|
2728
|
-
}
|
|
2729
|
-
addSpacer();
|
|
2730
|
-
try {
|
|
2731
|
-
const { cancelOrder } = await import('../kalshi.js');
|
|
2732
|
-
await cancelOrder(orderId);
|
|
2733
|
-
addSystemText(C.emerald(`\u2713 Order ${orderId} cancelled.`));
|
|
2734
|
-
}
|
|
2735
|
-
catch (err) {
|
|
2736
|
-
addSystemText(C.red('\u2717 ' + err.message));
|
|
2737
|
-
}
|
|
2738
|
-
addSpacer();
|
|
2739
|
-
return true;
|
|
2740
|
-
}
|
|
2741
|
-
case '/exit':
|
|
2742
|
-
case '/quit': {
|
|
2743
|
-
cleanup();
|
|
2744
|
-
return true;
|
|
2745
|
-
}
|
|
2746
|
-
default: {
|
|
2747
|
-
// Check if it's a skill trigger
|
|
2748
|
-
const skill = skills.find(s => s.trigger === command);
|
|
2749
|
-
if (skill) {
|
|
2750
|
-
addSpacer();
|
|
2751
|
-
addSystemText(C.zinc200(`Running skill: ${bold(skill.name)}`) + C.zinc600(` \u2014 ${skill.description.slice(0, 60)}`));
|
|
2752
|
-
addSpacer();
|
|
2753
|
-
tui.requestRender();
|
|
2754
|
-
// Inject the skill prompt → agent executes using existing tools
|
|
2755
|
-
isProcessing = true;
|
|
2756
|
-
try {
|
|
2757
|
-
await agent.prompt(skill.prompt);
|
|
2758
|
-
}
|
|
2759
|
-
catch (err) {
|
|
2760
|
-
addSystemText(C.red(`Skill error: ${err.message}`));
|
|
2761
|
-
}
|
|
2762
|
-
finally {
|
|
2763
|
-
isProcessing = false;
|
|
2764
|
-
}
|
|
2765
|
-
return true;
|
|
2766
|
-
}
|
|
2767
|
-
return false;
|
|
2768
|
-
}
|
|
2769
|
-
}
|
|
2770
|
-
}
|
|
2771
|
-
// ── Editor submit handler ──────────────────────────────────────────────────
|
|
2772
|
-
editor.onSubmit = async (input) => {
|
|
2773
|
-
const trimmed = input.trim();
|
|
2774
|
-
if (!trimmed)
|
|
2775
|
-
return;
|
|
2776
|
-
// If a tool is waiting for user confirmation, resolve it
|
|
2777
|
-
if (pendingPrompt) {
|
|
2778
|
-
const { resolve } = pendingPrompt;
|
|
2779
|
-
pendingPrompt = null;
|
|
2780
|
-
const userResponse = new Text(C.zinc400(' > ') + C.zinc200(trimmed), 1, 0);
|
|
2781
|
-
chatContainer.addChild(userResponse);
|
|
2782
|
-
addSpacer();
|
|
2783
|
-
tui.requestRender();
|
|
2784
|
-
resolve(trimmed);
|
|
2785
|
-
return;
|
|
2786
|
-
}
|
|
2787
|
-
if (isProcessing)
|
|
2788
|
-
return;
|
|
2789
|
-
// Add to editor history
|
|
2790
|
-
editor.addToHistory(trimmed);
|
|
2791
|
-
// Check for slash commands
|
|
2792
|
-
if (trimmed.startsWith('/')) {
|
|
2793
|
-
const handled = await handleSlashCommand(trimmed);
|
|
2794
|
-
if (handled)
|
|
2795
|
-
return;
|
|
2796
|
-
}
|
|
2797
|
-
// Regular message → send to agent
|
|
2798
|
-
isProcessing = true;
|
|
2799
|
-
// Add user message to chat
|
|
2800
|
-
const userMsg = new Text(C.emerald(bold('>')) + ' ' + C.white(trimmed), 1, 0);
|
|
2801
|
-
chatContainer.addChild(userMsg);
|
|
2802
|
-
addSpacer();
|
|
2803
|
-
tui.requestRender();
|
|
2804
|
-
try {
|
|
2805
|
-
await agent.prompt(trimmed);
|
|
2806
|
-
}
|
|
2807
|
-
catch (err) {
|
|
2808
|
-
// Remove loader if present
|
|
2809
|
-
if (currentLoader) {
|
|
2810
|
-
currentLoader.stop();
|
|
2811
|
-
chatContainer.removeChild(currentLoader);
|
|
2812
|
-
currentLoader = null;
|
|
2813
|
-
}
|
|
2814
|
-
addSystemText(C.red(`Error: ${err.message}`));
|
|
2815
|
-
addSpacer();
|
|
2816
|
-
isProcessing = false;
|
|
2817
|
-
}
|
|
2818
|
-
};
|
|
2819
|
-
// ── Ctrl+C handler ─────────────────────────────────────────────────────────
|
|
2820
|
-
function cleanup() {
|
|
2821
|
-
if (heartbeatPollTimer)
|
|
2822
|
-
clearInterval(heartbeatPollTimer);
|
|
2823
|
-
if (currentLoader)
|
|
2824
|
-
currentLoader.stop();
|
|
2825
|
-
persistSession();
|
|
2826
|
-
tui.stop();
|
|
2827
|
-
process.exit(0);
|
|
2828
|
-
}
|
|
2829
|
-
// Listen for Ctrl+C at the TUI level
|
|
2830
|
-
tui.addInputListener((data) => {
|
|
2831
|
-
// Ctrl+C = \x03
|
|
2832
|
-
if (data === '\x03') {
|
|
2833
|
-
cleanup();
|
|
2834
|
-
return { consume: true };
|
|
2835
|
-
}
|
|
2836
|
-
return undefined;
|
|
2837
|
-
});
|
|
2838
|
-
// Also handle SIGINT
|
|
2839
|
-
process.on('SIGINT', cleanup);
|
|
2840
|
-
process.on('SIGTERM', cleanup);
|
|
2841
|
-
// ── Welcome dashboard builder ────────────────────────────────────────────
|
|
2842
|
-
function buildWelcomeDashboard(ctx, positions) {
|
|
2843
|
-
const lines = [];
|
|
2844
|
-
// ── Explorer mode welcome ──────────────────────────────────────────────
|
|
2845
|
-
if (ctx._explorerMode) {
|
|
2846
|
-
const edgeCount = ctx.edges?.length || 0;
|
|
2847
|
-
const theseCount = ctx.theses?.length || 0;
|
|
2848
|
-
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2849
|
-
lines.push(' ' + C.emerald(bold('Explorer mode')) + C.zinc600(' — full market access, no thesis'));
|
|
2850
|
-
lines.push(' ' + C.zinc600(`${theseCount} public theses \u2502 ${edgeCount} edges tracked`));
|
|
2851
|
-
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2852
|
-
// Show top public edges
|
|
2853
|
-
const edges = ctx.edges || [];
|
|
2854
|
-
if (edges.length > 0) {
|
|
2855
|
-
const sorted = [...edges].sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge)).slice(0, 5);
|
|
2856
|
-
lines.push(' ' + C.zinc400(bold('TOP PUBLIC EDGES')) + C.zinc600(' mkt edge'));
|
|
2857
|
-
for (const e of sorted) {
|
|
2858
|
-
const name = (e.title || '').slice(0, 30).padEnd(30);
|
|
2859
|
-
const mkt = String(Math.round(e.price || 0)).padStart(3) + '\u00A2';
|
|
2860
|
-
const edge = e.edge || 0;
|
|
2861
|
-
const edgeStr = '+' + Math.round(edge);
|
|
2862
|
-
const edgeColor = Math.abs(edge) >= 15 ? C.emerald : Math.abs(edge) >= 8 ? C.amber : C.zinc400;
|
|
2863
|
-
lines.push(` ${C.zinc400(name)} ${C.zinc400(mkt)} ${edgeColor(edgeStr.padStart(4))}`);
|
|
2864
|
-
}
|
|
2865
|
-
}
|
|
2866
|
-
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2867
|
-
lines.push(' ' + C.zinc600('Ask anything, or describe a view to create a thesis.'));
|
|
2868
|
-
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2869
|
-
return lines.join('\n');
|
|
2870
|
-
}
|
|
2871
|
-
// ── Thesis mode welcome (existing) ────────────────────────────────────
|
|
2872
|
-
const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
|
|
2873
|
-
const truncated = thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText;
|
|
2874
|
-
const conf = typeof ctx.confidence === 'number'
|
|
2875
|
-
? Math.round(ctx.confidence * 100)
|
|
2876
|
-
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
2877
|
-
const delta = ctx.lastEvaluation?.confidenceDelta
|
|
2878
|
-
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
2879
|
-
: 0;
|
|
2880
|
-
const deltaStr = delta !== 0 ? ` (${delta > 0 ? '+' : ''}${delta})` : '';
|
|
2881
|
-
const evalAge = ctx.lastEvaluation?.evaluatedAt
|
|
2882
|
-
? Math.round((Date.now() - new Date(ctx.lastEvaluation.evaluatedAt).getTime()) / 3600000)
|
|
2883
|
-
: null;
|
|
2884
|
-
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2885
|
-
lines.push(' ' + C.zinc200(bold(truncated)));
|
|
2886
|
-
lines.push(' ' + C.zinc600(`${ctx.status || 'active'} ${conf}%${deltaStr}`) +
|
|
2887
|
-
(evalAge !== null ? C.zinc600(` \u2502 last eval: ${evalAge < 1 ? '<1' : evalAge}h ago`) : ''));
|
|
2888
|
-
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2889
|
-
// Positions section
|
|
2890
|
-
if (positions && positions.length > 0) {
|
|
2891
|
-
lines.push(' ' + C.zinc400(bold('POSITIONS')));
|
|
2892
|
-
let totalPnl = 0;
|
|
2893
|
-
for (const p of positions) {
|
|
2894
|
-
const pnlCents = p.unrealized_pnl || 0;
|
|
2895
|
-
totalPnl += pnlCents;
|
|
2896
|
-
const pnlStr = pnlCents >= 0
|
|
2897
|
-
? C.emerald(`+$${(pnlCents / 100).toFixed(2)}`)
|
|
2898
|
-
: C.red(`-$${(Math.abs(pnlCents) / 100).toFixed(2)}`);
|
|
2899
|
-
const ticker = (p.ticker || '').slice(0, 28).padEnd(28);
|
|
2900
|
-
const qty = String(p.quantity || 0).padStart(5);
|
|
2901
|
-
const side = p.side === 'yes' ? C.emerald('Y') : C.red('N');
|
|
2902
|
-
lines.push(` ${C.zinc400(ticker)} ${qty} ${side} ${pnlStr}`);
|
|
2903
|
-
}
|
|
2904
|
-
const totalStr = totalPnl >= 0
|
|
2905
|
-
? C.emerald(bold(`+$${(totalPnl / 100).toFixed(2)}`))
|
|
2906
|
-
: C.red(bold(`-$${(Math.abs(totalPnl) / 100).toFixed(2)}`));
|
|
2907
|
-
lines.push(` ${''.padEnd(28)} ${C.zinc600('Total')} ${totalStr}`);
|
|
2908
|
-
}
|
|
2909
|
-
// Top edges section
|
|
2910
|
-
const edges = ctx.edges || [];
|
|
2911
|
-
if (edges.length > 0) {
|
|
2912
|
-
const sorted = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0)).slice(0, 5);
|
|
2913
|
-
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2914
|
-
lines.push(' ' + C.zinc400(bold('TOP EDGES')) + C.zinc600(' mkt edge liq'));
|
|
2915
|
-
for (const e of sorted) {
|
|
2916
|
-
const name = (e.market || e.marketTitle || e.marketId || '').slice(0, 30).padEnd(30);
|
|
2917
|
-
const mkt = String(Math.round(e.marketPrice || 0)).padStart(3) + '\u00A2';
|
|
2918
|
-
const edge = e.edge || e.edgeSize || 0;
|
|
2919
|
-
const edgeStr = (edge > 0 ? '+' : '') + Math.round(edge);
|
|
2920
|
-
const liq = e.orderbook?.liquidityScore || (e.venue === 'polymarket' ? '-' : '?');
|
|
2921
|
-
const edgeColor = Math.abs(edge) >= 15 ? C.emerald : Math.abs(edge) >= 8 ? C.amber : C.zinc400;
|
|
2922
|
-
lines.push(` ${C.zinc400(name)} ${C.zinc400(mkt)} ${edgeColor(edgeStr.padStart(4))} ${C.zinc600(liq)}`);
|
|
2923
|
-
}
|
|
2924
|
-
}
|
|
2925
|
-
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
2926
|
-
return lines.join('\n');
|
|
2927
|
-
}
|
|
2928
|
-
// ── Show initial welcome ───────────────────────────────────────────────────
|
|
2929
|
-
const sessionStatus = sessionRestored
|
|
2930
|
-
? C.zinc600(`resumed (${agent.state.messages.length} messages)`)
|
|
2931
|
-
: C.zinc600('new session');
|
|
2932
|
-
addSystemText(buildWelcomeDashboard(latestContext, initialPositions));
|
|
2933
|
-
addSystemText(' ' + sessionStatus);
|
|
2934
|
-
addSpacer();
|
|
2935
|
-
// ── Heartbeat delta handler ───────────────────────────────────────────────
|
|
2936
|
-
const HEARTBEAT_CONFIDENCE_THRESHOLD = 0.03; // 3%
|
|
2937
|
-
function handleHeartbeatDelta(delta) {
|
|
2938
|
-
const absDelta = Math.abs(delta.confidenceDelta || 0);
|
|
2939
|
-
const confPct = Math.round((delta.confidence || 0) * 100);
|
|
2940
|
-
const deltaPct = Math.round((delta.confidenceDelta || 0) * 100);
|
|
2941
|
-
const sign = deltaPct > 0 ? '+' : '';
|
|
2942
|
-
if (absDelta >= HEARTBEAT_CONFIDENCE_THRESHOLD) {
|
|
2943
|
-
// Big change → auto-trigger agent analysis
|
|
2944
|
-
const arrow = deltaPct > 0 ? '\u25B2' : '\u25BC';
|
|
2945
|
-
const color = deltaPct > 0 ? C.emerald : C.red;
|
|
2946
|
-
addSystemText(color(` ${arrow} Heartbeat: confidence ${sign}${deltaPct}% → ${confPct}%`));
|
|
2947
|
-
if (delta.latestSummary) {
|
|
2948
|
-
addSystemText(C.zinc400(` ${delta.latestSummary.slice(0, 100)}`));
|
|
2949
|
-
}
|
|
2950
|
-
addSpacer();
|
|
2951
|
-
// Update header
|
|
2952
|
-
footerBar.setFromContext({ ...latestContext, confidence: delta.confidence, lastEvaluation: { confidenceDelta: delta.confidenceDelta } }, initialPositions || undefined);
|
|
2953
|
-
tui.requestRender();
|
|
2954
|
-
// Auto-trigger agent
|
|
2955
|
-
isProcessing = true;
|
|
2956
|
-
const prompt = `[HEARTBEAT ALERT] Confidence just changed ${sign}${deltaPct}% to ${confPct}%. ${delta.evaluationCount} evaluation(s) since last check. Latest: "${(delta.latestSummary || '').slice(0, 150)}". Briefly analyze what happened and whether any action is needed. Be concise.`;
|
|
2957
|
-
agent.prompt(prompt).catch((err) => {
|
|
2958
|
-
addSystemText(C.red(`Error: ${err.message}`));
|
|
2959
|
-
isProcessing = false;
|
|
2960
|
-
});
|
|
2961
|
-
}
|
|
2962
|
-
else if (absDelta > 0) {
|
|
2963
|
-
// Small change → silent notification line only
|
|
2964
|
-
addSystemText(C.zinc600(` \u2500 heartbeat: ${confPct}% (${sign}${deltaPct}%) \u2014 ${delta.evaluationCount || 0} eval(s)`));
|
|
2965
|
-
tui.requestRender();
|
|
2966
|
-
}
|
|
2967
|
-
// absDelta === 0: truly nothing changed, stay silent
|
|
2968
|
-
}
|
|
2969
|
-
// ── Start heartbeat polling (thesis mode only) ──────────────────────────
|
|
2970
|
-
if (!explorerMode)
|
|
2971
|
-
heartbeatPollTimer = setInterval(async () => {
|
|
2972
|
-
try {
|
|
2973
|
-
const delta = await sfClient.getChanges(resolvedThesisId, lastPollTimestamp);
|
|
2974
|
-
lastPollTimestamp = new Date().toISOString();
|
|
2975
|
-
if (!delta.changed)
|
|
2976
|
-
return;
|
|
2977
|
-
if (isProcessing || pendingPrompt) {
|
|
2978
|
-
// Agent is busy — queue for delivery after agent_end
|
|
2979
|
-
pendingHeartbeatDelta = delta;
|
|
2980
|
-
}
|
|
2981
|
-
else {
|
|
2982
|
-
handleHeartbeatDelta(delta);
|
|
2983
|
-
}
|
|
2984
|
-
}
|
|
2985
|
-
catch {
|
|
2986
|
-
// Silent — don't spam errors from background polling
|
|
2987
|
-
}
|
|
2988
|
-
}, 60_000); // every 60 seconds
|
|
2989
|
-
// ── Start TUI ──────────────────────────────────────────────────────────────
|
|
2990
|
-
tui.start();
|
|
2991
|
-
}
|
|
2992
|
-
// ============================================================================
|
|
2993
|
-
// PLAIN-TEXT MODE (--no-tui)
|
|
2994
|
-
// ============================================================================
|
|
2995
|
-
async function runPlainTextAgent(params) {
|
|
2996
|
-
const { openrouterKey, sfClient, resolvedThesisId, opts, useProxy, llmBaseUrl, sfApiKey, sfApiUrl } = params;
|
|
2997
|
-
let latestContext = params.latestContext;
|
|
2998
|
-
const readline = await import('readline');
|
|
2999
|
-
const piAi = await import('@mariozechner/pi-ai');
|
|
3000
|
-
const piAgent = await import('@mariozechner/pi-agent-core');
|
|
3001
|
-
const { getModel, streamSimple, Type } = piAi;
|
|
3002
|
-
const { Agent } = piAgent;
|
|
3003
|
-
const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
|
|
3004
|
-
let currentModelName = rawModelName.replace(/^openrouter\//, '');
|
|
3005
|
-
function resolveModel(name) {
|
|
3006
|
-
let m;
|
|
3007
|
-
try {
|
|
3008
|
-
m = getModel('openrouter', name);
|
|
3009
|
-
}
|
|
3010
|
-
catch {
|
|
3011
|
-
m = {
|
|
3012
|
-
modelId: name, provider: 'openrouter', api: 'openai-completions',
|
|
3013
|
-
baseUrl: 'https://openrouter.ai/api/v1', id: name, name,
|
|
3014
|
-
inputPrice: 0, outputPrice: 0, contextWindow: 200000,
|
|
3015
|
-
supportsImages: true, supportsTools: true,
|
|
3016
|
-
};
|
|
3017
|
-
}
|
|
3018
|
-
if (useProxy)
|
|
3019
|
-
m.baseUrl = llmBaseUrl;
|
|
3020
|
-
return m;
|
|
3021
|
-
}
|
|
3022
|
-
let model = resolveModel(currentModelName);
|
|
3023
|
-
// ── Tools (same definitions as TUI mode) ──────────────────────────────────
|
|
3024
|
-
const thesisIdParam = Type.Object({ thesisId: Type.String({ description: 'Thesis ID' }) });
|
|
3025
|
-
const signalParams = Type.Object({
|
|
3026
|
-
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
3027
|
-
content: Type.String({ description: 'Signal content' }),
|
|
3028
|
-
type: Type.Optional(Type.String({ description: 'Signal type: news, user_note, external' })),
|
|
3029
|
-
});
|
|
3030
|
-
const scanParams = Type.Object({
|
|
3031
|
-
query: Type.Optional(Type.String({ description: 'Keyword search' })),
|
|
3032
|
-
series: Type.Optional(Type.String({ description: 'Series ticker' })),
|
|
3033
|
-
market: Type.Optional(Type.String({ description: 'Market ticker' })),
|
|
3034
|
-
});
|
|
3035
|
-
const webSearchParams = Type.Object({ query: Type.String({ description: 'Search keywords' }) });
|
|
3036
|
-
const emptyParams = Type.Object({});
|
|
3037
|
-
const tools = [
|
|
3038
|
-
{
|
|
3039
|
-
name: 'get_context', label: 'Get Context',
|
|
3040
|
-
description: 'Get thesis snapshot: causal tree, edge prices, last evaluation, confidence',
|
|
3041
|
-
parameters: thesisIdParam,
|
|
3042
|
-
execute: async (_id, p) => {
|
|
3043
|
-
const ctx = await sfClient.getContext(p.thesisId);
|
|
3044
|
-
latestContext = ctx;
|
|
3045
|
-
return { content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }], details: {} };
|
|
3046
|
-
},
|
|
3047
|
-
},
|
|
3048
|
-
{
|
|
3049
|
-
name: 'global_context', label: 'Market Snapshot',
|
|
3050
|
-
description: 'Global market snapshot — top movers, expiring contracts, milestones, liquidity, signals. No thesis needed.',
|
|
3051
|
-
parameters: emptyParams,
|
|
3052
|
-
execute: async () => {
|
|
3053
|
-
const { fetchGlobalContext } = await import('../client.js');
|
|
3054
|
-
const data = await fetchGlobalContext();
|
|
3055
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3056
|
-
},
|
|
3057
|
-
},
|
|
3058
|
-
{
|
|
3059
|
-
name: 'query', label: 'Query',
|
|
3060
|
-
description: 'LLM-enhanced prediction market knowledge search. Returns structured answer with live prices, thesis data, key factors.',
|
|
3061
|
-
parameters: Type.Object({ q: Type.String({ description: 'Natural language query' }) }),
|
|
3062
|
-
execute: async (_id, p) => {
|
|
3063
|
-
const { fetchQuery } = await import('../client.js');
|
|
3064
|
-
const data = await fetchQuery(p.q);
|
|
3065
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3066
|
-
},
|
|
3067
|
-
},
|
|
3068
|
-
{
|
|
3069
|
-
name: 'get_markets', label: 'Traditional Markets',
|
|
3070
|
-
description: 'Traditional market prices via Databento: SPY, VIX, Treasury, Gold, Oil. Daily close + change.',
|
|
3071
|
-
parameters: emptyParams,
|
|
3072
|
-
execute: async () => {
|
|
3073
|
-
const { fetchTraditionalMarkets } = await import('../client.js');
|
|
3074
|
-
const data = await fetchTraditionalMarkets();
|
|
3075
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3076
|
-
},
|
|
3077
|
-
},
|
|
3078
|
-
{
|
|
3079
|
-
name: 'inject_signal', label: 'Inject Signal',
|
|
3080
|
-
description: 'Inject a signal into the thesis',
|
|
3081
|
-
parameters: signalParams,
|
|
3082
|
-
execute: async (_id, p) => {
|
|
3083
|
-
const result = await sfClient.injectSignal(p.thesisId, p.type || 'user_note', p.content);
|
|
3084
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
3085
|
-
},
|
|
3086
|
-
},
|
|
3087
|
-
{
|
|
3088
|
-
name: 'trigger_evaluation', label: 'Evaluate',
|
|
3089
|
-
description: 'Trigger a deep evaluation cycle',
|
|
3090
|
-
parameters: thesisIdParam,
|
|
3091
|
-
execute: async (_id, p) => {
|
|
3092
|
-
const result = await sfClient.evaluate(p.thesisId);
|
|
3093
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
3094
|
-
},
|
|
3095
|
-
},
|
|
3096
|
-
{
|
|
3097
|
-
name: 'scan_markets', label: 'Scan Markets',
|
|
3098
|
-
description: 'Search Kalshi prediction markets',
|
|
3099
|
-
parameters: scanParams,
|
|
3100
|
-
execute: async (_id, p) => {
|
|
3101
|
-
let result;
|
|
3102
|
-
if (p.market) {
|
|
3103
|
-
result = await (0, client_js_1.kalshiFetchMarket)(p.market);
|
|
3104
|
-
}
|
|
3105
|
-
else if (p.series) {
|
|
3106
|
-
result = await (0, client_js_1.kalshiFetchMarketsBySeries)(p.series);
|
|
3107
|
-
}
|
|
3108
|
-
else if (p.query) {
|
|
3109
|
-
const series = await (0, client_js_1.kalshiFetchAllSeries)();
|
|
3110
|
-
const kws = p.query.toLowerCase().split(/\s+/);
|
|
3111
|
-
result = series.filter((s) => kws.every((k) => ((s.title || '') + (s.ticker || '')).toLowerCase().includes(k))).filter((s) => parseFloat(s.volume_fp || '0') > 1000).sort((a, b) => parseFloat(b.volume_fp || '0') - parseFloat(a.volume_fp || '0')).slice(0, 15);
|
|
3112
|
-
}
|
|
3113
|
-
else {
|
|
3114
|
-
result = { error: 'Provide query, series, or market' };
|
|
3115
|
-
}
|
|
3116
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
3117
|
-
},
|
|
3118
|
-
},
|
|
3119
|
-
{
|
|
3120
|
-
name: 'list_theses', label: 'List Theses',
|
|
3121
|
-
description: 'List all theses',
|
|
3122
|
-
parameters: emptyParams,
|
|
3123
|
-
execute: async () => {
|
|
3124
|
-
const theses = await sfClient.listTheses();
|
|
3125
|
-
return { content: [{ type: 'text', text: JSON.stringify(theses, null, 2) }], details: {} };
|
|
3126
|
-
},
|
|
3127
|
-
},
|
|
3128
|
-
{
|
|
3129
|
-
name: 'get_positions', label: 'Get Positions',
|
|
3130
|
-
description: 'Get Kalshi positions with live prices',
|
|
3131
|
-
parameters: emptyParams,
|
|
3132
|
-
execute: async () => {
|
|
3133
|
-
const positions = await (0, kalshi_js_1.getPositions)();
|
|
3134
|
-
if (!positions)
|
|
3135
|
-
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
3136
|
-
for (const pos of positions) {
|
|
3137
|
-
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
3138
|
-
if (livePrice !== null) {
|
|
3139
|
-
pos.current_value = livePrice;
|
|
3140
|
-
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
3141
|
-
}
|
|
3142
|
-
}
|
|
3143
|
-
const formatted = positions.map((p) => ({
|
|
3144
|
-
ticker: p.ticker, side: p.side, quantity: p.quantity,
|
|
3145
|
-
avg_price: `${p.average_price_paid}¢`, current_price: `${p.current_value}¢`,
|
|
3146
|
-
unrealized_pnl: `$${(p.unrealized_pnl / 100).toFixed(2)}`,
|
|
3147
|
-
total_cost: `$${(p.total_cost / 100).toFixed(2)}`,
|
|
3148
|
-
realized_pnl: `$${(p.realized_pnl / 100).toFixed(2)}`,
|
|
3149
|
-
}));
|
|
3150
|
-
return { content: [{ type: 'text', text: JSON.stringify(formatted, null, 2) }], details: {} };
|
|
3151
|
-
},
|
|
3152
|
-
},
|
|
3153
|
-
{
|
|
3154
|
-
name: 'web_search', label: 'Web Search',
|
|
3155
|
-
description: 'Search latest news and information',
|
|
3156
|
-
parameters: webSearchParams,
|
|
3157
|
-
execute: async (_id, p) => {
|
|
3158
|
-
const tavilyKey2 = process.env.TAVILY_API_KEY;
|
|
3159
|
-
const canProxy2 = !tavilyKey2 && sfApiKey;
|
|
3160
|
-
if (!tavilyKey2 && !canProxy2)
|
|
3161
|
-
return { content: [{ type: 'text', text: 'Web search not available. Run sf login or set TAVILY_API_KEY.' }], details: {} };
|
|
3162
|
-
let res;
|
|
3163
|
-
if (tavilyKey2) {
|
|
3164
|
-
res = await fetch('https://api.tavily.com/search', {
|
|
3165
|
-
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
3166
|
-
body: JSON.stringify({ api_key: tavilyKey2, query: p.query, max_results: 5, search_depth: 'basic', include_answer: true }),
|
|
3167
|
-
});
|
|
3168
|
-
}
|
|
3169
|
-
else {
|
|
3170
|
-
res = await fetch(`${sfApiUrl}/api/proxy/search`, {
|
|
3171
|
-
method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${sfApiKey}` },
|
|
3172
|
-
body: JSON.stringify({ query: p.query, max_results: 5, search_depth: 'basic', include_answer: true }),
|
|
3173
|
-
});
|
|
3174
|
-
}
|
|
3175
|
-
if (!res.ok)
|
|
3176
|
-
return { content: [{ type: 'text', text: `Search failed: ${res.status}` }], details: {} };
|
|
3177
|
-
const data = await res.json();
|
|
3178
|
-
const results = (data.results || []).map((r) => `[${r.title}](${r.url})\n${r.content?.slice(0, 200)}`).join('\n\n');
|
|
3179
|
-
const answer = data.answer ? `Summary: ${data.answer}\n\n---\n\n` : '';
|
|
3180
|
-
return { content: [{ type: 'text', text: `${answer}${results}` }], details: {} };
|
|
3181
|
-
},
|
|
3182
|
-
},
|
|
3183
|
-
{
|
|
3184
|
-
name: 'get_milestones', label: 'Milestones',
|
|
3185
|
-
description: 'Get upcoming events from Kalshi calendar. Use to check economic releases, political events, or other catalysts.',
|
|
3186
|
-
parameters: Type.Object({
|
|
3187
|
-
hours: Type.Optional(Type.Number({ description: 'Hours ahead to look (default 168 = 1 week)' })),
|
|
3188
|
-
category: Type.Optional(Type.String({ description: 'Filter by category (e.g. Economics, Politics, Sports)' })),
|
|
3189
|
-
}),
|
|
3190
|
-
execute: async (_id, p) => {
|
|
3191
|
-
const hours = p.hours || 168;
|
|
3192
|
-
const now = new Date();
|
|
3193
|
-
const url = `https://api.elections.kalshi.com/trade-api/v2/milestones?limit=200&minimum_start_date=${now.toISOString()}` +
|
|
3194
|
-
(p.category ? `&category=${p.category}` : '');
|
|
3195
|
-
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
3196
|
-
if (!res.ok)
|
|
3197
|
-
return { content: [{ type: 'text', text: `Milestones API error: ${res.status}` }], details: {} };
|
|
3198
|
-
const data = await res.json();
|
|
3199
|
-
const cutoff = now.getTime() + hours * 3600000;
|
|
3200
|
-
const filtered = (data.milestones || [])
|
|
3201
|
-
.filter((m) => new Date(m.start_date).getTime() <= cutoff)
|
|
3202
|
-
.slice(0, 30)
|
|
3203
|
-
.map((m) => ({
|
|
3204
|
-
title: m.title, category: m.category, start_date: m.start_date,
|
|
3205
|
-
related_event_tickers: m.related_event_tickers,
|
|
3206
|
-
hours_until: Math.round((new Date(m.start_date).getTime() - now.getTime()) / 3600000),
|
|
3207
|
-
}));
|
|
3208
|
-
return { content: [{ type: 'text', text: JSON.stringify(filtered, null, 2) }], details: {} };
|
|
3209
|
-
},
|
|
3210
|
-
},
|
|
3211
|
-
{
|
|
3212
|
-
name: 'get_forecast', label: 'Forecast',
|
|
3213
|
-
description: 'Get market distribution (P50/P75/P90 percentile history) for a Kalshi event.',
|
|
3214
|
-
parameters: Type.Object({
|
|
3215
|
-
eventTicker: Type.String({ description: 'Kalshi event ticker (e.g. KXWTIMAX-26DEC31)' }),
|
|
3216
|
-
days: Type.Optional(Type.Number({ description: 'Days of history (default 7)' })),
|
|
3217
|
-
}),
|
|
3218
|
-
execute: async (_id, p) => {
|
|
3219
|
-
const { getForecastHistory } = await import('../kalshi.js');
|
|
3220
|
-
const days = p.days || 7;
|
|
3221
|
-
const evtRes = await fetch(`https://api.elections.kalshi.com/trade-api/v2/events/${p.eventTicker}`, { headers: { 'Accept': 'application/json' } });
|
|
3222
|
-
if (!evtRes.ok)
|
|
3223
|
-
return { content: [{ type: 'text', text: `Event not found: ${p.eventTicker}` }], details: {} };
|
|
3224
|
-
const evtData = await evtRes.json();
|
|
3225
|
-
const seriesTicker = evtData.event?.series_ticker;
|
|
3226
|
-
if (!seriesTicker)
|
|
3227
|
-
return { content: [{ type: 'text', text: `No series_ticker for ${p.eventTicker}` }], details: {} };
|
|
3228
|
-
const history = await getForecastHistory({
|
|
3229
|
-
seriesTicker, eventTicker: p.eventTicker, percentiles: [5000, 7500, 9000],
|
|
3230
|
-
startTs: Math.floor((Date.now() - days * 86400000) / 1000),
|
|
3231
|
-
endTs: Math.floor(Date.now() / 1000), periodInterval: 1440,
|
|
3232
|
-
});
|
|
3233
|
-
if (!history || history.length === 0)
|
|
3234
|
-
return { content: [{ type: 'text', text: 'No forecast data available' }], details: {} };
|
|
3235
|
-
return { content: [{ type: 'text', text: JSON.stringify(history, null, 2) }], details: {} };
|
|
3236
|
-
},
|
|
3237
|
-
},
|
|
3238
|
-
{
|
|
3239
|
-
name: 'get_settlements', label: 'Settlements',
|
|
3240
|
-
description: 'Get settled (resolved) contracts with P&L.',
|
|
3241
|
-
parameters: Type.Object({ ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })) }),
|
|
3242
|
-
execute: async (_id, p) => {
|
|
3243
|
-
const { getSettlements } = await import('../kalshi.js');
|
|
3244
|
-
const result = await getSettlements({ limit: 100, ticker: p.ticker });
|
|
3245
|
-
if (!result)
|
|
3246
|
-
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
3247
|
-
return { content: [{ type: 'text', text: JSON.stringify(result.settlements, null, 2) }], details: {} };
|
|
3248
|
-
},
|
|
3249
|
-
},
|
|
3250
|
-
{
|
|
3251
|
-
name: 'get_balance', label: 'Balance',
|
|
3252
|
-
description: 'Get Kalshi account balance and portfolio value.',
|
|
3253
|
-
parameters: emptyParams,
|
|
3254
|
-
execute: async () => {
|
|
3255
|
-
const { getBalance } = await import('../kalshi.js');
|
|
3256
|
-
const result = await getBalance();
|
|
3257
|
-
if (!result)
|
|
3258
|
-
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
3259
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
3260
|
-
},
|
|
3261
|
-
},
|
|
3262
|
-
{
|
|
3263
|
-
name: 'get_orders', label: 'Orders',
|
|
3264
|
-
description: 'Get current resting orders on Kalshi.',
|
|
3265
|
-
parameters: Type.Object({ status: Type.Optional(Type.String({ description: 'Filter by status: resting, canceled, executed. Default: resting' })) }),
|
|
3266
|
-
execute: async (_id, p) => {
|
|
3267
|
-
const { getOrders } = await import('../kalshi.js');
|
|
3268
|
-
const result = await getOrders({ status: p.status || 'resting', limit: 100 });
|
|
3269
|
-
if (!result)
|
|
3270
|
-
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
3271
|
-
return { content: [{ type: 'text', text: JSON.stringify(result.orders, null, 2) }], details: {} };
|
|
3272
|
-
},
|
|
3273
|
-
},
|
|
3274
|
-
{
|
|
3275
|
-
name: 'get_fills', label: 'Fills',
|
|
3276
|
-
description: 'Get recent trade fills (executed trades) on Kalshi.',
|
|
3277
|
-
parameters: Type.Object({ ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })) }),
|
|
3278
|
-
execute: async (_id, p) => {
|
|
3279
|
-
const { getFills } = await import('../kalshi.js');
|
|
3280
|
-
const result = await getFills({ ticker: p.ticker, limit: 50 });
|
|
3281
|
-
if (!result)
|
|
3282
|
-
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
3283
|
-
return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
|
|
3284
|
-
},
|
|
3285
|
-
},
|
|
3286
|
-
{
|
|
3287
|
-
name: 'get_liquidity',
|
|
3288
|
-
label: 'Liquidity Scanner',
|
|
3289
|
-
description: 'Scan orderbook liquidity for a topic across Kalshi + Polymarket. Returns spread, depth, liquidity scores. Topics: ' + Object.keys(topics_js_1.TOPIC_SERIES).join(', '),
|
|
3290
|
-
parameters: Type.Object({
|
|
3291
|
-
topic: Type.String({ description: 'Topic to scan (e.g. oil, crypto, fed, geopolitics)' }),
|
|
3292
|
-
}),
|
|
3293
|
-
execute: async (_toolCallId, params) => {
|
|
3294
|
-
const topicKey = params.topic.toLowerCase();
|
|
3295
|
-
const seriesList = topics_js_1.TOPIC_SERIES[topicKey];
|
|
3296
|
-
if (!seriesList) {
|
|
3297
|
-
return { content: [{ type: 'text', text: `Unknown topic "${params.topic}". Available: ${Object.keys(topics_js_1.TOPIC_SERIES).join(', ')}` }], details: {} };
|
|
3298
|
-
}
|
|
3299
|
-
const results = [];
|
|
3300
|
-
for (const series of seriesList) {
|
|
3301
|
-
try {
|
|
3302
|
-
const url = `https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=${series}&status=open&limit=200`;
|
|
3303
|
-
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
3304
|
-
if (!res.ok)
|
|
3305
|
-
continue;
|
|
3306
|
-
const markets = (await res.json()).markets || [];
|
|
3307
|
-
const obResults = await Promise.allSettled(markets.slice(0, 20).map((m) => (0, kalshi_js_1.getPublicOrderbook)(m.ticker).then(ob => ({ ticker: m.ticker, title: m.title, ob }))));
|
|
3308
|
-
for (const r of obResults) {
|
|
3309
|
-
if (r.status !== 'fulfilled' || !r.value.ob)
|
|
3310
|
-
continue;
|
|
3311
|
-
const { ticker, title, ob } = r.value;
|
|
3312
|
-
const yes = (ob.yes_dollars || []).map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), qty: parseFloat(q) })).filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
3313
|
-
const no = (ob.no_dollars || []).map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), qty: parseFloat(q) })).filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
3314
|
-
const bestBid = yes[0]?.price || 0;
|
|
3315
|
-
const bestAsk = no.length > 0 ? (100 - no[0].price) : 100;
|
|
3316
|
-
const spread = bestAsk - bestBid;
|
|
3317
|
-
const depth = yes.slice(0, 3).reduce((s, l) => s + l.qty, 0) + no.slice(0, 3).reduce((s, l) => s + l.qty, 0);
|
|
3318
|
-
const liq = spread <= 2 && depth >= 500 ? 'high' : spread <= 5 && depth >= 100 ? 'medium' : 'low';
|
|
3319
|
-
results.push({ venue: 'kalshi', ticker, title: (title || '').slice(0, 50), bestBid, bestAsk, spread, depth: Math.round(depth), liquidityScore: liq });
|
|
3320
|
-
}
|
|
3321
|
-
}
|
|
3322
|
-
catch { /* skip */ }
|
|
3323
|
-
}
|
|
3324
|
-
try {
|
|
3325
|
-
const events = await (0, polymarket_js_1.polymarketSearch)(topicKey, 5);
|
|
3326
|
-
for (const event of events) {
|
|
3327
|
-
for (const m of (event.markets || []).slice(0, 5)) {
|
|
3328
|
-
if (!m.active || m.closed || !m.clobTokenIds)
|
|
3329
|
-
continue;
|
|
3330
|
-
const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
|
|
3331
|
-
if (!ids)
|
|
3332
|
-
continue;
|
|
3333
|
-
const d = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
|
|
3334
|
-
if (!d)
|
|
3335
|
-
continue;
|
|
3336
|
-
results.push({ venue: 'polymarket', ticker: (m.question || event.title).slice(0, 50), bestBid: d.bestBid, bestAsk: d.bestAsk, spread: d.spread, depth: d.bidDepthTop3 + d.askDepthTop3, liquidityScore: d.liquidityScore });
|
|
3337
|
-
}
|
|
3338
|
-
}
|
|
3339
|
-
}
|
|
3340
|
-
catch { /* skip */ }
|
|
3341
|
-
results.sort((a, b) => a.spread - b.spread);
|
|
3342
|
-
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
|
|
3343
|
-
},
|
|
3344
|
-
},
|
|
3345
|
-
{
|
|
3346
|
-
name: 'inspect_book',
|
|
3347
|
-
label: 'Orderbook',
|
|
3348
|
-
description: 'Get orderbook depth, spread, and liquidity. Returns a status field per market: "ok", "empty_orderbook", "market_closed", or "api_error". Supports multiple tickers in one call — use tickers array for batch position checks.',
|
|
3349
|
-
parameters: Type.Object({
|
|
3350
|
-
ticker: Type.Optional(Type.String({ description: 'Single Kalshi market ticker (e.g. KXWTIMAX-26DEC31-T135)' })),
|
|
3351
|
-
tickers: Type.Optional(Type.Array(Type.String(), { description: 'Multiple Kalshi tickers for batch check (e.g. ["T$135", "T$140", "T$150"])' })),
|
|
3352
|
-
polyQuery: Type.Optional(Type.String({ description: 'Search Polymarket by keyword (e.g. "oil price above 100")' })),
|
|
3353
|
-
}),
|
|
3354
|
-
execute: async (_toolCallId, params) => {
|
|
3355
|
-
const results = [];
|
|
3356
|
-
// Batch: expand tickers array into individual lookups
|
|
3357
|
-
const tickerList = [];
|
|
3358
|
-
if (params.tickers?.length)
|
|
3359
|
-
tickerList.push(...params.tickers);
|
|
3360
|
-
else if (params.ticker)
|
|
3361
|
-
tickerList.push(params.ticker);
|
|
3362
|
-
for (const tkr of tickerList) {
|
|
3363
|
-
try {
|
|
3364
|
-
const market = await (0, client_js_1.kalshiFetchMarket)(tkr);
|
|
3365
|
-
const mStatus = market.status || 'unknown';
|
|
3366
|
-
if (mStatus !== 'open' && mStatus !== 'active') {
|
|
3367
|
-
results.push({
|
|
3368
|
-
venue: 'kalshi', ticker: tkr, title: market.title,
|
|
3369
|
-
status: 'market_closed', reason: `Market status: ${mStatus}. Orderbook unavailable for closed/settled markets.`,
|
|
3370
|
-
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
3371
|
-
});
|
|
3372
|
-
}
|
|
3373
|
-
else {
|
|
3374
|
-
const ob = await (0, kalshi_js_1.getPublicOrderbook)(tkr);
|
|
3375
|
-
const yesBids = (ob?.yes_dollars || [])
|
|
3376
|
-
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
3377
|
-
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
3378
|
-
const noAsks = (ob?.no_dollars || [])
|
|
3379
|
-
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
3380
|
-
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
3381
|
-
if (yesBids.length === 0 && noAsks.length === 0) {
|
|
3382
|
-
results.push({
|
|
3383
|
-
venue: 'kalshi', ticker: tkr, title: market.title,
|
|
3384
|
-
status: 'empty_orderbook', reason: 'Market open but no resting orders. Normal for illiquid/OTM contracts. Use lastPrice as reference.',
|
|
3385
|
-
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
3386
|
-
volume24h: parseFloat(market.volume_24h_fp || '0'),
|
|
3387
|
-
openInterest: parseFloat(market.open_interest_fp || '0'),
|
|
3388
|
-
expiry: market.close_time || null,
|
|
3389
|
-
});
|
|
3390
|
-
}
|
|
3391
|
-
else {
|
|
3392
|
-
const bestBid = yesBids[0]?.price || 0;
|
|
3393
|
-
const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : (yesBids[0] ? yesBids[0].price + 1 : 100);
|
|
3394
|
-
const spread = bestAsk - bestBid;
|
|
3395
|
-
const top3Depth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) + noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
|
|
3396
|
-
const liq = spread <= 2 && top3Depth >= 500 ? 'high' : spread <= 5 && top3Depth >= 100 ? 'medium' : 'low';
|
|
3397
|
-
results.push({
|
|
3398
|
-
venue: 'kalshi', ticker: tkr, title: market.title, status: 'ok',
|
|
3399
|
-
bestBid, bestAsk, spread, liquidityScore: liq,
|
|
3400
|
-
bidLevels: yesBids.slice(0, 5), askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
|
|
3401
|
-
totalBidDepth: yesBids.reduce((s, l) => s + l.size, 0),
|
|
3402
|
-
totalAskDepth: noAsks.reduce((s, l) => s + l.size, 0),
|
|
3403
|
-
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
3404
|
-
volume24h: parseFloat(market.volume_24h_fp || '0'),
|
|
3405
|
-
openInterest: parseFloat(market.open_interest_fp || '0'),
|
|
3406
|
-
expiry: market.close_time || null,
|
|
3407
|
-
});
|
|
3408
|
-
}
|
|
3409
|
-
}
|
|
3410
|
-
}
|
|
3411
|
-
catch (err) {
|
|
3412
|
-
results.push({ venue: 'kalshi', ticker: tkr, status: 'api_error', reason: `Kalshi API error: ${err.message}` });
|
|
3413
|
-
}
|
|
3414
|
-
}
|
|
3415
|
-
if (params.polyQuery) {
|
|
3416
|
-
try {
|
|
3417
|
-
const events = await (0, polymarket_js_1.polymarketSearch)(params.polyQuery, 5);
|
|
3418
|
-
for (const event of events) {
|
|
3419
|
-
for (const m of (event.markets || []).slice(0, 3)) {
|
|
3420
|
-
if (!m.active || m.closed || !m.clobTokenIds)
|
|
3421
|
-
continue;
|
|
3422
|
-
const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
|
|
3423
|
-
if (!ids)
|
|
3424
|
-
continue;
|
|
3425
|
-
const depth = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
|
|
3426
|
-
if (!depth)
|
|
3427
|
-
continue;
|
|
3428
|
-
const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
|
|
3429
|
-
results.push({
|
|
3430
|
-
venue: 'polymarket', title: m.question || event.title,
|
|
3431
|
-
bestBid: depth.bestBid, bestAsk: depth.bestAsk, spread: depth.spread,
|
|
3432
|
-
liquidityScore: depth.liquidityScore,
|
|
3433
|
-
totalBidDepth: depth.totalBidDepth, totalAskDepth: depth.totalAskDepth,
|
|
3434
|
-
lastPrice: prices[0] ? Math.round(prices[0] * 100) : 0,
|
|
3435
|
-
volume24h: m.volume24hr || 0,
|
|
3436
|
-
});
|
|
3437
|
-
}
|
|
3438
|
-
}
|
|
3439
|
-
}
|
|
3440
|
-
catch { /* skip */ }
|
|
3441
|
-
}
|
|
3442
|
-
if (results.length === 0) {
|
|
3443
|
-
return { content: [{ type: 'text', text: 'No markets found. Provide ticker (Kalshi) or polyQuery (Polymarket search).' }], details: {} };
|
|
3444
|
-
}
|
|
3445
|
-
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
|
|
3446
|
-
},
|
|
3447
|
-
},
|
|
3448
|
-
{
|
|
3449
|
-
name: 'get_schedule',
|
|
3450
|
-
label: 'Schedule',
|
|
3451
|
-
description: 'Get exchange status (open/closed) and trading hours. Use to check if low liquidity is due to off-hours.',
|
|
3452
|
-
parameters: emptyParams,
|
|
3453
|
-
execute: async () => {
|
|
3454
|
-
try {
|
|
3455
|
-
const res = await fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } });
|
|
3456
|
-
if (!res.ok)
|
|
3457
|
-
return { content: [{ type: 'text', text: `Exchange API error: ${res.status}` }], details: {} };
|
|
3458
|
-
const data = await res.json();
|
|
3459
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3460
|
-
}
|
|
3461
|
-
catch (err) {
|
|
3462
|
-
return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
|
|
3463
|
-
}
|
|
3464
|
-
},
|
|
3465
|
-
},
|
|
3466
|
-
{
|
|
3467
|
-
name: 'create_thesis',
|
|
3468
|
-
label: 'Create Thesis',
|
|
3469
|
-
description: 'Create a new thesis from a raw thesis statement. Returns the thesis ID, confidence, node count, and edge count.',
|
|
3470
|
-
parameters: Type.Object({
|
|
3471
|
-
rawThesis: Type.String({ description: 'The raw thesis statement to create' }),
|
|
3472
|
-
webhookUrl: Type.Optional(Type.String({ description: 'Optional webhook URL for notifications' })),
|
|
3473
|
-
}),
|
|
3474
|
-
execute: async (_id, p) => {
|
|
3475
|
-
const result = await sfClient.createThesis(p.rawThesis, true);
|
|
3476
|
-
const thesis = result.thesis || result;
|
|
3477
|
-
const nodeCount = thesis.causalTree?.nodes?.length || 0;
|
|
3478
|
-
const edgeCount = (thesis.edges || []).length;
|
|
3479
|
-
const confidence = typeof thesis.confidence === 'number' ? Math.round(thesis.confidence * 100) : 0;
|
|
3480
|
-
return {
|
|
3481
|
-
content: [{ type: 'text', text: `Thesis created.\nID: ${thesis.id}\nConfidence: ${confidence}%\nNodes: ${nodeCount}\nEdges: ${edgeCount}` }],
|
|
3482
|
-
details: {},
|
|
3483
|
-
};
|
|
3484
|
-
},
|
|
3485
|
-
},
|
|
3486
|
-
{
|
|
3487
|
-
name: 'get_edges',
|
|
3488
|
-
label: 'Get Edges',
|
|
3489
|
-
description: 'Get top edges across all active theses. Returns the top 10 edges sorted by absolute edge size with ticker, market name, edge size, direction, and venue.',
|
|
3490
|
-
parameters: emptyParams,
|
|
3491
|
-
execute: async () => {
|
|
3492
|
-
const { theses } = await sfClient.listTheses();
|
|
3493
|
-
const activeTheses = (theses || []).filter((t) => t.status === 'active' || t.status === 'monitoring');
|
|
3494
|
-
const results = await Promise.allSettled(activeTheses.map(async (t) => {
|
|
3495
|
-
const ctx = await sfClient.getContext(t.id);
|
|
3496
|
-
return (ctx.edges || []).map((e) => ({ ...e, thesisId: t.id }));
|
|
3497
|
-
}));
|
|
3498
|
-
const allEdges = [];
|
|
3499
|
-
for (const r of results) {
|
|
3500
|
-
if (r.status === 'fulfilled')
|
|
3501
|
-
allEdges.push(...r.value);
|
|
3502
|
-
}
|
|
3503
|
-
allEdges.sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0));
|
|
3504
|
-
const top10 = allEdges.slice(0, 10).map((e) => ({
|
|
3505
|
-
ticker: e.marketId || e.ticker || '-',
|
|
3506
|
-
market: e.market || e.marketTitle || '-',
|
|
3507
|
-
edge: e.edge || e.edgeSize || 0,
|
|
3508
|
-
direction: e.direction || 'yes',
|
|
3509
|
-
venue: e.venue || 'kalshi',
|
|
3510
|
-
}));
|
|
3511
|
-
return {
|
|
3512
|
-
content: [{ type: 'text', text: JSON.stringify(top10, null, 2) }],
|
|
3513
|
-
details: {},
|
|
3514
|
-
};
|
|
3515
|
-
},
|
|
3516
|
-
},
|
|
3517
|
-
{
|
|
3518
|
-
name: 'get_feed',
|
|
3519
|
-
label: 'Get Feed',
|
|
3520
|
-
description: 'Get evaluation history with topSignal highlighting. The most important signal is surfaced first.',
|
|
3521
|
-
parameters: Type.Object({
|
|
3522
|
-
hours: Type.Optional(Type.Number({ description: 'Hours of history to fetch (default 24)' })),
|
|
3523
|
-
}),
|
|
3524
|
-
execute: async (_id, p) => {
|
|
3525
|
-
const result = await sfClient.getFeed(p.hours || 24);
|
|
3526
|
-
const items = Array.isArray(result) ? result : (result?.evaluations || result?.items || []);
|
|
3527
|
-
let topSignal = null;
|
|
3528
|
-
let topScore = 0;
|
|
3529
|
-
for (const item of items) {
|
|
3530
|
-
let score = 0;
|
|
3531
|
-
const delta = Math.abs(item.confidenceDelta || item.confidence_delta || 0);
|
|
3532
|
-
if (delta > 0)
|
|
3533
|
-
score = delta * 100;
|
|
3534
|
-
else if (item.summary?.length > 50)
|
|
3535
|
-
score = 0.1;
|
|
3536
|
-
if (score > topScore) {
|
|
3537
|
-
topScore = score;
|
|
3538
|
-
topSignal = item;
|
|
3539
|
-
}
|
|
3540
|
-
}
|
|
3541
|
-
const output = { total: items.length };
|
|
3542
|
-
if (topSignal) {
|
|
3543
|
-
output.topSignal = {
|
|
3544
|
-
summary: topSignal.summary || topSignal.content || '',
|
|
3545
|
-
confidenceDelta: topSignal.confidenceDelta || topSignal.confidence_delta || 0,
|
|
3546
|
-
evaluatedAt: topSignal.evaluatedAt || topSignal.evaluated_at || '',
|
|
3547
|
-
why: topScore > 1 ? 'Largest confidence movement' : topScore > 0 ? 'Most substantive (no confidence change)' : 'Most recent',
|
|
3548
|
-
};
|
|
3549
|
-
}
|
|
3550
|
-
output.items = items;
|
|
3551
|
-
return {
|
|
3552
|
-
content: [{ type: 'text', text: JSON.stringify(output, null, 2) }],
|
|
3553
|
-
details: {},
|
|
3554
|
-
};
|
|
3555
|
-
},
|
|
3556
|
-
},
|
|
3557
|
-
{
|
|
3558
|
-
name: 'get_changes',
|
|
3559
|
-
label: 'Get Changes',
|
|
3560
|
-
description: 'Get recent market changes detected server-side. Returns real price moves (>5¢), new contracts, and removed/settled contracts across Kalshi, Polymarket, and traditional markets. Use for situational awareness and discovering new opportunities.',
|
|
3561
|
-
parameters: Type.Object({
|
|
3562
|
-
hours: Type.Optional(Type.Number({ description: 'Hours of history (default 1)' })),
|
|
3563
|
-
}),
|
|
3564
|
-
execute: async (_id, p) => {
|
|
3565
|
-
const hours = p.hours || 1;
|
|
3566
|
-
const since = new Date(Date.now() - hours * 60 * 60 * 1000).toISOString();
|
|
3567
|
-
const apiUrl = process.env.SF_API_URL || 'https://simplefunctions.dev';
|
|
3568
|
-
const res = await fetch(`${apiUrl}/api/changes?since=${encodeURIComponent(since)}&limit=100`);
|
|
3569
|
-
if (!res.ok)
|
|
3570
|
-
return { content: [{ type: 'text', text: JSON.stringify({ error: `API error ${res.status}` }) }], details: {} };
|
|
3571
|
-
const data = await res.json();
|
|
3572
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3573
|
-
},
|
|
3574
|
-
},
|
|
3575
|
-
{
|
|
3576
|
-
name: 'explore_public',
|
|
3577
|
-
label: 'Explore Public Theses',
|
|
3578
|
-
description: 'Browse public theses from other users. No auth required. Pass a slug to get details, or omit to list all.',
|
|
3579
|
-
parameters: Type.Object({
|
|
3580
|
-
slug: Type.Optional(Type.String({ description: 'Specific thesis slug, or empty to list all' })),
|
|
3581
|
-
}),
|
|
3582
|
-
execute: async (_id, p) => {
|
|
3583
|
-
const base = 'https://simplefunctions.dev';
|
|
3584
|
-
if (p.slug) {
|
|
3585
|
-
const res = await fetch(`${base}/api/public/thesis/${p.slug}`);
|
|
3586
|
-
if (!res.ok)
|
|
3587
|
-
return { content: [{ type: 'text', text: `Not found: ${p.slug}` }], details: {} };
|
|
3588
|
-
const data = await res.json();
|
|
3589
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3590
|
-
}
|
|
3591
|
-
const res = await fetch(`${base}/api/public/theses`);
|
|
3592
|
-
if (!res.ok)
|
|
3593
|
-
return { content: [{ type: 'text', text: 'Failed to fetch public theses' }], details: {} };
|
|
3594
|
-
const data = await res.json();
|
|
3595
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3596
|
-
},
|
|
3597
|
-
},
|
|
3598
|
-
{
|
|
3599
|
-
name: 'create_strategy',
|
|
3600
|
-
label: 'Create Strategy',
|
|
3601
|
-
description: 'Create a trading strategy for a thesis. Extract hard conditions (entryBelow/stopLoss/takeProfit as cents) and soft conditions from conversation. Called when user mentions specific trade ideas.',
|
|
3602
|
-
parameters: Type.Object({
|
|
3603
|
-
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
3604
|
-
marketId: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T150' }),
|
|
3605
|
-
market: Type.String({ description: 'Human-readable market name' }),
|
|
3606
|
-
direction: Type.String({ description: 'yes or no' }),
|
|
3607
|
-
horizon: Type.Optional(Type.String({ description: 'short, medium, or long. Default: medium' })),
|
|
3608
|
-
entryBelow: Type.Optional(Type.Number({ description: 'Entry trigger: ask <= this value (cents)' })),
|
|
3609
|
-
entryAbove: Type.Optional(Type.Number({ description: 'Entry trigger: ask >= this value (cents, for NO direction)' })),
|
|
3610
|
-
stopLoss: Type.Optional(Type.Number({ description: 'Stop loss: bid <= this value (cents)' })),
|
|
3611
|
-
takeProfit: Type.Optional(Type.Number({ description: 'Take profit: bid >= this value (cents)' })),
|
|
3612
|
-
maxQuantity: Type.Optional(Type.Number({ description: 'Max total contracts. Default: 500' })),
|
|
3613
|
-
perOrderQuantity: Type.Optional(Type.Number({ description: 'Contracts per order. Default: 50' })),
|
|
3614
|
-
softConditions: Type.Optional(Type.String({ description: 'LLM-evaluated conditions e.g. "only enter when n3 > 60%"' })),
|
|
3615
|
-
rationale: Type.Optional(Type.String({ description: 'Full logic description' })),
|
|
3616
|
-
}),
|
|
3617
|
-
execute: async (_id, p) => {
|
|
3618
|
-
const result = await sfClient.createStrategyAPI(p.thesisId, {
|
|
3619
|
-
marketId: p.marketId,
|
|
3620
|
-
market: p.market,
|
|
3621
|
-
direction: p.direction,
|
|
3622
|
-
horizon: p.horizon,
|
|
3623
|
-
entryBelow: p.entryBelow,
|
|
3624
|
-
entryAbove: p.entryAbove,
|
|
3625
|
-
stopLoss: p.stopLoss,
|
|
3626
|
-
takeProfit: p.takeProfit,
|
|
3627
|
-
maxQuantity: p.maxQuantity,
|
|
3628
|
-
perOrderQuantity: p.perOrderQuantity,
|
|
3629
|
-
softConditions: p.softConditions,
|
|
3630
|
-
rationale: p.rationale,
|
|
3631
|
-
createdBy: 'agent',
|
|
3632
|
-
});
|
|
3633
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
3634
|
-
},
|
|
3635
|
-
},
|
|
3636
|
-
{
|
|
3637
|
-
name: 'list_strategies',
|
|
3638
|
-
label: 'List Strategies',
|
|
3639
|
-
description: 'List strategies for a thesis. Filter by status (active/watching/executed/cancelled/review) or omit for all.',
|
|
3640
|
-
parameters: Type.Object({
|
|
3641
|
-
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
3642
|
-
status: Type.Optional(Type.String({ description: 'Filter by status. Omit for all.' })),
|
|
3643
|
-
}),
|
|
3644
|
-
execute: async (_id, p) => {
|
|
3645
|
-
const result = await sfClient.getStrategies(p.thesisId, p.status);
|
|
3646
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
3647
|
-
},
|
|
3648
|
-
},
|
|
3649
|
-
{
|
|
3650
|
-
name: 'update_strategy',
|
|
3651
|
-
label: 'Update Strategy',
|
|
3652
|
-
description: 'Update a strategy (change stop loss, take profit, status, priority, etc.)',
|
|
3653
|
-
parameters: Type.Object({
|
|
3654
|
-
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
3655
|
-
strategyId: Type.String({ description: 'Strategy ID (UUID)' }),
|
|
3656
|
-
stopLoss: Type.Optional(Type.Number({ description: 'New stop loss (cents)' })),
|
|
3657
|
-
takeProfit: Type.Optional(Type.Number({ description: 'New take profit (cents)' })),
|
|
3658
|
-
entryBelow: Type.Optional(Type.Number({ description: 'New entry below trigger (cents)' })),
|
|
3659
|
-
entryAbove: Type.Optional(Type.Number({ description: 'New entry above trigger (cents)' })),
|
|
3660
|
-
status: Type.Optional(Type.String({ description: 'New status: active|watching|executed|cancelled|review' })),
|
|
3661
|
-
priority: Type.Optional(Type.Number({ description: 'New priority' })),
|
|
3662
|
-
softConditions: Type.Optional(Type.String({ description: 'Updated soft conditions' })),
|
|
3663
|
-
rationale: Type.Optional(Type.String({ description: 'Updated rationale' })),
|
|
3664
|
-
}),
|
|
3665
|
-
execute: async (_id, p) => {
|
|
3666
|
-
const { thesisId, strategyId, ...updates } = p;
|
|
3667
|
-
const result = await sfClient.updateStrategyAPI(thesisId, strategyId, updates);
|
|
3668
|
-
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
3669
|
-
},
|
|
3670
|
-
},
|
|
3671
|
-
{
|
|
3672
|
-
name: 'what_if',
|
|
3673
|
-
label: 'What-If',
|
|
3674
|
-
description: 'Run a what-if scenario: override causal tree node probabilities and see how edges and confidence change. Zero LLM cost — pure computation.',
|
|
3675
|
-
parameters: Type.Object({
|
|
3676
|
-
overrides: Type.Array(Type.Object({
|
|
3677
|
-
nodeId: Type.String({ description: 'Causal tree node ID (e.g. n1, n3.1)' }),
|
|
3678
|
-
newProbability: Type.Number({ description: 'New probability 0-1' }),
|
|
3679
|
-
}), { description: 'Node probability overrides' }),
|
|
3680
|
-
}),
|
|
3681
|
-
execute: async (_id, p) => {
|
|
3682
|
-
const ctx = latestContext;
|
|
3683
|
-
const allNodes = [];
|
|
3684
|
-
function flatten(nodes) {
|
|
3685
|
-
for (const n of nodes) {
|
|
3686
|
-
allNodes.push(n);
|
|
3687
|
-
if (n.children?.length)
|
|
3688
|
-
flatten(n.children);
|
|
3689
|
-
}
|
|
3690
|
-
}
|
|
3691
|
-
const rawNodes = ctx.causalTree?.nodes || [];
|
|
3692
|
-
flatten(rawNodes);
|
|
3693
|
-
const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
|
|
3694
|
-
const overrideMap = new Map(p.overrides.map((o) => [o.nodeId, o.newProbability]));
|
|
3695
|
-
const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
|
|
3696
|
-
const newConf = treeNodes.reduce((s, n) => {
|
|
3697
|
-
const prob = overrideMap.get(n.id) ?? n.probability ?? 0;
|
|
3698
|
-
return s + prob * (n.importance || 0);
|
|
3699
|
-
}, 0);
|
|
3700
|
-
const nodeScales = new Map();
|
|
3701
|
-
for (const [nid, np] of overrideMap.entries()) {
|
|
3702
|
-
const nd = allNodes.find((n) => n.id === nid);
|
|
3703
|
-
if (nd && nd.probability > 0)
|
|
3704
|
-
nodeScales.set(nid, Math.max(0, Math.min(2, np / nd.probability)));
|
|
3705
|
-
}
|
|
3706
|
-
const edges = (ctx.edges || []).map((edge) => {
|
|
3707
|
-
const relNode = edge.relatedNodeId;
|
|
3708
|
-
let scaleFactor = 1;
|
|
3709
|
-
if (relNode) {
|
|
3710
|
-
const candidates = [relNode, relNode.split('.').slice(0, -1).join('.'), relNode.split('.')[0]].filter(Boolean);
|
|
3711
|
-
for (const cid of candidates) {
|
|
3712
|
-
if (nodeScales.has(cid)) {
|
|
3713
|
-
scaleFactor = nodeScales.get(cid);
|
|
3714
|
-
break;
|
|
3715
|
-
}
|
|
3716
|
-
}
|
|
3717
|
-
}
|
|
3718
|
-
const mkt = edge.marketPrice || 0;
|
|
3719
|
-
const oldTP = edge.thesisPrice || edge.thesisImpliedPrice || mkt;
|
|
3720
|
-
const oldEdge = edge.edge || edge.edgeSize || 0;
|
|
3721
|
-
const newTP = Math.round((mkt + (oldTP - mkt) * scaleFactor) * 100) / 100;
|
|
3722
|
-
const dir = edge.direction || 'yes';
|
|
3723
|
-
const newEdge = Math.round((dir === 'yes' ? newTP - mkt : mkt - newTP) * 100) / 100;
|
|
3724
|
-
return {
|
|
3725
|
-
market: edge.market || edge.marketTitle || edge.marketId,
|
|
3726
|
-
marketPrice: mkt,
|
|
3727
|
-
oldEdge,
|
|
3728
|
-
newEdge,
|
|
3729
|
-
delta: newEdge - oldEdge,
|
|
3730
|
-
signal: Math.abs(newEdge - oldEdge) < 1 ? 'unchanged' : (oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0) ? 'REVERSED' : Math.abs(newEdge) < 2 ? 'GONE' : 'reduced',
|
|
3731
|
-
};
|
|
3732
|
-
}).filter((e) => e.signal !== 'unchanged');
|
|
3733
|
-
const result = {
|
|
3734
|
-
overrides: p.overrides.map((o) => {
|
|
3735
|
-
const node = allNodes.find((n) => n.id === o.nodeId);
|
|
3736
|
-
return { nodeId: o.nodeId, label: node?.label || o.nodeId, oldProb: node?.probability, newProb: o.newProbability };
|
|
3737
|
-
}),
|
|
3738
|
-
confidence: { old: Math.round(oldConf * 100), new: Math.round(newConf * 100), delta: Math.round((newConf - oldConf) * 100) },
|
|
3739
|
-
affectedEdges: edges,
|
|
3740
|
-
};
|
|
3741
|
-
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
3742
|
-
},
|
|
3743
|
-
},
|
|
3744
|
-
];
|
|
3745
|
-
// ── X (Twitter) tools for plain mode ──────────────────────────────────────
|
|
3746
|
-
tools.push({
|
|
3747
|
-
name: 'search_x', label: 'X Search',
|
|
3748
|
-
description: 'Search X (Twitter) for recent discussions. Returns posts, sentiment, themes.',
|
|
3749
|
-
parameters: Type.Object({
|
|
3750
|
-
query: Type.String({ description: 'Search query' }),
|
|
3751
|
-
mode: Type.Optional(Type.String({ description: '"summary" or "raw"' })),
|
|
3752
|
-
hours: Type.Optional(Type.Number({ description: 'Hours (default 24)' })),
|
|
3753
|
-
}),
|
|
3754
|
-
execute: async (_id, p) => {
|
|
3755
|
-
const data = await sfClient.searchX(p.query, { mode: p.mode, hours: p.hours });
|
|
3756
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3757
|
-
},
|
|
3758
|
-
}, {
|
|
3759
|
-
name: 'x_volume', label: 'X Volume',
|
|
3760
|
-
description: 'X discussion volume trend — total posts, velocity, peak time, timeseries.',
|
|
3761
|
-
parameters: Type.Object({
|
|
3762
|
-
query: Type.String({ description: 'Search query' }),
|
|
3763
|
-
hours: Type.Optional(Type.Number({ description: 'Hours (default 72)' })),
|
|
3764
|
-
}),
|
|
3765
|
-
execute: async (_id, p) => {
|
|
3766
|
-
const data = await sfClient.getXVolume(p.query, { hours: p.hours });
|
|
3767
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3768
|
-
},
|
|
3769
|
-
}, {
|
|
3770
|
-
name: 'x_news', label: 'X News',
|
|
3771
|
-
description: 'News stories trending on X — titles, summaries, tickers.',
|
|
3772
|
-
parameters: Type.Object({
|
|
3773
|
-
query: Type.String({ description: 'Search query' }),
|
|
3774
|
-
limit: Type.Optional(Type.Number({ description: 'Max stories (default 10)' })),
|
|
3775
|
-
}),
|
|
3776
|
-
execute: async (_id, p) => {
|
|
3777
|
-
const data = await sfClient.searchXNews(p.query, { limit: p.limit });
|
|
3778
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3779
|
-
},
|
|
3780
|
-
}, {
|
|
3781
|
-
name: 'x_account', label: 'X Account',
|
|
3782
|
-
description: 'Recent posts from a specific X account.',
|
|
3783
|
-
parameters: Type.Object({
|
|
3784
|
-
username: Type.String({ description: 'X username (with or without @)' }),
|
|
3785
|
-
hours: Type.Optional(Type.Number({ description: 'Hours (default 24)' })),
|
|
3786
|
-
}),
|
|
3787
|
-
execute: async (_id, p) => {
|
|
3788
|
-
const data = await sfClient.getXAccount(p.username, { hours: p.hours });
|
|
3789
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3790
|
-
},
|
|
3791
|
-
}, {
|
|
3792
|
-
name: 'heartbeat_config', label: 'Heartbeat Config',
|
|
3793
|
-
description: 'View or update heartbeat settings: scan intervals, model tier, budget cap, pause/resume. Shows cost breakdown.',
|
|
3794
|
-
parameters: Type.Object({
|
|
3795
|
-
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
3796
|
-
newsIntervalMin: Type.Optional(Type.Number({ description: 'News scan interval (15-1440 min)' })),
|
|
3797
|
-
xIntervalMin: Type.Optional(Type.Number({ description: 'X scan interval (60-1440 min)' })),
|
|
3798
|
-
evalModelTier: Type.Optional(Type.String({ description: 'cheap, medium, or heavy' })),
|
|
3799
|
-
monthlyBudgetUsd: Type.Optional(Type.Number({ description: 'Monthly budget (0 = unlimited)' })),
|
|
3800
|
-
paused: Type.Optional(Type.Boolean({ description: 'Pause or resume heartbeat' })),
|
|
3801
|
-
}),
|
|
3802
|
-
execute: async (_id, p) => {
|
|
3803
|
-
const hasUp = p.newsIntervalMin || p.xIntervalMin || p.evalModelTier || p.monthlyBudgetUsd !== undefined || p.paused !== undefined;
|
|
3804
|
-
if (hasUp) {
|
|
3805
|
-
const u = {};
|
|
3806
|
-
if (p.newsIntervalMin)
|
|
3807
|
-
u.newsIntervalMin = p.newsIntervalMin;
|
|
3808
|
-
if (p.xIntervalMin)
|
|
3809
|
-
u.xIntervalMin = p.xIntervalMin;
|
|
3810
|
-
if (p.evalModelTier)
|
|
3811
|
-
u.evalModelTier = p.evalModelTier;
|
|
3812
|
-
if (p.monthlyBudgetUsd !== undefined)
|
|
3813
|
-
u.monthlyBudgetUsd = p.monthlyBudgetUsd;
|
|
3814
|
-
if (p.paused !== undefined)
|
|
3815
|
-
u.paused = p.paused;
|
|
3816
|
-
await sfClient.updateHeartbeatConfig(p.thesisId, u);
|
|
3817
|
-
}
|
|
3818
|
-
const data = await sfClient.getHeartbeatConfig(p.thesisId);
|
|
3819
|
-
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
3820
|
-
},
|
|
3821
|
-
});
|
|
3822
|
-
// ── Trading tools (conditional on tradingEnabled) for plain mode ──────────
|
|
3823
|
-
const config = (0, config_js_1.loadConfig)();
|
|
3824
|
-
if (config.tradingEnabled) {
|
|
3825
|
-
tools.push({
|
|
3826
|
-
name: 'place_order',
|
|
3827
|
-
label: 'Place Order',
|
|
3828
|
-
description: 'Place a buy or sell order on Kalshi. Shows preview and asks for confirmation.',
|
|
3829
|
-
parameters: Type.Object({
|
|
3830
|
-
ticker: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T135' }),
|
|
3831
|
-
side: Type.String({ description: 'yes or no' }),
|
|
3832
|
-
action: Type.String({ description: 'buy or sell' }),
|
|
3833
|
-
type: Type.String({ description: 'limit or market' }),
|
|
3834
|
-
count: Type.Number({ description: 'Number of contracts' }),
|
|
3835
|
-
price_cents: Type.Optional(Type.Number({ description: 'Limit price in cents (1-99). Required for limit orders.' })),
|
|
3836
|
-
}),
|
|
3837
|
-
execute: async (_id, p) => {
|
|
3838
|
-
const { createOrder } = await import('../kalshi.js');
|
|
3839
|
-
const readline = await import('readline');
|
|
3840
|
-
const priceCents = p.price_cents ? Math.round(Number(p.price_cents)) : undefined;
|
|
3841
|
-
const maxCost = ((priceCents || 99) * p.count / 100).toFixed(2);
|
|
3842
|
-
const preview = ` Order: ${p.action.toUpperCase()} ${p.count}x ${p.ticker} ${p.side.toUpperCase()} @ ${priceCents ? priceCents + '\u00A2' : 'market'} (max $${maxCost})`;
|
|
3843
|
-
process.stderr.write(preview + '\n');
|
|
3844
|
-
// Reject if stdin is piped (non-TTY) — too dangerous to auto-execute trades
|
|
3845
|
-
if (!process.stdin.isTTY) {
|
|
3846
|
-
return { content: [{ type: 'text', text: 'Order rejected: trading requires interactive terminal (stdin is piped). Use TUI mode for trading.' }], details: {} };
|
|
3847
|
-
}
|
|
3848
|
-
// Confirm in plain mode via readline
|
|
3849
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
3850
|
-
const answer = await new Promise(resolve => rl.question(' Execute? (y/n) ', resolve));
|
|
3851
|
-
rl.close();
|
|
3852
|
-
if (!answer.toLowerCase().startsWith('y')) {
|
|
3853
|
-
return { content: [{ type: 'text', text: 'Order cancelled by user.' }], details: {} };
|
|
3854
|
-
}
|
|
3855
|
-
try {
|
|
3856
|
-
const result = await createOrder({
|
|
3857
|
-
ticker: p.ticker,
|
|
3858
|
-
side: p.side,
|
|
3859
|
-
action: p.action,
|
|
3860
|
-
type: p.type,
|
|
3861
|
-
count: p.count,
|
|
3862
|
-
...(priceCents ? { yes_price: priceCents } : {}),
|
|
3863
|
-
});
|
|
3864
|
-
const order = result.order || result;
|
|
3865
|
-
return {
|
|
3866
|
-
content: [{ type: 'text', text: `Order placed: ${order.order_id || 'OK'}\nStatus: ${order.status || '-'}\nFilled: ${order.fill_count_fp || 0}/${order.initial_count_fp || p.count}` }],
|
|
3867
|
-
details: {},
|
|
3868
|
-
};
|
|
3869
|
-
}
|
|
3870
|
-
catch (err) {
|
|
3871
|
-
const msg = err.message || String(err);
|
|
3872
|
-
if (msg.includes('403')) {
|
|
3873
|
-
return { content: [{ type: 'text', text: '403 Forbidden \u2014 your Kalshi key lacks write permission. Get a read+write key at kalshi.com/account/api-keys' }], details: {} };
|
|
3874
|
-
}
|
|
3875
|
-
return { content: [{ type: 'text', text: `Order failed: ${msg}` }], details: {} };
|
|
3876
|
-
}
|
|
3877
|
-
},
|
|
3878
|
-
}, {
|
|
3879
|
-
name: 'cancel_order',
|
|
3880
|
-
label: 'Cancel Order',
|
|
3881
|
-
description: 'Cancel a resting order by order ID. Executes directly (no confirmation prompt in plain mode).',
|
|
3882
|
-
parameters: Type.Object({
|
|
3883
|
-
order_id: Type.String({ description: 'Order ID to cancel' }),
|
|
3884
|
-
}),
|
|
3885
|
-
execute: async (_id, p) => {
|
|
3886
|
-
const { cancelOrder } = await import('../kalshi.js');
|
|
3887
|
-
try {
|
|
3888
|
-
await cancelOrder(p.order_id);
|
|
3889
|
-
return { content: [{ type: 'text', text: `Order ${p.order_id} cancelled.` }], details: {} };
|
|
3890
|
-
}
|
|
3891
|
-
catch (err) {
|
|
3892
|
-
return { content: [{ type: 'text', text: `Cancel failed: ${err.message}` }], details: {} };
|
|
3893
|
-
}
|
|
3894
|
-
},
|
|
3895
|
-
});
|
|
3896
|
-
}
|
|
3897
|
-
// ── System prompt ─────────────────────────────────────────────────────────
|
|
3898
|
-
const ctx = latestContext;
|
|
3899
|
-
const isExplorerPlain = ctx._explorerMode || resolvedThesisId === '_explorer';
|
|
3900
|
-
let systemPrompt;
|
|
3901
|
-
if (isExplorerPlain) {
|
|
3902
|
-
const topEdges = (ctx.edges || [])
|
|
3903
|
-
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
3904
|
-
.slice(0, 5)
|
|
3905
|
-
.map((e) => ` ${(e.title || '').slice(0, 40)} | ${e.venue || 'kalshi'} | +${e.edge}`)
|
|
3906
|
-
.join('\n') || ' (no edges)';
|
|
3907
|
-
systemPrompt = `You are a prediction market research assistant in EXPLORER MODE — not bound to any thesis.
|
|
3908
|
-
|
|
3909
|
-
## What you can do
|
|
3910
|
-
- query: LLM-enhanced market search
|
|
3911
|
-
- scan_markets: search Kalshi + Polymarket
|
|
3912
|
-
- get_markets: traditional markets (SPY, VIX, gold, oil)
|
|
3913
|
-
- explore_public: browse public theses
|
|
3914
|
-
- search_x, x_volume, x_news: X/Twitter signals
|
|
3915
|
-
- get_positions: portfolio positions
|
|
3916
|
-
- create_thesis: create a thesis when user forms a view
|
|
3917
|
-
|
|
3918
|
-
## CRITICAL: When the user expresses a view worth tracking, use create_thesis. After creation, confirm and continue with the new thesis context.
|
|
3919
|
-
|
|
3920
|
-
## Rules
|
|
3921
|
-
- Be concise. Use tools for fresh data.
|
|
3922
|
-
- Use Chinese if user writes Chinese, English if English.
|
|
3923
|
-
- Prices in cents (¢). P&L in dollars ($).
|
|
3924
|
-
${config.tradingEnabled ? '- Trading ENABLED.' : '- Trading DISABLED.'}
|
|
3925
|
-
|
|
3926
|
-
## Market snapshot
|
|
3927
|
-
Public edges:
|
|
3928
|
-
${topEdges}`;
|
|
3929
|
-
}
|
|
3930
|
-
else {
|
|
3931
|
-
const edgesSummary = ctx.edges
|
|
3932
|
-
?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
3933
|
-
.slice(0, 5)
|
|
3934
|
-
.map((e) => ` ${(e.market || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}\u00A2 | edge ${e.edge > 0 ? '+' : ''}${e.edge}`)
|
|
3935
|
-
.join('\n') || ' (no edges)';
|
|
3936
|
-
const nodesSummary = ctx.causalTree?.nodes
|
|
3937
|
-
?.filter((n) => n.depth === 0)
|
|
3938
|
-
.map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
|
|
3939
|
-
.join('\n') || ' (no causal tree)';
|
|
3940
|
-
const conf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
|
|
3941
|
-
systemPrompt = `You are a prediction market trading assistant. Your job is not to please the user — it is to help them see reality clearly and make correct trading decisions.
|
|
3942
|
-
|
|
3943
|
-
## Framework
|
|
3944
|
-
Edge = thesis price - market price. Positive = market underprices. executableEdge = edge minus spread.
|
|
3945
|
-
|
|
3946
|
-
Edge types: "consensus_gap" (depth >= 500, real disagreement), "attention_gap" (depth < 100, illusory pricing), "timing_gap" (market lags), "risk_premium" (settlement/platform risk). Always classify when reporting edges.
|
|
3947
|
-
For edges > 20 cents, state what the market must believe for it to be right.
|
|
3948
|
-
|
|
3949
|
-
Price reliability: depth >= 500 = consensus. depth < 100 = unreliable. spread > 5 cents = noisy. Flag illiquid markets.
|
|
3950
|
-
|
|
3951
|
-
Kill conditions: each causal node has a falsifier. Check these first when evaluating news. If triggered, override other analysis.
|
|
3952
|
-
|
|
3953
|
-
Always state contract expiry and next catalyst. No catalyst = flag capital lock risk.
|
|
3954
|
-
|
|
3955
|
-
For complex questions, chain: get_context -> inspect_book -> get_liquidity -> web_search -> search_x -> synthesize.
|
|
3956
|
-
Use search_x for social sentiment on any topic. Use x_volume to detect discussion spikes. Use x_account to track key people.
|
|
3957
|
-
Use heartbeat_config to view/adjust scan intervals, model tier, budget cap, or check cost breakdown.
|
|
3958
|
-
|
|
3959
|
-
Flag correlated exposure across positions sharing upstream nodes. If nothing to do, say so.
|
|
3960
|
-
|
|
3961
|
-
## Rules
|
|
3962
|
-
- Be concise. Use tools for fresh data. Don't guess prices.
|
|
3963
|
-
- You do NOT know the user's positions at start. Call get_positions before discussing trades or portfolio.
|
|
3964
|
-
- If user mentions news, inject_signal immediately.
|
|
3965
|
-
- If user says "evaluate", trigger immediately. Don't confirm.
|
|
3966
|
-
- Don't end with "anything else?"
|
|
3967
|
-
- If an edge is narrowing or disappearing, say so proactively.
|
|
3968
|
-
- Use Chinese if user writes Chinese, English if English.
|
|
3969
|
-
- Prices in cents (¢). P&L in dollars ($). Don't re-convert tool output.
|
|
3970
|
-
- When a trade idea emerges, create_strategy to record it.
|
|
3971
|
-
${config.tradingEnabled ? '- Trading ENABLED. You have place_order and cancel_order tools.' : '- Trading DISABLED. Tell user: sf setup --enable-trading'}
|
|
3972
|
-
|
|
3973
|
-
## Current State
|
|
3974
|
-
Thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
|
|
3975
|
-
ID: ${resolvedThesisId} | Confidence: ${conf}% | Status: ${ctx.status}
|
|
3976
|
-
|
|
3977
|
-
Causal nodes:
|
|
3978
|
-
${nodesSummary}
|
|
3979
|
-
|
|
3980
|
-
Top edges:
|
|
3981
|
-
${edgesSummary}
|
|
3982
|
-
|
|
3983
|
-
${ctx.lastEvaluation?.summary ? `Latest eval: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
|
|
3984
|
-
}
|
|
3985
|
-
// ── Create agent ──────────────────────────────────────────────────────────
|
|
3986
|
-
const agent = new Agent({
|
|
3987
|
-
initialState: { systemPrompt, model, tools, thinkingLevel: 'off' },
|
|
3988
|
-
streamFn: streamSimple,
|
|
3989
|
-
getApiKey: (provider) => provider === 'openrouter' ? openrouterKey : undefined,
|
|
3990
|
-
});
|
|
3991
|
-
// ── Session restore ───────────────────────────────────────────────────────
|
|
3992
|
-
if (!opts?.newSession) {
|
|
3993
|
-
const saved = loadSession(resolvedThesisId);
|
|
3994
|
-
if (saved?.messages?.length > 0) {
|
|
3995
|
-
try {
|
|
3996
|
-
agent.replaceMessages(saved.messages);
|
|
3997
|
-
agent.setSystemPrompt(systemPrompt);
|
|
3998
|
-
}
|
|
3999
|
-
catch { /* start fresh */ }
|
|
4000
|
-
}
|
|
4001
|
-
}
|
|
4002
|
-
// ── Subscribe to agent events → plain stdout ──────────────────────────────
|
|
4003
|
-
let currentText = '';
|
|
4004
|
-
agent.subscribe((event) => {
|
|
4005
|
-
if (event.type === 'message_update') {
|
|
4006
|
-
const e = event.assistantMessageEvent;
|
|
4007
|
-
if (e.type === 'text_delta') {
|
|
4008
|
-
process.stdout.write(e.delta);
|
|
4009
|
-
currentText += e.delta;
|
|
4010
|
-
}
|
|
4011
|
-
}
|
|
4012
|
-
if (event.type === 'message_end') {
|
|
4013
|
-
if (currentText) {
|
|
4014
|
-
process.stdout.write('\n');
|
|
4015
|
-
currentText = '';
|
|
4016
|
-
}
|
|
4017
|
-
}
|
|
4018
|
-
if (event.type === 'tool_execution_start') {
|
|
4019
|
-
process.stderr.write(` \u25B8 ${event.toolName}...\n`);
|
|
4020
|
-
}
|
|
4021
|
-
if (event.type === 'tool_execution_end') {
|
|
4022
|
-
const status = event.isError ? '\u2717' : '\u2713';
|
|
4023
|
-
process.stderr.write(` ${status} ${event.toolName}\n`);
|
|
4024
|
-
}
|
|
4025
|
-
});
|
|
4026
|
-
// ── Welcome ───────────────────────────────────────────────────────────────
|
|
4027
|
-
if (isExplorerPlain) {
|
|
4028
|
-
console.log(`SF Agent — Explorer mode | ${currentModelName}`);
|
|
4029
|
-
console.log(`Public edges: ${(ctx.edges || []).length}`);
|
|
4030
|
-
console.log('Ask anything about prediction markets. Type /help for commands, /exit to quit.\n');
|
|
4031
|
-
}
|
|
4032
|
-
else {
|
|
4033
|
-
const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
|
|
4034
|
-
const plainConf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
|
|
4035
|
-
console.log(`SF Agent — ${resolvedThesisId.slice(0, 8)} | ${plainConf}% | ${currentModelName}`);
|
|
4036
|
-
console.log(`Thesis: ${thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText}`);
|
|
4037
|
-
console.log(`Edges: ${(ctx.edges || []).length} | Status: ${ctx.status}`);
|
|
4038
|
-
console.log('Type /help for commands, /exit to quit.\n');
|
|
4039
|
-
}
|
|
4040
|
-
// ── REPL loop ─────────────────────────────────────────────────────────────
|
|
4041
|
-
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' });
|
|
4042
|
-
rl.prompt();
|
|
4043
|
-
for await (const line of rl) {
|
|
4044
|
-
const trimmed = line.trim();
|
|
4045
|
-
if (!trimmed) {
|
|
4046
|
-
rl.prompt();
|
|
4047
|
-
continue;
|
|
4048
|
-
}
|
|
4049
|
-
if (trimmed === '/exit' || trimmed === '/quit') {
|
|
4050
|
-
try {
|
|
4051
|
-
saveSession(resolvedThesisId, currentModelName, agent.state.messages);
|
|
4052
|
-
}
|
|
4053
|
-
catch { }
|
|
4054
|
-
rl.close();
|
|
4055
|
-
return;
|
|
4056
|
-
}
|
|
4057
|
-
if (trimmed === '/help') {
|
|
4058
|
-
console.log('Commands: /help /exit /tree /edges /eval /model <name>');
|
|
4059
|
-
rl.prompt();
|
|
4060
|
-
continue;
|
|
4061
|
-
}
|
|
4062
|
-
if (trimmed === '/tree') {
|
|
4063
|
-
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
4064
|
-
const nodes = latestContext.causalTree?.nodes || [];
|
|
4065
|
-
for (const n of nodes) {
|
|
4066
|
-
const indent = ' '.repeat(n.depth || 0);
|
|
4067
|
-
console.log(`${indent}${n.id} ${(n.label || '').slice(0, 60)} — ${Math.round(n.probability * 100)}%`);
|
|
4068
|
-
}
|
|
4069
|
-
rl.prompt();
|
|
4070
|
-
continue;
|
|
4071
|
-
}
|
|
4072
|
-
if (trimmed === '/edges') {
|
|
4073
|
-
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
4074
|
-
const edges = (latestContext.edges || []).sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge)).slice(0, 15);
|
|
4075
|
-
for (const e of edges) {
|
|
4076
|
-
const sign = e.edge > 0 ? '+' : '';
|
|
4077
|
-
console.log(` ${(e.market || '').slice(0, 45).padEnd(45)} ${e.marketPrice}¢ edge ${sign}${e.edge} ${e.venue}`);
|
|
4078
|
-
}
|
|
4079
|
-
rl.prompt();
|
|
4080
|
-
continue;
|
|
4081
|
-
}
|
|
4082
|
-
if (trimmed === '/eval') {
|
|
4083
|
-
console.log('Triggering evaluation...');
|
|
4084
|
-
const result = await sfClient.evaluate(resolvedThesisId);
|
|
4085
|
-
console.log(`Confidence: ${result.previousConfidence} → ${result.newConfidence}`);
|
|
4086
|
-
if (result.summary)
|
|
4087
|
-
console.log(result.summary);
|
|
4088
|
-
rl.prompt();
|
|
4089
|
-
continue;
|
|
4090
|
-
}
|
|
4091
|
-
if (trimmed.startsWith('/model')) {
|
|
4092
|
-
const newModel = trimmed.slice(6).trim();
|
|
4093
|
-
if (!newModel) {
|
|
4094
|
-
console.log(`Current: ${currentModelName}`);
|
|
4095
|
-
rl.prompt();
|
|
4096
|
-
continue;
|
|
4097
|
-
}
|
|
4098
|
-
currentModelName = newModel.replace(/^openrouter\//, '');
|
|
4099
|
-
model = resolveModel(currentModelName);
|
|
4100
|
-
agent.setModel(model);
|
|
4101
|
-
console.log(`Model: ${currentModelName}`);
|
|
4102
|
-
rl.prompt();
|
|
4103
|
-
continue;
|
|
4104
|
-
}
|
|
4105
|
-
// Regular message → agent
|
|
4106
|
-
try {
|
|
4107
|
-
await agent.prompt(trimmed);
|
|
4108
|
-
}
|
|
4109
|
-
catch (err) {
|
|
4110
|
-
console.error(`Error: ${err.message}`);
|
|
4111
|
-
}
|
|
4112
|
-
// Save after each turn
|
|
4113
|
-
try {
|
|
4114
|
-
saveSession(resolvedThesisId, currentModelName, agent.state.messages);
|
|
4115
|
-
}
|
|
4116
|
-
catch { }
|
|
4117
|
-
rl.prompt();
|
|
4118
|
-
}
|
|
4119
|
-
}
|