@spfunctions/cli 1.4.4 → 1.5.0

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