@spfunctions/cli 1.4.3 → 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 +257 -70
- 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) => ({
|
|
@@ -1283,7 +1420,7 @@ When the conversation produces a concrete trade idea (specific contract, directi
|
|
|
1283
1420
|
- Extract hard conditions (specific prices in cents) into entryBelow/stopLoss/takeProfit.
|
|
1284
1421
|
- Put fuzzy conditions into softConditions (e.g. "only if n3 > 60%", "spread < 3¢").
|
|
1285
1422
|
- Put the full reasoning into rationale.
|
|
1286
|
-
- After creating, confirm the strategy details
|
|
1423
|
+
- After creating, confirm the strategy details.
|
|
1287
1424
|
- If the user says "change the stop loss on T150 to 30", use update_strategy.
|
|
1288
1425
|
|
|
1289
1426
|
## Trading status
|
|
@@ -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) => ({
|
|
@@ -2615,7 +2789,7 @@ async function runPlainTextAgent(params) {
|
|
|
2615
2789
|
tools.push({
|
|
2616
2790
|
name: 'place_order',
|
|
2617
2791
|
label: 'Place Order',
|
|
2618
|
-
description: 'Place a buy or sell order on Kalshi.
|
|
2792
|
+
description: 'Place a buy or sell order on Kalshi. Shows preview and asks for confirmation.',
|
|
2619
2793
|
parameters: Type.Object({
|
|
2620
2794
|
ticker: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T135' }),
|
|
2621
2795
|
side: Type.String({ description: 'yes or no' }),
|
|
@@ -2626,9 +2800,22 @@ async function runPlainTextAgent(params) {
|
|
|
2626
2800
|
}),
|
|
2627
2801
|
execute: async (_id, p) => {
|
|
2628
2802
|
const { createOrder } = await import('../kalshi.js');
|
|
2803
|
+
const readline = await import('readline');
|
|
2629
2804
|
const priceCents = p.price_cents ? Math.round(Number(p.price_cents)) : undefined;
|
|
2630
2805
|
const maxCost = ((priceCents || 99) * p.count / 100).toFixed(2);
|
|
2631
|
-
|
|
2806
|
+
const preview = ` Order: ${p.action.toUpperCase()} ${p.count}x ${p.ticker} ${p.side.toUpperCase()} @ ${priceCents ? priceCents + '\u00A2' : 'market'} (max $${maxCost})`;
|
|
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
|
+
}
|
|
2812
|
+
// Confirm in plain mode via readline
|
|
2813
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
2814
|
+
const answer = await new Promise(resolve => rl.question(' Execute? (y/n) ', resolve));
|
|
2815
|
+
rl.close();
|
|
2816
|
+
if (!answer.toLowerCase().startsWith('y')) {
|
|
2817
|
+
return { content: [{ type: 'text', text: 'Order cancelled by user.' }], details: {} };
|
|
2818
|
+
}
|
|
2632
2819
|
try {
|
|
2633
2820
|
const result = await createOrder({
|
|
2634
2821
|
ticker: p.ticker,
|
|
@@ -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>;
|