@spfunctions/cli 1.4.4 → 1.4.5
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 +245 -67
- package/dist/commands/dashboard.d.ts +6 -3
- package/dist/commands/dashboard.js +28 -26
- package/dist/commands/performance.js +9 -2
- package/dist/commands/telegram.d.ts +15 -0
- package/dist/commands/telegram.js +125 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +138 -0
- package/dist/index.js +16 -2
- 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.test.d.ts +1 -0
- package/dist/topics.test.js +54 -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 +779 -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 +33 -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 +58 -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
|
@@ -67,7 +67,7 @@ const C = {
|
|
|
67
67
|
zinc800: rgb(39, 39, 42), // #27272a
|
|
68
68
|
red: rgb(239, 68, 68), // #ef4444
|
|
69
69
|
amber: rgb(245, 158, 11), // #f59e0b
|
|
70
|
-
white: rgb(255, 255, 255),
|
|
70
|
+
white: rgb(255, 255, 255), // #ffffff
|
|
71
71
|
bgZinc900: bgRgb(24, 24, 27), // #18181b
|
|
72
72
|
bgZinc800: bgRgb(39, 39, 42), // #27272a
|
|
73
73
|
};
|
|
@@ -196,18 +196,56 @@ function createHeaderBar(piTui) {
|
|
|
196
196
|
}
|
|
197
197
|
};
|
|
198
198
|
}
|
|
199
|
-
/**
|
|
199
|
+
/** Combined footer bar: thesis info (line 1) + model/exchange (line 2) */
|
|
200
200
|
function createFooterBar(piTui) {
|
|
201
201
|
const { truncateToWidth, visibleWidth } = piTui;
|
|
202
202
|
return class FooterBar {
|
|
203
|
+
// Thesis info (was HeaderBar)
|
|
204
|
+
thesisId = '';
|
|
205
|
+
confidence = 0;
|
|
206
|
+
confidenceDelta = 0;
|
|
207
|
+
pnlDollars = 0;
|
|
208
|
+
positionCount = 0;
|
|
209
|
+
edgeCount = 0;
|
|
210
|
+
topEdge = '';
|
|
211
|
+
// Model info
|
|
203
212
|
tokens = 0;
|
|
204
213
|
cost = 0;
|
|
205
214
|
toolCount = 0;
|
|
206
215
|
modelName = '';
|
|
207
216
|
tradingEnabled = false;
|
|
208
|
-
exchangeOpen = null;
|
|
217
|
+
exchangeOpen = null;
|
|
209
218
|
cachedWidth;
|
|
210
219
|
cachedLines;
|
|
220
|
+
setFromContext(ctx, positions) {
|
|
221
|
+
this.thesisId = (ctx.thesisId || '').slice(0, 8);
|
|
222
|
+
this.confidence = typeof ctx.confidence === 'number'
|
|
223
|
+
? Math.round(ctx.confidence * 100)
|
|
224
|
+
: (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
|
|
225
|
+
this.confidenceDelta = ctx.lastEvaluation?.confidenceDelta
|
|
226
|
+
? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
|
|
227
|
+
: 0;
|
|
228
|
+
this.edgeCount = (ctx.edges || []).length;
|
|
229
|
+
const edges = ctx.edges || [];
|
|
230
|
+
if (edges.length > 0) {
|
|
231
|
+
const top = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0))[0];
|
|
232
|
+
const name = (top.market || top.marketTitle || top.marketId || '').slice(0, 20);
|
|
233
|
+
const edge = top.edge || top.edgeSize || 0;
|
|
234
|
+
this.topEdge = `${name} ${edge > 0 ? '+' : ''}${Math.round(edge)}\u00A2`;
|
|
235
|
+
}
|
|
236
|
+
if (positions && positions.length > 0) {
|
|
237
|
+
this.positionCount = positions.length;
|
|
238
|
+
this.pnlDollars = positions.reduce((sum, p) => sum + (p.unrealized_pnl || 0), 0) / 100;
|
|
239
|
+
}
|
|
240
|
+
this.cachedWidth = undefined;
|
|
241
|
+
this.cachedLines = undefined;
|
|
242
|
+
}
|
|
243
|
+
updateConfidence(newConf, delta) {
|
|
244
|
+
this.confidence = Math.round(newConf * 100);
|
|
245
|
+
this.confidenceDelta = Math.round(delta * 100);
|
|
246
|
+
this.cachedWidth = undefined;
|
|
247
|
+
this.cachedLines = undefined;
|
|
248
|
+
}
|
|
211
249
|
invalidate() {
|
|
212
250
|
this.cachedWidth = undefined;
|
|
213
251
|
this.cachedLines = undefined;
|
|
@@ -220,31 +258,43 @@ function createFooterBar(piTui) {
|
|
|
220
258
|
if (this.cachedLines && this.cachedWidth === width)
|
|
221
259
|
return this.cachedLines;
|
|
222
260
|
this.cachedWidth = width;
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
261
|
+
// Line 1: thesis info
|
|
262
|
+
const id = C.emerald(this.thesisId);
|
|
263
|
+
const arrow = this.confidenceDelta > 0 ? '\u25B2' : this.confidenceDelta < 0 ? '\u25BC' : '\u2500';
|
|
264
|
+
const arrowColor = this.confidenceDelta > 0 ? C.emerald : this.confidenceDelta < 0 ? C.red : C.zinc600;
|
|
265
|
+
const deltaStr = this.confidenceDelta !== 0 ? ` (${this.confidenceDelta > 0 ? '+' : ''}${this.confidenceDelta})` : '';
|
|
266
|
+
const conf = arrowColor(`${arrow} ${this.confidence}%${deltaStr}`);
|
|
267
|
+
let pnl = '';
|
|
268
|
+
if (this.positionCount > 0) {
|
|
269
|
+
const pnlStr = this.pnlDollars >= 0
|
|
270
|
+
? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
|
|
271
|
+
: C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
|
|
272
|
+
pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
|
|
273
|
+
}
|
|
274
|
+
const edges = C.zinc600(`${this.edgeCount} edges`);
|
|
275
|
+
const top = this.topEdge ? C.zinc400(this.topEdge) : '';
|
|
276
|
+
const sep = C.zinc600(' \u2502 ');
|
|
277
|
+
const line1Parts = [id, conf, pnl, edges, top].filter(Boolean);
|
|
278
|
+
let line1 = C.bgZinc800(' ' + truncateToWidth(line1Parts.join(sep), width - 2, '') + ' ');
|
|
279
|
+
const l1vw = visibleWidth(line1);
|
|
280
|
+
if (l1vw < width)
|
|
281
|
+
line1 += C.bgZinc800(' '.repeat(width - l1vw));
|
|
282
|
+
// Line 2: model + exchange
|
|
226
283
|
const model = C.zinc600(this.modelName.split('/').pop() || this.modelName);
|
|
284
|
+
const tokStr = this.tokens >= 1000 ? `${(this.tokens / 1000).toFixed(1)}k` : `${this.tokens}`;
|
|
227
285
|
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');
|
|
286
|
+
const exchange = this.exchangeOpen === true ? C.emerald('OPEN') : this.exchangeOpen === false ? C.red('CLOSED') : C.zinc600('...');
|
|
287
|
+
const trading = this.tradingEnabled ? C.amber('\u26A1 trading') : C.zinc600('\u26A1 read-only');
|
|
236
288
|
const help = C.zinc600('/help');
|
|
237
|
-
const sep = C.zinc600(' \u2502 ');
|
|
238
289
|
const leftText = [model, tokens, exchange, trading].join(sep);
|
|
239
290
|
const lw = visibleWidth(leftText);
|
|
240
291
|
const rw = visibleWidth(help);
|
|
241
292
|
const gap = Math.max(1, width - lw - rw - 2);
|
|
242
|
-
let
|
|
243
|
-
const
|
|
244
|
-
if (
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
this.cachedLines = [line];
|
|
293
|
+
let line2 = C.bgZinc900(' ' + leftText + ' '.repeat(gap) + help + ' ');
|
|
294
|
+
const l2vw = visibleWidth(line2);
|
|
295
|
+
if (l2vw < width)
|
|
296
|
+
line2 += C.bgZinc900(' '.repeat(width - l2vw));
|
|
297
|
+
this.cachedLines = [line1, line2];
|
|
248
298
|
return this.cachedLines;
|
|
249
299
|
}
|
|
250
300
|
};
|
|
@@ -340,6 +390,76 @@ function renderPositions(positions) {
|
|
|
340
390
|
: ` Total P&L: ${C.red(bold(`-$${Math.abs(parseFloat(totalDollars)).toFixed(2)}`))}`);
|
|
341
391
|
return lines.join('\n');
|
|
342
392
|
}
|
|
393
|
+
// ─── Thesis selector (arrow keys + enter, like Claude Code) ─────────────────
|
|
394
|
+
async function selectThesis(theses) {
|
|
395
|
+
return new Promise((resolve) => {
|
|
396
|
+
let selected = 0;
|
|
397
|
+
const items = theses.map((t) => {
|
|
398
|
+
const conf = typeof t.confidence === 'number' ? Math.round(t.confidence * 100) : 0;
|
|
399
|
+
const title = (t.rawThesis || t.thesis || t.title || '').slice(0, 55);
|
|
400
|
+
return { id: t.id, conf, title };
|
|
401
|
+
});
|
|
402
|
+
const write = process.stdout.write.bind(process.stdout);
|
|
403
|
+
// Use alternate screen buffer for clean rendering (like Claude Code)
|
|
404
|
+
write('\x1b[?1049h'); // enter alternate screen
|
|
405
|
+
write('\x1b[?25l'); // hide cursor
|
|
406
|
+
function render() {
|
|
407
|
+
write('\x1b[H\x1b[2J'); // cursor home + clear screen
|
|
408
|
+
write('\n \x1b[2mSelect thesis\x1b[22m\n\n');
|
|
409
|
+
for (let i = 0; i < items.length; i++) {
|
|
410
|
+
const item = items[i];
|
|
411
|
+
const sel = i === selected;
|
|
412
|
+
const cursor = sel ? '\x1b[38;2;16;185;129m › \x1b[39m' : ' ';
|
|
413
|
+
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`;
|
|
414
|
+
const conf = `\x1b[38;2;55;55;60m${item.conf}%\x1b[39m`;
|
|
415
|
+
const title = sel ? `\x1b[38;2;160;160;165m${item.title}\x1b[39m` : `\x1b[38;2;80;80;88m${item.title}\x1b[39m`;
|
|
416
|
+
write(`${cursor}${id} ${conf} ${title}\n`);
|
|
417
|
+
}
|
|
418
|
+
write(`\n \x1b[38;2;55;55;60m↑↓ navigate · enter select\x1b[39m`);
|
|
419
|
+
}
|
|
420
|
+
render();
|
|
421
|
+
if (process.stdin.isTTY)
|
|
422
|
+
process.stdin.setRawMode(true);
|
|
423
|
+
process.stdin.resume();
|
|
424
|
+
process.stdin.setEncoding('utf8');
|
|
425
|
+
const onKey = (key) => {
|
|
426
|
+
const buf = Buffer.from(key);
|
|
427
|
+
if (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x41) {
|
|
428
|
+
selected = (selected - 1 + items.length) % items.length;
|
|
429
|
+
render();
|
|
430
|
+
}
|
|
431
|
+
else if (buf[0] === 0x1b && buf[1] === 0x5b && buf[2] === 0x42) {
|
|
432
|
+
selected = (selected + 1) % items.length;
|
|
433
|
+
render();
|
|
434
|
+
}
|
|
435
|
+
else if (key === 'k') {
|
|
436
|
+
selected = (selected - 1 + items.length) % items.length;
|
|
437
|
+
render();
|
|
438
|
+
}
|
|
439
|
+
else if (key === 'j') {
|
|
440
|
+
selected = (selected + 1) % items.length;
|
|
441
|
+
render();
|
|
442
|
+
}
|
|
443
|
+
else if (key === '\r' || key === '\n') {
|
|
444
|
+
cleanup();
|
|
445
|
+
resolve(items[selected].id);
|
|
446
|
+
}
|
|
447
|
+
else if (buf[0] === 0x03) {
|
|
448
|
+
cleanup();
|
|
449
|
+
process.exit(0);
|
|
450
|
+
}
|
|
451
|
+
};
|
|
452
|
+
function cleanup() {
|
|
453
|
+
process.stdin.removeListener('data', onKey);
|
|
454
|
+
if (process.stdin.isTTY)
|
|
455
|
+
process.stdin.setRawMode(false);
|
|
456
|
+
process.stdin.pause();
|
|
457
|
+
write('\x1b[?25h'); // show cursor
|
|
458
|
+
write('\x1b[?1049l'); // exit alternate screen — restores original content
|
|
459
|
+
}
|
|
460
|
+
process.stdin.on('data', onKey);
|
|
461
|
+
});
|
|
462
|
+
}
|
|
343
463
|
// ─── Main command ────────────────────────────────────────────────────────────
|
|
344
464
|
async function agentCommand(thesisId, opts) {
|
|
345
465
|
// ── Validate API keys ──────────────────────────────────────────────────────
|
|
@@ -373,17 +493,42 @@ async function agentCommand(thesisId, opts) {
|
|
|
373
493
|
}
|
|
374
494
|
}
|
|
375
495
|
const sfClient = new client_js_1.SFClient();
|
|
376
|
-
// ── Resolve thesis ID
|
|
496
|
+
// ── Resolve thesis ID (interactive selection if needed) ─────────────────────
|
|
377
497
|
let resolvedThesisId = thesisId;
|
|
378
498
|
if (!resolvedThesisId) {
|
|
379
499
|
const data = await sfClient.listTheses();
|
|
380
|
-
const theses = data.theses || data;
|
|
381
|
-
const active = theses.
|
|
382
|
-
if (
|
|
383
|
-
|
|
384
|
-
|
|
500
|
+
const theses = (data.theses || data);
|
|
501
|
+
const active = theses.filter((t) => t.status === 'active');
|
|
502
|
+
if (active.length === 0) {
|
|
503
|
+
if (!process.stdin.isTTY) {
|
|
504
|
+
console.error('No active thesis. Create one first: sf create "..."');
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
// No theses — offer to create one
|
|
508
|
+
console.log('\n No active theses found.\n');
|
|
509
|
+
const readline = await import('readline');
|
|
510
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
|
|
511
|
+
const answer = await new Promise(resolve => rl.question(' Enter a thesis to create (or press Enter to exit):\n > ', resolve));
|
|
512
|
+
rl.close();
|
|
513
|
+
if (!answer.trim()) {
|
|
514
|
+
process.exit(0);
|
|
515
|
+
}
|
|
516
|
+
console.log('\n Creating thesis...\n');
|
|
517
|
+
const result = await sfClient.createThesis(answer.trim(), true);
|
|
518
|
+
resolvedThesisId = result.id;
|
|
519
|
+
console.log(` ✓ Created: ${result.id?.slice(0, 8)}\n`);
|
|
520
|
+
}
|
|
521
|
+
else if (active.length === 1) {
|
|
522
|
+
resolvedThesisId = active[0].id;
|
|
523
|
+
}
|
|
524
|
+
else if (process.stdin.isTTY && !opts?.noTui) {
|
|
525
|
+
// Multiple theses — interactive arrow key selector (TUI only)
|
|
526
|
+
resolvedThesisId = await selectThesis(active);
|
|
527
|
+
}
|
|
528
|
+
else {
|
|
529
|
+
// Non-interactive (--plain, telegram, piped) — use first active
|
|
530
|
+
resolvedThesisId = active[0].id;
|
|
385
531
|
}
|
|
386
|
-
resolvedThesisId = active.id;
|
|
387
532
|
}
|
|
388
533
|
// ── Fetch initial context ──────────────────────────────────────────────────
|
|
389
534
|
let latestContext = await sfClient.getContext(resolvedThesisId);
|
|
@@ -400,7 +545,6 @@ async function agentCommand(thesisId, opts) {
|
|
|
400
545
|
const { Agent } = piAgent;
|
|
401
546
|
// ── Component class factories (need piTui ref) ─────────────────────────────
|
|
402
547
|
const MutableLine = createMutableLine(piTui);
|
|
403
|
-
const HeaderBar = createHeaderBar(piTui);
|
|
404
548
|
const FooterBar = createFooterBar(piTui);
|
|
405
549
|
// ── Model setup ────────────────────────────────────────────────────────────
|
|
406
550
|
const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
|
|
@@ -479,8 +623,11 @@ async function agentCommand(thesisId, opts) {
|
|
|
479
623
|
},
|
|
480
624
|
};
|
|
481
625
|
// ── Build components ───────────────────────────────────────────────────────
|
|
482
|
-
|
|
483
|
-
|
|
626
|
+
// No header bar — all info in footer (2 lines)
|
|
627
|
+
const footerBar = new FooterBar();
|
|
628
|
+
footerBar.modelName = currentModelName;
|
|
629
|
+
footerBar.tradingEnabled = (0, config_js_1.loadConfig)().tradingEnabled || false;
|
|
630
|
+
// Fetch positions for footer P&L (non-blocking, best-effort)
|
|
484
631
|
let initialPositions = null;
|
|
485
632
|
try {
|
|
486
633
|
initialPositions = await (0, kalshi_js_1.getPositions)();
|
|
@@ -495,10 +642,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
495
642
|
}
|
|
496
643
|
}
|
|
497
644
|
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;
|
|
645
|
+
footerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
502
646
|
// Fetch exchange status for footer (non-blocking)
|
|
503
647
|
fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } })
|
|
504
648
|
.then(r => r.json())
|
|
@@ -537,13 +681,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
537
681
|
tui.addChild(bottomSpacer);
|
|
538
682
|
// Focus on editor
|
|
539
683
|
tui.setFocus(editor);
|
|
540
|
-
// ──
|
|
541
|
-
const headerOverlay = tui.showOverlay(headerBar, {
|
|
542
|
-
row: 0,
|
|
543
|
-
col: 0,
|
|
544
|
-
width: '100%',
|
|
545
|
-
nonCapturing: true,
|
|
546
|
-
});
|
|
684
|
+
// ── Footer overlay (2-line: thesis info + model/exchange) ──────────────────
|
|
547
685
|
const footerOverlay = tui.showOverlay(footerBar, {
|
|
548
686
|
anchor: 'bottom-left',
|
|
549
687
|
width: '100%',
|
|
@@ -598,7 +736,7 @@ async function agentCommand(thesisId, opts) {
|
|
|
598
736
|
execute: async (_toolCallId, params) => {
|
|
599
737
|
const ctx = await sfClient.getContext(params.thesisId);
|
|
600
738
|
latestContext = ctx;
|
|
601
|
-
|
|
739
|
+
footerBar.setFromContext(ctx, initialPositions || undefined);
|
|
602
740
|
tui.requestRender();
|
|
603
741
|
return {
|
|
604
742
|
content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
|
|
@@ -636,13 +774,13 @@ async function agentCommand(thesisId, opts) {
|
|
|
636
774
|
addSystemText(color(` ${arrow} Confidence ${prev}% \u2192 ${now}% (${delta > 0 ? '+' : ''}${Math.round(delta * 100)})`));
|
|
637
775
|
addSpacer();
|
|
638
776
|
// Update header
|
|
639
|
-
|
|
777
|
+
footerBar.updateConfidence(result.evaluation.newConfidence, delta);
|
|
640
778
|
tui.requestRender();
|
|
641
779
|
}
|
|
642
780
|
// Refresh context after eval
|
|
643
781
|
try {
|
|
644
782
|
latestContext = await sfClient.getContext(params.thesisId);
|
|
645
|
-
|
|
783
|
+
footerBar.setFromContext(latestContext, initialPositions || undefined);
|
|
646
784
|
tui.requestRender();
|
|
647
785
|
}
|
|
648
786
|
catch { }
|
|
@@ -671,8 +809,8 @@ async function agentCommand(thesisId, opts) {
|
|
|
671
809
|
const matched = series
|
|
672
810
|
.filter((s) => keywords.every((kw) => (s.title || '').toLowerCase().includes(kw) ||
|
|
673
811
|
(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'))
|
|
812
|
+
.filter((s) => parseFloat(s.volume_24h_fp || s.volume_fp || '0') > 0)
|
|
813
|
+
.sort((a, b) => parseFloat(b.volume_24h_fp || b.volume_fp || '0') - parseFloat(a.volume_24h_fp || a.volume_fp || '0'))
|
|
676
814
|
.slice(0, 15);
|
|
677
815
|
result = matched;
|
|
678
816
|
}
|
|
@@ -1037,15 +1175,14 @@ async function agentCommand(thesisId, opts) {
|
|
|
1037
1175
|
execute: async () => {
|
|
1038
1176
|
const { theses } = await sfClient.listTheses();
|
|
1039
1177
|
const activeTheses = (theses || []).filter((t) => t.status === 'active' || t.status === 'monitoring');
|
|
1178
|
+
const results = await Promise.allSettled(activeTheses.map(async (t) => {
|
|
1179
|
+
const ctx = await sfClient.getContext(t.id);
|
|
1180
|
+
return (ctx.edges || []).map((e) => ({ ...e, thesisId: t.id }));
|
|
1181
|
+
}));
|
|
1040
1182
|
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 */ }
|
|
1183
|
+
for (const r of results) {
|
|
1184
|
+
if (r.status === 'fulfilled')
|
|
1185
|
+
allEdges.push(...r.value);
|
|
1049
1186
|
}
|
|
1050
1187
|
allEdges.sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0));
|
|
1051
1188
|
const top10 = allEdges.slice(0, 10).map((e) => ({
|
|
@@ -1327,7 +1464,32 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1327
1464
|
const saved = loadSession(resolvedThesisId);
|
|
1328
1465
|
if (saved?.messages?.length > 0) {
|
|
1329
1466
|
try {
|
|
1330
|
-
|
|
1467
|
+
// Clean corrupted messages: empty content, missing role, broken alternation
|
|
1468
|
+
const filtered = saved.messages.filter((m) => {
|
|
1469
|
+
if (!m.role)
|
|
1470
|
+
return false;
|
|
1471
|
+
if (Array.isArray(m.content) && m.content.length === 0)
|
|
1472
|
+
return false;
|
|
1473
|
+
if (m.role === 'assistant' && !m.content && !m.tool_calls?.length)
|
|
1474
|
+
return false;
|
|
1475
|
+
return true;
|
|
1476
|
+
});
|
|
1477
|
+
// Fix alternation: ensure user → assistant → user → assistant
|
|
1478
|
+
// Drop consecutive messages of the same role (keep first)
|
|
1479
|
+
const cleaned = [];
|
|
1480
|
+
for (const m of filtered) {
|
|
1481
|
+
const lastRole = cleaned.length > 0 ? cleaned[cleaned.length - 1].role : null;
|
|
1482
|
+
if (m.role === lastRole && m.role !== 'tool') {
|
|
1483
|
+
// Skip consecutive same-role (except tool messages which can follow each other)
|
|
1484
|
+
continue;
|
|
1485
|
+
}
|
|
1486
|
+
cleaned.push(m);
|
|
1487
|
+
}
|
|
1488
|
+
// Ensure conversation doesn't end with user message (API needs assistant reply)
|
|
1489
|
+
if (cleaned.length > 0 && cleaned[cleaned.length - 1].role === 'user') {
|
|
1490
|
+
cleaned.pop();
|
|
1491
|
+
}
|
|
1492
|
+
agent.replaceMessages(cleaned);
|
|
1331
1493
|
// Always update system prompt with fresh context
|
|
1332
1494
|
agent.setSystemPrompt(systemPrompt);
|
|
1333
1495
|
sessionRestored = true;
|
|
@@ -1601,7 +1763,7 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
|
|
|
1601
1763
|
addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
|
|
1602
1764
|
}
|
|
1603
1765
|
// Update header
|
|
1604
|
-
|
|
1766
|
+
footerBar.setFromContext(newContext, initialPositions || undefined);
|
|
1605
1767
|
chatContainer.clear();
|
|
1606
1768
|
addSystemText(buildWelcomeDashboard(newContext, initialPositions));
|
|
1607
1769
|
}
|
|
@@ -1812,7 +1974,20 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
1812
1974
|
return true;
|
|
1813
1975
|
}
|
|
1814
1976
|
case '/clear': {
|
|
1815
|
-
chatContainer.clear()
|
|
1977
|
+
// Don't use chatContainer.clear() — it breaks pi-tui layout.
|
|
1978
|
+
// Instead, remove children one by one and add a fresh spacer
|
|
1979
|
+
// so the container still has content for layout calculations.
|
|
1980
|
+
const children = [...chatContainer.children || []];
|
|
1981
|
+
for (const child of children) {
|
|
1982
|
+
try {
|
|
1983
|
+
chatContainer.removeChild(child);
|
|
1984
|
+
}
|
|
1985
|
+
catch { /* ignore */ }
|
|
1986
|
+
}
|
|
1987
|
+
addSpacer();
|
|
1988
|
+
isProcessing = false;
|
|
1989
|
+
pendingPrompt = null;
|
|
1990
|
+
tui.setFocus(editor);
|
|
1816
1991
|
tui.requestRender();
|
|
1817
1992
|
return true;
|
|
1818
1993
|
}
|
|
@@ -2065,7 +2240,7 @@ Output a structured summary. Be concise but preserve every important detail —
|
|
|
2065
2240
|
}
|
|
2066
2241
|
addSpacer();
|
|
2067
2242
|
// Update header
|
|
2068
|
-
|
|
2243
|
+
footerBar.setFromContext({ ...latestContext, confidence: delta.confidence, lastEvaluation: { confidenceDelta: delta.confidenceDelta } }, initialPositions || undefined);
|
|
2069
2244
|
tui.requestRender();
|
|
2070
2245
|
// Auto-trigger agent
|
|
2071
2246
|
isProcessing = true;
|
|
@@ -2400,15 +2575,14 @@ async function runPlainTextAgent(params) {
|
|
|
2400
2575
|
execute: async () => {
|
|
2401
2576
|
const { theses } = await sfClient.listTheses();
|
|
2402
2577
|
const activeTheses = (theses || []).filter((t) => t.status === 'active' || t.status === 'monitoring');
|
|
2578
|
+
const results = await Promise.allSettled(activeTheses.map(async (t) => {
|
|
2579
|
+
const ctx = await sfClient.getContext(t.id);
|
|
2580
|
+
return (ctx.edges || []).map((e) => ({ ...e, thesisId: t.id }));
|
|
2581
|
+
}));
|
|
2403
2582
|
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 */ }
|
|
2583
|
+
for (const r of results) {
|
|
2584
|
+
if (r.status === 'fulfilled')
|
|
2585
|
+
allEdges.push(...r.value);
|
|
2412
2586
|
}
|
|
2413
2587
|
allEdges.sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0));
|
|
2414
2588
|
const top10 = allEdges.slice(0, 10).map((e) => ({
|
|
@@ -2631,6 +2805,10 @@ async function runPlainTextAgent(params) {
|
|
|
2631
2805
|
const maxCost = ((priceCents || 99) * p.count / 100).toFixed(2);
|
|
2632
2806
|
const preview = ` Order: ${p.action.toUpperCase()} ${p.count}x ${p.ticker} ${p.side.toUpperCase()} @ ${priceCents ? priceCents + '\u00A2' : 'market'} (max $${maxCost})`;
|
|
2633
2807
|
process.stderr.write(preview + '\n');
|
|
2808
|
+
// Reject if stdin is piped (non-TTY) — too dangerous to auto-execute trades
|
|
2809
|
+
if (!process.stdin.isTTY) {
|
|
2810
|
+
return { content: [{ type: 'text', text: 'Order rejected: trading requires interactive terminal (stdin is piped). Use TUI mode for trading.' }], details: {} };
|
|
2811
|
+
}
|
|
2634
2812
|
// Confirm in plain mode via readline
|
|
2635
2813
|
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
2636
2814
|
const answer = await new Promise(resolve => rl.question(' Execute? (y/n) ', resolve));
|
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* sf dashboard —
|
|
2
|
+
* sf dashboard — Commander entry point
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Three modes:
|
|
5
|
+
* --json → dump current state as JSON
|
|
6
|
+
* --once → one-time formatted print (no interactive TUI)
|
|
7
|
+
* default → launch interactive TUI dashboard
|
|
6
8
|
*/
|
|
7
9
|
export declare function dashboardCommand(opts?: {
|
|
8
10
|
json?: boolean;
|
|
11
|
+
once?: boolean;
|
|
9
12
|
apiKey?: string;
|
|
10
13
|
apiUrl?: string;
|
|
11
14
|
}): Promise<void>;
|
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* sf dashboard —
|
|
3
|
+
* sf dashboard — Commander entry point
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
5
|
+
* Three modes:
|
|
6
|
+
* --json → dump current state as JSON
|
|
7
|
+
* --once → one-time formatted print (no interactive TUI)
|
|
8
|
+
* default → launch interactive TUI dashboard
|
|
7
9
|
*/
|
|
8
10
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
9
11
|
exports.dashboardCommand = dashboardCommand;
|
|
10
12
|
const client_js_1 = require("../client.js");
|
|
11
13
|
const kalshi_js_1 = require("../kalshi.js");
|
|
12
14
|
const topics_js_1 = require("../topics.js");
|
|
15
|
+
const dashboard_js_1 = require("../tui/dashboard.js");
|
|
13
16
|
function categorize(ticker) {
|
|
14
|
-
// Match longest prefix first
|
|
15
17
|
const sorted = Object.keys(topics_js_1.RISK_CATEGORIES).sort((a, b) => b.length - a.length);
|
|
16
18
|
for (const prefix of sorted) {
|
|
17
19
|
if (ticker.startsWith(prefix))
|
|
@@ -31,8 +33,13 @@ function timeAgo(dateStr) {
|
|
|
31
33
|
return `${days}d ago`;
|
|
32
34
|
}
|
|
33
35
|
async function dashboardCommand(opts) {
|
|
36
|
+
// ── Default: interactive TUI ──
|
|
37
|
+
if (!opts?.json && !opts?.once) {
|
|
38
|
+
await (0, dashboard_js_1.startDashboard)();
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// ── JSON or one-time print modes (legacy behavior) ──
|
|
34
42
|
const client = new client_js_1.SFClient(opts?.apiKey, opts?.apiUrl);
|
|
35
|
-
// ── Fetch data in parallel ─────────────────────────────────────────────────
|
|
36
43
|
const [thesesResult, positions] = await Promise.all([
|
|
37
44
|
client.listTheses(),
|
|
38
45
|
(0, kalshi_js_1.getPositions)().catch(() => null),
|
|
@@ -59,7 +66,7 @@ async function dashboardCommand(opts) {
|
|
|
59
66
|
}
|
|
60
67
|
}
|
|
61
68
|
}
|
|
62
|
-
//
|
|
69
|
+
// Collect all edges across all theses
|
|
63
70
|
const allEdges = [];
|
|
64
71
|
for (const ctx of contexts) {
|
|
65
72
|
if (!ctx?.edges)
|
|
@@ -78,26 +85,22 @@ async function dashboardCommand(opts) {
|
|
|
78
85
|
}
|
|
79
86
|
// Find positioned tickers
|
|
80
87
|
const positionedTickers = new Set(positions?.map((p) => p.ticker) || []);
|
|
81
|
-
// Unpositioned edges
|
|
88
|
+
// Unpositioned edges
|
|
82
89
|
const unpositionedEdges = [...edgeMap.values()]
|
|
83
90
|
.filter(e => !positionedTickers.has(e.marketId))
|
|
84
91
|
.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
|
|
85
92
|
.slice(0, 10);
|
|
86
|
-
// ── JSON output
|
|
93
|
+
// ── JSON output ──
|
|
87
94
|
if (opts?.json) {
|
|
88
|
-
console.log(JSON.stringify({
|
|
89
|
-
theses,
|
|
90
|
-
positions,
|
|
91
|
-
unpositionedEdges,
|
|
92
|
-
}, null, 2));
|
|
95
|
+
console.log(JSON.stringify({ theses, positions, unpositionedEdges }, null, 2));
|
|
93
96
|
return;
|
|
94
97
|
}
|
|
95
|
-
// ──
|
|
98
|
+
// ── One-time formatted output ──
|
|
96
99
|
console.log();
|
|
97
100
|
console.log(' SimpleFunctions Dashboard');
|
|
98
|
-
console.log(' ' + '
|
|
101
|
+
console.log(' ' + '\u2500'.repeat(50));
|
|
99
102
|
console.log();
|
|
100
|
-
//
|
|
103
|
+
// Theses
|
|
101
104
|
console.log(' Theses');
|
|
102
105
|
if (theses.length === 0) {
|
|
103
106
|
console.log(' (none)');
|
|
@@ -115,7 +118,7 @@ async function dashboardCommand(opts) {
|
|
|
115
118
|
}
|
|
116
119
|
}
|
|
117
120
|
console.log();
|
|
118
|
-
//
|
|
121
|
+
// Positions
|
|
119
122
|
console.log(' Positions');
|
|
120
123
|
if (!positions || positions.length === 0) {
|
|
121
124
|
console.log(' (no Kalshi positions or Kalshi not configured)');
|
|
@@ -126,8 +129,8 @@ async function dashboardCommand(opts) {
|
|
|
126
129
|
for (const p of positions) {
|
|
127
130
|
const ticker = (p.ticker || '').padEnd(22);
|
|
128
131
|
const qty = String(p.quantity || 0).padStart(5);
|
|
129
|
-
const avg = `${p.average_price_paid || 0}
|
|
130
|
-
const now = typeof p.current_value === 'number' ? `${p.current_value}
|
|
132
|
+
const avg = `${p.average_price_paid || 0}\u00A2`;
|
|
133
|
+
const now = typeof p.current_value === 'number' ? `${p.current_value}\u00A2` : '?\u00A2';
|
|
131
134
|
const pnlCents = p.unrealized_pnl || 0;
|
|
132
135
|
const pnlDollars = (pnlCents / 100).toFixed(2);
|
|
133
136
|
const pnlStr = pnlCents >= 0 ? `+$${pnlDollars}` : `-$${Math.abs(parseFloat(pnlDollars)).toFixed(2)}`;
|
|
@@ -136,14 +139,14 @@ async function dashboardCommand(opts) {
|
|
|
136
139
|
totalPnl += pnlCents;
|
|
137
140
|
console.log(` ${ticker} ${qty} @ ${avg.padEnd(5)} now ${now.padEnd(5)} ${pnlStr}`);
|
|
138
141
|
}
|
|
139
|
-
console.log(' ' + '
|
|
142
|
+
console.log(' ' + '\u2500'.repeat(45));
|
|
140
143
|
const totalCostDollars = (totalCost / 100).toFixed(0);
|
|
141
144
|
const totalPnlDollars = (totalPnl / 100).toFixed(2);
|
|
142
145
|
const pnlDisplay = totalPnl >= 0 ? `+$${totalPnlDollars}` : `-$${Math.abs(parseFloat(totalPnlDollars)).toFixed(2)}`;
|
|
143
146
|
console.log(` Total cost: $${totalCostDollars} | P&L: ${pnlDisplay}`);
|
|
144
147
|
}
|
|
145
148
|
console.log();
|
|
146
|
-
//
|
|
149
|
+
// Risk Exposure
|
|
147
150
|
if (positions && positions.length > 0) {
|
|
148
151
|
console.log(' Risk Exposure');
|
|
149
152
|
const riskGroups = new Map();
|
|
@@ -157,7 +160,6 @@ async function dashboardCommand(opts) {
|
|
|
157
160
|
existing.tickers.push(p.ticker);
|
|
158
161
|
riskGroups.set(cat, existing);
|
|
159
162
|
}
|
|
160
|
-
// Sort by cost descending
|
|
161
163
|
const sorted = [...riskGroups.entries()].sort((a, b) => b[1].cost - a[1].cost);
|
|
162
164
|
for (const [category, data] of sorted) {
|
|
163
165
|
const costDollars = `$${(data.cost / 100).toFixed(0)}`;
|
|
@@ -168,16 +170,16 @@ async function dashboardCommand(opts) {
|
|
|
168
170
|
}
|
|
169
171
|
console.log();
|
|
170
172
|
}
|
|
171
|
-
//
|
|
173
|
+
// Top Unpositioned Edges
|
|
172
174
|
if (unpositionedEdges.length > 0) {
|
|
173
175
|
console.log(' Top Unpositioned Edges');
|
|
174
176
|
for (const e of unpositionedEdges) {
|
|
175
177
|
const name = (e.market || e.marketId || '').slice(0, 25).padEnd(25);
|
|
176
|
-
const mkt = `${e.marketPrice}
|
|
177
|
-
const thesis = `${e.thesisPrice}
|
|
178
|
+
const mkt = `${e.marketPrice}\u00A2`;
|
|
179
|
+
const thesis = `${e.thesisPrice}\u00A2`;
|
|
178
180
|
const edge = e.edge > 0 ? `+${e.edge}` : `${e.edge}`;
|
|
179
181
|
const liq = e.orderbook?.liquidityScore || '?';
|
|
180
|
-
console.log(` ${name} ${mkt.padStart(5)}
|
|
182
|
+
console.log(` ${name} ${mkt.padStart(5)} \u2192 ${thesis.padStart(5)} edge ${edge.padStart(4)} ${liq}`);
|
|
181
183
|
}
|
|
182
184
|
console.log();
|
|
183
185
|
}
|
|
@@ -56,15 +56,22 @@ async function performanceCommand(opts) {
|
|
|
56
56
|
if (!ticker)
|
|
57
57
|
continue;
|
|
58
58
|
const action = fill.action || 'buy';
|
|
59
|
+
const side = fill.side || 'yes';
|
|
59
60
|
const count = Math.round(parseFloat(fill.count_fp || fill.count || '0'));
|
|
60
61
|
const yesPrice = Math.round(parseFloat(fill.yes_price_dollars || '0') * 100);
|
|
62
|
+
// buy yes = +count, sell yes = -count
|
|
63
|
+
// buy no = -count (short yes), sell no = +count (close short)
|
|
61
64
|
let delta = count;
|
|
62
65
|
if (action === 'sell')
|
|
63
|
-
delta = -
|
|
66
|
+
delta = -delta;
|
|
67
|
+
if (side === 'no')
|
|
68
|
+
delta = -delta;
|
|
64
69
|
const info = tickerMap.get(ticker) || { ticker, netQty: 0, totalCostCents: 0, totalContracts: 0, earliestFillTs: Infinity };
|
|
65
70
|
info.netQty += delta;
|
|
66
71
|
if (delta > 0) {
|
|
67
|
-
|
|
72
|
+
// Entering a position — accumulate cost
|
|
73
|
+
const costPerContract = side === 'no' ? (100 - yesPrice) : yesPrice;
|
|
74
|
+
info.totalCostCents += costPerContract * count;
|
|
68
75
|
info.totalContracts += count;
|
|
69
76
|
}
|
|
70
77
|
const fillTime = fill.created_time || fill.ts || fill.created_at;
|