@spfunctions/cli 1.1.5 → 1.1.7
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/client.d.ts +5 -0
- package/dist/client.js +17 -0
- package/dist/commands/agent.d.ts +1 -0
- package/dist/commands/agent.js +1170 -76
- package/dist/commands/announcements.d.ts +3 -0
- package/dist/commands/announcements.js +28 -0
- package/dist/commands/balance.d.ts +3 -0
- package/dist/commands/balance.js +17 -0
- package/dist/commands/cancel.d.ts +5 -0
- package/dist/commands/cancel.js +41 -0
- package/dist/commands/dashboard.d.ts +11 -0
- package/dist/commands/dashboard.js +195 -0
- package/dist/commands/feed.d.ts +13 -0
- package/dist/commands/feed.js +73 -0
- package/dist/commands/fills.d.ts +4 -0
- package/dist/commands/fills.js +29 -0
- package/dist/commands/forecast.d.ts +4 -0
- package/dist/commands/forecast.js +53 -0
- package/dist/commands/history.d.ts +3 -0
- package/dist/commands/history.js +38 -0
- package/dist/commands/milestones.d.ts +8 -0
- package/dist/commands/milestones.js +56 -0
- package/dist/commands/orders.d.ts +4 -0
- package/dist/commands/orders.js +28 -0
- package/dist/commands/publish.js +21 -2
- package/dist/commands/rfq.d.ts +5 -0
- package/dist/commands/rfq.js +35 -0
- package/dist/commands/schedule.d.ts +3 -0
- package/dist/commands/schedule.js +38 -0
- package/dist/commands/settlements.d.ts +6 -0
- package/dist/commands/settlements.js +50 -0
- package/dist/commands/setup.d.ts +2 -0
- package/dist/commands/setup.js +45 -3
- package/dist/commands/signal.js +12 -1
- package/dist/commands/strategies.d.ts +11 -0
- package/dist/commands/strategies.js +130 -0
- package/dist/commands/trade.d.ts +12 -0
- package/dist/commands/trade.js +78 -0
- package/dist/commands/whatif.d.ts +17 -0
- package/dist/commands/whatif.js +209 -0
- package/dist/config.d.ts +2 -0
- package/dist/config.js +13 -0
- package/dist/index.js +177 -3
- package/dist/kalshi.d.ts +71 -0
- package/dist/kalshi.js +257 -17
- package/package.json +1 -1
package/dist/commands/agent.js
CHANGED
|
@@ -23,6 +23,7 @@ const path_1 = __importDefault(require("path"));
|
|
|
23
23
|
const os_1 = __importDefault(require("os"));
|
|
24
24
|
const client_js_1 = require("../client.js");
|
|
25
25
|
const kalshi_js_1 = require("../kalshi.js");
|
|
26
|
+
const config_js_1 = require("../config.js");
|
|
26
27
|
// ─── Session persistence ─────────────────────────────────────────────────────
|
|
27
28
|
function getSessionDir() {
|
|
28
29
|
return path_1.default.join(os_1.default.homedir(), '.sf', 'sessions');
|
|
@@ -99,27 +100,53 @@ function createMutableLine(piTui) {
|
|
|
99
100
|
}
|
|
100
101
|
};
|
|
101
102
|
}
|
|
102
|
-
/**
|
|
103
|
+
/**
|
|
104
|
+
* Header bar — trading terminal style.
|
|
105
|
+
* Shows: thesis ID, confidence+delta, positions P&L, edge count, top edge
|
|
106
|
+
*/
|
|
103
107
|
function createHeaderBar(piTui) {
|
|
104
108
|
const { truncateToWidth, visibleWidth } = piTui;
|
|
105
109
|
return class HeaderBar {
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
110
|
+
thesisId = '';
|
|
111
|
+
confidence = 0;
|
|
112
|
+
confidenceDelta = 0;
|
|
113
|
+
pnlDollars = 0;
|
|
114
|
+
positionCount = 0;
|
|
115
|
+
edgeCount = 0;
|
|
116
|
+
topEdge = ''; // e.g. "RECESSION +21¢"
|
|
109
117
|
cachedWidth;
|
|
110
118
|
cachedLines;
|
|
111
|
-
|
|
112
|
-
this.
|
|
113
|
-
this.
|
|
114
|
-
|
|
119
|
+
setFromContext(ctx, positions) {
|
|
120
|
+
this.thesisId = (ctx.thesisId || '').slice(0, 8);
|
|
121
|
+
this.confidence = typeof ctx.confidence === 'number'
|
|
122
|
+
? Math.round(ctx.confidence * 100)
|
|
123
|
+
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
124
|
+
this.confidenceDelta = ctx.lastEvaluation?.confidenceDelta
|
|
125
|
+
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
126
|
+
: 0;
|
|
127
|
+
this.edgeCount = (ctx.edges || []).length;
|
|
128
|
+
// Top edge by absolute size
|
|
129
|
+
const edges = ctx.edges || [];
|
|
130
|
+
if (edges.length > 0) {
|
|
131
|
+
const top = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0))[0];
|
|
132
|
+
const name = (top.market || top.marketTitle || top.marketId || '').slice(0, 20);
|
|
133
|
+
const edge = top.edge || top.edgeSize || 0;
|
|
134
|
+
this.topEdge = `${name} ${edge > 0 ? '+' : ''}${Math.round(edge)}\u00A2`;
|
|
135
|
+
}
|
|
136
|
+
// P&L from positions
|
|
137
|
+
if (positions && positions.length > 0) {
|
|
138
|
+
this.positionCount = positions.length;
|
|
139
|
+
this.pnlDollars = positions.reduce((sum, p) => {
|
|
140
|
+
const pnl = p.unrealized_pnl || 0;
|
|
141
|
+
return sum + pnl;
|
|
142
|
+
}, 0) / 100; // cents → dollars
|
|
143
|
+
}
|
|
144
|
+
this.cachedWidth = undefined;
|
|
145
|
+
this.cachedLines = undefined;
|
|
115
146
|
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
if (center !== undefined)
|
|
120
|
-
this.center = center;
|
|
121
|
-
if (right !== undefined)
|
|
122
|
-
this.right = right;
|
|
147
|
+
updateConfidence(newConf, delta) {
|
|
148
|
+
this.confidence = Math.round(newConf * 100);
|
|
149
|
+
this.confidenceDelta = Math.round(delta * 100);
|
|
123
150
|
this.cachedWidth = undefined;
|
|
124
151
|
this.cachedLines = undefined;
|
|
125
152
|
}
|
|
@@ -127,23 +154,39 @@ function createHeaderBar(piTui) {
|
|
|
127
154
|
this.cachedWidth = undefined;
|
|
128
155
|
this.cachedLines = undefined;
|
|
129
156
|
}
|
|
157
|
+
// Keep legacy update() for compatibility with /switch etc.
|
|
158
|
+
update(left, center, right) {
|
|
159
|
+
this.cachedWidth = undefined;
|
|
160
|
+
this.cachedLines = undefined;
|
|
161
|
+
}
|
|
130
162
|
render(width) {
|
|
131
163
|
if (this.cachedLines && this.cachedWidth === width)
|
|
132
164
|
return this.cachedLines;
|
|
133
165
|
this.cachedWidth = width;
|
|
134
|
-
|
|
135
|
-
const
|
|
136
|
-
|
|
137
|
-
const
|
|
138
|
-
const
|
|
139
|
-
const
|
|
140
|
-
const
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
166
|
+
// Build segments
|
|
167
|
+
const id = C.emerald(bold(this.thesisId));
|
|
168
|
+
// Confidence with arrow
|
|
169
|
+
const arrow = this.confidenceDelta > 0 ? '\u25B2' : this.confidenceDelta < 0 ? '\u25BC' : '\u2500';
|
|
170
|
+
const arrowColor = this.confidenceDelta > 0 ? C.emerald : this.confidenceDelta < 0 ? C.red : C.zinc600;
|
|
171
|
+
const deltaStr = this.confidenceDelta !== 0 ? ` (${this.confidenceDelta > 0 ? '+' : ''}${this.confidenceDelta})` : '';
|
|
172
|
+
const conf = arrowColor(`${arrow} ${this.confidence}%${deltaStr}`);
|
|
173
|
+
// P&L
|
|
174
|
+
let pnl = '';
|
|
175
|
+
if (this.positionCount > 0) {
|
|
176
|
+
const pnlStr = this.pnlDollars >= 0
|
|
177
|
+
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
178
|
+
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
179
|
+
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
180
|
+
}
|
|
181
|
+
// Edges
|
|
182
|
+
const edges = C.zinc600(`${this.edgeCount} edges`);
|
|
183
|
+
// Top edge
|
|
184
|
+
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
185
|
+
// Assemble with separators
|
|
186
|
+
const sep = C.zinc600(' \u2502 ');
|
|
187
|
+
const parts = [id, conf, pnl, edges, top].filter(Boolean);
|
|
188
|
+
const content = parts.join(sep);
|
|
189
|
+
let line = C.bgZinc900(' ' + truncateToWidth(content, width - 2, '') + ' ');
|
|
147
190
|
const lineVw = visibleWidth(line);
|
|
148
191
|
if (lineVw < width) {
|
|
149
192
|
line = line + C.bgZinc900(' '.repeat(width - lineVw));
|
|
@@ -153,13 +196,16 @@ function createHeaderBar(piTui) {
|
|
|
153
196
|
}
|
|
154
197
|
};
|
|
155
198
|
}
|
|
156
|
-
/** Footer bar:
|
|
199
|
+
/** Footer bar: model | tokens | exchange status | trading status | /help */
|
|
157
200
|
function createFooterBar(piTui) {
|
|
158
201
|
const { truncateToWidth, visibleWidth } = piTui;
|
|
159
202
|
return class FooterBar {
|
|
160
203
|
tokens = 0;
|
|
161
204
|
cost = 0;
|
|
162
205
|
toolCount = 0;
|
|
206
|
+
modelName = '';
|
|
207
|
+
tradingEnabled = false;
|
|
208
|
+
exchangeOpen = null; // null = unknown
|
|
163
209
|
cachedWidth;
|
|
164
210
|
cachedLines;
|
|
165
211
|
invalidate() {
|
|
@@ -177,13 +223,23 @@ function createFooterBar(piTui) {
|
|
|
177
223
|
const tokStr = this.tokens >= 1000
|
|
178
224
|
? `${(this.tokens / 1000).toFixed(1)}k`
|
|
179
225
|
: `${this.tokens}`;
|
|
180
|
-
const
|
|
181
|
-
const
|
|
226
|
+
const model = C.zinc600(this.modelName.split('/').pop() || this.modelName);
|
|
227
|
+
const tokens = C.zinc600(`${tokStr} tok`);
|
|
228
|
+
const exchange = this.exchangeOpen === true
|
|
229
|
+
? C.emerald('OPEN')
|
|
230
|
+
: this.exchangeOpen === false
|
|
231
|
+
? C.red('CLOSED')
|
|
232
|
+
: C.zinc600('...');
|
|
233
|
+
const trading = this.tradingEnabled
|
|
234
|
+
? C.amber('\u26A1 trading')
|
|
235
|
+
: C.zinc600('\u26A1 read-only');
|
|
236
|
+
const help = C.zinc600('/help');
|
|
237
|
+
const sep = C.zinc600(' \u2502 ');
|
|
238
|
+
const leftText = [model, tokens, exchange, trading].join(sep);
|
|
182
239
|
const lw = visibleWidth(leftText);
|
|
183
|
-
const rw = visibleWidth(
|
|
184
|
-
const gap = Math.max(1, width - lw - rw);
|
|
185
|
-
let line = leftText + ' '.repeat(gap) +
|
|
186
|
-
line = C.bgZinc900(truncateToWidth(line, width, ''));
|
|
240
|
+
const rw = visibleWidth(help);
|
|
241
|
+
const gap = Math.max(1, width - lw - rw - 2);
|
|
242
|
+
let line = C.bgZinc900(' ' + leftText + ' '.repeat(gap) + help + ' ');
|
|
187
243
|
const lineVw = visibleWidth(line);
|
|
188
244
|
if (lineVw < width) {
|
|
189
245
|
line = line + C.bgZinc900(' '.repeat(width - lineVw));
|
|
@@ -286,23 +342,36 @@ function renderPositions(positions) {
|
|
|
286
342
|
}
|
|
287
343
|
// ─── Main command ────────────────────────────────────────────────────────────
|
|
288
344
|
async function agentCommand(thesisId, opts) {
|
|
289
|
-
// ── Dynamic imports (all ESM-only packages) ────────────────────────────────
|
|
290
|
-
const piTui = await import('@mariozechner/pi-tui');
|
|
291
|
-
const piAi = await import('@mariozechner/pi-ai');
|
|
292
|
-
const piAgent = await import('@mariozechner/pi-agent-core');
|
|
293
|
-
const { TUI, ProcessTerminal, Container, Text, Markdown, Editor, Loader, Spacer, CombinedAutocompleteProvider, truncateToWidth, visibleWidth, } = piTui;
|
|
294
|
-
const { getModel, streamSimple, Type } = piAi;
|
|
295
|
-
const { Agent } = piAgent;
|
|
296
|
-
// ── Component class factories (need piTui ref) ─────────────────────────────
|
|
297
|
-
const MutableLine = createMutableLine(piTui);
|
|
298
|
-
const HeaderBar = createHeaderBar(piTui);
|
|
299
|
-
const FooterBar = createFooterBar(piTui);
|
|
300
345
|
// ── Validate API keys ──────────────────────────────────────────────────────
|
|
301
346
|
const openrouterKey = opts?.modelKey || process.env.OPENROUTER_API_KEY;
|
|
302
347
|
if (!openrouterKey) {
|
|
303
|
-
console.error('Need OpenRouter API key
|
|
348
|
+
console.error('Need OpenRouter API key to power the agent LLM.');
|
|
349
|
+
console.error('');
|
|
350
|
+
console.error(' 1. Get a key at https://openrouter.ai/keys');
|
|
351
|
+
console.error(' 2. Then either:');
|
|
352
|
+
console.error(' export OPENROUTER_API_KEY=sk-or-v1-...');
|
|
353
|
+
console.error(' sf agent --model-key sk-or-v1-...');
|
|
354
|
+
console.error(' sf setup (saves to ~/.sf/config.json)');
|
|
304
355
|
process.exit(1);
|
|
305
356
|
}
|
|
357
|
+
// Pre-flight: validate OpenRouter key
|
|
358
|
+
try {
|
|
359
|
+
const checkRes = await fetch('https://openrouter.ai/api/v1/auth/key', {
|
|
360
|
+
headers: { 'Authorization': `Bearer ${openrouterKey}` },
|
|
361
|
+
signal: AbortSignal.timeout(8000),
|
|
362
|
+
});
|
|
363
|
+
if (!checkRes.ok) {
|
|
364
|
+
console.error('OpenRouter API key is invalid or expired.');
|
|
365
|
+
console.error('Get a new key at https://openrouter.ai/keys');
|
|
366
|
+
process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
catch (err) {
|
|
370
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
371
|
+
if (!msg.includes('timeout')) {
|
|
372
|
+
console.warn(`Warning: Could not verify OpenRouter key (${msg}). Continuing anyway.`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
306
375
|
const sfClient = new client_js_1.SFClient();
|
|
307
376
|
// ── Resolve thesis ID ──────────────────────────────────────────────────────
|
|
308
377
|
let resolvedThesisId = thesisId;
|
|
@@ -318,6 +387,21 @@ async function agentCommand(thesisId, opts) {
|
|
|
318
387
|
}
|
|
319
388
|
// ── Fetch initial context ──────────────────────────────────────────────────
|
|
320
389
|
let latestContext = await sfClient.getContext(resolvedThesisId);
|
|
390
|
+
// ── Branch: plain-text mode ────────────────────────────────────────────────
|
|
391
|
+
if (opts?.noTui) {
|
|
392
|
+
return runPlainTextAgent({ openrouterKey, sfClient, resolvedThesisId: resolvedThesisId, latestContext, opts });
|
|
393
|
+
}
|
|
394
|
+
// ── Dynamic imports (all ESM-only packages) ────────────────────────────────
|
|
395
|
+
const piTui = await import('@mariozechner/pi-tui');
|
|
396
|
+
const piAi = await import('@mariozechner/pi-ai');
|
|
397
|
+
const piAgent = await import('@mariozechner/pi-agent-core');
|
|
398
|
+
const { TUI, ProcessTerminal, Container, Text, Markdown, Editor, Loader, Spacer, CombinedAutocompleteProvider, truncateToWidth, visibleWidth, } = piTui;
|
|
399
|
+
const { getModel, streamSimple, Type } = piAi;
|
|
400
|
+
const { Agent } = piAgent;
|
|
401
|
+
// ── Component class factories (need piTui ref) ─────────────────────────────
|
|
402
|
+
const MutableLine = createMutableLine(piTui);
|
|
403
|
+
const HeaderBar = createHeaderBar(piTui);
|
|
404
|
+
const FooterBar = createFooterBar(piTui);
|
|
321
405
|
// ── Model setup ────────────────────────────────────────────────────────────
|
|
322
406
|
const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
|
|
323
407
|
let currentModelName = rawModelName.replace(/^openrouter\//, '');
|
|
@@ -349,6 +433,10 @@ async function agentCommand(thesisId, opts) {
|
|
|
349
433
|
let isProcessing = false;
|
|
350
434
|
// Cache for positions (fetched by /pos or get_positions tool)
|
|
351
435
|
let cachedPositions = null;
|
|
436
|
+
// ── Inline confirmation mechanism ─────────────────────────────────────────
|
|
437
|
+
// Tools can call promptUser() during execution to ask the user a question.
|
|
438
|
+
// This temporarily unlocks the editor, waits for input, then resumes.
|
|
439
|
+
let pendingPrompt = null;
|
|
352
440
|
// ── Setup TUI ──────────────────────────────────────────────────────────────
|
|
353
441
|
const terminal = new ProcessTerminal();
|
|
354
442
|
const tui = new TUI(terminal);
|
|
@@ -372,9 +460,9 @@ async function agentCommand(thesisId, opts) {
|
|
|
372
460
|
const mdDefaultStyle = {
|
|
373
461
|
color: (s) => C.zinc400(s),
|
|
374
462
|
};
|
|
375
|
-
// Editor theme
|
|
463
|
+
// Editor theme — use dim zinc borders instead of default green
|
|
376
464
|
const editorTheme = {
|
|
377
|
-
borderColor: (s) =>
|
|
465
|
+
borderColor: (s) => `\x1b[38;2;50;50;55m${s}\x1b[39m`,
|
|
378
466
|
selectList: {
|
|
379
467
|
selectedPrefix: (s) => C.emerald(s),
|
|
380
468
|
selectedText: (s) => C.zinc200(s),
|
|
@@ -384,18 +472,37 @@ async function agentCommand(thesisId, opts) {
|
|
|
384
472
|
},
|
|
385
473
|
};
|
|
386
474
|
// ── Build components ───────────────────────────────────────────────────────
|
|
387
|
-
const
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
475
|
+
const headerBar = new HeaderBar();
|
|
476
|
+
// Fetch positions for header P&L (non-blocking, best-effort)
|
|
477
|
+
let initialPositions = null;
|
|
478
|
+
try {
|
|
479
|
+
initialPositions = await (0, kalshi_js_1.getPositions)();
|
|
480
|
+
if (initialPositions) {
|
|
481
|
+
for (const pos of initialPositions) {
|
|
482
|
+
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
483
|
+
if (livePrice !== null) {
|
|
484
|
+
pos.current_value = livePrice;
|
|
485
|
+
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
catch { /* positions not available, fine */ }
|
|
491
|
+
headerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
392
492
|
const footerBar = new FooterBar();
|
|
493
|
+
footerBar.modelName = currentModelName;
|
|
494
|
+
footerBar.tradingEnabled = (0, config_js_1.loadConfig)().tradingEnabled || false;
|
|
495
|
+
// Fetch exchange status for footer (non-blocking)
|
|
496
|
+
fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } })
|
|
497
|
+
.then(r => r.json())
|
|
498
|
+
.then(d => { footerBar.exchangeOpen = !!d.exchange_active; footerBar.update(); tui.requestRender(); })
|
|
499
|
+
.catch(() => { });
|
|
393
500
|
const topSpacer = new Spacer(1);
|
|
394
501
|
const bottomSpacer = new Spacer(1);
|
|
395
502
|
const chatContainer = new Container();
|
|
396
503
|
const editor = new Editor(tui, editorTheme, { paddingX: 1 });
|
|
397
504
|
// Slash command autocomplete
|
|
398
|
-
const
|
|
505
|
+
const slashCommands = [
|
|
399
506
|
{ name: 'help', description: 'Show available commands' },
|
|
400
507
|
{ name: 'tree', description: 'Display causal tree' },
|
|
401
508
|
{ name: 'edges', description: 'Display edge/spread table' },
|
|
@@ -408,7 +515,13 @@ async function agentCommand(thesisId, opts) {
|
|
|
408
515
|
{ name: 'env', description: 'Show environment variable status' },
|
|
409
516
|
{ name: 'clear', description: 'Clear screen (keeps history)' },
|
|
410
517
|
{ name: 'exit', description: 'Exit agent (auto-saves)' },
|
|
411
|
-
]
|
|
518
|
+
];
|
|
519
|
+
// Add trading commands if enabled
|
|
520
|
+
if ((0, config_js_1.loadConfig)().tradingEnabled) {
|
|
521
|
+
slashCommands.splice(-2, 0, // insert before /clear and /exit
|
|
522
|
+
{ name: 'buy', description: 'TICKER QTY PRICE — quick buy' }, { name: 'sell', description: 'TICKER QTY PRICE — quick sell' }, { name: 'cancel', description: 'ORDER_ID — cancel order' });
|
|
523
|
+
}
|
|
524
|
+
const autocompleteProvider = new CombinedAutocompleteProvider(slashCommands, process.cwd());
|
|
412
525
|
editor.setAutocompleteProvider(autocompleteProvider);
|
|
413
526
|
// Assemble TUI tree
|
|
414
527
|
tui.addChild(topSpacer);
|
|
@@ -438,6 +551,19 @@ async function agentCommand(thesisId, opts) {
|
|
|
438
551
|
function addSpacer() {
|
|
439
552
|
chatContainer.addChild(new Spacer(1));
|
|
440
553
|
}
|
|
554
|
+
/**
|
|
555
|
+
* Ask the user a question during tool execution.
|
|
556
|
+
* Temporarily unlocks the editor, waits for input, then resumes.
|
|
557
|
+
* Used for order confirmations and other dangerous operations.
|
|
558
|
+
*/
|
|
559
|
+
function promptUser(question) {
|
|
560
|
+
return new Promise(resolve => {
|
|
561
|
+
addSystemText(C.amber(bold('\u26A0 ')) + C.zinc200(question));
|
|
562
|
+
addSpacer();
|
|
563
|
+
tui.requestRender();
|
|
564
|
+
pendingPrompt = { resolve };
|
|
565
|
+
});
|
|
566
|
+
}
|
|
441
567
|
// ── Define agent tools (same as before) ────────────────────────────────────
|
|
442
568
|
const thesisIdParam = Type.Object({
|
|
443
569
|
thesisId: Type.String({ description: 'Thesis ID (short or full UUID)' }),
|
|
@@ -465,11 +591,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
465
591
|
execute: async (_toolCallId, params) => {
|
|
466
592
|
const ctx = await sfClient.getContext(params.thesisId);
|
|
467
593
|
latestContext = ctx;
|
|
468
|
-
|
|
469
|
-
const conf = typeof ctx.confidence === 'number'
|
|
470
|
-
? Math.round(ctx.confidence * 100)
|
|
471
|
-
: 0;
|
|
472
|
-
headerBar.update(undefined, conf > 0 ? C.zinc200(`${conf}%`) : '', undefined);
|
|
594
|
+
headerBar.setFromContext(ctx, initialPositions || undefined);
|
|
473
595
|
tui.requestRender();
|
|
474
596
|
return {
|
|
475
597
|
content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
|
|
@@ -497,6 +619,26 @@ async function agentCommand(thesisId, opts) {
|
|
|
497
619
|
parameters: thesisIdParam,
|
|
498
620
|
execute: async (_toolCallId, params) => {
|
|
499
621
|
const result = await sfClient.evaluate(params.thesisId);
|
|
622
|
+
// Show confidence change prominently
|
|
623
|
+
if (result.evaluation?.confidenceDelta && Math.abs(result.evaluation.confidenceDelta) >= 0.01) {
|
|
624
|
+
const delta = result.evaluation.confidenceDelta;
|
|
625
|
+
const prev = Math.round((result.evaluation.previousConfidence || 0) * 100);
|
|
626
|
+
const now = Math.round((result.evaluation.newConfidence || 0) * 100);
|
|
627
|
+
const arrow = delta > 0 ? '\u25B2' : '\u25BC';
|
|
628
|
+
const color = delta > 0 ? C.emerald : C.red;
|
|
629
|
+
addSystemText(color(` ${arrow} Confidence ${prev}% \u2192 ${now}% (${delta > 0 ? '+' : ''}${Math.round(delta * 100)})`));
|
|
630
|
+
addSpacer();
|
|
631
|
+
// Update header
|
|
632
|
+
headerBar.updateConfidence(result.evaluation.newConfidence, delta);
|
|
633
|
+
tui.requestRender();
|
|
634
|
+
}
|
|
635
|
+
// Refresh context after eval
|
|
636
|
+
try {
|
|
637
|
+
latestContext = await sfClient.getContext(params.thesisId);
|
|
638
|
+
headerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
639
|
+
tui.requestRender();
|
|
640
|
+
}
|
|
641
|
+
catch { }
|
|
500
642
|
return {
|
|
501
643
|
content: [{ type: 'text', text: JSON.stringify(result) }],
|
|
502
644
|
details: {},
|
|
@@ -636,7 +778,378 @@ async function agentCommand(thesisId, opts) {
|
|
|
636
778
|
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
637
779
|
},
|
|
638
780
|
},
|
|
781
|
+
{
|
|
782
|
+
name: 'create_strategy',
|
|
783
|
+
label: 'Create Strategy',
|
|
784
|
+
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.',
|
|
785
|
+
parameters: Type.Object({
|
|
786
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
787
|
+
marketId: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T150' }),
|
|
788
|
+
market: Type.String({ description: 'Human-readable market name' }),
|
|
789
|
+
direction: Type.String({ description: 'yes or no' }),
|
|
790
|
+
horizon: Type.Optional(Type.String({ description: 'short, medium, or long. Default: medium' })),
|
|
791
|
+
entryBelow: Type.Optional(Type.Number({ description: 'Entry trigger: ask <= this value (cents)' })),
|
|
792
|
+
entryAbove: Type.Optional(Type.Number({ description: 'Entry trigger: ask >= this value (cents, for NO direction)' })),
|
|
793
|
+
stopLoss: Type.Optional(Type.Number({ description: 'Stop loss: bid <= this value (cents)' })),
|
|
794
|
+
takeProfit: Type.Optional(Type.Number({ description: 'Take profit: bid >= this value (cents)' })),
|
|
795
|
+
maxQuantity: Type.Optional(Type.Number({ description: 'Max total contracts. Default: 500' })),
|
|
796
|
+
perOrderQuantity: Type.Optional(Type.Number({ description: 'Contracts per order. Default: 50' })),
|
|
797
|
+
softConditions: Type.Optional(Type.String({ description: 'LLM-evaluated conditions e.g. "only enter when n3 > 60%"' })),
|
|
798
|
+
rationale: Type.Optional(Type.String({ description: 'Full logic description' })),
|
|
799
|
+
}),
|
|
800
|
+
execute: async (_toolCallId, params) => {
|
|
801
|
+
const result = await sfClient.createStrategyAPI(params.thesisId, {
|
|
802
|
+
marketId: params.marketId,
|
|
803
|
+
market: params.market,
|
|
804
|
+
direction: params.direction,
|
|
805
|
+
horizon: params.horizon,
|
|
806
|
+
entryBelow: params.entryBelow,
|
|
807
|
+
entryAbove: params.entryAbove,
|
|
808
|
+
stopLoss: params.stopLoss,
|
|
809
|
+
takeProfit: params.takeProfit,
|
|
810
|
+
maxQuantity: params.maxQuantity,
|
|
811
|
+
perOrderQuantity: params.perOrderQuantity,
|
|
812
|
+
softConditions: params.softConditions,
|
|
813
|
+
rationale: params.rationale,
|
|
814
|
+
createdBy: 'agent',
|
|
815
|
+
});
|
|
816
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
817
|
+
},
|
|
818
|
+
},
|
|
819
|
+
{
|
|
820
|
+
name: 'list_strategies',
|
|
821
|
+
label: 'List Strategies',
|
|
822
|
+
description: 'List strategies for a thesis. Filter by status (active/watching/executed/cancelled/review) or omit for all.',
|
|
823
|
+
parameters: Type.Object({
|
|
824
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
825
|
+
status: Type.Optional(Type.String({ description: 'Filter by status. Omit for all.' })),
|
|
826
|
+
}),
|
|
827
|
+
execute: async (_toolCallId, params) => {
|
|
828
|
+
const result = await sfClient.getStrategies(params.thesisId, params.status);
|
|
829
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
830
|
+
},
|
|
831
|
+
},
|
|
832
|
+
{
|
|
833
|
+
name: 'update_strategy',
|
|
834
|
+
label: 'Update Strategy',
|
|
835
|
+
description: 'Update a strategy (change stop loss, take profit, status, priority, etc.)',
|
|
836
|
+
parameters: Type.Object({
|
|
837
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
838
|
+
strategyId: Type.String({ description: 'Strategy ID (UUID)' }),
|
|
839
|
+
stopLoss: Type.Optional(Type.Number({ description: 'New stop loss (cents)' })),
|
|
840
|
+
takeProfit: Type.Optional(Type.Number({ description: 'New take profit (cents)' })),
|
|
841
|
+
entryBelow: Type.Optional(Type.Number({ description: 'New entry below trigger (cents)' })),
|
|
842
|
+
entryAbove: Type.Optional(Type.Number({ description: 'New entry above trigger (cents)' })),
|
|
843
|
+
status: Type.Optional(Type.String({ description: 'New status: active|watching|executed|cancelled|review' })),
|
|
844
|
+
priority: Type.Optional(Type.Number({ description: 'New priority' })),
|
|
845
|
+
softConditions: Type.Optional(Type.String({ description: 'Updated soft conditions' })),
|
|
846
|
+
rationale: Type.Optional(Type.String({ description: 'Updated rationale' })),
|
|
847
|
+
}),
|
|
848
|
+
execute: async (_toolCallId, params) => {
|
|
849
|
+
const { thesisId, strategyId, ...updates } = params;
|
|
850
|
+
const result = await sfClient.updateStrategyAPI(thesisId, strategyId, updates);
|
|
851
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
852
|
+
},
|
|
853
|
+
},
|
|
854
|
+
{
|
|
855
|
+
name: 'get_milestones',
|
|
856
|
+
label: 'Milestones',
|
|
857
|
+
description: 'Get upcoming events from Kalshi calendar. Use to check economic releases, political events, or other catalysts coming up that might affect the thesis.',
|
|
858
|
+
parameters: Type.Object({
|
|
859
|
+
hours: Type.Optional(Type.Number({ description: 'Hours ahead to look (default 168 = 1 week)' })),
|
|
860
|
+
category: Type.Optional(Type.String({ description: 'Filter by category (e.g. Economics, Politics, Sports)' })),
|
|
861
|
+
}),
|
|
862
|
+
execute: async (_toolCallId, params) => {
|
|
863
|
+
const hours = params.hours || 168;
|
|
864
|
+
const now = new Date();
|
|
865
|
+
const url = `https://api.elections.kalshi.com/trade-api/v2/milestones?limit=200&minimum_start_date=${now.toISOString()}` +
|
|
866
|
+
(params.category ? `&category=${params.category}` : '');
|
|
867
|
+
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
868
|
+
if (!res.ok)
|
|
869
|
+
return { content: [{ type: 'text', text: `Milestones API error: ${res.status}` }], details: {} };
|
|
870
|
+
const data = await res.json();
|
|
871
|
+
const cutoff = now.getTime() + hours * 3600000;
|
|
872
|
+
const filtered = (data.milestones || [])
|
|
873
|
+
.filter((m) => new Date(m.start_date).getTime() <= cutoff)
|
|
874
|
+
.slice(0, 30)
|
|
875
|
+
.map((m) => ({
|
|
876
|
+
title: m.title,
|
|
877
|
+
category: m.category,
|
|
878
|
+
start_date: m.start_date,
|
|
879
|
+
related_event_tickers: m.related_event_tickers,
|
|
880
|
+
hours_until: Math.round((new Date(m.start_date).getTime() - now.getTime()) / 3600000),
|
|
881
|
+
}));
|
|
882
|
+
return { content: [{ type: 'text', text: JSON.stringify(filtered, null, 2) }], details: {} };
|
|
883
|
+
},
|
|
884
|
+
},
|
|
885
|
+
{
|
|
886
|
+
name: 'get_forecast',
|
|
887
|
+
label: 'Forecast',
|
|
888
|
+
description: 'Get market distribution (P50/P75/P90 percentile history) for a Kalshi event. Shows how market consensus has shifted over time.',
|
|
889
|
+
parameters: Type.Object({
|
|
890
|
+
eventTicker: Type.String({ description: 'Kalshi event ticker (e.g. KXWTIMAX-26DEC31)' }),
|
|
891
|
+
days: Type.Optional(Type.Number({ description: 'Days of history (default 7)' })),
|
|
892
|
+
}),
|
|
893
|
+
execute: async (_toolCallId, params) => {
|
|
894
|
+
const { getForecastHistory } = await import('../kalshi.js');
|
|
895
|
+
const days = params.days || 7;
|
|
896
|
+
// Get series ticker from event
|
|
897
|
+
const evtRes = await fetch(`https://api.elections.kalshi.com/trade-api/v2/events/${params.eventTicker}`, { headers: { 'Accept': 'application/json' } });
|
|
898
|
+
if (!evtRes.ok)
|
|
899
|
+
return { content: [{ type: 'text', text: `Event not found: ${params.eventTicker}` }], details: {} };
|
|
900
|
+
const evtData = await evtRes.json();
|
|
901
|
+
const seriesTicker = evtData.event?.series_ticker;
|
|
902
|
+
if (!seriesTicker)
|
|
903
|
+
return { content: [{ type: 'text', text: `No series_ticker for ${params.eventTicker}` }], details: {} };
|
|
904
|
+
const history = await getForecastHistory({
|
|
905
|
+
seriesTicker,
|
|
906
|
+
eventTicker: params.eventTicker,
|
|
907
|
+
percentiles: [5000, 7500, 9000],
|
|
908
|
+
startTs: Math.floor((Date.now() - days * 86400000) / 1000),
|
|
909
|
+
endTs: Math.floor(Date.now() / 1000),
|
|
910
|
+
periodInterval: 1440,
|
|
911
|
+
});
|
|
912
|
+
if (!history || history.length === 0)
|
|
913
|
+
return { content: [{ type: 'text', text: 'No forecast data available' }], details: {} };
|
|
914
|
+
return { content: [{ type: 'text', text: JSON.stringify(history, null, 2) }], details: {} };
|
|
915
|
+
},
|
|
916
|
+
},
|
|
917
|
+
{
|
|
918
|
+
name: 'get_settlements',
|
|
919
|
+
label: 'Settlements',
|
|
920
|
+
description: 'Get settled (resolved) contracts with P&L. Shows which contracts won/lost and realized returns.',
|
|
921
|
+
parameters: Type.Object({
|
|
922
|
+
ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })),
|
|
923
|
+
}),
|
|
924
|
+
execute: async (_toolCallId, params) => {
|
|
925
|
+
const { getSettlements } = await import('../kalshi.js');
|
|
926
|
+
const result = await getSettlements({ limit: 100, ticker: params.ticker });
|
|
927
|
+
if (!result)
|
|
928
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
929
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.settlements, null, 2) }], details: {} };
|
|
930
|
+
},
|
|
931
|
+
},
|
|
932
|
+
{
|
|
933
|
+
name: 'get_balance',
|
|
934
|
+
label: 'Balance',
|
|
935
|
+
description: 'Get Kalshi account balance and portfolio value.',
|
|
936
|
+
parameters: emptyParams,
|
|
937
|
+
execute: async () => {
|
|
938
|
+
const { getBalance } = await import('../kalshi.js');
|
|
939
|
+
const result = await getBalance();
|
|
940
|
+
if (!result)
|
|
941
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
942
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
name: 'get_orders',
|
|
947
|
+
label: 'Orders',
|
|
948
|
+
description: 'Get current resting orders on Kalshi.',
|
|
949
|
+
parameters: Type.Object({
|
|
950
|
+
status: Type.Optional(Type.String({ description: 'Filter by status: resting, canceled, executed. Default: resting' })),
|
|
951
|
+
}),
|
|
952
|
+
execute: async (_toolCallId, params) => {
|
|
953
|
+
const { getOrders } = await import('../kalshi.js');
|
|
954
|
+
const result = await getOrders({ status: params.status || 'resting', limit: 100 });
|
|
955
|
+
if (!result)
|
|
956
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
957
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.orders, null, 2) }], details: {} };
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
{
|
|
961
|
+
name: 'get_fills',
|
|
962
|
+
label: 'Fills',
|
|
963
|
+
description: 'Get recent trade fills (executed trades) on Kalshi.',
|
|
964
|
+
parameters: Type.Object({
|
|
965
|
+
ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })),
|
|
966
|
+
}),
|
|
967
|
+
execute: async (_toolCallId, params) => {
|
|
968
|
+
const { getFills } = await import('../kalshi.js');
|
|
969
|
+
const result = await getFills({ ticker: params.ticker, limit: 50 });
|
|
970
|
+
if (!result)
|
|
971
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
972
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
|
|
973
|
+
},
|
|
974
|
+
},
|
|
975
|
+
{
|
|
976
|
+
name: 'get_schedule',
|
|
977
|
+
label: 'Schedule',
|
|
978
|
+
description: 'Get exchange status (open/closed) and trading hours. Use to check if low liquidity is due to off-hours.',
|
|
979
|
+
parameters: emptyParams,
|
|
980
|
+
execute: async () => {
|
|
981
|
+
try {
|
|
982
|
+
const res = await fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } });
|
|
983
|
+
if (!res.ok)
|
|
984
|
+
return { content: [{ type: 'text', text: `Exchange API error: ${res.status}` }], details: {} };
|
|
985
|
+
const data = await res.json();
|
|
986
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
987
|
+
}
|
|
988
|
+
catch (err) {
|
|
989
|
+
return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
|
|
990
|
+
}
|
|
991
|
+
},
|
|
992
|
+
},
|
|
639
993
|
];
|
|
994
|
+
// ── What-if tool (always available) ────────────────────────────────────────
|
|
995
|
+
tools.push({
|
|
996
|
+
name: 'what_if',
|
|
997
|
+
label: 'What-If',
|
|
998
|
+
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%?".',
|
|
999
|
+
parameters: Type.Object({
|
|
1000
|
+
overrides: Type.Array(Type.Object({
|
|
1001
|
+
nodeId: Type.String({ description: 'Causal tree node ID (e.g. n1, n3.1)' }),
|
|
1002
|
+
newProbability: Type.Number({ description: 'New probability 0-1' }),
|
|
1003
|
+
}), { description: 'Node probability overrides' }),
|
|
1004
|
+
}),
|
|
1005
|
+
execute: async (_toolCallId, params) => {
|
|
1006
|
+
// Inline what-if simulation
|
|
1007
|
+
const ctx = latestContext;
|
|
1008
|
+
const allNodes = [];
|
|
1009
|
+
function flatten(nodes) {
|
|
1010
|
+
for (const n of nodes) {
|
|
1011
|
+
allNodes.push(n);
|
|
1012
|
+
if (n.children?.length)
|
|
1013
|
+
flatten(n.children);
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
const rawNodes = ctx.causalTree?.nodes || [];
|
|
1017
|
+
flatten(rawNodes);
|
|
1018
|
+
const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
|
|
1019
|
+
const overrideMap = new Map(params.overrides.map((o) => [o.nodeId, o.newProbability]));
|
|
1020
|
+
const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
|
|
1021
|
+
const newConf = treeNodes.reduce((s, n) => {
|
|
1022
|
+
const p = overrideMap.get(n.id) ?? n.probability ?? 0;
|
|
1023
|
+
return s + p * (n.importance || 0);
|
|
1024
|
+
}, 0);
|
|
1025
|
+
const nodeScales = new Map();
|
|
1026
|
+
for (const [nid, np] of overrideMap.entries()) {
|
|
1027
|
+
const nd = allNodes.find((n) => n.id === nid);
|
|
1028
|
+
if (nd && nd.probability > 0)
|
|
1029
|
+
nodeScales.set(nid, Math.max(0, Math.min(2, np / nd.probability)));
|
|
1030
|
+
}
|
|
1031
|
+
const edges = (ctx.edges || []).map((edge) => {
|
|
1032
|
+
const relNode = edge.relatedNodeId;
|
|
1033
|
+
let scaleFactor = 1;
|
|
1034
|
+
if (relNode) {
|
|
1035
|
+
const candidates = [relNode, relNode.split('.').slice(0, -1).join('.'), relNode.split('.')[0]].filter(Boolean);
|
|
1036
|
+
for (const cid of candidates) {
|
|
1037
|
+
if (nodeScales.has(cid)) {
|
|
1038
|
+
scaleFactor = nodeScales.get(cid);
|
|
1039
|
+
break;
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
}
|
|
1043
|
+
const mkt = edge.marketPrice || 0;
|
|
1044
|
+
const oldTP = edge.thesisPrice || edge.thesisImpliedPrice || mkt;
|
|
1045
|
+
const oldEdge = edge.edge || edge.edgeSize || 0;
|
|
1046
|
+
const newTP = Math.round((mkt + (oldTP - mkt) * scaleFactor) * 100) / 100;
|
|
1047
|
+
const dir = edge.direction || 'yes';
|
|
1048
|
+
const newEdge = Math.round((dir === 'yes' ? newTP - mkt : mkt - newTP) * 100) / 100;
|
|
1049
|
+
return {
|
|
1050
|
+
market: edge.market || edge.marketTitle || edge.marketId,
|
|
1051
|
+
marketPrice: mkt,
|
|
1052
|
+
oldEdge,
|
|
1053
|
+
newEdge,
|
|
1054
|
+
delta: newEdge - oldEdge,
|
|
1055
|
+
signal: Math.abs(newEdge - oldEdge) < 1 ? 'unchanged' : (oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0) ? 'REVERSED' : Math.abs(newEdge) < 2 ? 'GONE' : 'reduced',
|
|
1056
|
+
};
|
|
1057
|
+
}).filter((e) => e.signal !== 'unchanged');
|
|
1058
|
+
const result = {
|
|
1059
|
+
overrides: params.overrides.map((o) => {
|
|
1060
|
+
const node = allNodes.find((n) => n.id === o.nodeId);
|
|
1061
|
+
return { nodeId: o.nodeId, label: node?.label || o.nodeId, oldProb: node?.probability, newProb: o.newProbability };
|
|
1062
|
+
}),
|
|
1063
|
+
confidence: { old: Math.round(oldConf * 100), new: Math.round(newConf * 100), delta: Math.round((newConf - oldConf) * 100) },
|
|
1064
|
+
affectedEdges: edges,
|
|
1065
|
+
};
|
|
1066
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
1067
|
+
},
|
|
1068
|
+
});
|
|
1069
|
+
// ── Trading tools (conditional on tradingEnabled) ──────────────────────────
|
|
1070
|
+
const config = (0, config_js_1.loadConfig)();
|
|
1071
|
+
if (config.tradingEnabled) {
|
|
1072
|
+
tools.push({
|
|
1073
|
+
name: 'place_order',
|
|
1074
|
+
label: 'Place Order',
|
|
1075
|
+
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.',
|
|
1076
|
+
parameters: Type.Object({
|
|
1077
|
+
ticker: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T135' }),
|
|
1078
|
+
side: Type.String({ description: 'yes or no' }),
|
|
1079
|
+
action: Type.String({ description: 'buy or sell' }),
|
|
1080
|
+
type: Type.String({ description: 'limit or market' }),
|
|
1081
|
+
count: Type.Number({ description: 'Number of contracts' }),
|
|
1082
|
+
price_cents: Type.Optional(Type.Number({ description: 'Limit price in cents (1-99). Required for limit orders.' })),
|
|
1083
|
+
}),
|
|
1084
|
+
execute: async (_toolCallId, params) => {
|
|
1085
|
+
const { createOrder } = await import('../kalshi.js');
|
|
1086
|
+
const priceDollars = params.price_cents ? (params.price_cents / 100).toFixed(2) : undefined;
|
|
1087
|
+
const maxCost = ((params.price_cents || 99) * params.count / 100).toFixed(2);
|
|
1088
|
+
// Show preview
|
|
1089
|
+
const preview = [
|
|
1090
|
+
C.zinc200(bold('ORDER PREVIEW')),
|
|
1091
|
+
` Ticker: ${params.ticker}`,
|
|
1092
|
+
` Side: ${params.side === 'yes' ? C.emerald('YES') : C.red('NO')}`,
|
|
1093
|
+
` Action: ${params.action.toUpperCase()}`,
|
|
1094
|
+
` Quantity: ${params.count}`,
|
|
1095
|
+
` Type: ${params.type}`,
|
|
1096
|
+
params.price_cents ? ` Price: ${params.price_cents}\u00A2` : '',
|
|
1097
|
+
` Max cost: $${maxCost}`,
|
|
1098
|
+
].filter(Boolean).join('\n');
|
|
1099
|
+
addSystemText(preview);
|
|
1100
|
+
addSpacer();
|
|
1101
|
+
tui.requestRender();
|
|
1102
|
+
// Ask for confirmation via promptUser
|
|
1103
|
+
const answer = await promptUser('Execute this order? (y/n)');
|
|
1104
|
+
if (!answer.toLowerCase().startsWith('y')) {
|
|
1105
|
+
return { content: [{ type: 'text', text: 'Order cancelled by user.' }], details: {} };
|
|
1106
|
+
}
|
|
1107
|
+
try {
|
|
1108
|
+
const result = await createOrder({
|
|
1109
|
+
ticker: params.ticker,
|
|
1110
|
+
side: params.side,
|
|
1111
|
+
action: params.action,
|
|
1112
|
+
type: params.type,
|
|
1113
|
+
count: params.count,
|
|
1114
|
+
...(priceDollars ? { yes_price: priceDollars } : {}),
|
|
1115
|
+
});
|
|
1116
|
+
const order = result.order || result;
|
|
1117
|
+
return {
|
|
1118
|
+
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}` }],
|
|
1119
|
+
details: {},
|
|
1120
|
+
};
|
|
1121
|
+
}
|
|
1122
|
+
catch (err) {
|
|
1123
|
+
const msg = err.message || String(err);
|
|
1124
|
+
if (msg.includes('403')) {
|
|
1125
|
+
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: {} };
|
|
1126
|
+
}
|
|
1127
|
+
return { content: [{ type: 'text', text: `Order failed: ${msg}` }], details: {} };
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
}, {
|
|
1131
|
+
name: 'cancel_order',
|
|
1132
|
+
label: 'Cancel Order',
|
|
1133
|
+
description: 'Cancel a resting order by order ID.',
|
|
1134
|
+
parameters: Type.Object({
|
|
1135
|
+
order_id: Type.String({ description: 'Order ID to cancel' }),
|
|
1136
|
+
}),
|
|
1137
|
+
execute: async (_toolCallId, params) => {
|
|
1138
|
+
const { cancelOrder } = await import('../kalshi.js');
|
|
1139
|
+
const answer = await promptUser(`Cancel order ${params.order_id}? (y/n)`);
|
|
1140
|
+
if (!answer.toLowerCase().startsWith('y')) {
|
|
1141
|
+
return { content: [{ type: 'text', text: 'Cancel aborted by user.' }], details: {} };
|
|
1142
|
+
}
|
|
1143
|
+
try {
|
|
1144
|
+
await cancelOrder(params.order_id);
|
|
1145
|
+
return { content: [{ type: 'text', text: `Order ${params.order_id} cancelled.` }], details: {} };
|
|
1146
|
+
}
|
|
1147
|
+
catch (err) {
|
|
1148
|
+
return { content: [{ type: 'text', text: `Cancel failed: ${err.message}` }], details: {} };
|
|
1149
|
+
}
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
}
|
|
640
1153
|
// ── System prompt builder ──────────────────────────────────────────────────
|
|
641
1154
|
function buildSystemPrompt(ctx) {
|
|
642
1155
|
const edgesSummary = ctx.edges
|
|
@@ -677,6 +1190,15 @@ Short-term markets (weekly/monthly contracts) settle into hard data that calibra
|
|
|
677
1190
|
- 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.
|
|
678
1191
|
- Align tables. Be precise with numbers to the cent.
|
|
679
1192
|
|
|
1193
|
+
## Strategy rules
|
|
1194
|
+
|
|
1195
|
+
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."
|
|
1196
|
+
- Extract hard conditions (specific prices in cents) into entryBelow/stopLoss/takeProfit.
|
|
1197
|
+
- Put fuzzy conditions into softConditions (e.g. "only if n3 > 60%", "spread < 3¢").
|
|
1198
|
+
- Put the full reasoning into rationale.
|
|
1199
|
+
- After creating, confirm the strategy details and mention that sf runtime --dangerous can execute it.
|
|
1200
|
+
- If the user says "change the stop loss on T150 to 30", use update_strategy.
|
|
1201
|
+
|
|
680
1202
|
## Current thesis state
|
|
681
1203
|
|
|
682
1204
|
Thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
|
|
@@ -853,6 +1375,11 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
853
1375
|
C.emerald('/new ') + C.zinc400('Start fresh session') + '\n' +
|
|
854
1376
|
C.emerald('/model <m> ') + C.zinc400('Switch model') + '\n' +
|
|
855
1377
|
C.emerald('/env ') + C.zinc400('Show environment variable status') + '\n' +
|
|
1378
|
+
(config.tradingEnabled ? (C.zinc600('\u2500'.repeat(30)) + '\n' +
|
|
1379
|
+
C.emerald('/buy ') + C.zinc400('TICKER QTY PRICE \u2014 quick buy') + '\n' +
|
|
1380
|
+
C.emerald('/sell ') + C.zinc400('TICKER QTY PRICE \u2014 quick sell') + '\n' +
|
|
1381
|
+
C.emerald('/cancel ') + C.zinc400('ORDER_ID \u2014 cancel order') + '\n' +
|
|
1382
|
+
C.zinc600('\u2500'.repeat(30)) + '\n') : '') +
|
|
856
1383
|
C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
|
|
857
1384
|
C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
|
|
858
1385
|
addSpacer();
|
|
@@ -936,7 +1463,8 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
936
1463
|
model = resolveModel(currentModelName);
|
|
937
1464
|
// Update agent model
|
|
938
1465
|
agent.setModel(model);
|
|
939
|
-
|
|
1466
|
+
footerBar.modelName = currentModelName;
|
|
1467
|
+
footerBar.update();
|
|
940
1468
|
addSystemText(C.emerald(`Model switched to ${currentModelName}`));
|
|
941
1469
|
addSpacer();
|
|
942
1470
|
tui.requestRender();
|
|
@@ -976,13 +1504,9 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
976
1504
|
addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
|
|
977
1505
|
}
|
|
978
1506
|
// Update header
|
|
979
|
-
headerBar.
|
|
1507
|
+
headerBar.setFromContext(newContext, initialPositions || undefined);
|
|
980
1508
|
chatContainer.clear();
|
|
981
|
-
|
|
982
|
-
addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
|
|
983
|
-
C.zinc200(bold(thText)) + '\n' +
|
|
984
|
-
C.zinc600(`${newContext.status || 'active'} ${newConf > 0 ? newConf + '%' : ''} ${(newContext.edges || []).length} edges`) + '\n' +
|
|
985
|
-
C.zinc600('\u2500'.repeat(50)));
|
|
1509
|
+
addSystemText(buildWelcomeDashboard(newContext, initialPositions));
|
|
986
1510
|
}
|
|
987
1511
|
catch (err) {
|
|
988
1512
|
addSystemText(C.red(`Switch failed: ${err.message}`));
|
|
@@ -1195,6 +1719,93 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
1195
1719
|
tui.requestRender();
|
|
1196
1720
|
return true;
|
|
1197
1721
|
}
|
|
1722
|
+
case '/buy': {
|
|
1723
|
+
// /buy TICKER QTY PRICE — quick trade without LLM
|
|
1724
|
+
const [, ticker, qtyStr, priceStr] = parts;
|
|
1725
|
+
if (!ticker || !qtyStr || !priceStr) {
|
|
1726
|
+
addSystemText(C.zinc400('Usage: /buy TICKER QTY PRICE_CENTS (e.g. /buy KXWTIMAX-26DEC31-T135 100 50)'));
|
|
1727
|
+
return true;
|
|
1728
|
+
}
|
|
1729
|
+
if (!config.tradingEnabled) {
|
|
1730
|
+
addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
|
|
1731
|
+
return true;
|
|
1732
|
+
}
|
|
1733
|
+
addSpacer();
|
|
1734
|
+
const answer = await promptUser(`BUY ${qtyStr}x ${ticker} YES @ ${priceStr}\u00A2 — execute? (y/n)`);
|
|
1735
|
+
if (answer.toLowerCase().startsWith('y')) {
|
|
1736
|
+
try {
|
|
1737
|
+
const { createOrder } = await import('../kalshi.js');
|
|
1738
|
+
const result = await createOrder({
|
|
1739
|
+
ticker, side: 'yes', action: 'buy', type: 'limit',
|
|
1740
|
+
count: parseInt(qtyStr),
|
|
1741
|
+
yes_price: (parseInt(priceStr) / 100).toFixed(2),
|
|
1742
|
+
});
|
|
1743
|
+
addSystemText(C.emerald('\u2713 Order placed: ' + ((result.order || result).order_id || 'OK')));
|
|
1744
|
+
}
|
|
1745
|
+
catch (err) {
|
|
1746
|
+
addSystemText(C.red('\u2717 ' + err.message));
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
else {
|
|
1750
|
+
addSystemText(C.zinc400('Cancelled.'));
|
|
1751
|
+
}
|
|
1752
|
+
addSpacer();
|
|
1753
|
+
return true;
|
|
1754
|
+
}
|
|
1755
|
+
case '/sell': {
|
|
1756
|
+
const [, ticker, qtyStr, priceStr] = parts;
|
|
1757
|
+
if (!ticker || !qtyStr || !priceStr) {
|
|
1758
|
+
addSystemText(C.zinc400('Usage: /sell TICKER QTY PRICE_CENTS'));
|
|
1759
|
+
return true;
|
|
1760
|
+
}
|
|
1761
|
+
if (!config.tradingEnabled) {
|
|
1762
|
+
addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
|
|
1763
|
+
return true;
|
|
1764
|
+
}
|
|
1765
|
+
addSpacer();
|
|
1766
|
+
const answer = await promptUser(`SELL ${qtyStr}x ${ticker} YES @ ${priceStr}\u00A2 — execute? (y/n)`);
|
|
1767
|
+
if (answer.toLowerCase().startsWith('y')) {
|
|
1768
|
+
try {
|
|
1769
|
+
const { createOrder } = await import('../kalshi.js');
|
|
1770
|
+
const result = await createOrder({
|
|
1771
|
+
ticker, side: 'yes', action: 'sell', type: 'limit',
|
|
1772
|
+
count: parseInt(qtyStr),
|
|
1773
|
+
yes_price: (parseInt(priceStr) / 100).toFixed(2),
|
|
1774
|
+
});
|
|
1775
|
+
addSystemText(C.emerald('\u2713 Order placed: ' + ((result.order || result).order_id || 'OK')));
|
|
1776
|
+
}
|
|
1777
|
+
catch (err) {
|
|
1778
|
+
addSystemText(C.red('\u2717 ' + err.message));
|
|
1779
|
+
}
|
|
1780
|
+
}
|
|
1781
|
+
else {
|
|
1782
|
+
addSystemText(C.zinc400('Cancelled.'));
|
|
1783
|
+
}
|
|
1784
|
+
addSpacer();
|
|
1785
|
+
return true;
|
|
1786
|
+
}
|
|
1787
|
+
case '/cancel': {
|
|
1788
|
+
const [, orderId] = parts;
|
|
1789
|
+
if (!orderId) {
|
|
1790
|
+
addSystemText(C.zinc400('Usage: /cancel ORDER_ID'));
|
|
1791
|
+
return true;
|
|
1792
|
+
}
|
|
1793
|
+
if (!config.tradingEnabled) {
|
|
1794
|
+
addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
|
|
1795
|
+
return true;
|
|
1796
|
+
}
|
|
1797
|
+
addSpacer();
|
|
1798
|
+
try {
|
|
1799
|
+
const { cancelOrder } = await import('../kalshi.js');
|
|
1800
|
+
await cancelOrder(orderId);
|
|
1801
|
+
addSystemText(C.emerald(`\u2713 Order ${orderId} cancelled.`));
|
|
1802
|
+
}
|
|
1803
|
+
catch (err) {
|
|
1804
|
+
addSystemText(C.red('\u2717 ' + err.message));
|
|
1805
|
+
}
|
|
1806
|
+
addSpacer();
|
|
1807
|
+
return true;
|
|
1808
|
+
}
|
|
1198
1809
|
case '/exit':
|
|
1199
1810
|
case '/quit': {
|
|
1200
1811
|
cleanup();
|
|
@@ -1209,6 +1820,17 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
1209
1820
|
const trimmed = input.trim();
|
|
1210
1821
|
if (!trimmed)
|
|
1211
1822
|
return;
|
|
1823
|
+
// If a tool is waiting for user confirmation, resolve it
|
|
1824
|
+
if (pendingPrompt) {
|
|
1825
|
+
const { resolve } = pendingPrompt;
|
|
1826
|
+
pendingPrompt = null;
|
|
1827
|
+
const userResponse = new Text(C.zinc400(' > ') + C.zinc200(trimmed), 1, 0);
|
|
1828
|
+
chatContainer.addChild(userResponse);
|
|
1829
|
+
addSpacer();
|
|
1830
|
+
tui.requestRender();
|
|
1831
|
+
resolve(trimmed);
|
|
1832
|
+
return;
|
|
1833
|
+
}
|
|
1212
1834
|
if (isProcessing)
|
|
1213
1835
|
return;
|
|
1214
1836
|
// Add to editor history
|
|
@@ -1261,17 +1883,489 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
1261
1883
|
// Also handle SIGINT
|
|
1262
1884
|
process.on('SIGINT', cleanup);
|
|
1263
1885
|
process.on('SIGTERM', cleanup);
|
|
1886
|
+
// ── Welcome dashboard builder ────────────────────────────────────────────
|
|
1887
|
+
function buildWelcomeDashboard(ctx, positions) {
|
|
1888
|
+
const lines = [];
|
|
1889
|
+
const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
|
|
1890
|
+
const truncated = thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText;
|
|
1891
|
+
const conf = typeof ctx.confidence === 'number'
|
|
1892
|
+
? Math.round(ctx.confidence * 100)
|
|
1893
|
+
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
1894
|
+
const delta = ctx.lastEvaluation?.confidenceDelta
|
|
1895
|
+
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
1896
|
+
: 0;
|
|
1897
|
+
const deltaStr = delta !== 0 ? ` (${delta > 0 ? '+' : ''}${delta})` : '';
|
|
1898
|
+
const evalAge = ctx.lastEvaluation?.evaluatedAt
|
|
1899
|
+
? Math.round((Date.now() - new Date(ctx.lastEvaluation.evaluatedAt).getTime()) / 3600000)
|
|
1900
|
+
: null;
|
|
1901
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
1902
|
+
lines.push(' ' + C.zinc200(bold(truncated)));
|
|
1903
|
+
lines.push(' ' + C.zinc600(`${ctx.status || 'active'} ${conf}%${deltaStr}`) +
|
|
1904
|
+
(evalAge !== null ? C.zinc600(` \u2502 last eval: ${evalAge < 1 ? '<1' : evalAge}h ago`) : ''));
|
|
1905
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
1906
|
+
// Positions section
|
|
1907
|
+
if (positions && positions.length > 0) {
|
|
1908
|
+
lines.push(' ' + C.zinc400(bold('POSITIONS')));
|
|
1909
|
+
let totalPnl = 0;
|
|
1910
|
+
for (const p of positions) {
|
|
1911
|
+
const pnlCents = p.unrealized_pnl || 0;
|
|
1912
|
+
totalPnl += pnlCents;
|
|
1913
|
+
const pnlStr = pnlCents >= 0
|
|
1914
|
+
? C.emerald(`+$${(pnlCents / 100).toFixed(2)}`)
|
|
1915
|
+
: C.red(`-$${(Math.abs(pnlCents) / 100).toFixed(2)}`);
|
|
1916
|
+
const ticker = (p.ticker || '').slice(0, 28).padEnd(28);
|
|
1917
|
+
const qty = String(p.quantity || 0).padStart(5);
|
|
1918
|
+
const side = p.side === 'yes' ? C.emerald('Y') : C.red('N');
|
|
1919
|
+
lines.push(` ${C.zinc400(ticker)} ${qty} ${side} ${pnlStr}`);
|
|
1920
|
+
}
|
|
1921
|
+
const totalStr = totalPnl >= 0
|
|
1922
|
+
? C.emerald(bold(`+$${(totalPnl / 100).toFixed(2)}`))
|
|
1923
|
+
: C.red(bold(`-$${(Math.abs(totalPnl) / 100).toFixed(2)}`));
|
|
1924
|
+
lines.push(` ${''.padEnd(28)} ${C.zinc600('Total')} ${totalStr}`);
|
|
1925
|
+
}
|
|
1926
|
+
// Top edges section
|
|
1927
|
+
const edges = ctx.edges || [];
|
|
1928
|
+
if (edges.length > 0) {
|
|
1929
|
+
const sorted = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0)).slice(0, 5);
|
|
1930
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
1931
|
+
lines.push(' ' + C.zinc400(bold('TOP EDGES')) + C.zinc600(' mkt edge liq'));
|
|
1932
|
+
for (const e of sorted) {
|
|
1933
|
+
const name = (e.market || e.marketTitle || e.marketId || '').slice(0, 30).padEnd(30);
|
|
1934
|
+
const mkt = String(Math.round(e.marketPrice || 0)).padStart(3) + '\u00A2';
|
|
1935
|
+
const edge = e.edge || e.edgeSize || 0;
|
|
1936
|
+
const edgeStr = (edge > 0 ? '+' : '') + Math.round(edge);
|
|
1937
|
+
const liq = e.orderbook?.liquidityScore || (e.venue === 'polymarket' ? '-' : '?');
|
|
1938
|
+
const edgeColor = Math.abs(edge) >= 15 ? C.emerald : Math.abs(edge) >= 8 ? C.amber : C.zinc400;
|
|
1939
|
+
lines.push(` ${C.zinc400(name)} ${C.zinc400(mkt)} ${edgeColor(edgeStr.padStart(4))} ${C.zinc600(liq)}`);
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
lines.push(C.zinc600('\u2500'.repeat(55)));
|
|
1943
|
+
return lines.join('\n');
|
|
1944
|
+
}
|
|
1264
1945
|
// ── Show initial welcome ───────────────────────────────────────────────────
|
|
1265
|
-
const thesisText = latestContext.thesis || latestContext.rawThesis || 'N/A';
|
|
1266
|
-
const truncatedThesis = thesisText.length > 120 ? thesisText.slice(0, 120) + '...' : thesisText;
|
|
1267
1946
|
const sessionStatus = sessionRestored
|
|
1268
|
-
? C.zinc600(`
|
|
1269
|
-
: C.zinc600('
|
|
1270
|
-
addSystemText(
|
|
1271
|
-
|
|
1272
|
-
C.zinc600(`${latestContext.status || 'active'} ${confidencePct > 0 ? confidencePct + '%' : ''} ${(latestContext.edges || []).length} edges`) + sessionStatus + '\n' +
|
|
1273
|
-
C.zinc600('\u2500'.repeat(50)));
|
|
1947
|
+
? C.zinc600(`resumed (${agent.state.messages.length} messages)`)
|
|
1948
|
+
: C.zinc600('new session');
|
|
1949
|
+
addSystemText(buildWelcomeDashboard(latestContext, initialPositions));
|
|
1950
|
+
addSystemText(' ' + sessionStatus);
|
|
1274
1951
|
addSpacer();
|
|
1275
1952
|
// ── Start TUI ──────────────────────────────────────────────────────────────
|
|
1276
1953
|
tui.start();
|
|
1277
1954
|
}
|
|
1955
|
+
// ============================================================================
|
|
1956
|
+
// PLAIN-TEXT MODE (--no-tui)
|
|
1957
|
+
// ============================================================================
|
|
1958
|
+
async function runPlainTextAgent(params) {
|
|
1959
|
+
const { openrouterKey, sfClient, resolvedThesisId, opts } = params;
|
|
1960
|
+
let latestContext = params.latestContext;
|
|
1961
|
+
const readline = await import('readline');
|
|
1962
|
+
const piAi = await import('@mariozechner/pi-ai');
|
|
1963
|
+
const piAgent = await import('@mariozechner/pi-agent-core');
|
|
1964
|
+
const { getModel, streamSimple, Type } = piAi;
|
|
1965
|
+
const { Agent } = piAgent;
|
|
1966
|
+
const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
|
|
1967
|
+
let currentModelName = rawModelName.replace(/^openrouter\//, '');
|
|
1968
|
+
function resolveModel(name) {
|
|
1969
|
+
try {
|
|
1970
|
+
return getModel('openrouter', name);
|
|
1971
|
+
}
|
|
1972
|
+
catch {
|
|
1973
|
+
return {
|
|
1974
|
+
modelId: name, provider: 'openrouter', api: 'openai-completions',
|
|
1975
|
+
baseUrl: 'https://openrouter.ai/api/v1', id: name, name,
|
|
1976
|
+
inputPrice: 0, outputPrice: 0, contextWindow: 200000,
|
|
1977
|
+
supportsImages: true, supportsTools: true,
|
|
1978
|
+
};
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
let model = resolveModel(currentModelName);
|
|
1982
|
+
// ── Tools (same definitions as TUI mode) ──────────────────────────────────
|
|
1983
|
+
const thesisIdParam = Type.Object({ thesisId: Type.String({ description: 'Thesis ID' }) });
|
|
1984
|
+
const signalParams = Type.Object({
|
|
1985
|
+
thesisId: Type.String({ description: 'Thesis ID' }),
|
|
1986
|
+
content: Type.String({ description: 'Signal content' }),
|
|
1987
|
+
type: Type.Optional(Type.String({ description: 'Signal type: news, user_note, external' })),
|
|
1988
|
+
});
|
|
1989
|
+
const scanParams = Type.Object({
|
|
1990
|
+
query: Type.Optional(Type.String({ description: 'Keyword search' })),
|
|
1991
|
+
series: Type.Optional(Type.String({ description: 'Series ticker' })),
|
|
1992
|
+
market: Type.Optional(Type.String({ description: 'Market ticker' })),
|
|
1993
|
+
});
|
|
1994
|
+
const webSearchParams = Type.Object({ query: Type.String({ description: 'Search keywords' }) });
|
|
1995
|
+
const emptyParams = Type.Object({});
|
|
1996
|
+
const tools = [
|
|
1997
|
+
{
|
|
1998
|
+
name: 'get_context', label: 'Get Context',
|
|
1999
|
+
description: 'Get thesis snapshot: causal tree, edge prices, last evaluation, confidence',
|
|
2000
|
+
parameters: thesisIdParam,
|
|
2001
|
+
execute: async (_id, p) => {
|
|
2002
|
+
const ctx = await sfClient.getContext(p.thesisId);
|
|
2003
|
+
latestContext = ctx;
|
|
2004
|
+
return { content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }], details: {} };
|
|
2005
|
+
},
|
|
2006
|
+
},
|
|
2007
|
+
{
|
|
2008
|
+
name: 'inject_signal', label: 'Inject Signal',
|
|
2009
|
+
description: 'Inject a signal into the thesis',
|
|
2010
|
+
parameters: signalParams,
|
|
2011
|
+
execute: async (_id, p) => {
|
|
2012
|
+
const result = await sfClient.injectSignal(p.thesisId, p.type || 'user_note', p.content);
|
|
2013
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
2014
|
+
},
|
|
2015
|
+
},
|
|
2016
|
+
{
|
|
2017
|
+
name: 'trigger_evaluation', label: 'Evaluate',
|
|
2018
|
+
description: 'Trigger a deep evaluation cycle',
|
|
2019
|
+
parameters: thesisIdParam,
|
|
2020
|
+
execute: async (_id, p) => {
|
|
2021
|
+
const result = await sfClient.evaluate(p.thesisId);
|
|
2022
|
+
return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
|
|
2023
|
+
},
|
|
2024
|
+
},
|
|
2025
|
+
{
|
|
2026
|
+
name: 'scan_markets', label: 'Scan Markets',
|
|
2027
|
+
description: 'Search Kalshi prediction markets',
|
|
2028
|
+
parameters: scanParams,
|
|
2029
|
+
execute: async (_id, p) => {
|
|
2030
|
+
let result;
|
|
2031
|
+
if (p.market) {
|
|
2032
|
+
result = await (0, client_js_1.kalshiFetchMarket)(p.market);
|
|
2033
|
+
}
|
|
2034
|
+
else if (p.series) {
|
|
2035
|
+
result = await (0, client_js_1.kalshiFetchMarketsBySeries)(p.series);
|
|
2036
|
+
}
|
|
2037
|
+
else if (p.query) {
|
|
2038
|
+
const series = await (0, client_js_1.kalshiFetchAllSeries)();
|
|
2039
|
+
const kws = p.query.toLowerCase().split(/\s+/);
|
|
2040
|
+
result = series.filter((s) => kws.every((k) => ((s.title || '') + (s.ticker || '')).toLowerCase().includes(k))).slice(0, 15);
|
|
2041
|
+
}
|
|
2042
|
+
else {
|
|
2043
|
+
result = { error: 'Provide query, series, or market' };
|
|
2044
|
+
}
|
|
2045
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
2046
|
+
},
|
|
2047
|
+
},
|
|
2048
|
+
{
|
|
2049
|
+
name: 'list_theses', label: 'List Theses',
|
|
2050
|
+
description: 'List all theses',
|
|
2051
|
+
parameters: emptyParams,
|
|
2052
|
+
execute: async () => {
|
|
2053
|
+
const theses = await sfClient.listTheses();
|
|
2054
|
+
return { content: [{ type: 'text', text: JSON.stringify(theses, null, 2) }], details: {} };
|
|
2055
|
+
},
|
|
2056
|
+
},
|
|
2057
|
+
{
|
|
2058
|
+
name: 'get_positions', label: 'Get Positions',
|
|
2059
|
+
description: 'Get Kalshi positions with live prices',
|
|
2060
|
+
parameters: emptyParams,
|
|
2061
|
+
execute: async () => {
|
|
2062
|
+
const positions = await (0, kalshi_js_1.getPositions)();
|
|
2063
|
+
if (!positions)
|
|
2064
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
2065
|
+
for (const pos of positions) {
|
|
2066
|
+
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
2067
|
+
if (livePrice !== null) {
|
|
2068
|
+
pos.current_value = livePrice;
|
|
2069
|
+
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
2070
|
+
}
|
|
2071
|
+
}
|
|
2072
|
+
return { content: [{ type: 'text', text: JSON.stringify(positions, null, 2) }], details: {} };
|
|
2073
|
+
},
|
|
2074
|
+
},
|
|
2075
|
+
{
|
|
2076
|
+
name: 'web_search', label: 'Web Search',
|
|
2077
|
+
description: 'Search latest news and information',
|
|
2078
|
+
parameters: webSearchParams,
|
|
2079
|
+
execute: async (_id, p) => {
|
|
2080
|
+
const apiKey = process.env.TAVILY_API_KEY;
|
|
2081
|
+
if (!apiKey)
|
|
2082
|
+
return { content: [{ type: 'text', text: 'Tavily not configured. Set TAVILY_API_KEY.' }], details: {} };
|
|
2083
|
+
const res = await fetch('https://api.tavily.com/search', {
|
|
2084
|
+
method: 'POST', headers: { 'Content-Type': 'application/json' },
|
|
2085
|
+
body: JSON.stringify({ api_key: apiKey, query: p.query, max_results: 5, search_depth: 'basic', include_answer: true }),
|
|
2086
|
+
});
|
|
2087
|
+
if (!res.ok)
|
|
2088
|
+
return { content: [{ type: 'text', text: `Search failed: ${res.status}` }], details: {} };
|
|
2089
|
+
const data = await res.json();
|
|
2090
|
+
const results = (data.results || []).map((r) => `[${r.title}](${r.url})\n${r.content?.slice(0, 200)}`).join('\n\n');
|
|
2091
|
+
const answer = data.answer ? `Summary: ${data.answer}\n\n---\n\n` : '';
|
|
2092
|
+
return { content: [{ type: 'text', text: `${answer}${results}` }], details: {} };
|
|
2093
|
+
},
|
|
2094
|
+
},
|
|
2095
|
+
{
|
|
2096
|
+
name: 'get_milestones', label: 'Milestones',
|
|
2097
|
+
description: 'Get upcoming events from Kalshi calendar. Use to check economic releases, political events, or other catalysts.',
|
|
2098
|
+
parameters: Type.Object({
|
|
2099
|
+
hours: Type.Optional(Type.Number({ description: 'Hours ahead to look (default 168 = 1 week)' })),
|
|
2100
|
+
category: Type.Optional(Type.String({ description: 'Filter by category (e.g. Economics, Politics, Sports)' })),
|
|
2101
|
+
}),
|
|
2102
|
+
execute: async (_id, p) => {
|
|
2103
|
+
const hours = p.hours || 168;
|
|
2104
|
+
const now = new Date();
|
|
2105
|
+
const url = `https://api.elections.kalshi.com/trade-api/v2/milestones?limit=200&minimum_start_date=${now.toISOString()}` +
|
|
2106
|
+
(p.category ? `&category=${p.category}` : '');
|
|
2107
|
+
const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
|
|
2108
|
+
if (!res.ok)
|
|
2109
|
+
return { content: [{ type: 'text', text: `Milestones API error: ${res.status}` }], details: {} };
|
|
2110
|
+
const data = await res.json();
|
|
2111
|
+
const cutoff = now.getTime() + hours * 3600000;
|
|
2112
|
+
const filtered = (data.milestones || [])
|
|
2113
|
+
.filter((m) => new Date(m.start_date).getTime() <= cutoff)
|
|
2114
|
+
.slice(0, 30)
|
|
2115
|
+
.map((m) => ({
|
|
2116
|
+
title: m.title, category: m.category, start_date: m.start_date,
|
|
2117
|
+
related_event_tickers: m.related_event_tickers,
|
|
2118
|
+
hours_until: Math.round((new Date(m.start_date).getTime() - now.getTime()) / 3600000),
|
|
2119
|
+
}));
|
|
2120
|
+
return { content: [{ type: 'text', text: JSON.stringify(filtered, null, 2) }], details: {} };
|
|
2121
|
+
},
|
|
2122
|
+
},
|
|
2123
|
+
{
|
|
2124
|
+
name: 'get_forecast', label: 'Forecast',
|
|
2125
|
+
description: 'Get market distribution (P50/P75/P90 percentile history) for a Kalshi event.',
|
|
2126
|
+
parameters: Type.Object({
|
|
2127
|
+
eventTicker: Type.String({ description: 'Kalshi event ticker (e.g. KXWTIMAX-26DEC31)' }),
|
|
2128
|
+
days: Type.Optional(Type.Number({ description: 'Days of history (default 7)' })),
|
|
2129
|
+
}),
|
|
2130
|
+
execute: async (_id, p) => {
|
|
2131
|
+
const { getForecastHistory } = await import('../kalshi.js');
|
|
2132
|
+
const days = p.days || 7;
|
|
2133
|
+
const evtRes = await fetch(`https://api.elections.kalshi.com/trade-api/v2/events/${p.eventTicker}`, { headers: { 'Accept': 'application/json' } });
|
|
2134
|
+
if (!evtRes.ok)
|
|
2135
|
+
return { content: [{ type: 'text', text: `Event not found: ${p.eventTicker}` }], details: {} };
|
|
2136
|
+
const evtData = await evtRes.json();
|
|
2137
|
+
const seriesTicker = evtData.event?.series_ticker;
|
|
2138
|
+
if (!seriesTicker)
|
|
2139
|
+
return { content: [{ type: 'text', text: `No series_ticker for ${p.eventTicker}` }], details: {} };
|
|
2140
|
+
const history = await getForecastHistory({
|
|
2141
|
+
seriesTicker, eventTicker: p.eventTicker, percentiles: [5000, 7500, 9000],
|
|
2142
|
+
startTs: Math.floor((Date.now() - days * 86400000) / 1000),
|
|
2143
|
+
endTs: Math.floor(Date.now() / 1000), periodInterval: 1440,
|
|
2144
|
+
});
|
|
2145
|
+
if (!history || history.length === 0)
|
|
2146
|
+
return { content: [{ type: 'text', text: 'No forecast data available' }], details: {} };
|
|
2147
|
+
return { content: [{ type: 'text', text: JSON.stringify(history, null, 2) }], details: {} };
|
|
2148
|
+
},
|
|
2149
|
+
},
|
|
2150
|
+
{
|
|
2151
|
+
name: 'get_settlements', label: 'Settlements',
|
|
2152
|
+
description: 'Get settled (resolved) contracts with P&L.',
|
|
2153
|
+
parameters: Type.Object({ ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })) }),
|
|
2154
|
+
execute: async (_id, p) => {
|
|
2155
|
+
const { getSettlements } = await import('../kalshi.js');
|
|
2156
|
+
const result = await getSettlements({ limit: 100, ticker: p.ticker });
|
|
2157
|
+
if (!result)
|
|
2158
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
2159
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.settlements, null, 2) }], details: {} };
|
|
2160
|
+
},
|
|
2161
|
+
},
|
|
2162
|
+
{
|
|
2163
|
+
name: 'get_balance', label: 'Balance',
|
|
2164
|
+
description: 'Get Kalshi account balance and portfolio value.',
|
|
2165
|
+
parameters: emptyParams,
|
|
2166
|
+
execute: async () => {
|
|
2167
|
+
const { getBalance } = await import('../kalshi.js');
|
|
2168
|
+
const result = await getBalance();
|
|
2169
|
+
if (!result)
|
|
2170
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
2171
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
2172
|
+
},
|
|
2173
|
+
},
|
|
2174
|
+
{
|
|
2175
|
+
name: 'get_orders', label: 'Orders',
|
|
2176
|
+
description: 'Get current resting orders on Kalshi.',
|
|
2177
|
+
parameters: Type.Object({ status: Type.Optional(Type.String({ description: 'Filter by status: resting, canceled, executed. Default: resting' })) }),
|
|
2178
|
+
execute: async (_id, p) => {
|
|
2179
|
+
const { getOrders } = await import('../kalshi.js');
|
|
2180
|
+
const result = await getOrders({ status: p.status || 'resting', limit: 100 });
|
|
2181
|
+
if (!result)
|
|
2182
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
2183
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.orders, null, 2) }], details: {} };
|
|
2184
|
+
},
|
|
2185
|
+
},
|
|
2186
|
+
{
|
|
2187
|
+
name: 'get_fills', label: 'Fills',
|
|
2188
|
+
description: 'Get recent trade fills (executed trades) on Kalshi.',
|
|
2189
|
+
parameters: Type.Object({ ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })) }),
|
|
2190
|
+
execute: async (_id, p) => {
|
|
2191
|
+
const { getFills } = await import('../kalshi.js');
|
|
2192
|
+
const result = await getFills({ ticker: p.ticker, limit: 50 });
|
|
2193
|
+
if (!result)
|
|
2194
|
+
return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
|
|
2195
|
+
return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
|
|
2196
|
+
},
|
|
2197
|
+
},
|
|
2198
|
+
{
|
|
2199
|
+
name: 'get_schedule',
|
|
2200
|
+
label: 'Schedule',
|
|
2201
|
+
description: 'Get exchange status (open/closed) and trading hours. Use to check if low liquidity is due to off-hours.',
|
|
2202
|
+
parameters: emptyParams,
|
|
2203
|
+
execute: async () => {
|
|
2204
|
+
try {
|
|
2205
|
+
const res = await fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } });
|
|
2206
|
+
if (!res.ok)
|
|
2207
|
+
return { content: [{ type: 'text', text: `Exchange API error: ${res.status}` }], details: {} };
|
|
2208
|
+
const data = await res.json();
|
|
2209
|
+
return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
|
|
2210
|
+
}
|
|
2211
|
+
catch (err) {
|
|
2212
|
+
return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
|
|
2213
|
+
}
|
|
2214
|
+
},
|
|
2215
|
+
},
|
|
2216
|
+
];
|
|
2217
|
+
// ── System prompt ─────────────────────────────────────────────────────────
|
|
2218
|
+
const ctx = latestContext;
|
|
2219
|
+
const edgesSummary = ctx.edges
|
|
2220
|
+
?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
2221
|
+
.slice(0, 5)
|
|
2222
|
+
.map((e) => ` ${(e.market || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}\u00A2 | edge ${e.edge > 0 ? '+' : ''}${e.edge}`)
|
|
2223
|
+
.join('\n') || ' (no edges)';
|
|
2224
|
+
const nodesSummary = ctx.causalTree?.nodes
|
|
2225
|
+
?.filter((n) => n.depth === 0)
|
|
2226
|
+
.map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
|
|
2227
|
+
.join('\n') || ' (no causal tree)';
|
|
2228
|
+
const conf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
|
|
2229
|
+
const systemPrompt = `You are a prediction market trading assistant. Help the user make correct trading decisions.
|
|
2230
|
+
|
|
2231
|
+
Current thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
|
|
2232
|
+
ID: ${resolvedThesisId}
|
|
2233
|
+
Confidence: ${conf}%
|
|
2234
|
+
Status: ${ctx.status}
|
|
2235
|
+
|
|
2236
|
+
Causal tree nodes:
|
|
2237
|
+
${nodesSummary}
|
|
2238
|
+
|
|
2239
|
+
Top edges:
|
|
2240
|
+
${edgesSummary}
|
|
2241
|
+
|
|
2242
|
+
${ctx.lastEvaluation?.summary ? `Latest evaluation: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}
|
|
2243
|
+
|
|
2244
|
+
Rules: Be concise. Use tools when needed. Don't ask "anything else?".`;
|
|
2245
|
+
// ── Create agent ──────────────────────────────────────────────────────────
|
|
2246
|
+
const agent = new Agent({
|
|
2247
|
+
initialState: { systemPrompt, model, tools, thinkingLevel: 'off' },
|
|
2248
|
+
streamFn: streamSimple,
|
|
2249
|
+
getApiKey: (provider) => provider === 'openrouter' ? openrouterKey : undefined,
|
|
2250
|
+
});
|
|
2251
|
+
// ── Session restore ───────────────────────────────────────────────────────
|
|
2252
|
+
if (!opts?.newSession) {
|
|
2253
|
+
const saved = loadSession(resolvedThesisId);
|
|
2254
|
+
if (saved?.messages?.length > 0) {
|
|
2255
|
+
try {
|
|
2256
|
+
agent.replaceMessages(saved.messages);
|
|
2257
|
+
agent.setSystemPrompt(systemPrompt);
|
|
2258
|
+
}
|
|
2259
|
+
catch { /* start fresh */ }
|
|
2260
|
+
}
|
|
2261
|
+
}
|
|
2262
|
+
// ── Subscribe to agent events → plain stdout ──────────────────────────────
|
|
2263
|
+
let currentText = '';
|
|
2264
|
+
agent.subscribe((event) => {
|
|
2265
|
+
if (event.type === 'message_update') {
|
|
2266
|
+
const e = event.assistantMessageEvent;
|
|
2267
|
+
if (e.type === 'text_delta') {
|
|
2268
|
+
process.stdout.write(e.delta);
|
|
2269
|
+
currentText += e.delta;
|
|
2270
|
+
}
|
|
2271
|
+
}
|
|
2272
|
+
if (event.type === 'message_end') {
|
|
2273
|
+
if (currentText) {
|
|
2274
|
+
process.stdout.write('\n');
|
|
2275
|
+
currentText = '';
|
|
2276
|
+
}
|
|
2277
|
+
}
|
|
2278
|
+
if (event.type === 'tool_execution_start') {
|
|
2279
|
+
process.stderr.write(` \u26A1 ${event.toolName}...\n`);
|
|
2280
|
+
}
|
|
2281
|
+
if (event.type === 'tool_execution_end') {
|
|
2282
|
+
const status = event.isError ? '\u2717' : '\u2713';
|
|
2283
|
+
process.stderr.write(` ${status} ${event.toolName}\n`);
|
|
2284
|
+
}
|
|
2285
|
+
});
|
|
2286
|
+
// ── Welcome ───────────────────────────────────────────────────────────────
|
|
2287
|
+
const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
|
|
2288
|
+
console.log(`SF Agent — ${resolvedThesisId.slice(0, 8)} | ${conf}% | ${currentModelName}`);
|
|
2289
|
+
console.log(`Thesis: ${thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText}`);
|
|
2290
|
+
console.log(`Edges: ${(ctx.edges || []).length} | Status: ${ctx.status}`);
|
|
2291
|
+
console.log('Type /help for commands, /exit to quit.\n');
|
|
2292
|
+
// ── REPL loop ─────────────────────────────────────────────────────────────
|
|
2293
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' });
|
|
2294
|
+
rl.prompt();
|
|
2295
|
+
for await (const line of rl) {
|
|
2296
|
+
const trimmed = line.trim();
|
|
2297
|
+
if (!trimmed) {
|
|
2298
|
+
rl.prompt();
|
|
2299
|
+
continue;
|
|
2300
|
+
}
|
|
2301
|
+
if (trimmed === '/exit' || trimmed === '/quit') {
|
|
2302
|
+
try {
|
|
2303
|
+
saveSession(resolvedThesisId, currentModelName, agent.state.messages);
|
|
2304
|
+
}
|
|
2305
|
+
catch { }
|
|
2306
|
+
rl.close();
|
|
2307
|
+
return;
|
|
2308
|
+
}
|
|
2309
|
+
if (trimmed === '/help') {
|
|
2310
|
+
console.log('Commands: /help /exit /tree /edges /eval /model <name>');
|
|
2311
|
+
rl.prompt();
|
|
2312
|
+
continue;
|
|
2313
|
+
}
|
|
2314
|
+
if (trimmed === '/tree') {
|
|
2315
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2316
|
+
const nodes = latestContext.causalTree?.nodes || [];
|
|
2317
|
+
for (const n of nodes) {
|
|
2318
|
+
const indent = ' '.repeat(n.depth || 0);
|
|
2319
|
+
console.log(`${indent}${n.id} ${(n.label || '').slice(0, 60)} — ${Math.round(n.probability * 100)}%`);
|
|
2320
|
+
}
|
|
2321
|
+
rl.prompt();
|
|
2322
|
+
continue;
|
|
2323
|
+
}
|
|
2324
|
+
if (trimmed === '/edges') {
|
|
2325
|
+
latestContext = await sfClient.getContext(resolvedThesisId);
|
|
2326
|
+
const edges = (latestContext.edges || []).sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge)).slice(0, 15);
|
|
2327
|
+
for (const e of edges) {
|
|
2328
|
+
const sign = e.edge > 0 ? '+' : '';
|
|
2329
|
+
console.log(` ${(e.market || '').slice(0, 45).padEnd(45)} ${e.marketPrice}¢ edge ${sign}${e.edge} ${e.venue}`);
|
|
2330
|
+
}
|
|
2331
|
+
rl.prompt();
|
|
2332
|
+
continue;
|
|
2333
|
+
}
|
|
2334
|
+
if (trimmed === '/eval') {
|
|
2335
|
+
console.log('Triggering evaluation...');
|
|
2336
|
+
const result = await sfClient.evaluate(resolvedThesisId);
|
|
2337
|
+
console.log(`Confidence: ${result.previousConfidence} → ${result.newConfidence}`);
|
|
2338
|
+
if (result.summary)
|
|
2339
|
+
console.log(result.summary);
|
|
2340
|
+
rl.prompt();
|
|
2341
|
+
continue;
|
|
2342
|
+
}
|
|
2343
|
+
if (trimmed.startsWith('/model')) {
|
|
2344
|
+
const newModel = trimmed.slice(6).trim();
|
|
2345
|
+
if (!newModel) {
|
|
2346
|
+
console.log(`Current: ${currentModelName}`);
|
|
2347
|
+
rl.prompt();
|
|
2348
|
+
continue;
|
|
2349
|
+
}
|
|
2350
|
+
currentModelName = newModel.replace(/^openrouter\//, '');
|
|
2351
|
+
model = resolveModel(currentModelName);
|
|
2352
|
+
agent.setModel(model);
|
|
2353
|
+
console.log(`Model: ${currentModelName}`);
|
|
2354
|
+
rl.prompt();
|
|
2355
|
+
continue;
|
|
2356
|
+
}
|
|
2357
|
+
// Regular message → agent
|
|
2358
|
+
try {
|
|
2359
|
+
await agent.prompt(trimmed);
|
|
2360
|
+
}
|
|
2361
|
+
catch (err) {
|
|
2362
|
+
console.error(`Error: ${err.message}`);
|
|
2363
|
+
}
|
|
2364
|
+
// Save after each turn
|
|
2365
|
+
try {
|
|
2366
|
+
saveSession(resolvedThesisId, currentModelName, agent.state.messages);
|
|
2367
|
+
}
|
|
2368
|
+
catch { }
|
|
2369
|
+
rl.prompt();
|
|
2370
|
+
}
|
|
2371
|
+
}
|