@spfunctions/cli 1.1.5 → 1.1.7

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 (46) hide show
  1. package/dist/client.d.ts +5 -0
  2. package/dist/client.js +17 -0
  3. package/dist/commands/agent.d.ts +1 -0
  4. package/dist/commands/agent.js +1170 -76
  5. package/dist/commands/announcements.d.ts +3 -0
  6. package/dist/commands/announcements.js +28 -0
  7. package/dist/commands/balance.d.ts +3 -0
  8. package/dist/commands/balance.js +17 -0
  9. package/dist/commands/cancel.d.ts +5 -0
  10. package/dist/commands/cancel.js +41 -0
  11. package/dist/commands/dashboard.d.ts +11 -0
  12. package/dist/commands/dashboard.js +195 -0
  13. package/dist/commands/feed.d.ts +13 -0
  14. package/dist/commands/feed.js +73 -0
  15. package/dist/commands/fills.d.ts +4 -0
  16. package/dist/commands/fills.js +29 -0
  17. package/dist/commands/forecast.d.ts +4 -0
  18. package/dist/commands/forecast.js +53 -0
  19. package/dist/commands/history.d.ts +3 -0
  20. package/dist/commands/history.js +38 -0
  21. package/dist/commands/milestones.d.ts +8 -0
  22. package/dist/commands/milestones.js +56 -0
  23. package/dist/commands/orders.d.ts +4 -0
  24. package/dist/commands/orders.js +28 -0
  25. package/dist/commands/publish.js +21 -2
  26. package/dist/commands/rfq.d.ts +5 -0
  27. package/dist/commands/rfq.js +35 -0
  28. package/dist/commands/schedule.d.ts +3 -0
  29. package/dist/commands/schedule.js +38 -0
  30. package/dist/commands/settlements.d.ts +6 -0
  31. package/dist/commands/settlements.js +50 -0
  32. package/dist/commands/setup.d.ts +2 -0
  33. package/dist/commands/setup.js +45 -3
  34. package/dist/commands/signal.js +12 -1
  35. package/dist/commands/strategies.d.ts +11 -0
  36. package/dist/commands/strategies.js +130 -0
  37. package/dist/commands/trade.d.ts +12 -0
  38. package/dist/commands/trade.js +78 -0
  39. package/dist/commands/whatif.d.ts +17 -0
  40. package/dist/commands/whatif.js +209 -0
  41. package/dist/config.d.ts +2 -0
  42. package/dist/config.js +13 -0
  43. package/dist/index.js +177 -3
  44. package/dist/kalshi.d.ts +71 -0
  45. package/dist/kalshi.js +257 -17
  46. package/package.json +1 -1
@@ -23,6 +23,7 @@ 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 config_js_1 = require("../config.js");
26
27
  // ─── Session persistence ─────────────────────────────────────────────────────
27
28
  function getSessionDir() {
28
29
  return path_1.default.join(os_1.default.homedir(), '.sf', 'sessions');
@@ -99,27 +100,53 @@ function createMutableLine(piTui) {
99
100
  }
100
101
  };
101
102
  }
102
- /** Header bar: [SF Agent — {id}] [{confidence}%] [{model}] */
103
+ /**
104
+ * Header bar — trading terminal style.
105
+ * Shows: thesis ID, confidence+delta, positions P&L, edge count, top edge
106
+ */
103
107
  function createHeaderBar(piTui) {
104
108
  const { truncateToWidth, visibleWidth } = piTui;
105
109
  return class HeaderBar {
106
- left;
107
- center;
108
- right;
110
+ thesisId = '';
111
+ confidence = 0;
112
+ confidenceDelta = 0;
113
+ pnlDollars = 0;
114
+ positionCount = 0;
115
+ edgeCount = 0;
116
+ topEdge = ''; // e.g. "RECESSION +21¢"
109
117
  cachedWidth;
110
118
  cachedLines;
111
- constructor(left, center, right) {
112
- this.left = left;
113
- this.center = center;
114
- this.right = right;
119
+ setFromContext(ctx, positions) {
120
+ this.thesisId = (ctx.thesisId || '').slice(0, 8);
121
+ this.confidence = typeof ctx.confidence === 'number'
122
+ ? Math.round(ctx.confidence * 100)
123
+ : (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
124
+ this.confidenceDelta = ctx.lastEvaluation?.confidenceDelta
125
+ ? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
126
+ : 0;
127
+ this.edgeCount = (ctx.edges || []).length;
128
+ // Top edge by absolute size
129
+ const edges = ctx.edges || [];
130
+ if (edges.length > 0) {
131
+ const top = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0))[0];
132
+ const name = (top.market || top.marketTitle || top.marketId || '').slice(0, 20);
133
+ const edge = top.edge || top.edgeSize || 0;
134
+ this.topEdge = `${name} ${edge > 0 ? '+' : ''}${Math.round(edge)}\u00A2`;
135
+ }
136
+ // P&L from positions
137
+ if (positions && positions.length > 0) {
138
+ this.positionCount = positions.length;
139
+ this.pnlDollars = positions.reduce((sum, p) => {
140
+ const pnl = p.unrealized_pnl || 0;
141
+ return sum + pnl;
142
+ }, 0) / 100; // cents → dollars
143
+ }
144
+ this.cachedWidth = undefined;
145
+ this.cachedLines = undefined;
115
146
  }
116
- update(left, center, right) {
117
- if (left !== undefined)
118
- this.left = left;
119
- if (center !== undefined)
120
- this.center = center;
121
- if (right !== undefined)
122
- this.right = right;
147
+ updateConfidence(newConf, delta) {
148
+ this.confidence = Math.round(newConf * 100);
149
+ this.confidenceDelta = Math.round(delta * 100);
123
150
  this.cachedWidth = undefined;
124
151
  this.cachedLines = undefined;
125
152
  }
@@ -127,23 +154,39 @@ function createHeaderBar(piTui) {
127
154
  this.cachedWidth = undefined;
128
155
  this.cachedLines = undefined;
129
156
  }
157
+ // Keep legacy update() for compatibility with /switch etc.
158
+ update(left, center, right) {
159
+ this.cachedWidth = undefined;
160
+ this.cachedLines = undefined;
161
+ }
130
162
  render(width) {
131
163
  if (this.cachedLines && this.cachedWidth === width)
132
164
  return this.cachedLines;
133
165
  this.cachedWidth = width;
134
- const l = this.left;
135
- const c = this.center;
136
- const r = this.right;
137
- const lw = visibleWidth(l);
138
- const cw = visibleWidth(c);
139
- const rw = visibleWidth(r);
140
- const totalContent = lw + cw + rw;
141
- const totalPad = Math.max(0, width - totalContent);
142
- const leftPad = Math.max(1, Math.floor(totalPad / 2));
143
- const rightPad = Math.max(1, totalPad - leftPad);
144
- let line = l + ' '.repeat(leftPad) + c + ' '.repeat(rightPad) + r;
145
- line = C.bgZinc900(truncateToWidth(line, width, ''));
146
- // Pad to full width for background
166
+ // Build segments
167
+ const id = C.emerald(bold(this.thesisId));
168
+ // Confidence with arrow
169
+ const arrow = this.confidenceDelta > 0 ? '\u25B2' : this.confidenceDelta < 0 ? '\u25BC' : '\u2500';
170
+ const arrowColor = this.confidenceDelta > 0 ? C.emerald : this.confidenceDelta < 0 ? C.red : C.zinc600;
171
+ const deltaStr = this.confidenceDelta !== 0 ? ` (${this.confidenceDelta > 0 ? '+' : ''}${this.confidenceDelta})` : '';
172
+ const conf = arrowColor(`${arrow} ${this.confidence}%${deltaStr}`);
173
+ // P&L
174
+ let pnl = '';
175
+ if (this.positionCount > 0) {
176
+ const pnlStr = this.pnlDollars >= 0
177
+ ? C.emerald(`+$${this.pnlDollars.toFixed(2)}`)
178
+ : C.red(`-$${Math.abs(this.pnlDollars).toFixed(2)}`);
179
+ pnl = C.zinc600(`${this.positionCount} pos `) + pnlStr;
180
+ }
181
+ // Edges
182
+ const edges = C.zinc600(`${this.edgeCount} edges`);
183
+ // Top edge
184
+ const top = this.topEdge ? C.zinc400(this.topEdge) : '';
185
+ // Assemble with separators
186
+ const sep = C.zinc600(' \u2502 ');
187
+ const parts = [id, conf, pnl, edges, top].filter(Boolean);
188
+ const content = parts.join(sep);
189
+ let line = C.bgZinc900(' ' + truncateToWidth(content, width - 2, '') + ' ');
147
190
  const lineVw = visibleWidth(line);
148
191
  if (lineVw < width) {
149
192
  line = line + C.bgZinc900(' '.repeat(width - lineVw));
@@ -153,13 +196,16 @@ function createHeaderBar(piTui) {
153
196
  }
154
197
  };
155
198
  }
156
- /** Footer bar: [tokens: N | cost: $N | tools: N] [/help] */
199
+ /** Footer bar: model | tokens | exchange status | trading status | /help */
157
200
  function createFooterBar(piTui) {
158
201
  const { truncateToWidth, visibleWidth } = piTui;
159
202
  return class FooterBar {
160
203
  tokens = 0;
161
204
  cost = 0;
162
205
  toolCount = 0;
206
+ modelName = '';
207
+ tradingEnabled = false;
208
+ exchangeOpen = null; // null = unknown
163
209
  cachedWidth;
164
210
  cachedLines;
165
211
  invalidate() {
@@ -177,13 +223,23 @@ function createFooterBar(piTui) {
177
223
  const tokStr = this.tokens >= 1000
178
224
  ? `${(this.tokens / 1000).toFixed(1)}k`
179
225
  : `${this.tokens}`;
180
- const leftText = C.zinc600(`tokens: ${tokStr} cost: $${this.cost.toFixed(3)} tools: ${this.toolCount}`);
181
- const rightText = C.zinc600('/help');
226
+ const model = C.zinc600(this.modelName.split('/').pop() || this.modelName);
227
+ 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');
236
+ const help = C.zinc600('/help');
237
+ const sep = C.zinc600(' \u2502 ');
238
+ const leftText = [model, tokens, exchange, trading].join(sep);
182
239
  const lw = visibleWidth(leftText);
183
- const rw = visibleWidth(rightText);
184
- const gap = Math.max(1, width - lw - rw);
185
- let line = leftText + ' '.repeat(gap) + rightText;
186
- line = C.bgZinc900(truncateToWidth(line, width, ''));
240
+ const rw = visibleWidth(help);
241
+ const gap = Math.max(1, width - lw - rw - 2);
242
+ let line = C.bgZinc900(' ' + leftText + ' '.repeat(gap) + help + ' ');
187
243
  const lineVw = visibleWidth(line);
188
244
  if (lineVw < width) {
189
245
  line = line + C.bgZinc900(' '.repeat(width - lineVw));
@@ -286,23 +342,36 @@ function renderPositions(positions) {
286
342
  }
287
343
  // ─── Main command ────────────────────────────────────────────────────────────
288
344
  async function agentCommand(thesisId, opts) {
289
- // ── Dynamic imports (all ESM-only packages) ────────────────────────────────
290
- const piTui = await import('@mariozechner/pi-tui');
291
- const piAi = await import('@mariozechner/pi-ai');
292
- const piAgent = await import('@mariozechner/pi-agent-core');
293
- const { TUI, ProcessTerminal, Container, Text, Markdown, Editor, Loader, Spacer, CombinedAutocompleteProvider, truncateToWidth, visibleWidth, } = piTui;
294
- const { getModel, streamSimple, Type } = piAi;
295
- const { Agent } = piAgent;
296
- // ── Component class factories (need piTui ref) ─────────────────────────────
297
- const MutableLine = createMutableLine(piTui);
298
- const HeaderBar = createHeaderBar(piTui);
299
- const FooterBar = createFooterBar(piTui);
300
345
  // ── Validate API keys ──────────────────────────────────────────────────────
301
346
  const openrouterKey = opts?.modelKey || process.env.OPENROUTER_API_KEY;
302
347
  if (!openrouterKey) {
303
- console.error('Need OpenRouter API key. Set OPENROUTER_API_KEY or use --model-key.');
348
+ console.error('Need OpenRouter API key to power the agent LLM.');
349
+ console.error('');
350
+ console.error(' 1. Get a key at https://openrouter.ai/keys');
351
+ console.error(' 2. Then either:');
352
+ console.error(' export OPENROUTER_API_KEY=sk-or-v1-...');
353
+ console.error(' sf agent --model-key sk-or-v1-...');
354
+ console.error(' sf setup (saves to ~/.sf/config.json)');
304
355
  process.exit(1);
305
356
  }
357
+ // Pre-flight: validate OpenRouter key
358
+ try {
359
+ const checkRes = await fetch('https://openrouter.ai/api/v1/auth/key', {
360
+ headers: { 'Authorization': `Bearer ${openrouterKey}` },
361
+ signal: AbortSignal.timeout(8000),
362
+ });
363
+ if (!checkRes.ok) {
364
+ console.error('OpenRouter API key is invalid or expired.');
365
+ console.error('Get a new key at https://openrouter.ai/keys');
366
+ process.exit(1);
367
+ }
368
+ }
369
+ catch (err) {
370
+ const msg = err instanceof Error ? err.message : String(err);
371
+ if (!msg.includes('timeout')) {
372
+ console.warn(`Warning: Could not verify OpenRouter key (${msg}). Continuing anyway.`);
373
+ }
374
+ }
306
375
  const sfClient = new client_js_1.SFClient();
307
376
  // ── Resolve thesis ID ──────────────────────────────────────────────────────
308
377
  let resolvedThesisId = thesisId;
@@ -318,6 +387,21 @@ async function agentCommand(thesisId, opts) {
318
387
  }
319
388
  // ── Fetch initial context ──────────────────────────────────────────────────
320
389
  let latestContext = await sfClient.getContext(resolvedThesisId);
390
+ // ── Branch: plain-text mode ────────────────────────────────────────────────
391
+ if (opts?.noTui) {
392
+ return runPlainTextAgent({ openrouterKey, sfClient, resolvedThesisId: resolvedThesisId, latestContext, opts });
393
+ }
394
+ // ── Dynamic imports (all ESM-only packages) ────────────────────────────────
395
+ const piTui = await import('@mariozechner/pi-tui');
396
+ const piAi = await import('@mariozechner/pi-ai');
397
+ const piAgent = await import('@mariozechner/pi-agent-core');
398
+ const { TUI, ProcessTerminal, Container, Text, Markdown, Editor, Loader, Spacer, CombinedAutocompleteProvider, truncateToWidth, visibleWidth, } = piTui;
399
+ const { getModel, streamSimple, Type } = piAi;
400
+ const { Agent } = piAgent;
401
+ // ── Component class factories (need piTui ref) ─────────────────────────────
402
+ const MutableLine = createMutableLine(piTui);
403
+ const HeaderBar = createHeaderBar(piTui);
404
+ const FooterBar = createFooterBar(piTui);
321
405
  // ── Model setup ────────────────────────────────────────────────────────────
322
406
  const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
323
407
  let currentModelName = rawModelName.replace(/^openrouter\//, '');
@@ -349,6 +433,10 @@ async function agentCommand(thesisId, opts) {
349
433
  let isProcessing = false;
350
434
  // Cache for positions (fetched by /pos or get_positions tool)
351
435
  let cachedPositions = null;
436
+ // ── Inline confirmation mechanism ─────────────────────────────────────────
437
+ // Tools can call promptUser() during execution to ask the user a question.
438
+ // This temporarily unlocks the editor, waits for input, then resumes.
439
+ let pendingPrompt = null;
352
440
  // ── Setup TUI ──────────────────────────────────────────────────────────────
353
441
  const terminal = new ProcessTerminal();
354
442
  const tui = new TUI(terminal);
@@ -372,9 +460,9 @@ async function agentCommand(thesisId, opts) {
372
460
  const mdDefaultStyle = {
373
461
  color: (s) => C.zinc400(s),
374
462
  };
375
- // Editor theme
463
+ // Editor theme — use dim zinc borders instead of default green
376
464
  const editorTheme = {
377
- borderColor: (s) => C.zinc800(s),
465
+ borderColor: (s) => `\x1b[38;2;50;50;55m${s}\x1b[39m`,
378
466
  selectList: {
379
467
  selectedPrefix: (s) => C.emerald(s),
380
468
  selectedText: (s) => C.zinc200(s),
@@ -384,18 +472,37 @@ async function agentCommand(thesisId, opts) {
384
472
  },
385
473
  };
386
474
  // ── Build components ───────────────────────────────────────────────────────
387
- const shortId = (resolvedThesisId || '').slice(0, 8);
388
- const confidencePct = typeof latestContext.confidence === 'number'
389
- ? Math.round(latestContext.confidence * 100)
390
- : (typeof latestContext.confidence === 'string' ? parseInt(latestContext.confidence) : 0);
391
- const headerBar = new HeaderBar(C.emerald(bold('SF Agent')) + C.zinc600(` \u2014 ${shortId}`), confidencePct > 0 ? C.zinc200(`${confidencePct}%`) : '', C.zinc600(currentModelName.split('/').pop() || currentModelName));
475
+ const headerBar = new HeaderBar();
476
+ // Fetch positions for header P&L (non-blocking, best-effort)
477
+ let initialPositions = null;
478
+ try {
479
+ initialPositions = await (0, kalshi_js_1.getPositions)();
480
+ if (initialPositions) {
481
+ for (const pos of initialPositions) {
482
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
483
+ if (livePrice !== null) {
484
+ pos.current_value = livePrice;
485
+ pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
486
+ }
487
+ }
488
+ }
489
+ }
490
+ catch { /* positions not available, fine */ }
491
+ headerBar.setFromContext(latestContext, initialPositions || undefined);
392
492
  const footerBar = new FooterBar();
493
+ footerBar.modelName = currentModelName;
494
+ footerBar.tradingEnabled = (0, config_js_1.loadConfig)().tradingEnabled || false;
495
+ // Fetch exchange status for footer (non-blocking)
496
+ fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } })
497
+ .then(r => r.json())
498
+ .then(d => { footerBar.exchangeOpen = !!d.exchange_active; footerBar.update(); tui.requestRender(); })
499
+ .catch(() => { });
393
500
  const topSpacer = new Spacer(1);
394
501
  const bottomSpacer = new Spacer(1);
395
502
  const chatContainer = new Container();
396
503
  const editor = new Editor(tui, editorTheme, { paddingX: 1 });
397
504
  // Slash command autocomplete
398
- const autocompleteProvider = new CombinedAutocompleteProvider([
505
+ const slashCommands = [
399
506
  { name: 'help', description: 'Show available commands' },
400
507
  { name: 'tree', description: 'Display causal tree' },
401
508
  { name: 'edges', description: 'Display edge/spread table' },
@@ -408,7 +515,13 @@ async function agentCommand(thesisId, opts) {
408
515
  { name: 'env', description: 'Show environment variable status' },
409
516
  { name: 'clear', description: 'Clear screen (keeps history)' },
410
517
  { name: 'exit', description: 'Exit agent (auto-saves)' },
411
- ], process.cwd());
518
+ ];
519
+ // Add trading commands if enabled
520
+ if ((0, config_js_1.loadConfig)().tradingEnabled) {
521
+ slashCommands.splice(-2, 0, // insert before /clear and /exit
522
+ { name: 'buy', description: 'TICKER QTY PRICE — quick buy' }, { name: 'sell', description: 'TICKER QTY PRICE — quick sell' }, { name: 'cancel', description: 'ORDER_ID — cancel order' });
523
+ }
524
+ const autocompleteProvider = new CombinedAutocompleteProvider(slashCommands, process.cwd());
412
525
  editor.setAutocompleteProvider(autocompleteProvider);
413
526
  // Assemble TUI tree
414
527
  tui.addChild(topSpacer);
@@ -438,6 +551,19 @@ async function agentCommand(thesisId, opts) {
438
551
  function addSpacer() {
439
552
  chatContainer.addChild(new Spacer(1));
440
553
  }
554
+ /**
555
+ * Ask the user a question during tool execution.
556
+ * Temporarily unlocks the editor, waits for input, then resumes.
557
+ * Used for order confirmations and other dangerous operations.
558
+ */
559
+ function promptUser(question) {
560
+ return new Promise(resolve => {
561
+ addSystemText(C.amber(bold('\u26A0 ')) + C.zinc200(question));
562
+ addSpacer();
563
+ tui.requestRender();
564
+ pendingPrompt = { resolve };
565
+ });
566
+ }
441
567
  // ── Define agent tools (same as before) ────────────────────────────────────
442
568
  const thesisIdParam = Type.Object({
443
569
  thesisId: Type.String({ description: 'Thesis ID (short or full UUID)' }),
@@ -465,11 +591,7 @@ async function agentCommand(thesisId, opts) {
465
591
  execute: async (_toolCallId, params) => {
466
592
  const ctx = await sfClient.getContext(params.thesisId);
467
593
  latestContext = ctx;
468
- // Update header with new confidence
469
- const conf = typeof ctx.confidence === 'number'
470
- ? Math.round(ctx.confidence * 100)
471
- : 0;
472
- headerBar.update(undefined, conf > 0 ? C.zinc200(`${conf}%`) : '', undefined);
594
+ headerBar.setFromContext(ctx, initialPositions || undefined);
473
595
  tui.requestRender();
474
596
  return {
475
597
  content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
@@ -497,6 +619,26 @@ async function agentCommand(thesisId, opts) {
497
619
  parameters: thesisIdParam,
498
620
  execute: async (_toolCallId, params) => {
499
621
  const result = await sfClient.evaluate(params.thesisId);
622
+ // Show confidence change prominently
623
+ if (result.evaluation?.confidenceDelta && Math.abs(result.evaluation.confidenceDelta) >= 0.01) {
624
+ const delta = result.evaluation.confidenceDelta;
625
+ const prev = Math.round((result.evaluation.previousConfidence || 0) * 100);
626
+ const now = Math.round((result.evaluation.newConfidence || 0) * 100);
627
+ const arrow = delta > 0 ? '\u25B2' : '\u25BC';
628
+ const color = delta > 0 ? C.emerald : C.red;
629
+ addSystemText(color(` ${arrow} Confidence ${prev}% \u2192 ${now}% (${delta > 0 ? '+' : ''}${Math.round(delta * 100)})`));
630
+ addSpacer();
631
+ // Update header
632
+ headerBar.updateConfidence(result.evaluation.newConfidence, delta);
633
+ tui.requestRender();
634
+ }
635
+ // Refresh context after eval
636
+ try {
637
+ latestContext = await sfClient.getContext(params.thesisId);
638
+ headerBar.setFromContext(latestContext, initialPositions || undefined);
639
+ tui.requestRender();
640
+ }
641
+ catch { }
500
642
  return {
501
643
  content: [{ type: 'text', text: JSON.stringify(result) }],
502
644
  details: {},
@@ -636,7 +778,378 @@ async function agentCommand(thesisId, opts) {
636
778
  return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
637
779
  },
638
780
  },
781
+ {
782
+ name: 'create_strategy',
783
+ label: 'Create Strategy',
784
+ description: 'Create a trading strategy for a thesis. Extract hard conditions (entryBelow/stopLoss/takeProfit as cents) and soft conditions from conversation. Called when user mentions specific trade ideas.',
785
+ parameters: Type.Object({
786
+ thesisId: Type.String({ description: 'Thesis ID' }),
787
+ marketId: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T150' }),
788
+ market: Type.String({ description: 'Human-readable market name' }),
789
+ direction: Type.String({ description: 'yes or no' }),
790
+ horizon: Type.Optional(Type.String({ description: 'short, medium, or long. Default: medium' })),
791
+ entryBelow: Type.Optional(Type.Number({ description: 'Entry trigger: ask <= this value (cents)' })),
792
+ entryAbove: Type.Optional(Type.Number({ description: 'Entry trigger: ask >= this value (cents, for NO direction)' })),
793
+ stopLoss: Type.Optional(Type.Number({ description: 'Stop loss: bid <= this value (cents)' })),
794
+ takeProfit: Type.Optional(Type.Number({ description: 'Take profit: bid >= this value (cents)' })),
795
+ maxQuantity: Type.Optional(Type.Number({ description: 'Max total contracts. Default: 500' })),
796
+ perOrderQuantity: Type.Optional(Type.Number({ description: 'Contracts per order. Default: 50' })),
797
+ softConditions: Type.Optional(Type.String({ description: 'LLM-evaluated conditions e.g. "only enter when n3 > 60%"' })),
798
+ rationale: Type.Optional(Type.String({ description: 'Full logic description' })),
799
+ }),
800
+ execute: async (_toolCallId, params) => {
801
+ const result = await sfClient.createStrategyAPI(params.thesisId, {
802
+ marketId: params.marketId,
803
+ market: params.market,
804
+ direction: params.direction,
805
+ horizon: params.horizon,
806
+ entryBelow: params.entryBelow,
807
+ entryAbove: params.entryAbove,
808
+ stopLoss: params.stopLoss,
809
+ takeProfit: params.takeProfit,
810
+ maxQuantity: params.maxQuantity,
811
+ perOrderQuantity: params.perOrderQuantity,
812
+ softConditions: params.softConditions,
813
+ rationale: params.rationale,
814
+ createdBy: 'agent',
815
+ });
816
+ return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
817
+ },
818
+ },
819
+ {
820
+ name: 'list_strategies',
821
+ label: 'List Strategies',
822
+ description: 'List strategies for a thesis. Filter by status (active/watching/executed/cancelled/review) or omit for all.',
823
+ parameters: Type.Object({
824
+ thesisId: Type.String({ description: 'Thesis ID' }),
825
+ status: Type.Optional(Type.String({ description: 'Filter by status. Omit for all.' })),
826
+ }),
827
+ execute: async (_toolCallId, params) => {
828
+ const result = await sfClient.getStrategies(params.thesisId, params.status);
829
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
830
+ },
831
+ },
832
+ {
833
+ name: 'update_strategy',
834
+ label: 'Update Strategy',
835
+ description: 'Update a strategy (change stop loss, take profit, status, priority, etc.)',
836
+ parameters: Type.Object({
837
+ thesisId: Type.String({ description: 'Thesis ID' }),
838
+ strategyId: Type.String({ description: 'Strategy ID (UUID)' }),
839
+ stopLoss: Type.Optional(Type.Number({ description: 'New stop loss (cents)' })),
840
+ takeProfit: Type.Optional(Type.Number({ description: 'New take profit (cents)' })),
841
+ entryBelow: Type.Optional(Type.Number({ description: 'New entry below trigger (cents)' })),
842
+ entryAbove: Type.Optional(Type.Number({ description: 'New entry above trigger (cents)' })),
843
+ status: Type.Optional(Type.String({ description: 'New status: active|watching|executed|cancelled|review' })),
844
+ priority: Type.Optional(Type.Number({ description: 'New priority' })),
845
+ softConditions: Type.Optional(Type.String({ description: 'Updated soft conditions' })),
846
+ rationale: Type.Optional(Type.String({ description: 'Updated rationale' })),
847
+ }),
848
+ execute: async (_toolCallId, params) => {
849
+ const { thesisId, strategyId, ...updates } = params;
850
+ const result = await sfClient.updateStrategyAPI(thesisId, strategyId, updates);
851
+ return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
852
+ },
853
+ },
854
+ {
855
+ name: 'get_milestones',
856
+ label: 'Milestones',
857
+ description: 'Get upcoming events from Kalshi calendar. Use to check economic releases, political events, or other catalysts coming up that might affect the thesis.',
858
+ parameters: Type.Object({
859
+ hours: Type.Optional(Type.Number({ description: 'Hours ahead to look (default 168 = 1 week)' })),
860
+ category: Type.Optional(Type.String({ description: 'Filter by category (e.g. Economics, Politics, Sports)' })),
861
+ }),
862
+ execute: async (_toolCallId, params) => {
863
+ const hours = params.hours || 168;
864
+ const now = new Date();
865
+ const url = `https://api.elections.kalshi.com/trade-api/v2/milestones?limit=200&minimum_start_date=${now.toISOString()}` +
866
+ (params.category ? `&category=${params.category}` : '');
867
+ const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
868
+ if (!res.ok)
869
+ return { content: [{ type: 'text', text: `Milestones API error: ${res.status}` }], details: {} };
870
+ const data = await res.json();
871
+ const cutoff = now.getTime() + hours * 3600000;
872
+ const filtered = (data.milestones || [])
873
+ .filter((m) => new Date(m.start_date).getTime() <= cutoff)
874
+ .slice(0, 30)
875
+ .map((m) => ({
876
+ title: m.title,
877
+ category: m.category,
878
+ start_date: m.start_date,
879
+ related_event_tickers: m.related_event_tickers,
880
+ hours_until: Math.round((new Date(m.start_date).getTime() - now.getTime()) / 3600000),
881
+ }));
882
+ return { content: [{ type: 'text', text: JSON.stringify(filtered, null, 2) }], details: {} };
883
+ },
884
+ },
885
+ {
886
+ name: 'get_forecast',
887
+ label: 'Forecast',
888
+ description: 'Get market distribution (P50/P75/P90 percentile history) for a Kalshi event. Shows how market consensus has shifted over time.',
889
+ parameters: Type.Object({
890
+ eventTicker: Type.String({ description: 'Kalshi event ticker (e.g. KXWTIMAX-26DEC31)' }),
891
+ days: Type.Optional(Type.Number({ description: 'Days of history (default 7)' })),
892
+ }),
893
+ execute: async (_toolCallId, params) => {
894
+ const { getForecastHistory } = await import('../kalshi.js');
895
+ const days = params.days || 7;
896
+ // Get series ticker from event
897
+ const evtRes = await fetch(`https://api.elections.kalshi.com/trade-api/v2/events/${params.eventTicker}`, { headers: { 'Accept': 'application/json' } });
898
+ if (!evtRes.ok)
899
+ return { content: [{ type: 'text', text: `Event not found: ${params.eventTicker}` }], details: {} };
900
+ const evtData = await evtRes.json();
901
+ const seriesTicker = evtData.event?.series_ticker;
902
+ if (!seriesTicker)
903
+ return { content: [{ type: 'text', text: `No series_ticker for ${params.eventTicker}` }], details: {} };
904
+ const history = await getForecastHistory({
905
+ seriesTicker,
906
+ eventTicker: params.eventTicker,
907
+ percentiles: [5000, 7500, 9000],
908
+ startTs: Math.floor((Date.now() - days * 86400000) / 1000),
909
+ endTs: Math.floor(Date.now() / 1000),
910
+ periodInterval: 1440,
911
+ });
912
+ if (!history || history.length === 0)
913
+ return { content: [{ type: 'text', text: 'No forecast data available' }], details: {} };
914
+ return { content: [{ type: 'text', text: JSON.stringify(history, null, 2) }], details: {} };
915
+ },
916
+ },
917
+ {
918
+ name: 'get_settlements',
919
+ label: 'Settlements',
920
+ description: 'Get settled (resolved) contracts with P&L. Shows which contracts won/lost and realized returns.',
921
+ parameters: Type.Object({
922
+ ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })),
923
+ }),
924
+ execute: async (_toolCallId, params) => {
925
+ const { getSettlements } = await import('../kalshi.js');
926
+ const result = await getSettlements({ limit: 100, ticker: params.ticker });
927
+ if (!result)
928
+ return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
929
+ return { content: [{ type: 'text', text: JSON.stringify(result.settlements, null, 2) }], details: {} };
930
+ },
931
+ },
932
+ {
933
+ name: 'get_balance',
934
+ label: 'Balance',
935
+ description: 'Get Kalshi account balance and portfolio value.',
936
+ parameters: emptyParams,
937
+ execute: async () => {
938
+ const { getBalance } = await import('../kalshi.js');
939
+ const result = await getBalance();
940
+ if (!result)
941
+ return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
942
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
943
+ },
944
+ },
945
+ {
946
+ name: 'get_orders',
947
+ label: 'Orders',
948
+ description: 'Get current resting orders on Kalshi.',
949
+ parameters: Type.Object({
950
+ status: Type.Optional(Type.String({ description: 'Filter by status: resting, canceled, executed. Default: resting' })),
951
+ }),
952
+ execute: async (_toolCallId, params) => {
953
+ const { getOrders } = await import('../kalshi.js');
954
+ const result = await getOrders({ status: params.status || 'resting', limit: 100 });
955
+ if (!result)
956
+ return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
957
+ return { content: [{ type: 'text', text: JSON.stringify(result.orders, null, 2) }], details: {} };
958
+ },
959
+ },
960
+ {
961
+ name: 'get_fills',
962
+ label: 'Fills',
963
+ description: 'Get recent trade fills (executed trades) on Kalshi.',
964
+ parameters: Type.Object({
965
+ ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })),
966
+ }),
967
+ execute: async (_toolCallId, params) => {
968
+ const { getFills } = await import('../kalshi.js');
969
+ const result = await getFills({ ticker: params.ticker, limit: 50 });
970
+ if (!result)
971
+ return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
972
+ return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
973
+ },
974
+ },
975
+ {
976
+ name: 'get_schedule',
977
+ label: 'Schedule',
978
+ description: 'Get exchange status (open/closed) and trading hours. Use to check if low liquidity is due to off-hours.',
979
+ parameters: emptyParams,
980
+ execute: async () => {
981
+ try {
982
+ const res = await fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } });
983
+ if (!res.ok)
984
+ return { content: [{ type: 'text', text: `Exchange API error: ${res.status}` }], details: {} };
985
+ const data = await res.json();
986
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
987
+ }
988
+ catch (err) {
989
+ return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
990
+ }
991
+ },
992
+ },
639
993
  ];
994
+ // ── What-if tool (always available) ────────────────────────────────────────
995
+ tools.push({
996
+ name: 'what_if',
997
+ label: 'What-If',
998
+ description: 'Run a what-if scenario: override causal tree node probabilities and see how edges and confidence change. Zero LLM cost — pure computation. Use when user asks "what if X happens?" or "what if this node drops to Y%?".',
999
+ parameters: Type.Object({
1000
+ overrides: Type.Array(Type.Object({
1001
+ nodeId: Type.String({ description: 'Causal tree node ID (e.g. n1, n3.1)' }),
1002
+ newProbability: Type.Number({ description: 'New probability 0-1' }),
1003
+ }), { description: 'Node probability overrides' }),
1004
+ }),
1005
+ execute: async (_toolCallId, params) => {
1006
+ // Inline what-if simulation
1007
+ const ctx = latestContext;
1008
+ const allNodes = [];
1009
+ function flatten(nodes) {
1010
+ for (const n of nodes) {
1011
+ allNodes.push(n);
1012
+ if (n.children?.length)
1013
+ flatten(n.children);
1014
+ }
1015
+ }
1016
+ const rawNodes = ctx.causalTree?.nodes || [];
1017
+ flatten(rawNodes);
1018
+ const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
1019
+ const overrideMap = new Map(params.overrides.map((o) => [o.nodeId, o.newProbability]));
1020
+ const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
1021
+ const newConf = treeNodes.reduce((s, n) => {
1022
+ const p = overrideMap.get(n.id) ?? n.probability ?? 0;
1023
+ return s + p * (n.importance || 0);
1024
+ }, 0);
1025
+ const nodeScales = new Map();
1026
+ for (const [nid, np] of overrideMap.entries()) {
1027
+ const nd = allNodes.find((n) => n.id === nid);
1028
+ if (nd && nd.probability > 0)
1029
+ nodeScales.set(nid, Math.max(0, Math.min(2, np / nd.probability)));
1030
+ }
1031
+ const edges = (ctx.edges || []).map((edge) => {
1032
+ const relNode = edge.relatedNodeId;
1033
+ let scaleFactor = 1;
1034
+ if (relNode) {
1035
+ const candidates = [relNode, relNode.split('.').slice(0, -1).join('.'), relNode.split('.')[0]].filter(Boolean);
1036
+ for (const cid of candidates) {
1037
+ if (nodeScales.has(cid)) {
1038
+ scaleFactor = nodeScales.get(cid);
1039
+ break;
1040
+ }
1041
+ }
1042
+ }
1043
+ const mkt = edge.marketPrice || 0;
1044
+ const oldTP = edge.thesisPrice || edge.thesisImpliedPrice || mkt;
1045
+ const oldEdge = edge.edge || edge.edgeSize || 0;
1046
+ const newTP = Math.round((mkt + (oldTP - mkt) * scaleFactor) * 100) / 100;
1047
+ const dir = edge.direction || 'yes';
1048
+ const newEdge = Math.round((dir === 'yes' ? newTP - mkt : mkt - newTP) * 100) / 100;
1049
+ return {
1050
+ market: edge.market || edge.marketTitle || edge.marketId,
1051
+ marketPrice: mkt,
1052
+ oldEdge,
1053
+ newEdge,
1054
+ delta: newEdge - oldEdge,
1055
+ signal: Math.abs(newEdge - oldEdge) < 1 ? 'unchanged' : (oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0) ? 'REVERSED' : Math.abs(newEdge) < 2 ? 'GONE' : 'reduced',
1056
+ };
1057
+ }).filter((e) => e.signal !== 'unchanged');
1058
+ const result = {
1059
+ overrides: params.overrides.map((o) => {
1060
+ const node = allNodes.find((n) => n.id === o.nodeId);
1061
+ return { nodeId: o.nodeId, label: node?.label || o.nodeId, oldProb: node?.probability, newProb: o.newProbability };
1062
+ }),
1063
+ confidence: { old: Math.round(oldConf * 100), new: Math.round(newConf * 100), delta: Math.round((newConf - oldConf) * 100) },
1064
+ affectedEdges: edges,
1065
+ };
1066
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
1067
+ },
1068
+ });
1069
+ // ── Trading tools (conditional on tradingEnabled) ──────────────────────────
1070
+ const config = (0, config_js_1.loadConfig)();
1071
+ if (config.tradingEnabled) {
1072
+ tools.push({
1073
+ name: 'place_order',
1074
+ label: 'Place Order',
1075
+ description: 'Place a buy or sell order on Kalshi. Shows a preview and asks for user confirmation before executing. Use for limit or market orders.',
1076
+ parameters: Type.Object({
1077
+ ticker: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T135' }),
1078
+ side: Type.String({ description: 'yes or no' }),
1079
+ action: Type.String({ description: 'buy or sell' }),
1080
+ type: Type.String({ description: 'limit or market' }),
1081
+ count: Type.Number({ description: 'Number of contracts' }),
1082
+ price_cents: Type.Optional(Type.Number({ description: 'Limit price in cents (1-99). Required for limit orders.' })),
1083
+ }),
1084
+ execute: async (_toolCallId, params) => {
1085
+ const { createOrder } = await import('../kalshi.js');
1086
+ const priceDollars = params.price_cents ? (params.price_cents / 100).toFixed(2) : undefined;
1087
+ const maxCost = ((params.price_cents || 99) * params.count / 100).toFixed(2);
1088
+ // Show preview
1089
+ const preview = [
1090
+ C.zinc200(bold('ORDER PREVIEW')),
1091
+ ` Ticker: ${params.ticker}`,
1092
+ ` Side: ${params.side === 'yes' ? C.emerald('YES') : C.red('NO')}`,
1093
+ ` Action: ${params.action.toUpperCase()}`,
1094
+ ` Quantity: ${params.count}`,
1095
+ ` Type: ${params.type}`,
1096
+ params.price_cents ? ` Price: ${params.price_cents}\u00A2` : '',
1097
+ ` Max cost: $${maxCost}`,
1098
+ ].filter(Boolean).join('\n');
1099
+ addSystemText(preview);
1100
+ addSpacer();
1101
+ tui.requestRender();
1102
+ // Ask for confirmation via promptUser
1103
+ const answer = await promptUser('Execute this order? (y/n)');
1104
+ if (!answer.toLowerCase().startsWith('y')) {
1105
+ return { content: [{ type: 'text', text: 'Order cancelled by user.' }], details: {} };
1106
+ }
1107
+ try {
1108
+ const result = await createOrder({
1109
+ ticker: params.ticker,
1110
+ side: params.side,
1111
+ action: params.action,
1112
+ type: params.type,
1113
+ count: params.count,
1114
+ ...(priceDollars ? { yes_price: priceDollars } : {}),
1115
+ });
1116
+ const order = result.order || result;
1117
+ return {
1118
+ content: [{ type: 'text', text: `Order placed: ${order.order_id || 'OK'}\nStatus: ${order.status || '-'}\nFilled: ${order.fill_count_fp || 0}/${order.initial_count_fp || params.count}` }],
1119
+ details: {},
1120
+ };
1121
+ }
1122
+ catch (err) {
1123
+ const msg = err.message || String(err);
1124
+ if (msg.includes('403')) {
1125
+ return { content: [{ type: 'text', text: '403 Forbidden \u2014 your Kalshi key lacks write permission. Get a read+write key at kalshi.com/account/api-keys' }], details: {} };
1126
+ }
1127
+ return { content: [{ type: 'text', text: `Order failed: ${msg}` }], details: {} };
1128
+ }
1129
+ },
1130
+ }, {
1131
+ name: 'cancel_order',
1132
+ label: 'Cancel Order',
1133
+ description: 'Cancel a resting order by order ID.',
1134
+ parameters: Type.Object({
1135
+ order_id: Type.String({ description: 'Order ID to cancel' }),
1136
+ }),
1137
+ execute: async (_toolCallId, params) => {
1138
+ const { cancelOrder } = await import('../kalshi.js');
1139
+ const answer = await promptUser(`Cancel order ${params.order_id}? (y/n)`);
1140
+ if (!answer.toLowerCase().startsWith('y')) {
1141
+ return { content: [{ type: 'text', text: 'Cancel aborted by user.' }], details: {} };
1142
+ }
1143
+ try {
1144
+ await cancelOrder(params.order_id);
1145
+ return { content: [{ type: 'text', text: `Order ${params.order_id} cancelled.` }], details: {} };
1146
+ }
1147
+ catch (err) {
1148
+ return { content: [{ type: 'text', text: `Cancel failed: ${err.message}` }], details: {} };
1149
+ }
1150
+ },
1151
+ });
1152
+ }
640
1153
  // ── System prompt builder ──────────────────────────────────────────────────
641
1154
  function buildSystemPrompt(ctx) {
642
1155
  const edgesSummary = ctx.edges
@@ -677,6 +1190,15 @@ Short-term markets (weekly/monthly contracts) settle into hard data that calibra
677
1190
  - For any question about prices, positions, or P&L, ALWAYS call a tool to get fresh data first. Never answer price-related questions using the cached data in this system prompt.
678
1191
  - Align tables. Be precise with numbers to the cent.
679
1192
 
1193
+ ## Strategy rules
1194
+
1195
+ When the conversation produces a concrete trade idea (specific contract, direction, price conditions), use create_strategy to record it immediately. Don't wait for the user to say "record this."
1196
+ - Extract hard conditions (specific prices in cents) into entryBelow/stopLoss/takeProfit.
1197
+ - Put fuzzy conditions into softConditions (e.g. "only if n3 > 60%", "spread < 3¢").
1198
+ - Put the full reasoning into rationale.
1199
+ - After creating, confirm the strategy details and mention that sf runtime --dangerous can execute it.
1200
+ - If the user says "change the stop loss on T150 to 30", use update_strategy.
1201
+
680
1202
  ## Current thesis state
681
1203
 
682
1204
  Thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
@@ -853,6 +1375,11 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
853
1375
  C.emerald('/new ') + C.zinc400('Start fresh session') + '\n' +
854
1376
  C.emerald('/model <m> ') + C.zinc400('Switch model') + '\n' +
855
1377
  C.emerald('/env ') + C.zinc400('Show environment variable status') + '\n' +
1378
+ (config.tradingEnabled ? (C.zinc600('\u2500'.repeat(30)) + '\n' +
1379
+ C.emerald('/buy ') + C.zinc400('TICKER QTY PRICE \u2014 quick buy') + '\n' +
1380
+ C.emerald('/sell ') + C.zinc400('TICKER QTY PRICE \u2014 quick sell') + '\n' +
1381
+ C.emerald('/cancel ') + C.zinc400('ORDER_ID \u2014 cancel order') + '\n' +
1382
+ C.zinc600('\u2500'.repeat(30)) + '\n') : '') +
856
1383
  C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
857
1384
  C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
858
1385
  addSpacer();
@@ -936,7 +1463,8 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
936
1463
  model = resolveModel(currentModelName);
937
1464
  // Update agent model
938
1465
  agent.setModel(model);
939
- headerBar.update(undefined, undefined, C.zinc600(currentModelName.split('/').pop() || currentModelName));
1466
+ footerBar.modelName = currentModelName;
1467
+ footerBar.update();
940
1468
  addSystemText(C.emerald(`Model switched to ${currentModelName}`));
941
1469
  addSpacer();
942
1470
  tui.requestRender();
@@ -976,13 +1504,9 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
976
1504
  addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
977
1505
  }
978
1506
  // Update header
979
- headerBar.update(C.emerald(bold('SF Agent')) + C.zinc600(` \u2014 ${resolvedThesisId.slice(0, 8)}`), newConf > 0 ? C.zinc200(`${newConf}%`) : '', undefined);
1507
+ headerBar.setFromContext(newContext, initialPositions || undefined);
980
1508
  chatContainer.clear();
981
- const thText = (newContext.thesis || newContext.rawThesis || '').slice(0, 120);
982
- addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
983
- C.zinc200(bold(thText)) + '\n' +
984
- C.zinc600(`${newContext.status || 'active'} ${newConf > 0 ? newConf + '%' : ''} ${(newContext.edges || []).length} edges`) + '\n' +
985
- C.zinc600('\u2500'.repeat(50)));
1509
+ addSystemText(buildWelcomeDashboard(newContext, initialPositions));
986
1510
  }
987
1511
  catch (err) {
988
1512
  addSystemText(C.red(`Switch failed: ${err.message}`));
@@ -1195,6 +1719,93 @@ Output a structured summary. Be concise but preserve every important detail —
1195
1719
  tui.requestRender();
1196
1720
  return true;
1197
1721
  }
1722
+ case '/buy': {
1723
+ // /buy TICKER QTY PRICE — quick trade without LLM
1724
+ const [, ticker, qtyStr, priceStr] = parts;
1725
+ if (!ticker || !qtyStr || !priceStr) {
1726
+ addSystemText(C.zinc400('Usage: /buy TICKER QTY PRICE_CENTS (e.g. /buy KXWTIMAX-26DEC31-T135 100 50)'));
1727
+ return true;
1728
+ }
1729
+ if (!config.tradingEnabled) {
1730
+ addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
1731
+ return true;
1732
+ }
1733
+ addSpacer();
1734
+ const answer = await promptUser(`BUY ${qtyStr}x ${ticker} YES @ ${priceStr}\u00A2 — execute? (y/n)`);
1735
+ if (answer.toLowerCase().startsWith('y')) {
1736
+ try {
1737
+ const { createOrder } = await import('../kalshi.js');
1738
+ const result = await createOrder({
1739
+ ticker, side: 'yes', action: 'buy', type: 'limit',
1740
+ count: parseInt(qtyStr),
1741
+ yes_price: (parseInt(priceStr) / 100).toFixed(2),
1742
+ });
1743
+ addSystemText(C.emerald('\u2713 Order placed: ' + ((result.order || result).order_id || 'OK')));
1744
+ }
1745
+ catch (err) {
1746
+ addSystemText(C.red('\u2717 ' + err.message));
1747
+ }
1748
+ }
1749
+ else {
1750
+ addSystemText(C.zinc400('Cancelled.'));
1751
+ }
1752
+ addSpacer();
1753
+ return true;
1754
+ }
1755
+ case '/sell': {
1756
+ const [, ticker, qtyStr, priceStr] = parts;
1757
+ if (!ticker || !qtyStr || !priceStr) {
1758
+ addSystemText(C.zinc400('Usage: /sell TICKER QTY PRICE_CENTS'));
1759
+ return true;
1760
+ }
1761
+ if (!config.tradingEnabled) {
1762
+ addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
1763
+ return true;
1764
+ }
1765
+ addSpacer();
1766
+ const answer = await promptUser(`SELL ${qtyStr}x ${ticker} YES @ ${priceStr}\u00A2 — execute? (y/n)`);
1767
+ if (answer.toLowerCase().startsWith('y')) {
1768
+ try {
1769
+ const { createOrder } = await import('../kalshi.js');
1770
+ const result = await createOrder({
1771
+ ticker, side: 'yes', action: 'sell', type: 'limit',
1772
+ count: parseInt(qtyStr),
1773
+ yes_price: (parseInt(priceStr) / 100).toFixed(2),
1774
+ });
1775
+ addSystemText(C.emerald('\u2713 Order placed: ' + ((result.order || result).order_id || 'OK')));
1776
+ }
1777
+ catch (err) {
1778
+ addSystemText(C.red('\u2717 ' + err.message));
1779
+ }
1780
+ }
1781
+ else {
1782
+ addSystemText(C.zinc400('Cancelled.'));
1783
+ }
1784
+ addSpacer();
1785
+ return true;
1786
+ }
1787
+ case '/cancel': {
1788
+ const [, orderId] = parts;
1789
+ if (!orderId) {
1790
+ addSystemText(C.zinc400('Usage: /cancel ORDER_ID'));
1791
+ return true;
1792
+ }
1793
+ if (!config.tradingEnabled) {
1794
+ addSystemText(C.red('Trading disabled. Run: sf setup --enable-trading'));
1795
+ return true;
1796
+ }
1797
+ addSpacer();
1798
+ try {
1799
+ const { cancelOrder } = await import('../kalshi.js');
1800
+ await cancelOrder(orderId);
1801
+ addSystemText(C.emerald(`\u2713 Order ${orderId} cancelled.`));
1802
+ }
1803
+ catch (err) {
1804
+ addSystemText(C.red('\u2717 ' + err.message));
1805
+ }
1806
+ addSpacer();
1807
+ return true;
1808
+ }
1198
1809
  case '/exit':
1199
1810
  case '/quit': {
1200
1811
  cleanup();
@@ -1209,6 +1820,17 @@ Output a structured summary. Be concise but preserve every important detail —
1209
1820
  const trimmed = input.trim();
1210
1821
  if (!trimmed)
1211
1822
  return;
1823
+ // If a tool is waiting for user confirmation, resolve it
1824
+ if (pendingPrompt) {
1825
+ const { resolve } = pendingPrompt;
1826
+ pendingPrompt = null;
1827
+ const userResponse = new Text(C.zinc400(' > ') + C.zinc200(trimmed), 1, 0);
1828
+ chatContainer.addChild(userResponse);
1829
+ addSpacer();
1830
+ tui.requestRender();
1831
+ resolve(trimmed);
1832
+ return;
1833
+ }
1212
1834
  if (isProcessing)
1213
1835
  return;
1214
1836
  // Add to editor history
@@ -1261,17 +1883,489 @@ Output a structured summary. Be concise but preserve every important detail —
1261
1883
  // Also handle SIGINT
1262
1884
  process.on('SIGINT', cleanup);
1263
1885
  process.on('SIGTERM', cleanup);
1886
+ // ── Welcome dashboard builder ────────────────────────────────────────────
1887
+ function buildWelcomeDashboard(ctx, positions) {
1888
+ const lines = [];
1889
+ const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
1890
+ const truncated = thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText;
1891
+ const conf = typeof ctx.confidence === 'number'
1892
+ ? Math.round(ctx.confidence * 100)
1893
+ : (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
1894
+ const delta = ctx.lastEvaluation?.confidenceDelta
1895
+ ? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
1896
+ : 0;
1897
+ const deltaStr = delta !== 0 ? ` (${delta > 0 ? '+' : ''}${delta})` : '';
1898
+ const evalAge = ctx.lastEvaluation?.evaluatedAt
1899
+ ? Math.round((Date.now() - new Date(ctx.lastEvaluation.evaluatedAt).getTime()) / 3600000)
1900
+ : null;
1901
+ lines.push(C.zinc600('\u2500'.repeat(55)));
1902
+ lines.push(' ' + C.zinc200(bold(truncated)));
1903
+ lines.push(' ' + C.zinc600(`${ctx.status || 'active'} ${conf}%${deltaStr}`) +
1904
+ (evalAge !== null ? C.zinc600(` \u2502 last eval: ${evalAge < 1 ? '<1' : evalAge}h ago`) : ''));
1905
+ lines.push(C.zinc600('\u2500'.repeat(55)));
1906
+ // Positions section
1907
+ if (positions && positions.length > 0) {
1908
+ lines.push(' ' + C.zinc400(bold('POSITIONS')));
1909
+ let totalPnl = 0;
1910
+ for (const p of positions) {
1911
+ const pnlCents = p.unrealized_pnl || 0;
1912
+ totalPnl += pnlCents;
1913
+ const pnlStr = pnlCents >= 0
1914
+ ? C.emerald(`+$${(pnlCents / 100).toFixed(2)}`)
1915
+ : C.red(`-$${(Math.abs(pnlCents) / 100).toFixed(2)}`);
1916
+ const ticker = (p.ticker || '').slice(0, 28).padEnd(28);
1917
+ const qty = String(p.quantity || 0).padStart(5);
1918
+ const side = p.side === 'yes' ? C.emerald('Y') : C.red('N');
1919
+ lines.push(` ${C.zinc400(ticker)} ${qty} ${side} ${pnlStr}`);
1920
+ }
1921
+ const totalStr = totalPnl >= 0
1922
+ ? C.emerald(bold(`+$${(totalPnl / 100).toFixed(2)}`))
1923
+ : C.red(bold(`-$${(Math.abs(totalPnl) / 100).toFixed(2)}`));
1924
+ lines.push(` ${''.padEnd(28)} ${C.zinc600('Total')} ${totalStr}`);
1925
+ }
1926
+ // Top edges section
1927
+ const edges = ctx.edges || [];
1928
+ if (edges.length > 0) {
1929
+ const sorted = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0)).slice(0, 5);
1930
+ lines.push(C.zinc600('\u2500'.repeat(55)));
1931
+ lines.push(' ' + C.zinc400(bold('TOP EDGES')) + C.zinc600(' mkt edge liq'));
1932
+ for (const e of sorted) {
1933
+ const name = (e.market || e.marketTitle || e.marketId || '').slice(0, 30).padEnd(30);
1934
+ const mkt = String(Math.round(e.marketPrice || 0)).padStart(3) + '\u00A2';
1935
+ const edge = e.edge || e.edgeSize || 0;
1936
+ const edgeStr = (edge > 0 ? '+' : '') + Math.round(edge);
1937
+ const liq = e.orderbook?.liquidityScore || (e.venue === 'polymarket' ? '-' : '?');
1938
+ const edgeColor = Math.abs(edge) >= 15 ? C.emerald : Math.abs(edge) >= 8 ? C.amber : C.zinc400;
1939
+ lines.push(` ${C.zinc400(name)} ${C.zinc400(mkt)} ${edgeColor(edgeStr.padStart(4))} ${C.zinc600(liq)}`);
1940
+ }
1941
+ }
1942
+ lines.push(C.zinc600('\u2500'.repeat(55)));
1943
+ return lines.join('\n');
1944
+ }
1264
1945
  // ── Show initial welcome ───────────────────────────────────────────────────
1265
- const thesisText = latestContext.thesis || latestContext.rawThesis || 'N/A';
1266
- const truncatedThesis = thesisText.length > 120 ? thesisText.slice(0, 120) + '...' : thesisText;
1267
1946
  const sessionStatus = sessionRestored
1268
- ? C.zinc600(` resumed (${agent.state.messages.length} messages)`)
1269
- : C.zinc600(' new session');
1270
- addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
1271
- C.zinc200(bold(truncatedThesis)) + '\n' +
1272
- C.zinc600(`${latestContext.status || 'active'} ${confidencePct > 0 ? confidencePct + '%' : ''} ${(latestContext.edges || []).length} edges`) + sessionStatus + '\n' +
1273
- C.zinc600('\u2500'.repeat(50)));
1947
+ ? C.zinc600(`resumed (${agent.state.messages.length} messages)`)
1948
+ : C.zinc600('new session');
1949
+ addSystemText(buildWelcomeDashboard(latestContext, initialPositions));
1950
+ addSystemText(' ' + sessionStatus);
1274
1951
  addSpacer();
1275
1952
  // ── Start TUI ──────────────────────────────────────────────────────────────
1276
1953
  tui.start();
1277
1954
  }
1955
+ // ============================================================================
1956
+ // PLAIN-TEXT MODE (--no-tui)
1957
+ // ============================================================================
1958
+ async function runPlainTextAgent(params) {
1959
+ const { openrouterKey, sfClient, resolvedThesisId, opts } = params;
1960
+ let latestContext = params.latestContext;
1961
+ const readline = await import('readline');
1962
+ const piAi = await import('@mariozechner/pi-ai');
1963
+ const piAgent = await import('@mariozechner/pi-agent-core');
1964
+ const { getModel, streamSimple, Type } = piAi;
1965
+ const { Agent } = piAgent;
1966
+ const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
1967
+ let currentModelName = rawModelName.replace(/^openrouter\//, '');
1968
+ function resolveModel(name) {
1969
+ try {
1970
+ return getModel('openrouter', name);
1971
+ }
1972
+ catch {
1973
+ return {
1974
+ modelId: name, provider: 'openrouter', api: 'openai-completions',
1975
+ baseUrl: 'https://openrouter.ai/api/v1', id: name, name,
1976
+ inputPrice: 0, outputPrice: 0, contextWindow: 200000,
1977
+ supportsImages: true, supportsTools: true,
1978
+ };
1979
+ }
1980
+ }
1981
+ let model = resolveModel(currentModelName);
1982
+ // ── Tools (same definitions as TUI mode) ──────────────────────────────────
1983
+ const thesisIdParam = Type.Object({ thesisId: Type.String({ description: 'Thesis ID' }) });
1984
+ const signalParams = Type.Object({
1985
+ thesisId: Type.String({ description: 'Thesis ID' }),
1986
+ content: Type.String({ description: 'Signal content' }),
1987
+ type: Type.Optional(Type.String({ description: 'Signal type: news, user_note, external' })),
1988
+ });
1989
+ const scanParams = Type.Object({
1990
+ query: Type.Optional(Type.String({ description: 'Keyword search' })),
1991
+ series: Type.Optional(Type.String({ description: 'Series ticker' })),
1992
+ market: Type.Optional(Type.String({ description: 'Market ticker' })),
1993
+ });
1994
+ const webSearchParams = Type.Object({ query: Type.String({ description: 'Search keywords' }) });
1995
+ const emptyParams = Type.Object({});
1996
+ const tools = [
1997
+ {
1998
+ name: 'get_context', label: 'Get Context',
1999
+ description: 'Get thesis snapshot: causal tree, edge prices, last evaluation, confidence',
2000
+ parameters: thesisIdParam,
2001
+ execute: async (_id, p) => {
2002
+ const ctx = await sfClient.getContext(p.thesisId);
2003
+ latestContext = ctx;
2004
+ return { content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }], details: {} };
2005
+ },
2006
+ },
2007
+ {
2008
+ name: 'inject_signal', label: 'Inject Signal',
2009
+ description: 'Inject a signal into the thesis',
2010
+ parameters: signalParams,
2011
+ execute: async (_id, p) => {
2012
+ const result = await sfClient.injectSignal(p.thesisId, p.type || 'user_note', p.content);
2013
+ return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
2014
+ },
2015
+ },
2016
+ {
2017
+ name: 'trigger_evaluation', label: 'Evaluate',
2018
+ description: 'Trigger a deep evaluation cycle',
2019
+ parameters: thesisIdParam,
2020
+ execute: async (_id, p) => {
2021
+ const result = await sfClient.evaluate(p.thesisId);
2022
+ return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
2023
+ },
2024
+ },
2025
+ {
2026
+ name: 'scan_markets', label: 'Scan Markets',
2027
+ description: 'Search Kalshi prediction markets',
2028
+ parameters: scanParams,
2029
+ execute: async (_id, p) => {
2030
+ let result;
2031
+ if (p.market) {
2032
+ result = await (0, client_js_1.kalshiFetchMarket)(p.market);
2033
+ }
2034
+ else if (p.series) {
2035
+ result = await (0, client_js_1.kalshiFetchMarketsBySeries)(p.series);
2036
+ }
2037
+ else if (p.query) {
2038
+ const series = await (0, client_js_1.kalshiFetchAllSeries)();
2039
+ const kws = p.query.toLowerCase().split(/\s+/);
2040
+ result = series.filter((s) => kws.every((k) => ((s.title || '') + (s.ticker || '')).toLowerCase().includes(k))).slice(0, 15);
2041
+ }
2042
+ else {
2043
+ result = { error: 'Provide query, series, or market' };
2044
+ }
2045
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
2046
+ },
2047
+ },
2048
+ {
2049
+ name: 'list_theses', label: 'List Theses',
2050
+ description: 'List all theses',
2051
+ parameters: emptyParams,
2052
+ execute: async () => {
2053
+ const theses = await sfClient.listTheses();
2054
+ return { content: [{ type: 'text', text: JSON.stringify(theses, null, 2) }], details: {} };
2055
+ },
2056
+ },
2057
+ {
2058
+ name: 'get_positions', label: 'Get Positions',
2059
+ description: 'Get Kalshi positions with live prices',
2060
+ parameters: emptyParams,
2061
+ execute: async () => {
2062
+ const positions = await (0, kalshi_js_1.getPositions)();
2063
+ if (!positions)
2064
+ return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
2065
+ for (const pos of positions) {
2066
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
2067
+ if (livePrice !== null) {
2068
+ pos.current_value = livePrice;
2069
+ pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
2070
+ }
2071
+ }
2072
+ return { content: [{ type: 'text', text: JSON.stringify(positions, null, 2) }], details: {} };
2073
+ },
2074
+ },
2075
+ {
2076
+ name: 'web_search', label: 'Web Search',
2077
+ description: 'Search latest news and information',
2078
+ parameters: webSearchParams,
2079
+ execute: async (_id, p) => {
2080
+ const apiKey = process.env.TAVILY_API_KEY;
2081
+ if (!apiKey)
2082
+ return { content: [{ type: 'text', text: 'Tavily not configured. Set TAVILY_API_KEY.' }], details: {} };
2083
+ const res = await fetch('https://api.tavily.com/search', {
2084
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2085
+ body: JSON.stringify({ api_key: apiKey, query: p.query, max_results: 5, search_depth: 'basic', include_answer: true }),
2086
+ });
2087
+ if (!res.ok)
2088
+ return { content: [{ type: 'text', text: `Search failed: ${res.status}` }], details: {} };
2089
+ const data = await res.json();
2090
+ const results = (data.results || []).map((r) => `[${r.title}](${r.url})\n${r.content?.slice(0, 200)}`).join('\n\n');
2091
+ const answer = data.answer ? `Summary: ${data.answer}\n\n---\n\n` : '';
2092
+ return { content: [{ type: 'text', text: `${answer}${results}` }], details: {} };
2093
+ },
2094
+ },
2095
+ {
2096
+ name: 'get_milestones', label: 'Milestones',
2097
+ description: 'Get upcoming events from Kalshi calendar. Use to check economic releases, political events, or other catalysts.',
2098
+ parameters: Type.Object({
2099
+ hours: Type.Optional(Type.Number({ description: 'Hours ahead to look (default 168 = 1 week)' })),
2100
+ category: Type.Optional(Type.String({ description: 'Filter by category (e.g. Economics, Politics, Sports)' })),
2101
+ }),
2102
+ execute: async (_id, p) => {
2103
+ const hours = p.hours || 168;
2104
+ const now = new Date();
2105
+ const url = `https://api.elections.kalshi.com/trade-api/v2/milestones?limit=200&minimum_start_date=${now.toISOString()}` +
2106
+ (p.category ? `&category=${p.category}` : '');
2107
+ const res = await fetch(url, { headers: { 'Accept': 'application/json' } });
2108
+ if (!res.ok)
2109
+ return { content: [{ type: 'text', text: `Milestones API error: ${res.status}` }], details: {} };
2110
+ const data = await res.json();
2111
+ const cutoff = now.getTime() + hours * 3600000;
2112
+ const filtered = (data.milestones || [])
2113
+ .filter((m) => new Date(m.start_date).getTime() <= cutoff)
2114
+ .slice(0, 30)
2115
+ .map((m) => ({
2116
+ title: m.title, category: m.category, start_date: m.start_date,
2117
+ related_event_tickers: m.related_event_tickers,
2118
+ hours_until: Math.round((new Date(m.start_date).getTime() - now.getTime()) / 3600000),
2119
+ }));
2120
+ return { content: [{ type: 'text', text: JSON.stringify(filtered, null, 2) }], details: {} };
2121
+ },
2122
+ },
2123
+ {
2124
+ name: 'get_forecast', label: 'Forecast',
2125
+ description: 'Get market distribution (P50/P75/P90 percentile history) for a Kalshi event.',
2126
+ parameters: Type.Object({
2127
+ eventTicker: Type.String({ description: 'Kalshi event ticker (e.g. KXWTIMAX-26DEC31)' }),
2128
+ days: Type.Optional(Type.Number({ description: 'Days of history (default 7)' })),
2129
+ }),
2130
+ execute: async (_id, p) => {
2131
+ const { getForecastHistory } = await import('../kalshi.js');
2132
+ const days = p.days || 7;
2133
+ const evtRes = await fetch(`https://api.elections.kalshi.com/trade-api/v2/events/${p.eventTicker}`, { headers: { 'Accept': 'application/json' } });
2134
+ if (!evtRes.ok)
2135
+ return { content: [{ type: 'text', text: `Event not found: ${p.eventTicker}` }], details: {} };
2136
+ const evtData = await evtRes.json();
2137
+ const seriesTicker = evtData.event?.series_ticker;
2138
+ if (!seriesTicker)
2139
+ return { content: [{ type: 'text', text: `No series_ticker for ${p.eventTicker}` }], details: {} };
2140
+ const history = await getForecastHistory({
2141
+ seriesTicker, eventTicker: p.eventTicker, percentiles: [5000, 7500, 9000],
2142
+ startTs: Math.floor((Date.now() - days * 86400000) / 1000),
2143
+ endTs: Math.floor(Date.now() / 1000), periodInterval: 1440,
2144
+ });
2145
+ if (!history || history.length === 0)
2146
+ return { content: [{ type: 'text', text: 'No forecast data available' }], details: {} };
2147
+ return { content: [{ type: 'text', text: JSON.stringify(history, null, 2) }], details: {} };
2148
+ },
2149
+ },
2150
+ {
2151
+ name: 'get_settlements', label: 'Settlements',
2152
+ description: 'Get settled (resolved) contracts with P&L.',
2153
+ parameters: Type.Object({ ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })) }),
2154
+ execute: async (_id, p) => {
2155
+ const { getSettlements } = await import('../kalshi.js');
2156
+ const result = await getSettlements({ limit: 100, ticker: p.ticker });
2157
+ if (!result)
2158
+ return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
2159
+ return { content: [{ type: 'text', text: JSON.stringify(result.settlements, null, 2) }], details: {} };
2160
+ },
2161
+ },
2162
+ {
2163
+ name: 'get_balance', label: 'Balance',
2164
+ description: 'Get Kalshi account balance and portfolio value.',
2165
+ parameters: emptyParams,
2166
+ execute: async () => {
2167
+ const { getBalance } = await import('../kalshi.js');
2168
+ const result = await getBalance();
2169
+ if (!result)
2170
+ return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
2171
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
2172
+ },
2173
+ },
2174
+ {
2175
+ name: 'get_orders', label: 'Orders',
2176
+ description: 'Get current resting orders on Kalshi.',
2177
+ parameters: Type.Object({ status: Type.Optional(Type.String({ description: 'Filter by status: resting, canceled, executed. Default: resting' })) }),
2178
+ execute: async (_id, p) => {
2179
+ const { getOrders } = await import('../kalshi.js');
2180
+ const result = await getOrders({ status: p.status || 'resting', limit: 100 });
2181
+ if (!result)
2182
+ return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
2183
+ return { content: [{ type: 'text', text: JSON.stringify(result.orders, null, 2) }], details: {} };
2184
+ },
2185
+ },
2186
+ {
2187
+ name: 'get_fills', label: 'Fills',
2188
+ description: 'Get recent trade fills (executed trades) on Kalshi.',
2189
+ parameters: Type.Object({ ticker: Type.Optional(Type.String({ description: 'Filter by market ticker' })) }),
2190
+ execute: async (_id, p) => {
2191
+ const { getFills } = await import('../kalshi.js');
2192
+ const result = await getFills({ ticker: p.ticker, limit: 50 });
2193
+ if (!result)
2194
+ return { content: [{ type: 'text', text: 'Kalshi not configured.' }], details: {} };
2195
+ return { content: [{ type: 'text', text: JSON.stringify(result.fills, null, 2) }], details: {} };
2196
+ },
2197
+ },
2198
+ {
2199
+ name: 'get_schedule',
2200
+ label: 'Schedule',
2201
+ description: 'Get exchange status (open/closed) and trading hours. Use to check if low liquidity is due to off-hours.',
2202
+ parameters: emptyParams,
2203
+ execute: async () => {
2204
+ try {
2205
+ const res = await fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } });
2206
+ if (!res.ok)
2207
+ return { content: [{ type: 'text', text: `Exchange API error: ${res.status}` }], details: {} };
2208
+ const data = await res.json();
2209
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
2210
+ }
2211
+ catch (err) {
2212
+ return { content: [{ type: 'text', text: `Failed: ${err.message}` }], details: {} };
2213
+ }
2214
+ },
2215
+ },
2216
+ ];
2217
+ // ── System prompt ─────────────────────────────────────────────────────────
2218
+ const ctx = latestContext;
2219
+ const edgesSummary = ctx.edges
2220
+ ?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
2221
+ .slice(0, 5)
2222
+ .map((e) => ` ${(e.market || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}\u00A2 | edge ${e.edge > 0 ? '+' : ''}${e.edge}`)
2223
+ .join('\n') || ' (no edges)';
2224
+ const nodesSummary = ctx.causalTree?.nodes
2225
+ ?.filter((n) => n.depth === 0)
2226
+ .map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
2227
+ .join('\n') || ' (no causal tree)';
2228
+ const conf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 0;
2229
+ const systemPrompt = `You are a prediction market trading assistant. Help the user make correct trading decisions.
2230
+
2231
+ Current thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
2232
+ ID: ${resolvedThesisId}
2233
+ Confidence: ${conf}%
2234
+ Status: ${ctx.status}
2235
+
2236
+ Causal tree nodes:
2237
+ ${nodesSummary}
2238
+
2239
+ Top edges:
2240
+ ${edgesSummary}
2241
+
2242
+ ${ctx.lastEvaluation?.summary ? `Latest evaluation: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}
2243
+
2244
+ Rules: Be concise. Use tools when needed. Don't ask "anything else?".`;
2245
+ // ── Create agent ──────────────────────────────────────────────────────────
2246
+ const agent = new Agent({
2247
+ initialState: { systemPrompt, model, tools, thinkingLevel: 'off' },
2248
+ streamFn: streamSimple,
2249
+ getApiKey: (provider) => provider === 'openrouter' ? openrouterKey : undefined,
2250
+ });
2251
+ // ── Session restore ───────────────────────────────────────────────────────
2252
+ if (!opts?.newSession) {
2253
+ const saved = loadSession(resolvedThesisId);
2254
+ if (saved?.messages?.length > 0) {
2255
+ try {
2256
+ agent.replaceMessages(saved.messages);
2257
+ agent.setSystemPrompt(systemPrompt);
2258
+ }
2259
+ catch { /* start fresh */ }
2260
+ }
2261
+ }
2262
+ // ── Subscribe to agent events → plain stdout ──────────────────────────────
2263
+ let currentText = '';
2264
+ agent.subscribe((event) => {
2265
+ if (event.type === 'message_update') {
2266
+ const e = event.assistantMessageEvent;
2267
+ if (e.type === 'text_delta') {
2268
+ process.stdout.write(e.delta);
2269
+ currentText += e.delta;
2270
+ }
2271
+ }
2272
+ if (event.type === 'message_end') {
2273
+ if (currentText) {
2274
+ process.stdout.write('\n');
2275
+ currentText = '';
2276
+ }
2277
+ }
2278
+ if (event.type === 'tool_execution_start') {
2279
+ process.stderr.write(` \u26A1 ${event.toolName}...\n`);
2280
+ }
2281
+ if (event.type === 'tool_execution_end') {
2282
+ const status = event.isError ? '\u2717' : '\u2713';
2283
+ process.stderr.write(` ${status} ${event.toolName}\n`);
2284
+ }
2285
+ });
2286
+ // ── Welcome ───────────────────────────────────────────────────────────────
2287
+ const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
2288
+ console.log(`SF Agent — ${resolvedThesisId.slice(0, 8)} | ${conf}% | ${currentModelName}`);
2289
+ console.log(`Thesis: ${thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText}`);
2290
+ console.log(`Edges: ${(ctx.edges || []).length} | Status: ${ctx.status}`);
2291
+ console.log('Type /help for commands, /exit to quit.\n');
2292
+ // ── REPL loop ─────────────────────────────────────────────────────────────
2293
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout, prompt: '> ' });
2294
+ rl.prompt();
2295
+ for await (const line of rl) {
2296
+ const trimmed = line.trim();
2297
+ if (!trimmed) {
2298
+ rl.prompt();
2299
+ continue;
2300
+ }
2301
+ if (trimmed === '/exit' || trimmed === '/quit') {
2302
+ try {
2303
+ saveSession(resolvedThesisId, currentModelName, agent.state.messages);
2304
+ }
2305
+ catch { }
2306
+ rl.close();
2307
+ return;
2308
+ }
2309
+ if (trimmed === '/help') {
2310
+ console.log('Commands: /help /exit /tree /edges /eval /model <name>');
2311
+ rl.prompt();
2312
+ continue;
2313
+ }
2314
+ if (trimmed === '/tree') {
2315
+ latestContext = await sfClient.getContext(resolvedThesisId);
2316
+ const nodes = latestContext.causalTree?.nodes || [];
2317
+ for (const n of nodes) {
2318
+ const indent = ' '.repeat(n.depth || 0);
2319
+ console.log(`${indent}${n.id} ${(n.label || '').slice(0, 60)} — ${Math.round(n.probability * 100)}%`);
2320
+ }
2321
+ rl.prompt();
2322
+ continue;
2323
+ }
2324
+ if (trimmed === '/edges') {
2325
+ latestContext = await sfClient.getContext(resolvedThesisId);
2326
+ const edges = (latestContext.edges || []).sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge)).slice(0, 15);
2327
+ for (const e of edges) {
2328
+ const sign = e.edge > 0 ? '+' : '';
2329
+ console.log(` ${(e.market || '').slice(0, 45).padEnd(45)} ${e.marketPrice}¢ edge ${sign}${e.edge} ${e.venue}`);
2330
+ }
2331
+ rl.prompt();
2332
+ continue;
2333
+ }
2334
+ if (trimmed === '/eval') {
2335
+ console.log('Triggering evaluation...');
2336
+ const result = await sfClient.evaluate(resolvedThesisId);
2337
+ console.log(`Confidence: ${result.previousConfidence} → ${result.newConfidence}`);
2338
+ if (result.summary)
2339
+ console.log(result.summary);
2340
+ rl.prompt();
2341
+ continue;
2342
+ }
2343
+ if (trimmed.startsWith('/model')) {
2344
+ const newModel = trimmed.slice(6).trim();
2345
+ if (!newModel) {
2346
+ console.log(`Current: ${currentModelName}`);
2347
+ rl.prompt();
2348
+ continue;
2349
+ }
2350
+ currentModelName = newModel.replace(/^openrouter\//, '');
2351
+ model = resolveModel(currentModelName);
2352
+ agent.setModel(model);
2353
+ console.log(`Model: ${currentModelName}`);
2354
+ rl.prompt();
2355
+ continue;
2356
+ }
2357
+ // Regular message → agent
2358
+ try {
2359
+ await agent.prompt(trimmed);
2360
+ }
2361
+ catch (err) {
2362
+ console.error(`Error: ${err.message}`);
2363
+ }
2364
+ // Save after each turn
2365
+ try {
2366
+ saveSession(resolvedThesisId, currentModelName, agent.state.messages);
2367
+ }
2368
+ catch { }
2369
+ rl.prompt();
2370
+ }
2371
+ }