@spfunctions/cli 1.4.4 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +205 -48
- package/dist/cache.d.ts +6 -0
- package/dist/cache.js +31 -0
- package/dist/cache.test.d.ts +1 -0
- package/dist/cache.test.js +73 -0
- package/dist/client.test.d.ts +1 -0
- package/dist/client.test.js +89 -0
- package/dist/commands/agent.js +594 -106
- package/dist/commands/book.d.ts +17 -0
- package/dist/commands/book.js +220 -0
- package/dist/commands/dashboard.d.ts +6 -3
- package/dist/commands/dashboard.js +53 -22
- package/dist/commands/liquidity.d.ts +2 -0
- package/dist/commands/liquidity.js +128 -43
- package/dist/commands/performance.js +9 -2
- package/dist/commands/positions.js +50 -0
- package/dist/commands/scan.d.ts +1 -0
- package/dist/commands/scan.js +66 -15
- package/dist/commands/setup.d.ts +1 -0
- package/dist/commands/setup.js +71 -6
- package/dist/commands/telegram.d.ts +15 -0
- package/dist/commands/telegram.js +125 -0
- package/dist/config.d.ts +3 -0
- package/dist/config.js +9 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +138 -0
- package/dist/index.js +107 -9
- package/dist/polymarket.d.ts +237 -0
- package/dist/polymarket.js +353 -0
- package/dist/polymarket.test.d.ts +1 -0
- package/dist/polymarket.test.js +424 -0
- package/dist/telegram/agent-bridge.d.ts +15 -0
- package/dist/telegram/agent-bridge.js +368 -0
- package/dist/telegram/bot.d.ts +10 -0
- package/dist/telegram/bot.js +297 -0
- package/dist/telegram/commands.d.ts +11 -0
- package/dist/telegram/commands.js +120 -0
- package/dist/telegram/format.d.ts +11 -0
- package/dist/telegram/format.js +51 -0
- package/dist/telegram/format.test.d.ts +1 -0
- package/dist/telegram/format.test.js +73 -0
- package/dist/telegram/poller.d.ts +6 -0
- package/dist/telegram/poller.js +32 -0
- package/dist/topics.d.ts +3 -0
- package/dist/topics.js +65 -7
- package/dist/topics.test.d.ts +1 -0
- package/dist/topics.test.js +131 -0
- package/dist/tui/border.d.ts +33 -0
- package/dist/tui/border.js +87 -0
- package/dist/tui/chart.d.ts +19 -0
- package/dist/tui/chart.js +117 -0
- package/dist/tui/dashboard.d.ts +9 -0
- package/dist/tui/dashboard.js +814 -0
- package/dist/tui/layout.d.ts +16 -0
- package/dist/tui/layout.js +41 -0
- package/dist/tui/screen.d.ts +33 -0
- package/dist/tui/screen.js +102 -0
- package/dist/tui/state.d.ts +40 -0
- package/dist/tui/state.js +36 -0
- package/dist/tui/widgets/commandbar.d.ts +8 -0
- package/dist/tui/widgets/commandbar.js +82 -0
- package/dist/tui/widgets/detail.d.ts +9 -0
- package/dist/tui/widgets/detail.js +151 -0
- package/dist/tui/widgets/edges.d.ts +4 -0
- package/dist/tui/widgets/edges.js +34 -0
- package/dist/tui/widgets/liquidity.d.ts +9 -0
- package/dist/tui/widgets/liquidity.js +142 -0
- package/dist/tui/widgets/orders.d.ts +4 -0
- package/dist/tui/widgets/orders.js +37 -0
- package/dist/tui/widgets/portfolio.d.ts +4 -0
- package/dist/tui/widgets/portfolio.js +59 -0
- package/dist/tui/widgets/signals.d.ts +4 -0
- package/dist/tui/widgets/signals.js +31 -0
- package/dist/tui/widgets/statusbar.d.ts +8 -0
- package/dist/tui/widgets/statusbar.js +72 -0
- package/dist/tui/widgets/thesis.d.ts +4 -0
- package/dist/tui/widgets/thesis.js +66 -0
- package/dist/tui/widgets/trade.d.ts +9 -0
- package/dist/tui/widgets/trade.js +117 -0
- package/dist/tui/widgets/upcoming.d.ts +4 -0
- package/dist/tui/widgets/upcoming.js +41 -0
- package/dist/tui/widgets/whatif.d.ts +7 -0
- package/dist/tui/widgets/whatif.js +113 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +111 -0
- package/package.json +6 -2
package/dist/commands/agent.js
CHANGED
|
@@ -23,6 +23,8 @@ 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 polymarket_js_1 = require("../polymarket.js");
|
|
27
|
+
const topics_js_1 = require("../topics.js");
|
|
26
28
|
const config_js_1 = require("../config.js");
|
|
27
29
|
// ─── Session persistence ─────────────────────────────────────────────────────
|
|
28
30
|
function getSessionDir() {
|
|
@@ -67,7 +69,7 @@ const C = {
|
|
|
67
69
|
zinc800: rgb(39, 39, 42), // #27272a
|
|
68
70
|
red: rgb(239, 68, 68), // #ef4444
|
|
69
71
|
amber: rgb(245, 158, 11), // #f59e0b
|
|
70
|
-
white: rgb(255, 255, 255),
|
|
72
|
+
white: rgb(255, 255, 255), // #ffffff
|
|
71
73
|
bgZinc900: bgRgb(24, 24, 27), // #18181b
|
|
72
74
|
bgZinc800: bgRgb(39, 39, 42), // #27272a
|
|
73
75
|
};
|
|
@@ -196,18 +198,56 @@ function createHeaderBar(piTui) {
|
|
|
196
198
|
}
|
|
197
199
|
};
|
|
198
200
|
}
|
|
199
|
-
/**
|
|
201
|
+
/** Combined footer bar: thesis info (line 1) + model/exchange (line 2) */
|
|
200
202
|
function createFooterBar(piTui) {
|
|
201
203
|
const { truncateToWidth, visibleWidth } = piTui;
|
|
202
204
|
return class FooterBar {
|
|
205
|
+
// Thesis info (was HeaderBar)
|
|
206
|
+
thesisId = '';
|
|
207
|
+
confidence = 0;
|
|
208
|
+
confidenceDelta = 0;
|
|
209
|
+
pnlDollars = 0;
|
|
210
|
+
positionCount = 0;
|
|
211
|
+
edgeCount = 0;
|
|
212
|
+
topEdge = '';
|
|
213
|
+
// Model info
|
|
203
214
|
tokens = 0;
|
|
204
215
|
cost = 0;
|
|
205
216
|
toolCount = 0;
|
|
206
217
|
modelName = '';
|
|
207
218
|
tradingEnabled = false;
|
|
208
|
-
exchangeOpen = null;
|
|
219
|
+
exchangeOpen = null;
|
|
209
220
|
cachedWidth;
|
|
210
221
|
cachedLines;
|
|
222
|
+
setFromContext(ctx, positions) {
|
|
223
|
+
this.thesisId = (ctx.thesisId || '').slice(0, 8);
|
|
224
|
+
this.confidence = typeof ctx.confidence === 'number'
|
|
225
|
+
? Math.round(ctx.confidence * 100)
|
|
226
|
+
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
227
|
+
this.confidenceDelta = ctx.lastEvaluation?.confidenceDelta
|
|
228
|
+
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
229
|
+
: 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 || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0))[0];
|
|
234
|
+
const name = (top.market || top.marketTitle || top.marketId || '').slice(0, 20);
|
|
235
|
+
const edge = top.edge || top.edgeSize || 0;
|
|
236
|
+
this.topEdge = `${name} ${edge > 0 ? '+' : ''}${Math.round(edge)}\u00A2`;
|
|
237
|
+
}
|
|
238
|
+
if (positions && positions.length > 0) {
|
|
239
|
+
this.positionCount = positions.length;
|
|
240
|
+
this.pnlDollars = positions.reduce((sum, p) => sum + (p.unrealized_pnl || 0), 0) / 100;
|
|
241
|
+
}
|
|
242
|
+
this.cachedWidth = undefined;
|
|
243
|
+
this.cachedLines = undefined;
|
|
244
|
+
}
|
|
245
|
+
updateConfidence(newConf, delta) {
|
|
246
|
+
this.confidence = Math.round(newConf * 100);
|
|
247
|
+
this.confidenceDelta = Math.round(delta * 100);
|
|
248
|
+
this.cachedWidth = undefined;
|
|
249
|
+
this.cachedLines = undefined;
|
|
250
|
+
}
|
|
211
251
|
invalidate() {
|
|
212
252
|
this.cachedWidth = undefined;
|
|
213
253
|
this.cachedLines = undefined;
|
|
@@ -220,31 +260,43 @@ function createFooterBar(piTui) {
|
|
|
220
260
|
if (this.cachedLines && this.cachedWidth === width)
|
|
221
261
|
return this.cachedLines;
|
|
222
262
|
this.cachedWidth = width;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
263
|
+
// Line 1: thesis info
|
|
264
|
+
const id = C.emerald(this.thesisId);
|
|
265
|
+
const arrow = this.confidenceDelta > 0 ? '\u25B2' : this.confidenceDelta < 0 ? '\u25BC' : '\u2500';
|
|
266
|
+
const arrowColor = this.confidenceDelta > 0 ? C.emerald : this.confidenceDelta < 0 ? C.red : C.zinc600;
|
|
267
|
+
const deltaStr = this.confidenceDelta !== 0 ? ` (${this.confidenceDelta > 0 ? '+' : ''}${this.confidenceDelta})` : '';
|
|
268
|
+
const conf = arrowColor(`${arrow} ${this.confidence}%${deltaStr}`);
|
|
269
|
+
let pnl = '';
|
|
270
|
+
if (this.positionCount > 0) {
|
|
271
|
+
const pnlStr = this.pnlDollars >= 0
|
|
272
|
+
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
273
|
+
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
274
|
+
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
275
|
+
}
|
|
276
|
+
const edges = C.zinc600(`${this.edgeCount} edges`);
|
|
277
|
+
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
278
|
+
const sep = C.zinc600(' \u2502 ');
|
|
279
|
+
const line1Parts = [id, conf, pnl, edges, top].filter(Boolean);
|
|
280
|
+
let line1 = C.bgZinc800(' ' + truncateToWidth(line1Parts.join(sep), width - 2, '') + ' ');
|
|
281
|
+
const l1vw = visibleWidth(line1);
|
|
282
|
+
if (l1vw < width)
|
|
283
|
+
line1 += C.bgZinc800(' '.repeat(width - l1vw));
|
|
284
|
+
// Line 2: model + exchange
|
|
226
285
|
const model = C.zinc600(this.modelName.split('/').pop() || this.modelName);
|
|
286
|
+
const tokStr = this.tokens >= 1000 ? `${(this.tokens / 1000).toFixed(1)}k` : `${this.tokens}`;
|
|
227
287
|
const tokens = C.zinc600(`${tokStr} tok`);
|
|
228
|
-
const exchange = this.exchangeOpen === true
|
|
229
|
-
|
|
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');
|
|
288
|
+
const exchange = this.exchangeOpen === true ? C.emerald('OPEN') : this.exchangeOpen === false ? C.red('CLOSED') : C.zinc600('...');
|
|
289
|
+
const trading = this.tradingEnabled ? C.amber('\u26A1 trading') : C.zinc600('\u26A1 read-only');
|
|
236
290
|
const help = C.zinc600('/help');
|
|
237
|
-
const sep = C.zinc600(' \u2502 ');
|
|
238
291
|
const leftText = [model, tokens, exchange, trading].join(sep);
|
|
239
292
|
const lw = visibleWidth(leftText);
|
|
240
293
|
const rw = visibleWidth(help);
|
|
241
294
|
const gap = Math.max(1, width - lw - rw - 2);
|
|
242
|
-
let
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.cachedLines = [line];
|
|
295
|
+
let line2 = C.bgZinc900(' ' + leftText + ' '.repeat(gap) + help + ' ');
|
|
296
|
+
const l2vw = visibleWidth(line2);
|
|
297
|
+
if (l2vw < width)
|
|
298
|
+
line2 += C.bgZinc900(' '.repeat(width - l2vw));
|
|
299
|
+
this.cachedLines = [line1, line2];
|
|
248
300
|
return this.cachedLines;
|
|
249
301
|
}
|
|
250
302
|
};
|
|
@@ -340,6 +392,76 @@ function renderPositions(positions) {
|
|
|
340
392
|
: ` Total P&L: ${C.red(bold(`-$${Math.abs(parseFloat(totalDollars)).toFixed(2)}`))}`);
|
|
341
393
|
return lines.join('\n');
|
|
342
394
|
}
|
|
395
|
+
// ─── Thesis selector (arrow keys + enter, like Claude Code) ─────────────────
|
|
396
|
+
async function selectThesis(theses) {
|
|
397
|
+
return new Promise((resolve) => {
|
|
398
|
+
let selected = 0;
|
|
399
|
+
const items = theses.map((t) => {
|
|
400
|
+
const conf = typeof t.confidence === 'number' ? Math.round(t.confidence * 100) : 0;
|
|
401
|
+
const title = (t.rawThesis || t.thesis || t.title || '').slice(0, 55);
|
|
402
|
+
return { id: t.id, conf, title };
|
|
403
|
+
});
|
|
404
|
+
const write = process.stdout.write.bind(process.stdout);
|
|
405
|
+
// Use alternate screen buffer for clean rendering (like Claude Code)
|
|
406
|
+
write('\x1b[?1049h'); // enter alternate screen
|
|
407
|
+
write('\x1b[?25l'); // hide cursor
|
|
408
|
+
function render() {
|
|
409
|
+
write('\x1b[H\x1b[2J'); // cursor home + clear screen
|
|
410
|
+
write('\n \x1b[2mSelect thesis\x1b[22m\n\n');
|
|
411
|
+
for (let i = 0; i < items.length; i++) {
|
|
412
|
+
const item = items[i];
|
|
413
|
+
const sel = i === selected;
|
|
414
|
+
const cursor = sel ? '\x1b[38;2;16;185;129m › \x1b[39m' : ' ';
|
|
415
|
+
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`;
|
|
416
|
+
const conf = `\x1b[38;2;55;55;60m${item.conf}%\x1b[39m`;
|
|
417
|
+
const title = sel ? `\x1b[38;2;160;160;165m${item.title}\x1b[39m` : `\x1b[38;2;80;80;88m${item.title}\x1b[39m`;
|
|
418
|
+
write(`${cursor}${id} ${conf} ${title}\n`);
|
|
419
|
+
}
|
|
420
|
+
write(`\n \x1b[38;2;55;55;60m↑↓ navigate · enter select\x1b[39m`);
|
|
421
|
+
}
|
|
422
|
+
render();
|
|
423
|
+
if (process.stdin.isTTY)
|
|
424
|
+
process.stdin.setRawMode(true);
|
|
425
|
+
process.stdin.resume();
|
|
426
|
+
process.stdin.setEncoding('utf8');
|
|
427
|
+
const onKey = (key) => {
|
|
428
|
+
const buf = Buffer.from(key);
|
|
429
|
+
if (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x41) {
|
|
430
|
+
selected = (selected - 1 + items.length) % items.length;
|
|
431
|
+
render();
|
|
432
|
+
}
|
|
433
|
+
else if (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x42) {
|
|
434
|
+
selected = (selected + 1) % items.length;
|
|
435
|
+
render();
|
|
436
|
+
}
|
|
437
|
+
else if (key === 'k') {
|
|
438
|
+
selected = (selected - 1 + items.length) % items.length;
|
|
439
|
+
render();
|
|
440
|
+
}
|
|
441
|
+
else if (key === 'j') {
|
|
442
|
+
selected = (selected + 1) % items.length;
|
|
443
|
+
render();
|
|
444
|
+
}
|
|
445
|
+
else if (key === '\r' || key === '\n') {
|
|
446
|
+
cleanup();
|
|
447
|
+
resolve(items[selected].id);
|
|
448
|
+
}
|
|
449
|
+
else if (buf[0] === 0x03) {
|
|
450
|
+
cleanup();
|
|
451
|
+
process.exit(0);
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
function cleanup() {
|
|
455
|
+
process.stdin.removeListener('data', onKey);
|
|
456
|
+
if (process.stdin.isTTY)
|
|
457
|
+
process.stdin.setRawMode(false);
|
|
458
|
+
process.stdin.pause();
|
|
459
|
+
write('\x1b[?25h'); // show cursor
|
|
460
|
+
write('\x1b[?1049l'); // exit alternate screen — restores original content
|
|
461
|
+
}
|
|
462
|
+
process.stdin.on('data', onKey);
|
|
463
|
+
});
|
|
464
|
+
}
|
|
343
465
|
// ─── Main command ────────────────────────────────────────────────────────────
|
|
344
466
|
async function agentCommand(thesisId, opts) {
|
|
345
467
|
// ── Validate API keys ──────────────────────────────────────────────────────
|
|
@@ -373,17 +495,42 @@ async function agentCommand(thesisId, opts) {
|
|
|
373
495
|
}
|
|
374
496
|
}
|
|
375
497
|
const sfClient = new client_js_1.SFClient();
|
|
376
|
-
// ── Resolve thesis ID
|
|
498
|
+
// ── Resolve thesis ID (interactive selection if needed) ─────────────────────
|
|
377
499
|
let resolvedThesisId = thesisId;
|
|
378
500
|
if (!resolvedThesisId) {
|
|
379
501
|
const data = await sfClient.listTheses();
|
|
380
|
-
const theses = data.theses || data;
|
|
381
|
-
const active = theses.
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
|
|
502
|
+
const theses = (data.theses || data);
|
|
503
|
+
const active = theses.filter((t) => t.status === 'active');
|
|
504
|
+
if (active.length === 0) {
|
|
505
|
+
if (!process.stdin.isTTY) {
|
|
506
|
+
console.error('No active thesis. Create one first: sf create "..."');
|
|
507
|
+
process.exit(1);
|
|
508
|
+
}
|
|
509
|
+
// No theses — offer to create one
|
|
510
|
+
console.log('\n No active theses found.\n');
|
|
511
|
+
const readline = await import('readline');
|
|
512
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
513
|
+
const answer = await new Promise(resolve => rl.question(' Enter a thesis to create (or press Enter to exit):\n > ', resolve));
|
|
514
|
+
rl.close();
|
|
515
|
+
if (!answer.trim()) {
|
|
516
|
+
process.exit(0);
|
|
517
|
+
}
|
|
518
|
+
console.log('\n Creating thesis...\n');
|
|
519
|
+
const result = await sfClient.createThesis(answer.trim(), true);
|
|
520
|
+
resolvedThesisId = result.id;
|
|
521
|
+
console.log(` ✓ Created: ${result.id?.slice(0, 8)}\n`);
|
|
522
|
+
}
|
|
523
|
+
else if (active.length === 1) {
|
|
524
|
+
resolvedThesisId = active[0].id;
|
|
525
|
+
}
|
|
526
|
+
else if (process.stdin.isTTY && !opts?.noTui) {
|
|
527
|
+
// Multiple theses — interactive arrow key selector (TUI only)
|
|
528
|
+
resolvedThesisId = await selectThesis(active);
|
|
529
|
+
}
|
|
530
|
+
else {
|
|
531
|
+
// Non-interactive (--plain, telegram, piped) — use first active
|
|
532
|
+
resolvedThesisId = active[0].id;
|
|
385
533
|
}
|
|
386
|
-
resolvedThesisId = active.id;
|
|
387
534
|
}
|
|
388
535
|
// ── Fetch initial context ──────────────────────────────────────────────────
|
|
389
536
|
let latestContext = await sfClient.getContext(resolvedThesisId);
|
|
@@ -400,7 +547,6 @@ async function agentCommand(thesisId, opts) {
|
|
|
400
547
|
const { Agent } = piAgent;
|
|
401
548
|
// ── Component class factories (need piTui ref) ─────────────────────────────
|
|
402
549
|
const MutableLine = createMutableLine(piTui);
|
|
403
|
-
const HeaderBar = createHeaderBar(piTui);
|
|
404
550
|
const FooterBar = createFooterBar(piTui);
|
|
405
551
|
// ── Model setup ────────────────────────────────────────────────────────────
|
|
406
552
|
const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
|
|
@@ -479,8 +625,11 @@ async function agentCommand(thesisId, opts) {
|
|
|
479
625
|
},
|
|
480
626
|
};
|
|
481
627
|
// ── Build components ───────────────────────────────────────────────────────
|
|
482
|
-
|
|
483
|
-
|
|
628
|
+
// No header bar — all info in footer (2 lines)
|
|
629
|
+
const footerBar = new FooterBar();
|
|
630
|
+
footerBar.modelName = currentModelName;
|
|
631
|
+
footerBar.tradingEnabled = (0, config_js_1.loadConfig)().tradingEnabled || false;
|
|
632
|
+
// Fetch positions for footer P&L (non-blocking, best-effort)
|
|
484
633
|
let initialPositions = null;
|
|
485
634
|
try {
|
|
486
635
|
initialPositions = await (0, kalshi_js_1.getPositions)();
|
|
@@ -495,10 +644,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
495
644
|
}
|
|
496
645
|
}
|
|
497
646
|
catch { /* positions not available, fine */ }
|
|
498
|
-
|
|
499
|
-
const footerBar = new FooterBar();
|
|
500
|
-
footerBar.modelName = currentModelName;
|
|
501
|
-
footerBar.tradingEnabled = (0, config_js_1.loadConfig)().tradingEnabled || false;
|
|
647
|
+
footerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
502
648
|
// Fetch exchange status for footer (non-blocking)
|
|
503
649
|
fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } })
|
|
504
650
|
.then(r => r.json())
|
|
@@ -537,13 +683,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
537
683
|
tui.addChild(bottomSpacer);
|
|
538
684
|
// Focus on editor
|
|
539
685
|
tui.setFocus(editor);
|
|
540
|
-
// ──
|
|
541
|
-
const headerOverlay = tui.showOverlay(headerBar, {
|
|
542
|
-
row: 0,
|
|
543
|
-
col: 0,
|
|
544
|
-
width: '100%',
|
|
545
|
-
nonCapturing: true,
|
|
546
|
-
});
|
|
686
|
+
// ── Footer overlay (2-line: thesis info + model/exchange) ──────────────────
|
|
547
687
|
const footerOverlay = tui.showOverlay(footerBar, {
|
|
548
688
|
anchor: 'bottom-left',
|
|
549
689
|
width: '100%',
|
|
@@ -598,7 +738,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
598
738
|
execute: async (_toolCallId, params) => {
|
|
599
739
|
const ctx = await sfClient.getContext(params.thesisId);
|
|
600
740
|
latestContext = ctx;
|
|
601
|
-
|
|
741
|
+
footerBar.setFromContext(ctx, initialPositions || undefined);
|
|
602
742
|
tui.requestRender();
|
|
603
743
|
return {
|
|
604
744
|
content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
|
|
@@ -636,13 +776,13 @@ async function agentCommand(thesisId, opts) {
|
|
|
636
776
|
addSystemText(color(` ${arrow} Confidence ${prev}% \u2192 ${now}% (${delta > 0 ? '+' : ''}${Math.round(delta * 100)})`));
|
|
637
777
|
addSpacer();
|
|
638
778
|
// Update header
|
|
639
|
-
|
|
779
|
+
footerBar.updateConfidence(result.evaluation.newConfidence, delta);
|
|
640
780
|
tui.requestRender();
|
|
641
781
|
}
|
|
642
782
|
// Refresh context after eval
|
|
643
783
|
try {
|
|
644
784
|
latestContext = await sfClient.getContext(params.thesisId);
|
|
645
|
-
|
|
785
|
+
footerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
646
786
|
tui.requestRender();
|
|
647
787
|
}
|
|
648
788
|
catch { }
|
|
@@ -655,34 +795,55 @@ async function agentCommand(thesisId, opts) {
|
|
|
655
795
|
{
|
|
656
796
|
name: 'scan_markets',
|
|
657
797
|
label: 'Scan Markets',
|
|
658
|
-
description: 'Search Kalshi prediction markets. Provide exactly one of: query (keyword search), series (series ticker), or market (specific ticker).
|
|
798
|
+
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.',
|
|
659
799
|
parameters: scanParams,
|
|
660
800
|
execute: async (_toolCallId, params) => {
|
|
661
|
-
let result;
|
|
662
801
|
if (params.market) {
|
|
663
|
-
result = await (0, client_js_1.kalshiFetchMarket)(params.market);
|
|
802
|
+
const result = await (0, client_js_1.kalshiFetchMarket)(params.market);
|
|
803
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
664
804
|
}
|
|
665
|
-
|
|
666
|
-
result = await (0, client_js_1.kalshiFetchMarketsBySeries)(params.series);
|
|
805
|
+
if (params.series) {
|
|
806
|
+
const result = await (0, client_js_1.kalshiFetchMarketsBySeries)(params.series);
|
|
807
|
+
return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
|
|
667
808
|
}
|
|
668
|
-
|
|
809
|
+
if (params.query) {
|
|
810
|
+
// Kalshi: keyword grep on series
|
|
669
811
|
const series = await (0, client_js_1.kalshiFetchAllSeries)();
|
|
670
812
|
const keywords = params.query.toLowerCase().split(/\s+/);
|
|
671
|
-
const
|
|
672
|
-
.filter((s) => keywords.
|
|
813
|
+
const kalshiMatched = series
|
|
814
|
+
.filter((s) => keywords.some((kw) => (s.title || '').toLowerCase().includes(kw) ||
|
|
673
815
|
(s.ticker || '').toLowerCase().includes(kw)))
|
|
674
|
-
.filter((s) => parseFloat(s.volume_fp || '0') >
|
|
675
|
-
.sort((a, b) => parseFloat(b.volume_fp || '0') - parseFloat(a.volume_fp || '0'))
|
|
676
|
-
.slice(0,
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
816
|
+
.filter((s) => parseFloat(s.volume_24h_fp || s.volume_fp || '0') > 0)
|
|
817
|
+
.sort((a, b) => parseFloat(b.volume_24h_fp || b.volume_fp || '0') - parseFloat(a.volume_24h_fp || a.volume_fp || '0'))
|
|
818
|
+
.slice(0, 10)
|
|
819
|
+
.map((s) => ({ venue: 'kalshi', ticker: s.ticker, title: s.title, volume: s.volume_fp }));
|
|
820
|
+
// Polymarket: Gamma API search
|
|
821
|
+
let polyMatched = [];
|
|
822
|
+
try {
|
|
823
|
+
const events = await (0, polymarket_js_1.polymarketSearch)(params.query, 10);
|
|
824
|
+
for (const event of events) {
|
|
825
|
+
for (const m of (event.markets || []).slice(0, 3)) {
|
|
826
|
+
if (!m.active || m.closed)
|
|
827
|
+
continue;
|
|
828
|
+
const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
|
|
829
|
+
polyMatched.push({
|
|
830
|
+
venue: 'polymarket',
|
|
831
|
+
id: m.conditionId || m.id,
|
|
832
|
+
title: m.groupItemTitle ? `${event.title}: ${m.groupItemTitle}` : m.question || event.title,
|
|
833
|
+
price: prices[0] ? Math.round(prices[0] * 100) : null,
|
|
834
|
+
volume24h: m.volume24hr,
|
|
835
|
+
liquidity: m.liquidityNum,
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
}
|
|
839
|
+
}
|
|
840
|
+
catch { /* Polymarket search optional */ }
|
|
841
|
+
return {
|
|
842
|
+
content: [{ type: 'text', text: JSON.stringify({ kalshi: kalshiMatched, polymarket: polyMatched }, null, 2) }],
|
|
843
|
+
details: {},
|
|
844
|
+
};
|
|
681
845
|
}
|
|
682
|
-
return {
|
|
683
|
-
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
684
|
-
details: {},
|
|
685
|
-
};
|
|
846
|
+
return { content: [{ type: 'text', text: '{"error":"Provide query, series, or market parameter"}' }], details: {} };
|
|
686
847
|
},
|
|
687
848
|
},
|
|
688
849
|
{
|
|
@@ -701,36 +862,57 @@ async function agentCommand(thesisId, opts) {
|
|
|
701
862
|
{
|
|
702
863
|
name: 'get_positions',
|
|
703
864
|
label: 'Get Positions',
|
|
704
|
-
description: 'Get Kalshi
|
|
865
|
+
description: 'Get positions across Kalshi + Polymarket with live prices and PnL',
|
|
705
866
|
parameters: emptyParams,
|
|
706
867
|
execute: async () => {
|
|
868
|
+
const result = { kalshi: [], polymarket: [] };
|
|
869
|
+
// Kalshi positions
|
|
707
870
|
const positions = await (0, kalshi_js_1.getPositions)();
|
|
708
|
-
if (
|
|
871
|
+
if (positions) {
|
|
872
|
+
for (const pos of positions) {
|
|
873
|
+
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
874
|
+
if (livePrice !== null) {
|
|
875
|
+
pos.current_value = livePrice;
|
|
876
|
+
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
877
|
+
}
|
|
878
|
+
}
|
|
879
|
+
cachedPositions = positions;
|
|
880
|
+
result.kalshi = positions.map((p) => ({
|
|
881
|
+
venue: 'kalshi',
|
|
882
|
+
ticker: p.ticker,
|
|
883
|
+
side: p.side,
|
|
884
|
+
quantity: p.quantity,
|
|
885
|
+
avg_price: `${p.average_price_paid}¢`,
|
|
886
|
+
current_price: `${p.current_value}¢`,
|
|
887
|
+
unrealized_pnl: `$${(p.unrealized_pnl / 100).toFixed(2)}`,
|
|
888
|
+
total_cost: `$${(p.total_cost / 100).toFixed(2)}`,
|
|
889
|
+
}));
|
|
890
|
+
}
|
|
891
|
+
// Polymarket positions
|
|
892
|
+
const config = (0, config_js_1.loadConfig)();
|
|
893
|
+
if (config.polymarketWalletAddress) {
|
|
894
|
+
try {
|
|
895
|
+
const polyPos = await (0, polymarket_js_1.polymarketGetPositions)(config.polymarketWalletAddress);
|
|
896
|
+
result.polymarket = polyPos.map((p) => ({
|
|
897
|
+
venue: 'polymarket',
|
|
898
|
+
market: p.title || p.slug || p.asset,
|
|
899
|
+
side: p.outcome || 'Yes',
|
|
900
|
+
size: p.size,
|
|
901
|
+
avg_price: `${Math.round((p.avgPrice || 0) * 100)}¢`,
|
|
902
|
+
current_price: `${Math.round((p.curPrice || p.currentPrice || 0) * 100)}¢`,
|
|
903
|
+
pnl: `$${(p.cashPnl || 0).toFixed(2)}`,
|
|
904
|
+
}));
|
|
905
|
+
}
|
|
906
|
+
catch { /* skip */ }
|
|
907
|
+
}
|
|
908
|
+
if (result.kalshi.length === 0 && result.polymarket.length === 0) {
|
|
709
909
|
return {
|
|
710
|
-
content: [{ type: 'text', text: '
|
|
910
|
+
content: [{ type: 'text', text: 'No positions found. Configure Kalshi (KALSHI_API_KEY_ID) or Polymarket (sf setup --polymarket) to see positions.' }],
|
|
711
911
|
details: {},
|
|
712
912
|
};
|
|
713
913
|
}
|
|
714
|
-
for (const pos of positions) {
|
|
715
|
-
const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
|
|
716
|
-
if (livePrice !== null) {
|
|
717
|
-
pos.current_value = livePrice;
|
|
718
|
-
pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
|
|
719
|
-
}
|
|
720
|
-
}
|
|
721
|
-
cachedPositions = positions;
|
|
722
|
-
const formatted = positions.map((p) => ({
|
|
723
|
-
ticker: p.ticker,
|
|
724
|
-
side: p.side,
|
|
725
|
-
quantity: p.quantity,
|
|
726
|
-
avg_price: `${p.average_price_paid}¢`,
|
|
727
|
-
current_price: `${p.current_value}¢`,
|
|
728
|
-
unrealized_pnl: `$${(p.unrealized_pnl / 100).toFixed(2)}`,
|
|
729
|
-
total_cost: `$${(p.total_cost / 100).toFixed(2)}`,
|
|
730
|
-
realized_pnl: `$${(p.realized_pnl / 100).toFixed(2)}`,
|
|
731
|
-
}));
|
|
732
914
|
return {
|
|
733
|
-
content: [{ type: 'text', text: JSON.stringify(
|
|
915
|
+
content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
|
|
734
916
|
details: {},
|
|
735
917
|
};
|
|
736
918
|
},
|
|
@@ -991,6 +1173,139 @@ async function agentCommand(thesisId, opts) {
|
|
|
991
1173
|
return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
|
|
992
1174
|
},
|
|
993
1175
|
},
|
|
1176
|
+
{
|
|
1177
|
+
name: 'get_liquidity',
|
|
1178
|
+
label: 'Liquidity Scanner',
|
|
1179
|
+
description: 'Scan orderbook liquidity for a topic across Kalshi + Polymarket. Returns spread, depth, liquidity scores. Topics: ' + Object.keys(topics_js_1.TOPIC_SERIES).join(', '),
|
|
1180
|
+
parameters: Type.Object({
|
|
1181
|
+
topic: Type.String({ description: 'Topic to scan (e.g. oil, crypto, fed, geopolitics)' }),
|
|
1182
|
+
}),
|
|
1183
|
+
execute: async (_toolCallId, params) => {
|
|
1184
|
+
const topicKey = params.topic.toLowerCase();
|
|
1185
|
+
const seriesList = topics_js_1.TOPIC_SERIES[topicKey];
|
|
1186
|
+
if (!seriesList) {
|
|
1187
|
+
return { content: [{ type: 'text', text: `Unknown topic "${params.topic}". Available: ${Object.keys(topics_js_1.TOPIC_SERIES).join(', ')}` }], details: {} };
|
|
1188
|
+
}
|
|
1189
|
+
const results = [];
|
|
1190
|
+
for (const series of seriesList) {
|
|
1191
|
+
try {
|
|
1192
|
+
const url = `https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=${series}&status=open&limit=200`;
|
|
1193
|
+
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
1194
|
+
if (!res.ok)
|
|
1195
|
+
continue;
|
|
1196
|
+
const markets = (await res.json()).markets || [];
|
|
1197
|
+
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 }))));
|
|
1198
|
+
for (const r of obResults) {
|
|
1199
|
+
if (r.status !== 'fulfilled' || !r.value.ob)
|
|
1200
|
+
continue;
|
|
1201
|
+
const { ticker, title, ob } = r.value;
|
|
1202
|
+
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);
|
|
1203
|
+
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);
|
|
1204
|
+
const bestBid = yes[0]?.price || 0;
|
|
1205
|
+
const bestAsk = no.length > 0 ? (100 - no[0].price) : 100;
|
|
1206
|
+
const spread = bestAsk - bestBid;
|
|
1207
|
+
const depth = yes.slice(0, 3).reduce((s, l) => s + l.qty, 0) + no.slice(0, 3).reduce((s, l) => s + l.qty, 0);
|
|
1208
|
+
const liq = spread <= 2 && depth >= 500 ? 'high' : spread <= 5 && depth >= 100 ? 'medium' : 'low';
|
|
1209
|
+
results.push({ venue: 'kalshi', ticker, title: (title || '').slice(0, 50), bestBid, bestAsk, spread, depth: Math.round(depth), liquidityScore: liq });
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1212
|
+
catch { /* skip */ }
|
|
1213
|
+
}
|
|
1214
|
+
try {
|
|
1215
|
+
const events = await (0, polymarket_js_1.polymarketSearch)(topicKey, 5);
|
|
1216
|
+
for (const event of events) {
|
|
1217
|
+
for (const m of (event.markets || []).slice(0, 5)) {
|
|
1218
|
+
if (!m.active || m.closed || !m.clobTokenIds)
|
|
1219
|
+
continue;
|
|
1220
|
+
const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
|
|
1221
|
+
if (!ids)
|
|
1222
|
+
continue;
|
|
1223
|
+
const d = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
|
|
1224
|
+
if (!d)
|
|
1225
|
+
continue;
|
|
1226
|
+
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 });
|
|
1227
|
+
}
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
catch { /* skip */ }
|
|
1231
|
+
results.sort((a, b) => a.spread - b.spread);
|
|
1232
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
|
|
1233
|
+
},
|
|
1234
|
+
},
|
|
1235
|
+
{
|
|
1236
|
+
name: 'inspect_book',
|
|
1237
|
+
label: 'Orderbook',
|
|
1238
|
+
description: 'Get orderbook depth, spread, and liquidity for a specific market. Works with Kalshi tickers or Polymarket search queries. Returns bid/ask levels, depth, spread, liquidity score.',
|
|
1239
|
+
parameters: Type.Object({
|
|
1240
|
+
ticker: Type.Optional(Type.String({ description: 'Kalshi market ticker (e.g. KXWTIMAX-26DEC31-T135)' })),
|
|
1241
|
+
polyQuery: Type.Optional(Type.String({ description: 'Search Polymarket by keyword (e.g. "oil price above 100")' })),
|
|
1242
|
+
}),
|
|
1243
|
+
execute: async (_toolCallId, params) => {
|
|
1244
|
+
const results = [];
|
|
1245
|
+
if (params.ticker) {
|
|
1246
|
+
try {
|
|
1247
|
+
const market = await (0, client_js_1.kalshiFetchMarket)(params.ticker);
|
|
1248
|
+
const ob = await (0, kalshi_js_1.getPublicOrderbook)(params.ticker);
|
|
1249
|
+
const yesBids = (ob?.yes_dollars || [])
|
|
1250
|
+
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
1251
|
+
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
1252
|
+
const noAsks = (ob?.no_dollars || [])
|
|
1253
|
+
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
1254
|
+
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
1255
|
+
const bestBid = yesBids[0]?.price || 0;
|
|
1256
|
+
const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : 100;
|
|
1257
|
+
const spread = bestAsk - bestBid;
|
|
1258
|
+
const top3Depth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) + noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
|
|
1259
|
+
const liq = spread <= 2 && top3Depth >= 500 ? 'high' : spread <= 5 && top3Depth >= 100 ? 'medium' : 'low';
|
|
1260
|
+
results.push({
|
|
1261
|
+
venue: 'kalshi', ticker: params.ticker, title: market.title,
|
|
1262
|
+
bestBid, bestAsk, spread, liquidityScore: liq,
|
|
1263
|
+
bidLevels: yesBids.slice(0, 5), askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
|
|
1264
|
+
totalBidDepth: yesBids.reduce((s, l) => s + l.size, 0),
|
|
1265
|
+
totalAskDepth: noAsks.reduce((s, l) => s + l.size, 0),
|
|
1266
|
+
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
1267
|
+
volume24h: parseFloat(market.volume_24h_fp || '0'),
|
|
1268
|
+
openInterest: parseFloat(market.open_interest_fp || '0'),
|
|
1269
|
+
expiry: market.close_time || null,
|
|
1270
|
+
});
|
|
1271
|
+
}
|
|
1272
|
+
catch (err) {
|
|
1273
|
+
return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
|
|
1274
|
+
}
|
|
1275
|
+
}
|
|
1276
|
+
if (params.polyQuery) {
|
|
1277
|
+
try {
|
|
1278
|
+
const events = await (0, polymarket_js_1.polymarketSearch)(params.polyQuery, 5);
|
|
1279
|
+
for (const event of events) {
|
|
1280
|
+
for (const m of (event.markets || []).slice(0, 3)) {
|
|
1281
|
+
if (!m.active || m.closed || !m.clobTokenIds)
|
|
1282
|
+
continue;
|
|
1283
|
+
const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
|
|
1284
|
+
if (!ids)
|
|
1285
|
+
continue;
|
|
1286
|
+
const depth = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
|
|
1287
|
+
if (!depth)
|
|
1288
|
+
continue;
|
|
1289
|
+
const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
|
|
1290
|
+
results.push({
|
|
1291
|
+
venue: 'polymarket', title: m.question || event.title,
|
|
1292
|
+
bestBid: depth.bestBid, bestAsk: depth.bestAsk, spread: depth.spread,
|
|
1293
|
+
liquidityScore: depth.liquidityScore,
|
|
1294
|
+
totalBidDepth: depth.totalBidDepth, totalAskDepth: depth.totalAskDepth,
|
|
1295
|
+
lastPrice: prices[0] ? Math.round(prices[0] * 100) : 0,
|
|
1296
|
+
volume24h: m.volume24hr || 0,
|
|
1297
|
+
});
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
}
|
|
1301
|
+
catch { /* skip */ }
|
|
1302
|
+
}
|
|
1303
|
+
if (results.length === 0) {
|
|
1304
|
+
return { content: [{ type: 'text', text: 'No markets found. Provide ticker (Kalshi) or polyQuery (Polymarket search).' }], details: {} };
|
|
1305
|
+
}
|
|
1306
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
|
|
1307
|
+
},
|
|
1308
|
+
},
|
|
994
1309
|
{
|
|
995
1310
|
name: 'get_schedule',
|
|
996
1311
|
label: 'Schedule',
|
|
@@ -1037,15 +1352,14 @@ async function agentCommand(thesisId, opts) {
|
|
|
1037
1352
|
execute: async () => {
|
|
1038
1353
|
const { theses } = await sfClient.listTheses();
|
|
1039
1354
|
const activeTheses = (theses || []).filter((t) => t.status === 'active' || t.status === 'monitoring');
|
|
1355
|
+
const results = await Promise.allSettled(activeTheses.map(async (t) => {
|
|
1356
|
+
const ctx = await sfClient.getContext(t.id);
|
|
1357
|
+
return (ctx.edges || []).map((e) => ({ ...e, thesisId: t.id }));
|
|
1358
|
+
}));
|
|
1040
1359
|
const allEdges = [];
|
|
1041
|
-
for (const
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
for (const edge of (ctx.edges || [])) {
|
|
1045
|
-
allEdges.push({ ...edge, thesisId: t.id });
|
|
1046
|
-
}
|
|
1047
|
-
}
|
|
1048
|
-
catch { /* skip inaccessible theses */ }
|
|
1360
|
+
for (const r of results) {
|
|
1361
|
+
if (r.status === 'fulfilled')
|
|
1362
|
+
allEdges.push(...r.value);
|
|
1049
1363
|
}
|
|
1050
1364
|
allEdges.sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0));
|
|
1051
1365
|
const top10 = allEdges.slice(0, 10).map((e) => ({
|
|
@@ -1327,7 +1641,32 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1327
1641
|
const saved = loadSession(resolvedThesisId);
|
|
1328
1642
|
if (saved?.messages?.length > 0) {
|
|
1329
1643
|
try {
|
|
1330
|
-
|
|
1644
|
+
// Clean corrupted messages: empty content, missing role, broken alternation
|
|
1645
|
+
const filtered = saved.messages.filter((m) => {
|
|
1646
|
+
if (!m.role)
|
|
1647
|
+
return false;
|
|
1648
|
+
if (Array.isArray(m.content) && m.content.length === 0)
|
|
1649
|
+
return false;
|
|
1650
|
+
if (m.role === 'assistant' && !m.content && !m.tool_calls?.length)
|
|
1651
|
+
return false;
|
|
1652
|
+
return true;
|
|
1653
|
+
});
|
|
1654
|
+
// Fix alternation: ensure user → assistant → user → assistant
|
|
1655
|
+
// Drop consecutive messages of the same role (keep first)
|
|
1656
|
+
const cleaned = [];
|
|
1657
|
+
for (const m of filtered) {
|
|
1658
|
+
const lastRole = cleaned.length > 0 ? cleaned[cleaned.length - 1].role : null;
|
|
1659
|
+
if (m.role === lastRole && m.role !== 'tool') {
|
|
1660
|
+
// Skip consecutive same-role (except tool messages which can follow each other)
|
|
1661
|
+
continue;
|
|
1662
|
+
}
|
|
1663
|
+
cleaned.push(m);
|
|
1664
|
+
}
|
|
1665
|
+
// Ensure conversation doesn't end with user message (API needs assistant reply)
|
|
1666
|
+
if (cleaned.length > 0 && cleaned[cleaned.length - 1].role === 'user') {
|
|
1667
|
+
cleaned.pop();
|
|
1668
|
+
}
|
|
1669
|
+
agent.replaceMessages(cleaned);
|
|
1331
1670
|
// Always update system prompt with fresh context
|
|
1332
1671
|
agent.setSystemPrompt(systemPrompt);
|
|
1333
1672
|
sessionRestored = true;
|
|
@@ -1601,7 +1940,7 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1601
1940
|
addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
|
|
1602
1941
|
}
|
|
1603
1942
|
// Update header
|
|
1604
|
-
|
|
1943
|
+
footerBar.setFromContext(newContext, initialPositions || undefined);
|
|
1605
1944
|
chatContainer.clear();
|
|
1606
1945
|
addSystemText(buildWelcomeDashboard(newContext, initialPositions));
|
|
1607
1946
|
}
|
|
@@ -1812,7 +2151,20 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
1812
2151
|
return true;
|
|
1813
2152
|
}
|
|
1814
2153
|
case '/clear': {
|
|
1815
|
-
chatContainer.clear()
|
|
2154
|
+
// Don't use chatContainer.clear() — it breaks pi-tui layout.
|
|
2155
|
+
// Instead, remove children one by one and add a fresh spacer
|
|
2156
|
+
// so the container still has content for layout calculations.
|
|
2157
|
+
const children = [...chatContainer.children || []];
|
|
2158
|
+
for (const child of children) {
|
|
2159
|
+
try {
|
|
2160
|
+
chatContainer.removeChild(child);
|
|
2161
|
+
}
|
|
2162
|
+
catch { /* ignore */ }
|
|
2163
|
+
}
|
|
2164
|
+
addSpacer();
|
|
2165
|
+
isProcessing = false;
|
|
2166
|
+
pendingPrompt = null;
|
|
2167
|
+
tui.setFocus(editor);
|
|
1816
2168
|
tui.requestRender();
|
|
1817
2169
|
return true;
|
|
1818
2170
|
}
|
|
@@ -2065,7 +2417,7 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
2065
2417
|
}
|
|
2066
2418
|
addSpacer();
|
|
2067
2419
|
// Update header
|
|
2068
|
-
|
|
2420
|
+
footerBar.setFromContext({ ...latestContext, confidence: delta.confidence, lastEvaluation: { confidenceDelta: delta.confidenceDelta } }, initialPositions || undefined);
|
|
2069
2421
|
tui.requestRender();
|
|
2070
2422
|
// Auto-trigger agent
|
|
2071
2423
|
isProcessing = true;
|
|
@@ -2354,6 +2706,139 @@ async function runPlainTextAgent(params) {
|
|
|
2354
2706
|
return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
|
|
2355
2707
|
},
|
|
2356
2708
|
},
|
|
2709
|
+
{
|
|
2710
|
+
name: 'get_liquidity',
|
|
2711
|
+
label: 'Liquidity Scanner',
|
|
2712
|
+
description: 'Scan orderbook liquidity for a topic across Kalshi + Polymarket. Returns spread, depth, liquidity scores. Topics: ' + Object.keys(topics_js_1.TOPIC_SERIES).join(', '),
|
|
2713
|
+
parameters: Type.Object({
|
|
2714
|
+
topic: Type.String({ description: 'Topic to scan (e.g. oil, crypto, fed, geopolitics)' }),
|
|
2715
|
+
}),
|
|
2716
|
+
execute: async (_toolCallId, params) => {
|
|
2717
|
+
const topicKey = params.topic.toLowerCase();
|
|
2718
|
+
const seriesList = topics_js_1.TOPIC_SERIES[topicKey];
|
|
2719
|
+
if (!seriesList) {
|
|
2720
|
+
return { content: [{ type: 'text', text: `Unknown topic "${params.topic}". Available: ${Object.keys(topics_js_1.TOPIC_SERIES).join(', ')}` }], details: {} };
|
|
2721
|
+
}
|
|
2722
|
+
const results = [];
|
|
2723
|
+
for (const series of seriesList) {
|
|
2724
|
+
try {
|
|
2725
|
+
const url = `https://api.elections.kalshi.com/trade-api/v2/markets?series_ticker=${series}&status=open&limit=200`;
|
|
2726
|
+
const res = await fetch(url, { headers: { Accept: 'application/json' } });
|
|
2727
|
+
if (!res.ok)
|
|
2728
|
+
continue;
|
|
2729
|
+
const markets = (await res.json()).markets || [];
|
|
2730
|
+
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 }))));
|
|
2731
|
+
for (const r of obResults) {
|
|
2732
|
+
if (r.status !== 'fulfilled' || !r.value.ob)
|
|
2733
|
+
continue;
|
|
2734
|
+
const { ticker, title, ob } = r.value;
|
|
2735
|
+
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);
|
|
2736
|
+
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);
|
|
2737
|
+
const bestBid = yes[0]?.price || 0;
|
|
2738
|
+
const bestAsk = no.length > 0 ? (100 - no[0].price) : 100;
|
|
2739
|
+
const spread = bestAsk - bestBid;
|
|
2740
|
+
const depth = yes.slice(0, 3).reduce((s, l) => s + l.qty, 0) + no.slice(0, 3).reduce((s, l) => s + l.qty, 0);
|
|
2741
|
+
const liq = spread <= 2 && depth >= 500 ? 'high' : spread <= 5 && depth >= 100 ? 'medium' : 'low';
|
|
2742
|
+
results.push({ venue: 'kalshi', ticker, title: (title || '').slice(0, 50), bestBid, bestAsk, spread, depth: Math.round(depth), liquidityScore: liq });
|
|
2743
|
+
}
|
|
2744
|
+
}
|
|
2745
|
+
catch { /* skip */ }
|
|
2746
|
+
}
|
|
2747
|
+
try {
|
|
2748
|
+
const events = await (0, polymarket_js_1.polymarketSearch)(topicKey, 5);
|
|
2749
|
+
for (const event of events) {
|
|
2750
|
+
for (const m of (event.markets || []).slice(0, 5)) {
|
|
2751
|
+
if (!m.active || m.closed || !m.clobTokenIds)
|
|
2752
|
+
continue;
|
|
2753
|
+
const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
|
|
2754
|
+
if (!ids)
|
|
2755
|
+
continue;
|
|
2756
|
+
const d = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
|
|
2757
|
+
if (!d)
|
|
2758
|
+
continue;
|
|
2759
|
+
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 });
|
|
2760
|
+
}
|
|
2761
|
+
}
|
|
2762
|
+
}
|
|
2763
|
+
catch { /* skip */ }
|
|
2764
|
+
results.sort((a, b) => a.spread - b.spread);
|
|
2765
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
|
|
2766
|
+
},
|
|
2767
|
+
},
|
|
2768
|
+
{
|
|
2769
|
+
name: 'inspect_book',
|
|
2770
|
+
label: 'Orderbook',
|
|
2771
|
+
description: 'Get orderbook depth, spread, and liquidity for a specific market. Works with Kalshi tickers or Polymarket search queries. Returns bid/ask levels, depth, spread, liquidity score.',
|
|
2772
|
+
parameters: Type.Object({
|
|
2773
|
+
ticker: Type.Optional(Type.String({ description: 'Kalshi market ticker (e.g. KXWTIMAX-26DEC31-T135)' })),
|
|
2774
|
+
polyQuery: Type.Optional(Type.String({ description: 'Search Polymarket by keyword (e.g. "oil price above 100")' })),
|
|
2775
|
+
}),
|
|
2776
|
+
execute: async (_toolCallId, params) => {
|
|
2777
|
+
const results = [];
|
|
2778
|
+
if (params.ticker) {
|
|
2779
|
+
try {
|
|
2780
|
+
const market = await (0, client_js_1.kalshiFetchMarket)(params.ticker);
|
|
2781
|
+
const ob = await (0, kalshi_js_1.getPublicOrderbook)(params.ticker);
|
|
2782
|
+
const yesBids = (ob?.yes_dollars || [])
|
|
2783
|
+
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
2784
|
+
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
2785
|
+
const noAsks = (ob?.no_dollars || [])
|
|
2786
|
+
.map(([p, q]) => ({ price: Math.round(parseFloat(p) * 100), size: Math.round(parseFloat(q)) }))
|
|
2787
|
+
.filter((l) => l.price > 0).sort((a, b) => b.price - a.price);
|
|
2788
|
+
const bestBid = yesBids[0]?.price || 0;
|
|
2789
|
+
const bestAsk = noAsks.length > 0 ? (100 - noAsks[0].price) : 100;
|
|
2790
|
+
const spread = bestAsk - bestBid;
|
|
2791
|
+
const top3Depth = yesBids.slice(0, 3).reduce((s, l) => s + l.size, 0) + noAsks.slice(0, 3).reduce((s, l) => s + l.size, 0);
|
|
2792
|
+
const liq = spread <= 2 && top3Depth >= 500 ? 'high' : spread <= 5 && top3Depth >= 100 ? 'medium' : 'low';
|
|
2793
|
+
results.push({
|
|
2794
|
+
venue: 'kalshi', ticker: params.ticker, title: market.title,
|
|
2795
|
+
bestBid, bestAsk, spread, liquidityScore: liq,
|
|
2796
|
+
bidLevels: yesBids.slice(0, 5), askLevels: noAsks.slice(0, 5).map((l) => ({ price: 100 - l.price, size: l.size })),
|
|
2797
|
+
totalBidDepth: yesBids.reduce((s, l) => s + l.size, 0),
|
|
2798
|
+
totalAskDepth: noAsks.reduce((s, l) => s + l.size, 0),
|
|
2799
|
+
lastPrice: Math.round(parseFloat(market.last_price_dollars || '0') * 100),
|
|
2800
|
+
volume24h: parseFloat(market.volume_24h_fp || '0'),
|
|
2801
|
+
openInterest: parseFloat(market.open_interest_fp || '0'),
|
|
2802
|
+
expiry: market.close_time || null,
|
|
2803
|
+
});
|
|
2804
|
+
}
|
|
2805
|
+
catch (err) {
|
|
2806
|
+
return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
|
|
2807
|
+
}
|
|
2808
|
+
}
|
|
2809
|
+
if (params.polyQuery) {
|
|
2810
|
+
try {
|
|
2811
|
+
const events = await (0, polymarket_js_1.polymarketSearch)(params.polyQuery, 5);
|
|
2812
|
+
for (const event of events) {
|
|
2813
|
+
for (const m of (event.markets || []).slice(0, 3)) {
|
|
2814
|
+
if (!m.active || m.closed || !m.clobTokenIds)
|
|
2815
|
+
continue;
|
|
2816
|
+
const ids = (0, polymarket_js_1.parseClobTokenIds)(m.clobTokenIds);
|
|
2817
|
+
if (!ids)
|
|
2818
|
+
continue;
|
|
2819
|
+
const depth = await (0, polymarket_js_1.polymarketGetOrderbookWithDepth)(ids[0]);
|
|
2820
|
+
if (!depth)
|
|
2821
|
+
continue;
|
|
2822
|
+
const prices = (0, polymarket_js_1.parseOutcomePrices)(m.outcomePrices);
|
|
2823
|
+
results.push({
|
|
2824
|
+
venue: 'polymarket', title: m.question || event.title,
|
|
2825
|
+
bestBid: depth.bestBid, bestAsk: depth.bestAsk, spread: depth.spread,
|
|
2826
|
+
liquidityScore: depth.liquidityScore,
|
|
2827
|
+
totalBidDepth: depth.totalBidDepth, totalAskDepth: depth.totalAskDepth,
|
|
2828
|
+
lastPrice: prices[0] ? Math.round(prices[0] * 100) : 0,
|
|
2829
|
+
volume24h: m.volume24hr || 0,
|
|
2830
|
+
});
|
|
2831
|
+
}
|
|
2832
|
+
}
|
|
2833
|
+
}
|
|
2834
|
+
catch { /* skip */ }
|
|
2835
|
+
}
|
|
2836
|
+
if (results.length === 0) {
|
|
2837
|
+
return { content: [{ type: 'text', text: 'No markets found. Provide ticker (Kalshi) or polyQuery (Polymarket search).' }], details: {} };
|
|
2838
|
+
}
|
|
2839
|
+
return { content: [{ type: 'text', text: JSON.stringify(results, null, 2) }], details: {} };
|
|
2840
|
+
},
|
|
2841
|
+
},
|
|
2357
2842
|
{
|
|
2358
2843
|
name: 'get_schedule',
|
|
2359
2844
|
label: 'Schedule',
|
|
@@ -2400,15 +2885,14 @@ async function runPlainTextAgent(params) {
|
|
|
2400
2885
|
execute: async () => {
|
|
2401
2886
|
const { theses } = await sfClient.listTheses();
|
|
2402
2887
|
const activeTheses = (theses || []).filter((t) => t.status === 'active' || t.status === 'monitoring');
|
|
2888
|
+
const results = await Promise.allSettled(activeTheses.map(async (t) => {
|
|
2889
|
+
const ctx = await sfClient.getContext(t.id);
|
|
2890
|
+
return (ctx.edges || []).map((e) => ({ ...e, thesisId: t.id }));
|
|
2891
|
+
}));
|
|
2403
2892
|
const allEdges = [];
|
|
2404
|
-
for (const
|
|
2405
|
-
|
|
2406
|
-
|
|
2407
|
-
for (const edge of (ctx.edges || [])) {
|
|
2408
|
-
allEdges.push({ ...edge, thesisId: t.id });
|
|
2409
|
-
}
|
|
2410
|
-
}
|
|
2411
|
-
catch { /* skip inaccessible theses */ }
|
|
2893
|
+
for (const r of results) {
|
|
2894
|
+
if (r.status === 'fulfilled')
|
|
2895
|
+
allEdges.push(...r.value);
|
|
2412
2896
|
}
|
|
2413
2897
|
allEdges.sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0));
|
|
2414
2898
|
const top10 = allEdges.slice(0, 10).map((e) => ({
|
|
@@ -2631,6 +3115,10 @@ async function runPlainTextAgent(params) {
|
|
|
2631
3115
|
const maxCost = ((priceCents || 99) * p.count / 100).toFixed(2);
|
|
2632
3116
|
const preview = ` Order: ${p.action.toUpperCase()} ${p.count}x ${p.ticker} ${p.side.toUpperCase()} @ ${priceCents ? priceCents + '\u00A2' : 'market'} (max $${maxCost})`;
|
|
2633
3117
|
process.stderr.write(preview + '\n');
|
|
3118
|
+
// Reject if stdin is piped (non-TTY) — too dangerous to auto-execute trades
|
|
3119
|
+
if (!process.stdin.isTTY) {
|
|
3120
|
+
return { content: [{ type: 'text', text: 'Order rejected: trading requires interactive terminal (stdin is piped). Use TUI mode for trading.' }], details: {} };
|
|
3121
|
+
}
|
|
2634
3122
|
// Confirm in plain mode via readline
|
|
2635
3123
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
2636
3124
|
const answer = await new Promise(resolve => rl.question(' Execute? (y/n) ', resolve));
|