@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.
Files changed (71) hide show
  1. package/README.md +205 -48
  2. package/dist/cache.d.ts +6 -0
  3. package/dist/cache.js +31 -0
  4. package/dist/cache.test.d.ts +1 -0
  5. package/dist/cache.test.js +73 -0
  6. package/dist/client.test.d.ts +1 -0
  7. package/dist/client.test.js +89 -0
  8. package/dist/commands/agent.js +245 -67
  9. package/dist/commands/dashboard.d.ts +6 -3
  10. package/dist/commands/dashboard.js +28 -26
  11. package/dist/commands/performance.js +9 -2
  12. package/dist/commands/telegram.d.ts +15 -0
  13. package/dist/commands/telegram.js +125 -0
  14. package/dist/config.d.ts +1 -0
  15. package/dist/config.js +1 -0
  16. package/dist/config.test.d.ts +1 -0
  17. package/dist/config.test.js +138 -0
  18. package/dist/index.js +16 -2
  19. package/dist/telegram/agent-bridge.d.ts +15 -0
  20. package/dist/telegram/agent-bridge.js +368 -0
  21. package/dist/telegram/bot.d.ts +10 -0
  22. package/dist/telegram/bot.js +297 -0
  23. package/dist/telegram/commands.d.ts +11 -0
  24. package/dist/telegram/commands.js +120 -0
  25. package/dist/telegram/format.d.ts +11 -0
  26. package/dist/telegram/format.js +51 -0
  27. package/dist/telegram/format.test.d.ts +1 -0
  28. package/dist/telegram/format.test.js +73 -0
  29. package/dist/telegram/poller.d.ts +6 -0
  30. package/dist/telegram/poller.js +32 -0
  31. package/dist/topics.test.d.ts +1 -0
  32. package/dist/topics.test.js +54 -0
  33. package/dist/tui/border.d.ts +33 -0
  34. package/dist/tui/border.js +87 -0
  35. package/dist/tui/chart.d.ts +19 -0
  36. package/dist/tui/chart.js +117 -0
  37. package/dist/tui/dashboard.d.ts +9 -0
  38. package/dist/tui/dashboard.js +779 -0
  39. package/dist/tui/layout.d.ts +16 -0
  40. package/dist/tui/layout.js +41 -0
  41. package/dist/tui/screen.d.ts +33 -0
  42. package/dist/tui/screen.js +102 -0
  43. package/dist/tui/state.d.ts +40 -0
  44. package/dist/tui/state.js +36 -0
  45. package/dist/tui/widgets/commandbar.d.ts +8 -0
  46. package/dist/tui/widgets/commandbar.js +82 -0
  47. package/dist/tui/widgets/detail.d.ts +9 -0
  48. package/dist/tui/widgets/detail.js +151 -0
  49. package/dist/tui/widgets/edges.d.ts +4 -0
  50. package/dist/tui/widgets/edges.js +33 -0
  51. package/dist/tui/widgets/liquidity.d.ts +9 -0
  52. package/dist/tui/widgets/liquidity.js +142 -0
  53. package/dist/tui/widgets/orders.d.ts +4 -0
  54. package/dist/tui/widgets/orders.js +37 -0
  55. package/dist/tui/widgets/portfolio.d.ts +4 -0
  56. package/dist/tui/widgets/portfolio.js +58 -0
  57. package/dist/tui/widgets/signals.d.ts +4 -0
  58. package/dist/tui/widgets/signals.js +31 -0
  59. package/dist/tui/widgets/statusbar.d.ts +8 -0
  60. package/dist/tui/widgets/statusbar.js +72 -0
  61. package/dist/tui/widgets/thesis.d.ts +4 -0
  62. package/dist/tui/widgets/thesis.js +66 -0
  63. package/dist/tui/widgets/trade.d.ts +9 -0
  64. package/dist/tui/widgets/trade.js +117 -0
  65. package/dist/tui/widgets/upcoming.d.ts +4 -0
  66. package/dist/tui/widgets/upcoming.js +41 -0
  67. package/dist/tui/widgets/whatif.d.ts +7 -0
  68. package/dist/tui/widgets/whatif.js +113 -0
  69. package/dist/utils.test.d.ts +1 -0
  70. package/dist/utils.test.js +111 -0
  71. package/package.json +6 -2
@@ -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
- /** Footer bar: model | tokens | exchange status | trading status | /help */
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; // null = unknown
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
- const tokStr = this.tokens >= 1000
224
- ? `${(this.tokens / 1000).toFixed(1)}k`
225
- : `${this.tokens}`;
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
- ? C.emerald('OPEN')
230
- : this.exchangeOpen === false
231
- ? C.red('CLOSED')
232
- : C.zinc600('...');
233
- const trading = this.tradingEnabled
234
- ? C.amber('\u26A1 trading')
235
- : C.zinc600('\u26A1 read-only');
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 line = C.bgZinc900(' ' + leftText + ' '.repeat(gap) + help + ' ');
243
- const lineVw = visibleWidth(line);
244
- if (lineVw < width) {
245
- line = line + C.bgZinc900(' '.repeat(width - lineVw));
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.find((t) => t.status === 'active');
382
- if (!active) {
383
- console.error('No active thesis. Create one first: sf create "..."');
384
- process.exit(1);
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
- const headerBar = new HeaderBar();
483
- // Fetch positions for header P&L (non-blocking, best-effort)
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
- headerBar.setFromContext(latestContext, initialPositions || undefined);
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
- // ── Overlays (pinned header + footer) ──────────────────────────────────────
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
- headerBar.setFromContext(ctx, initialPositions || undefined);
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
- headerBar.updateConfidence(result.evaluation.newConfidence, delta);
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
- headerBar.setFromContext(latestContext, initialPositions || undefined);
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') > 1000)
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 t of activeTheses) {
1042
- try {
1043
- const ctx = await sfClient.getContext(t.id);
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
- agent.replaceMessages(saved.messages);
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
- headerBar.setFromContext(newContext, initialPositions || undefined);
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
- headerBar.setFromContext({ ...latestContext, confidence: delta.confidence, lastEvaluation: { confidenceDelta: delta.confidenceDelta } }, initialPositions || undefined);
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 t of activeTheses) {
2405
- try {
2406
- const ctx = await sfClient.getContext(t.id);
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 — Portfolio overview
2
+ * sf dashboard — Commander entry point
3
3
  *
4
- * One-screen summary: theses, positions, risk exposure, top unpositioned edges.
5
- * Uses existing APIs + local Kalshi positions.
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 — Portfolio overview
3
+ * sf dashboard — Commander entry point
4
4
  *
5
- * One-screen summary: theses, positions, risk exposure, top unpositioned edges.
6
- * Uses existing APIs + local Kalshi positions.
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
- // ── Collect all edges across all theses ────────────────────────────────────
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 = edges where no position exists on that marketId
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
- // ── Formatted output ───────────────────────────────────────────────────────
98
+ // ── One-time formatted output ──
96
99
  console.log();
97
100
  console.log(' SimpleFunctions Dashboard');
98
- console.log(' ' + ''.repeat(50));
101
+ console.log(' ' + '\u2500'.repeat(50));
99
102
  console.log();
100
- // ── Theses ─────────────────────────────────────────────────────────────────
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
- // ── Positions ──────────────────────────────────────────────────────────────
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(' ' + ''.repeat(45));
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
- // ── Risk Exposure ──────────────────────────────────────────────────────────
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
- // ── Top Unpositioned Edges ─────────────────────────────────────────────────
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)} ${thesis.padStart(5)} edge ${edge.padStart(4)} ${liq}`);
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 = -count;
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
- info.totalCostCents += yesPrice * count;
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;