@spfunctions/cli 1.1.6 → 1.1.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -17,6 +17,8 @@ export declare class SFClient {
17
17
  createThesis(rawThesis: string, sync?: boolean): Promise<any>;
18
18
  injectSignal(id: string, type: string, content: string, source?: string): Promise<any>;
19
19
  evaluate(id: string): Promise<any>;
20
+ getFeed(hours?: number, limit?: number): Promise<any>;
21
+ getChanges(id: string, since: string): Promise<any>;
20
22
  updateThesis(id: string, data: Record<string, unknown>): Promise<any>;
21
23
  publish(id: string, slug: string, description?: string): Promise<any>;
22
24
  unpublish(id: string): Promise<any>;
package/dist/client.js CHANGED
@@ -62,6 +62,12 @@ class SFClient {
62
62
  async evaluate(id) {
63
63
  return this.request('POST', `/api/thesis/${id}/evaluate`);
64
64
  }
65
+ async getFeed(hours = 24, limit = 200) {
66
+ return this.request('GET', `/api/feed?hours=${hours}&limit=${limit}`);
67
+ }
68
+ async getChanges(id, since) {
69
+ return this.request('GET', `/api/thesis/${id}/changes?since=${encodeURIComponent(since)}`);
70
+ }
65
71
  async updateThesis(id, data) {
66
72
  return this.request('PATCH', `/api/thesis/${id}`, data);
67
73
  }
@@ -100,27 +100,53 @@ function createMutableLine(piTui) {
100
100
  }
101
101
  };
102
102
  }
103
- /** 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
+ */
104
107
  function createHeaderBar(piTui) {
105
108
  const { truncateToWidth, visibleWidth } = piTui;
106
109
  return class HeaderBar {
107
- left;
108
- center;
109
- right;
110
+ thesisId = '';
111
+ confidence = 0;
112
+ confidenceDelta = 0;
113
+ pnlDollars = 0;
114
+ positionCount = 0;
115
+ edgeCount = 0;
116
+ topEdge = ''; // e.g. "RECESSION +21¢"
110
117
  cachedWidth;
111
118
  cachedLines;
112
- constructor(left, center, right) {
113
- this.left = left;
114
- this.center = center;
115
- 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;
116
146
  }
117
- update(left, center, right) {
118
- if (left !== undefined)
119
- this.left = left;
120
- if (center !== undefined)
121
- this.center = center;
122
- if (right !== undefined)
123
- this.right = right;
147
+ updateConfidence(newConf, delta) {
148
+ this.confidence = Math.round(newConf * 100);
149
+ this.confidenceDelta = Math.round(delta * 100);
124
150
  this.cachedWidth = undefined;
125
151
  this.cachedLines = undefined;
126
152
  }
@@ -128,23 +154,39 @@ function createHeaderBar(piTui) {
128
154
  this.cachedWidth = undefined;
129
155
  this.cachedLines = undefined;
130
156
  }
157
+ // Keep legacy update() for compatibility with /switch etc.
158
+ update(left, center, right) {
159
+ this.cachedWidth = undefined;
160
+ this.cachedLines = undefined;
161
+ }
131
162
  render(width) {
132
163
  if (this.cachedLines && this.cachedWidth === width)
133
164
  return this.cachedLines;
134
165
  this.cachedWidth = width;
135
- const l = this.left;
136
- const c = this.center;
137
- const r = this.right;
138
- const lw = visibleWidth(l);
139
- const cw = visibleWidth(c);
140
- const rw = visibleWidth(r);
141
- const totalContent = lw + cw + rw;
142
- const totalPad = Math.max(0, width - totalContent);
143
- const leftPad = Math.max(1, Math.floor(totalPad / 2));
144
- const rightPad = Math.max(1, totalPad - leftPad);
145
- let line = l + ' '.repeat(leftPad) + c + ' '.repeat(rightPad) + r;
146
- line = C.bgZinc900(truncateToWidth(line, width, ''));
147
- // 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, '') + ' ');
148
190
  const lineVw = visibleWidth(line);
149
191
  if (lineVw < width) {
150
192
  line = line + C.bgZinc900(' '.repeat(width - lineVw));
@@ -154,13 +196,16 @@ function createHeaderBar(piTui) {
154
196
  }
155
197
  };
156
198
  }
157
- /** Footer bar: [tokens: N | cost: $N | tools: N] [/help] */
199
+ /** Footer bar: model | tokens | exchange status | trading status | /help */
158
200
  function createFooterBar(piTui) {
159
201
  const { truncateToWidth, visibleWidth } = piTui;
160
202
  return class FooterBar {
161
203
  tokens = 0;
162
204
  cost = 0;
163
205
  toolCount = 0;
206
+ modelName = '';
207
+ tradingEnabled = false;
208
+ exchangeOpen = null; // null = unknown
164
209
  cachedWidth;
165
210
  cachedLines;
166
211
  invalidate() {
@@ -178,13 +223,23 @@ function createFooterBar(piTui) {
178
223
  const tokStr = this.tokens >= 1000
179
224
  ? `${(this.tokens / 1000).toFixed(1)}k`
180
225
  : `${this.tokens}`;
181
- const leftText = C.zinc600(`tokens: ${tokStr} cost: $${this.cost.toFixed(3)} tools: ${this.toolCount}`);
182
- 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);
183
239
  const lw = visibleWidth(leftText);
184
- const rw = visibleWidth(rightText);
185
- const gap = Math.max(1, width - lw - rw);
186
- let line = leftText + ' '.repeat(gap) + rightText;
187
- 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 + ' ');
188
243
  const lineVw = visibleWidth(line);
189
244
  if (lineVw < width) {
190
245
  line = line + C.bgZinc900(' '.repeat(width - lineVw));
@@ -378,6 +433,13 @@ async function agentCommand(thesisId, opts) {
378
433
  let isProcessing = false;
379
434
  // Cache for positions (fetched by /pos or get_positions tool)
380
435
  let cachedPositions = null;
436
+ // ── Heartbeat polling state ───────────────────────────────────────────────
437
+ // Background poll delta endpoint every 60s.
438
+ // If confidence changed ≥ 3%, auto-trigger agent analysis.
439
+ // If agent is busy (isProcessing), queue and deliver after agent finishes.
440
+ let lastPollTimestamp = new Date().toISOString();
441
+ let pendingHeartbeatDelta = null; // queued delta when agent is busy
442
+ let heartbeatPollTimer = null;
381
443
  // ── Inline confirmation mechanism ─────────────────────────────────────────
382
444
  // Tools can call promptUser() during execution to ask the user a question.
383
445
  // This temporarily unlocks the editor, waits for input, then resumes.
@@ -405,9 +467,9 @@ async function agentCommand(thesisId, opts) {
405
467
  const mdDefaultStyle = {
406
468
  color: (s) => C.zinc400(s),
407
469
  };
408
- // Editor theme
470
+ // Editor theme — use dim zinc borders instead of default green
409
471
  const editorTheme = {
410
- borderColor: (s) => C.zinc800(s),
472
+ borderColor: (s) => `\x1b[38;2;50;50;55m${s}\x1b[39m`,
411
473
  selectList: {
412
474
  selectedPrefix: (s) => C.emerald(s),
413
475
  selectedText: (s) => C.zinc200(s),
@@ -417,12 +479,31 @@ async function agentCommand(thesisId, opts) {
417
479
  },
418
480
  };
419
481
  // ── Build components ───────────────────────────────────────────────────────
420
- const shortId = (resolvedThesisId || '').slice(0, 8);
421
- const confidencePct = typeof latestContext.confidence === 'number'
422
- ? Math.round(latestContext.confidence * 100)
423
- : (typeof latestContext.confidence === 'string' ? parseInt(latestContext.confidence) : 0);
424
- const headerBar = new HeaderBar(C.emerald(bold('SF Agent')) + C.zinc600(` \u2014 ${shortId}`), confidencePct > 0 ? C.zinc200(`${confidencePct}%`) : '', C.zinc600(currentModelName.split('/').pop() || currentModelName));
482
+ const headerBar = new HeaderBar();
483
+ // Fetch positions for header P&L (non-blocking, best-effort)
484
+ let initialPositions = null;
485
+ try {
486
+ initialPositions = await (0, kalshi_js_1.getPositions)();
487
+ if (initialPositions) {
488
+ for (const pos of initialPositions) {
489
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
490
+ if (livePrice !== null) {
491
+ pos.current_value = livePrice;
492
+ pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
493
+ }
494
+ }
495
+ }
496
+ }
497
+ catch { /* positions not available, fine */ }
498
+ headerBar.setFromContext(latestContext, initialPositions || undefined);
425
499
  const footerBar = new FooterBar();
500
+ footerBar.modelName = currentModelName;
501
+ footerBar.tradingEnabled = (0, config_js_1.loadConfig)().tradingEnabled || false;
502
+ // Fetch exchange status for footer (non-blocking)
503
+ fetch('https://api.elections.kalshi.com/trade-api/v2/exchange/status', { headers: { 'Accept': 'application/json' } })
504
+ .then(r => r.json())
505
+ .then(d => { footerBar.exchangeOpen = !!d.exchange_active; footerBar.update(); tui.requestRender(); })
506
+ .catch(() => { });
426
507
  const topSpacer = new Spacer(1);
427
508
  const bottomSpacer = new Spacer(1);
428
509
  const chatContainer = new Container();
@@ -517,11 +598,7 @@ async function agentCommand(thesisId, opts) {
517
598
  execute: async (_toolCallId, params) => {
518
599
  const ctx = await sfClient.getContext(params.thesisId);
519
600
  latestContext = ctx;
520
- // Update header with new confidence
521
- const conf = typeof ctx.confidence === 'number'
522
- ? Math.round(ctx.confidence * 100)
523
- : 0;
524
- headerBar.update(undefined, conf > 0 ? C.zinc200(`${conf}%`) : '', undefined);
601
+ headerBar.setFromContext(ctx, initialPositions || undefined);
525
602
  tui.requestRender();
526
603
  return {
527
604
  content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
@@ -549,6 +626,26 @@ async function agentCommand(thesisId, opts) {
549
626
  parameters: thesisIdParam,
550
627
  execute: async (_toolCallId, params) => {
551
628
  const result = await sfClient.evaluate(params.thesisId);
629
+ // Show confidence change prominently
630
+ if (result.evaluation?.confidenceDelta && Math.abs(result.evaluation.confidenceDelta) >= 0.01) {
631
+ const delta = result.evaluation.confidenceDelta;
632
+ const prev = Math.round((result.evaluation.previousConfidence || 0) * 100);
633
+ const now = Math.round((result.evaluation.newConfidence || 0) * 100);
634
+ const arrow = delta > 0 ? '\u25B2' : '\u25BC';
635
+ const color = delta > 0 ? C.emerald : C.red;
636
+ addSystemText(color(` ${arrow} Confidence ${prev}% \u2192 ${now}% (${delta > 0 ? '+' : ''}${Math.round(delta * 100)})`));
637
+ addSpacer();
638
+ // Update header
639
+ headerBar.updateConfidence(result.evaluation.newConfidence, delta);
640
+ tui.requestRender();
641
+ }
642
+ // Refresh context after eval
643
+ try {
644
+ latestContext = await sfClient.getContext(params.thesisId);
645
+ headerBar.setFromContext(latestContext, initialPositions || undefined);
646
+ tui.requestRender();
647
+ }
648
+ catch { }
552
649
  return {
553
650
  content: [{ type: 'text', text: JSON.stringify(result) }],
554
651
  details: {},
@@ -901,6 +998,81 @@ async function agentCommand(thesisId, opts) {
901
998
  },
902
999
  },
903
1000
  ];
1001
+ // ── What-if tool (always available) ────────────────────────────────────────
1002
+ tools.push({
1003
+ name: 'what_if',
1004
+ label: 'What-If',
1005
+ 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%?".',
1006
+ parameters: Type.Object({
1007
+ overrides: Type.Array(Type.Object({
1008
+ nodeId: Type.String({ description: 'Causal tree node ID (e.g. n1, n3.1)' }),
1009
+ newProbability: Type.Number({ description: 'New probability 0-1' }),
1010
+ }), { description: 'Node probability overrides' }),
1011
+ }),
1012
+ execute: async (_toolCallId, params) => {
1013
+ // Inline what-if simulation
1014
+ const ctx = latestContext;
1015
+ const allNodes = [];
1016
+ function flatten(nodes) {
1017
+ for (const n of nodes) {
1018
+ allNodes.push(n);
1019
+ if (n.children?.length)
1020
+ flatten(n.children);
1021
+ }
1022
+ }
1023
+ const rawNodes = ctx.causalTree?.nodes || [];
1024
+ flatten(rawNodes);
1025
+ const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
1026
+ const overrideMap = new Map(params.overrides.map((o) => [o.nodeId, o.newProbability]));
1027
+ const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
1028
+ const newConf = treeNodes.reduce((s, n) => {
1029
+ const p = overrideMap.get(n.id) ?? n.probability ?? 0;
1030
+ return s + p * (n.importance || 0);
1031
+ }, 0);
1032
+ const nodeScales = new Map();
1033
+ for (const [nid, np] of overrideMap.entries()) {
1034
+ const nd = allNodes.find((n) => n.id === nid);
1035
+ if (nd && nd.probability > 0)
1036
+ nodeScales.set(nid, Math.max(0, Math.min(2, np / nd.probability)));
1037
+ }
1038
+ const edges = (ctx.edges || []).map((edge) => {
1039
+ const relNode = edge.relatedNodeId;
1040
+ let scaleFactor = 1;
1041
+ if (relNode) {
1042
+ const candidates = [relNode, relNode.split('.').slice(0, -1).join('.'), relNode.split('.')[0]].filter(Boolean);
1043
+ for (const cid of candidates) {
1044
+ if (nodeScales.has(cid)) {
1045
+ scaleFactor = nodeScales.get(cid);
1046
+ break;
1047
+ }
1048
+ }
1049
+ }
1050
+ const mkt = edge.marketPrice || 0;
1051
+ const oldTP = edge.thesisPrice || edge.thesisImpliedPrice || mkt;
1052
+ const oldEdge = edge.edge || edge.edgeSize || 0;
1053
+ const newTP = Math.round((mkt + (oldTP - mkt) * scaleFactor) * 100) / 100;
1054
+ const dir = edge.direction || 'yes';
1055
+ const newEdge = Math.round((dir === 'yes' ? newTP - mkt : mkt - newTP) * 100) / 100;
1056
+ return {
1057
+ market: edge.market || edge.marketTitle || edge.marketId,
1058
+ marketPrice: mkt,
1059
+ oldEdge,
1060
+ newEdge,
1061
+ delta: newEdge - oldEdge,
1062
+ signal: Math.abs(newEdge - oldEdge) < 1 ? 'unchanged' : (oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0) ? 'REVERSED' : Math.abs(newEdge) < 2 ? 'GONE' : 'reduced',
1063
+ };
1064
+ }).filter((e) => e.signal !== 'unchanged');
1065
+ const result = {
1066
+ overrides: params.overrides.map((o) => {
1067
+ const node = allNodes.find((n) => n.id === o.nodeId);
1068
+ return { nodeId: o.nodeId, label: node?.label || o.nodeId, oldProb: node?.probability, newProb: o.newProbability };
1069
+ }),
1070
+ confidence: { old: Math.round(oldConf * 100), new: Math.round(newConf * 100), delta: Math.round((newConf - oldConf) * 100) },
1071
+ affectedEdges: edges,
1072
+ };
1073
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
1074
+ },
1075
+ });
904
1076
  // ── Trading tools (conditional on tradingEnabled) ──────────────────────────
905
1077
  const config = (0, config_js_1.loadConfig)();
906
1078
  if (config.tradingEnabled) {
@@ -1163,6 +1335,12 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
1163
1335
  isProcessing = false;
1164
1336
  persistSession();
1165
1337
  flushRender();
1338
+ // Deliver queued heartbeat notification if any
1339
+ if (pendingHeartbeatDelta) {
1340
+ const delta = pendingHeartbeatDelta;
1341
+ pendingHeartbeatDelta = null;
1342
+ handleHeartbeatDelta(delta);
1343
+ }
1166
1344
  }
1167
1345
  if (event.type === 'tool_execution_start') {
1168
1346
  const toolLine = new MutableLine(C.zinc600(` \u26A1 ${event.toolName}...`));
@@ -1298,7 +1476,8 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
1298
1476
  model = resolveModel(currentModelName);
1299
1477
  // Update agent model
1300
1478
  agent.setModel(model);
1301
- headerBar.update(undefined, undefined, C.zinc600(currentModelName.split('/').pop() || currentModelName));
1479
+ footerBar.modelName = currentModelName;
1480
+ footerBar.update();
1302
1481
  addSystemText(C.emerald(`Model switched to ${currentModelName}`));
1303
1482
  addSpacer();
1304
1483
  tui.requestRender();
@@ -1338,13 +1517,9 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
1338
1517
  addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
1339
1518
  }
1340
1519
  // Update header
1341
- headerBar.update(C.emerald(bold('SF Agent')) + C.zinc600(` \u2014 ${resolvedThesisId.slice(0, 8)}`), newConf > 0 ? C.zinc200(`${newConf}%`) : '', undefined);
1520
+ headerBar.setFromContext(newContext, initialPositions || undefined);
1342
1521
  chatContainer.clear();
1343
- const thText = (newContext.thesis || newContext.rawThesis || '').slice(0, 120);
1344
- addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
1345
- C.zinc200(bold(thText)) + '\n' +
1346
- C.zinc600(`${newContext.status || 'active'} ${newConf > 0 ? newConf + '%' : ''} ${(newContext.edges || []).length} edges`) + '\n' +
1347
- C.zinc600('\u2500'.repeat(50)));
1522
+ addSystemText(buildWelcomeDashboard(newContext, initialPositions));
1348
1523
  }
1349
1524
  catch (err) {
1350
1525
  addSystemText(C.red(`Switch failed: ${err.message}`));
@@ -1703,6 +1878,8 @@ Output a structured summary. Be concise but preserve every important detail —
1703
1878
  };
1704
1879
  // ── Ctrl+C handler ─────────────────────────────────────────────────────────
1705
1880
  function cleanup() {
1881
+ if (heartbeatPollTimer)
1882
+ clearInterval(heartbeatPollTimer);
1706
1883
  if (currentLoader)
1707
1884
  currentLoader.stop();
1708
1885
  persistSession();
@@ -1721,17 +1898,125 @@ Output a structured summary. Be concise but preserve every important detail —
1721
1898
  // Also handle SIGINT
1722
1899
  process.on('SIGINT', cleanup);
1723
1900
  process.on('SIGTERM', cleanup);
1901
+ // ── Welcome dashboard builder ────────────────────────────────────────────
1902
+ function buildWelcomeDashboard(ctx, positions) {
1903
+ const lines = [];
1904
+ const thesisText = ctx.thesis || ctx.rawThesis || 'N/A';
1905
+ const truncated = thesisText.length > 100 ? thesisText.slice(0, 100) + '...' : thesisText;
1906
+ const conf = typeof ctx.confidence === 'number'
1907
+ ? Math.round(ctx.confidence * 100)
1908
+ : (typeof ctx.confidence === 'string' ? Math.round(parseFloat(ctx.confidence) * 100) : 0);
1909
+ const delta = ctx.lastEvaluation?.confidenceDelta
1910
+ ? Math.round(ctx.lastEvaluation.confidenceDelta * 100)
1911
+ : 0;
1912
+ const deltaStr = delta !== 0 ? ` (${delta > 0 ? '+' : ''}${delta})` : '';
1913
+ const evalAge = ctx.lastEvaluation?.evaluatedAt
1914
+ ? Math.round((Date.now() - new Date(ctx.lastEvaluation.evaluatedAt).getTime()) / 3600000)
1915
+ : null;
1916
+ lines.push(C.zinc600('\u2500'.repeat(55)));
1917
+ lines.push(' ' + C.zinc200(bold(truncated)));
1918
+ lines.push(' ' + C.zinc600(`${ctx.status || 'active'} ${conf}%${deltaStr}`) +
1919
+ (evalAge !== null ? C.zinc600(` \u2502 last eval: ${evalAge < 1 ? '<1' : evalAge}h ago`) : ''));
1920
+ lines.push(C.zinc600('\u2500'.repeat(55)));
1921
+ // Positions section
1922
+ if (positions && positions.length > 0) {
1923
+ lines.push(' ' + C.zinc400(bold('POSITIONS')));
1924
+ let totalPnl = 0;
1925
+ for (const p of positions) {
1926
+ const pnlCents = p.unrealized_pnl || 0;
1927
+ totalPnl += pnlCents;
1928
+ const pnlStr = pnlCents >= 0
1929
+ ? C.emerald(`+$${(pnlCents / 100).toFixed(2)}`)
1930
+ : C.red(`-$${(Math.abs(pnlCents) / 100).toFixed(2)}`);
1931
+ const ticker = (p.ticker || '').slice(0, 28).padEnd(28);
1932
+ const qty = String(p.quantity || 0).padStart(5);
1933
+ const side = p.side === 'yes' ? C.emerald('Y') : C.red('N');
1934
+ lines.push(` ${C.zinc400(ticker)} ${qty} ${side} ${pnlStr}`);
1935
+ }
1936
+ const totalStr = totalPnl >= 0
1937
+ ? C.emerald(bold(`+$${(totalPnl / 100).toFixed(2)}`))
1938
+ : C.red(bold(`-$${(Math.abs(totalPnl) / 100).toFixed(2)}`));
1939
+ lines.push(` ${''.padEnd(28)} ${C.zinc600('Total')} ${totalStr}`);
1940
+ }
1941
+ // Top edges section
1942
+ const edges = ctx.edges || [];
1943
+ if (edges.length > 0) {
1944
+ const sorted = [...edges].sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0)).slice(0, 5);
1945
+ lines.push(C.zinc600('\u2500'.repeat(55)));
1946
+ lines.push(' ' + C.zinc400(bold('TOP EDGES')) + C.zinc600(' mkt edge liq'));
1947
+ for (const e of sorted) {
1948
+ const name = (e.market || e.marketTitle || e.marketId || '').slice(0, 30).padEnd(30);
1949
+ const mkt = String(Math.round(e.marketPrice || 0)).padStart(3) + '\u00A2';
1950
+ const edge = e.edge || e.edgeSize || 0;
1951
+ const edgeStr = (edge > 0 ? '+' : '') + Math.round(edge);
1952
+ const liq = e.orderbook?.liquidityScore || (e.venue === 'polymarket' ? '-' : '?');
1953
+ const edgeColor = Math.abs(edge) >= 15 ? C.emerald : Math.abs(edge) >= 8 ? C.amber : C.zinc400;
1954
+ lines.push(` ${C.zinc400(name)} ${C.zinc400(mkt)} ${edgeColor(edgeStr.padStart(4))} ${C.zinc600(liq)}`);
1955
+ }
1956
+ }
1957
+ lines.push(C.zinc600('\u2500'.repeat(55)));
1958
+ return lines.join('\n');
1959
+ }
1724
1960
  // ── Show initial welcome ───────────────────────────────────────────────────
1725
- const thesisText = latestContext.thesis || latestContext.rawThesis || 'N/A';
1726
- const truncatedThesis = thesisText.length > 120 ? thesisText.slice(0, 120) + '...' : thesisText;
1727
1961
  const sessionStatus = sessionRestored
1728
- ? C.zinc600(` resumed (${agent.state.messages.length} messages)`)
1729
- : C.zinc600(' new session');
1730
- addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
1731
- C.zinc200(bold(truncatedThesis)) + '\n' +
1732
- C.zinc600(`${latestContext.status || 'active'} ${confidencePct > 0 ? confidencePct + '%' : ''} ${(latestContext.edges || []).length} edges`) + sessionStatus + '\n' +
1733
- C.zinc600('\u2500'.repeat(50)));
1962
+ ? C.zinc600(`resumed (${agent.state.messages.length} messages)`)
1963
+ : C.zinc600('new session');
1964
+ addSystemText(buildWelcomeDashboard(latestContext, initialPositions));
1965
+ addSystemText(' ' + sessionStatus);
1734
1966
  addSpacer();
1967
+ // ── Heartbeat delta handler ───────────────────────────────────────────────
1968
+ const HEARTBEAT_CONFIDENCE_THRESHOLD = 0.03; // 3%
1969
+ function handleHeartbeatDelta(delta) {
1970
+ const absDelta = Math.abs(delta.confidenceDelta || 0);
1971
+ const confPct = Math.round((delta.confidence || 0) * 100);
1972
+ const deltaPct = Math.round((delta.confidenceDelta || 0) * 100);
1973
+ const sign = deltaPct > 0 ? '+' : '';
1974
+ if (absDelta >= HEARTBEAT_CONFIDENCE_THRESHOLD) {
1975
+ // Big change → auto-trigger agent analysis
1976
+ const arrow = deltaPct > 0 ? '\u25B2' : '\u25BC';
1977
+ const color = deltaPct > 0 ? C.emerald : C.red;
1978
+ addSystemText(color(` ${arrow} Heartbeat: confidence ${sign}${deltaPct}% → ${confPct}%`));
1979
+ if (delta.latestSummary) {
1980
+ addSystemText(C.zinc400(` ${delta.latestSummary.slice(0, 100)}`));
1981
+ }
1982
+ addSpacer();
1983
+ // Update header
1984
+ headerBar.setFromContext({ ...latestContext, confidence: delta.confidence, lastEvaluation: { confidenceDelta: delta.confidenceDelta } }, initialPositions || undefined);
1985
+ tui.requestRender();
1986
+ // Auto-trigger agent
1987
+ isProcessing = true;
1988
+ const prompt = `[HEARTBEAT ALERT] Confidence just changed ${sign}${deltaPct}% to ${confPct}%. ${delta.evaluationCount} evaluation(s) since last check. Latest: "${(delta.latestSummary || '').slice(0, 150)}". Briefly analyze what happened and whether any action is needed. Be concise.`;
1989
+ agent.prompt(prompt).catch((err) => {
1990
+ addSystemText(C.red(`Error: ${err.message}`));
1991
+ isProcessing = false;
1992
+ });
1993
+ }
1994
+ else if (absDelta > 0) {
1995
+ // Small change → silent notification line only
1996
+ addSystemText(C.zinc600(` \u2500 heartbeat: ${confPct}% (${sign}${deltaPct}%) \u2014 ${delta.evaluationCount || 0} eval(s)`));
1997
+ tui.requestRender();
1998
+ }
1999
+ // absDelta === 0: truly nothing changed, stay silent
2000
+ }
2001
+ // ── Start heartbeat polling ───────────────────────────────────────────────
2002
+ heartbeatPollTimer = setInterval(async () => {
2003
+ try {
2004
+ const delta = await sfClient.getChanges(resolvedThesisId, lastPollTimestamp);
2005
+ lastPollTimestamp = new Date().toISOString();
2006
+ if (!delta.changed)
2007
+ return;
2008
+ if (isProcessing || pendingPrompt) {
2009
+ // Agent is busy — queue for delivery after agent_end
2010
+ pendingHeartbeatDelta = delta;
2011
+ }
2012
+ else {
2013
+ handleHeartbeatDelta(delta);
2014
+ }
2015
+ }
2016
+ catch {
2017
+ // Silent — don't spam errors from background polling
2018
+ }
2019
+ }, 60_000); // every 60 seconds
1735
2020
  // ── Start TUI ──────────────────────────────────────────────────────────────
1736
2021
  tui.start();
1737
2022
  }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * sf feed — Evaluation history stream.
3
+ *
4
+ * Shows what the heartbeat engine has been thinking.
5
+ * One line per evaluation cycle, newest first.
6
+ */
7
+ export declare function feedCommand(opts: {
8
+ hours?: string;
9
+ thesis?: string;
10
+ json?: boolean;
11
+ apiKey?: string;
12
+ apiUrl?: string;
13
+ }): Promise<void>;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ /**
3
+ * sf feed — Evaluation history stream.
4
+ *
5
+ * Shows what the heartbeat engine has been thinking.
6
+ * One line per evaluation cycle, newest first.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.feedCommand = feedCommand;
10
+ const client_js_1 = require("../client.js");
11
+ const utils_js_1 = require("../utils.js");
12
+ async function feedCommand(opts) {
13
+ const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
14
+ const hours = parseInt(opts.hours || '24');
15
+ const data = await client.getFeed(hours, 200);
16
+ let feed = data.feed || [];
17
+ if (feed.length === 0) {
18
+ console.log(`${utils_js_1.c.dim}No evaluations in the last ${hours} hours.${utils_js_1.c.reset}`);
19
+ return;
20
+ }
21
+ // Filter by thesis if specified
22
+ if (opts.thesis) {
23
+ feed = feed.filter((e) => e.thesisId.startsWith(opts.thesis) || e.thesisShortId === opts.thesis);
24
+ if (feed.length === 0) {
25
+ console.log(`${utils_js_1.c.dim}No evaluations for ${opts.thesis} in the last ${hours} hours.${utils_js_1.c.reset}`);
26
+ return;
27
+ }
28
+ }
29
+ if (opts.json) {
30
+ console.log(JSON.stringify(feed, null, 2));
31
+ return;
32
+ }
33
+ // Render feed
34
+ console.log();
35
+ console.log(`${utils_js_1.c.bold}${utils_js_1.c.cyan}Evaluation Feed${utils_js_1.c.reset}${utils_js_1.c.dim} — last ${hours}h, ${feed.length} cycles${utils_js_1.c.reset}`);
36
+ console.log(`${utils_js_1.c.dim}${'─'.repeat(75)}${utils_js_1.c.reset}`);
37
+ for (const entry of feed) {
38
+ const time = new Date(entry.evaluatedAt);
39
+ const timeStr = time.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
40
+ // Confidence + delta
41
+ const conf = Math.round(entry.confidence * 100);
42
+ const delta = Math.round(entry.delta * 100);
43
+ let deltaStr;
44
+ if (delta > 0) {
45
+ deltaStr = `${utils_js_1.c.green}+${delta}%${utils_js_1.c.reset}`;
46
+ }
47
+ else if (delta < 0) {
48
+ deltaStr = `${utils_js_1.c.red}${delta}%${utils_js_1.c.reset}`;
49
+ }
50
+ else {
51
+ deltaStr = `${utils_js_1.c.dim}0%${utils_js_1.c.reset}`;
52
+ }
53
+ // Thesis ID (short)
54
+ const id = entry.thesisShortId || entry.thesisId?.slice(0, 8) || '?';
55
+ // Summary — truncate to fit one line
56
+ const summary = (entry.summary || 'No summary').replace(/\n/g, ' ').slice(0, 80);
57
+ // Node changes
58
+ const nodes = entry.updatedNodes || [];
59
+ const nodeStr = nodes.length > 0
60
+ ? nodes.slice(0, 3).map((n) => `${n.nodeId}→${Math.round((n.newProb || 0) * 100)}%`).join(', ')
61
+ : '';
62
+ // Format line
63
+ console.log(`${utils_js_1.c.dim}[${timeStr}]${utils_js_1.c.reset} ` +
64
+ `${utils_js_1.c.cyan}${id}${utils_js_1.c.reset} ` +
65
+ `${conf}% (${deltaStr}) ` +
66
+ `${utils_js_1.c.dim}${summary}${utils_js_1.c.reset}`);
67
+ if (nodeStr) {
68
+ console.log(`${' '.repeat(9)} ${utils_js_1.c.dim}nodes: ${nodeStr}${utils_js_1.c.reset}`);
69
+ }
70
+ }
71
+ console.log(`${utils_js_1.c.dim}${'─'.repeat(75)}${utils_js_1.c.reset}`);
72
+ console.log();
73
+ }
@@ -0,0 +1,17 @@
1
+ /**
2
+ * sf whatif — What-if scenario analysis.
3
+ *
4
+ * Pure computation, zero LLM cost. Answers:
5
+ * "If node X drops to 10%, what happens to my edges and positions?"
6
+ *
7
+ * Usage:
8
+ * sf whatif f582bf76 --set "n1=0.1"
9
+ * sf whatif f582bf76 --set "n1=0.1" --set "n3.1=0.2"
10
+ * sf whatif f582bf76 --set "n1=0.1" --json
11
+ */
12
+ export declare function whatifCommand(thesisId: string, opts: {
13
+ set?: string[];
14
+ json?: boolean;
15
+ apiKey?: string;
16
+ apiUrl?: string;
17
+ }): Promise<void>;
@@ -0,0 +1,209 @@
1
+ "use strict";
2
+ /**
3
+ * sf whatif — What-if scenario analysis.
4
+ *
5
+ * Pure computation, zero LLM cost. Answers:
6
+ * "If node X drops to 10%, what happens to my edges and positions?"
7
+ *
8
+ * Usage:
9
+ * sf whatif f582bf76 --set "n1=0.1"
10
+ * sf whatif f582bf76 --set "n1=0.1" --set "n3.1=0.2"
11
+ * sf whatif f582bf76 --set "n1=0.1" --json
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.whatifCommand = whatifCommand;
15
+ const client_js_1 = require("../client.js");
16
+ const kalshi_js_1 = require("../kalshi.js");
17
+ const utils_js_1 = require("../utils.js");
18
+ // Inline what-if simulation (mirrors server-side logic, zero dependency)
19
+ function simulateWhatIf(ctx, overrides) {
20
+ const allNodes = [];
21
+ function flatten(nodes) {
22
+ for (const n of nodes) {
23
+ allNodes.push(n);
24
+ if (n.children?.length)
25
+ flatten(n.children);
26
+ }
27
+ }
28
+ const rawNodes = ctx.causalTree?.nodes || [];
29
+ flatten(rawNodes);
30
+ // Top-level nodes only (depth=0 or no depth field + no dot in id)
31
+ const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
32
+ const overrideMap = new Map(overrides.map(o => [o.nodeId, o.newProbability]));
33
+ const overrideDetails = overrides.map(o => {
34
+ const node = allNodes.find((n) => n.id === o.nodeId);
35
+ return {
36
+ nodeId: o.nodeId,
37
+ oldProb: node?.probability ?? 0,
38
+ newProb: o.newProbability,
39
+ label: node?.label || o.nodeId,
40
+ };
41
+ });
42
+ // Confidence (only top-level nodes)
43
+ const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
44
+ const newConf = treeNodes.reduce((s, n) => {
45
+ const p = overrideMap.get(n.id) ?? n.probability ?? 0;
46
+ return s + p * (n.importance || 0);
47
+ }, 0);
48
+ // Per-node override ratios — only scale edges directly related to overridden nodes.
49
+ // No global scale: edges unrelated to any override stay unchanged.
50
+ // User must explicitly override each node they think is affected.
51
+ const nodeScales = new Map();
52
+ for (const [nodeId, newProb] of overrideMap.entries()) {
53
+ const node = allNodes.find((n) => n.id === nodeId);
54
+ if (node && node.probability > 0) {
55
+ nodeScales.set(nodeId, Math.max(0, Math.min(2, newProb / node.probability)));
56
+ }
57
+ }
58
+ // Edges
59
+ const edges = (ctx.edges || []).map((edge) => {
60
+ const relNode = edge.relatedNodeId;
61
+ let scaleFactor = 1; // default: no change
62
+ // Only scale if edge's related node (or its ancestor) was overridden
63
+ if (relNode) {
64
+ const candidates = [relNode, relNode.split('.').slice(0, -1).join('.'), relNode.split('.')[0]].filter(Boolean);
65
+ for (const cid of candidates) {
66
+ if (nodeScales.has(cid)) {
67
+ scaleFactor = nodeScales.get(cid);
68
+ break;
69
+ }
70
+ }
71
+ }
72
+ const mkt = edge.marketPrice || 0;
73
+ const oldTP = edge.thesisPrice || edge.thesisImpliedPrice || mkt;
74
+ const oldEdge = edge.edge || edge.edgeSize || 0;
75
+ const premium = oldTP - mkt;
76
+ const newTP = Math.round((mkt + premium * scaleFactor) * 100) / 100;
77
+ const dir = edge.direction || 'yes';
78
+ const newEdge = Math.round((dir === 'yes' ? newTP - mkt : mkt - newTP) * 100) / 100;
79
+ const delta = Math.round((newEdge - oldEdge) * 100) / 100;
80
+ let signal = 'unchanged';
81
+ if (Math.abs(delta) < 1)
82
+ signal = 'unchanged';
83
+ else if ((oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0))
84
+ signal = 'reversed';
85
+ else if (Math.abs(newEdge) < 2)
86
+ signal = 'gone';
87
+ else if (Math.abs(newEdge) < Math.abs(oldEdge))
88
+ signal = 'reduced';
89
+ return {
90
+ marketId: edge.marketId,
91
+ market: edge.market || edge.marketTitle || edge.marketId,
92
+ venue: edge.venue,
93
+ direction: dir,
94
+ marketPrice: mkt,
95
+ oldThesisPrice: oldTP,
96
+ newThesisPrice: newTP,
97
+ oldEdge,
98
+ newEdge,
99
+ delta,
100
+ signal,
101
+ relatedNodeId: relNode,
102
+ };
103
+ });
104
+ edges.sort((a, b) => Math.abs(b.delta) - Math.abs(a.delta));
105
+ return {
106
+ overrides: overrideDetails,
107
+ oldConfidence: oldConf,
108
+ newConfidence: newConf,
109
+ confidenceDelta: Math.round((newConf - oldConf) * 100) / 100,
110
+ edges,
111
+ };
112
+ }
113
+ async function whatifCommand(thesisId, opts) {
114
+ if (!opts.set || opts.set.length === 0) {
115
+ throw new Error('Usage: sf whatif <thesisId> --set "n1=0.1" [--set "n3=0.5"]');
116
+ }
117
+ // Parse overrides
118
+ const overrides = opts.set.map(s => {
119
+ const [nodeId, valStr] = s.split('=');
120
+ if (!nodeId || !valStr)
121
+ throw new Error(`Invalid override: "${s}". Format: nodeId=probability`);
122
+ const prob = parseFloat(valStr);
123
+ if (isNaN(prob) || prob < 0 || prob > 1)
124
+ throw new Error(`Invalid probability: "${valStr}". Must be 0-1.`);
125
+ return { nodeId: nodeId.trim(), newProbability: prob };
126
+ });
127
+ const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
128
+ const ctx = await client.getContext(thesisId);
129
+ const result = simulateWhatIf(ctx, overrides);
130
+ if (opts.json) {
131
+ console.log(JSON.stringify(result, null, 2));
132
+ return;
133
+ }
134
+ // Render
135
+ console.log();
136
+ console.log(`${utils_js_1.c.bold}${utils_js_1.c.cyan}WHAT-IF Scenario${utils_js_1.c.reset}`);
137
+ console.log(`${utils_js_1.c.dim}${'─'.repeat(65)}${utils_js_1.c.reset}`);
138
+ // Overrides
139
+ for (const o of result.overrides) {
140
+ const oldPct = Math.round(o.oldProb * 100);
141
+ const newPct = Math.round(o.newProb * 100);
142
+ const arrow = newPct > oldPct ? utils_js_1.c.green + '↑' + utils_js_1.c.reset : utils_js_1.c.red + '↓' + utils_js_1.c.reset;
143
+ console.log(` ${utils_js_1.c.cyan}${o.nodeId}${utils_js_1.c.reset} ${o.label.slice(0, 40)}`);
144
+ console.log(` ${oldPct}% ${arrow} ${utils_js_1.c.bold}${newPct}%${utils_js_1.c.reset}`);
145
+ }
146
+ // Confidence
147
+ const oldPct = Math.round(result.oldConfidence * 100);
148
+ const newPct = Math.round(result.newConfidence * 100);
149
+ const deltaSign = result.confidenceDelta > 0 ? '+' : '';
150
+ const confColor = result.confidenceDelta >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
151
+ console.log();
152
+ console.log(` Confidence: ${oldPct}% → ${confColor}${utils_js_1.c.bold}${newPct}%${utils_js_1.c.reset} (${confColor}${deltaSign}${Math.round(result.confidenceDelta * 100)}${utils_js_1.c.reset})`);
153
+ console.log();
154
+ // Edges
155
+ const affected = result.edges.filter((e) => e.signal !== 'unchanged');
156
+ if (affected.length === 0) {
157
+ console.log(` ${utils_js_1.c.dim}No edges affected.${utils_js_1.c.reset}`);
158
+ }
159
+ else {
160
+ console.log(` ${utils_js_1.c.bold}Edges Affected${utils_js_1.c.reset}`);
161
+ console.log(` ${'Market'.padEnd(35)} ${'Now'.padEnd(6)} ${'Edge'.padEnd(8)} ${'→'.padEnd(3)} ${'New Edge'.padEnd(8)} Signal`);
162
+ console.log(` ${utils_js_1.c.dim}${'─'.repeat(65)}${utils_js_1.c.reset}`);
163
+ for (const e of affected) {
164
+ const name = (e.market || e.marketId).slice(0, 33).padEnd(35);
165
+ const mkt = `${Math.round(e.marketPrice)}¢`.padEnd(6);
166
+ const oldE = `${e.oldEdge > 0 ? '+' : ''}${Math.round(e.oldEdge)}`.padEnd(8);
167
+ const newE = `${e.newEdge > 0 ? '+' : ''}${Math.round(e.newEdge)}`.padEnd(8);
168
+ let signalStr;
169
+ switch (e.signal) {
170
+ case 'reversed':
171
+ signalStr = `${utils_js_1.c.red}${utils_js_1.c.bold}REVERSED${utils_js_1.c.reset}`;
172
+ break;
173
+ case 'gone':
174
+ signalStr = `${utils_js_1.c.red}GONE${utils_js_1.c.reset}`;
175
+ break;
176
+ case 'reduced':
177
+ signalStr = `${utils_js_1.c.dim}reduced${utils_js_1.c.reset}`;
178
+ break;
179
+ default: signalStr = `${utils_js_1.c.dim}-${utils_js_1.c.reset}`;
180
+ }
181
+ console.log(` ${utils_js_1.c.dim}${name}${utils_js_1.c.reset} ${mkt} ${oldE} → ${newE} ${signalStr}`);
182
+ }
183
+ }
184
+ // Position risk (if positions available)
185
+ try {
186
+ const positions = await (0, kalshi_js_1.getPositions)();
187
+ if (positions && positions.length > 0) {
188
+ const edgeMap = new Map(result.edges.map((e) => [e.marketId, e]));
189
+ const atRisk = positions.filter((p) => {
190
+ const e = edgeMap.get(p.ticker);
191
+ return e && (e.signal === 'reversed' || e.signal === 'gone');
192
+ });
193
+ if (atRisk.length > 0) {
194
+ console.log();
195
+ console.log(` ${utils_js_1.c.red}${utils_js_1.c.bold}⚠ Positions at Risk${utils_js_1.c.reset}`);
196
+ for (const p of atRisk) {
197
+ const e = edgeMap.get(p.ticker);
198
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(p.ticker);
199
+ const currentPnl = livePrice !== null
200
+ ? ((livePrice - p.average_price_paid) * p.quantity / 100).toFixed(2)
201
+ : '?';
202
+ console.log(` ${utils_js_1.c.red}${p.ticker}${utils_js_1.c.reset} ${p.quantity} ${p.side} P&L $${currentPnl} edge ${e.oldEdge > 0 ? '+' : ''}${Math.round(e.oldEdge)} → ${utils_js_1.c.red}${e.newEdge > 0 ? '+' : ''}${Math.round(e.newEdge)}${utils_js_1.c.reset}`);
203
+ }
204
+ }
205
+ }
206
+ }
207
+ catch { /* positions not available, skip */ }
208
+ console.log();
209
+ }
package/dist/index.js CHANGED
@@ -42,6 +42,8 @@ const settlements_js_1 = require("./commands/settlements.js");
42
42
  const balance_js_1 = require("./commands/balance.js");
43
43
  const orders_js_1 = require("./commands/orders.js");
44
44
  const fills_js_1 = require("./commands/fills.js");
45
+ const feed_js_1 = require("./commands/feed.js");
46
+ const whatif_js_1 = require("./commands/whatif.js");
45
47
  const trade_js_1 = require("./commands/trade.js");
46
48
  const cancel_js_1 = require("./commands/cancel.js");
47
49
  const schedule_js_1 = require("./commands/schedule.js");
@@ -296,6 +298,27 @@ program
296
298
  .action(async (opts) => {
297
299
  await run(() => (0, fills_js_1.fillsCommand)(opts));
298
300
  });
301
+ // ── sf feed ──────────────────────────────────────────────────────────────────
302
+ program
303
+ .command('feed')
304
+ .description('Evaluation history stream — what the heartbeat engine has been thinking')
305
+ .option('--hours <n>', 'Hours to look back (default 24)', '24')
306
+ .option('--thesis <id>', 'Filter by thesis')
307
+ .option('--json', 'JSON output')
308
+ .action(async (opts, cmd) => {
309
+ const g = cmd.optsWithGlobals();
310
+ await run(() => (0, feed_js_1.feedCommand)({ ...opts, apiKey: g.apiKey, apiUrl: g.apiUrl }));
311
+ });
312
+ // ── sf whatif <thesisId> ──────────────────────────────────────────────────────
313
+ program
314
+ .command('whatif <thesisId>')
315
+ .description('What-if scenario — "if node X drops to 10%, what happens to my edges?"')
316
+ .option('--set <override>', 'Node override: nodeId=probability (0-1). Repeatable.', (val, prev) => [...prev, val], [])
317
+ .option('--json', 'JSON output')
318
+ .action(async (thesisId, opts, cmd) => {
319
+ const g = cmd.optsWithGlobals();
320
+ await run(() => (0, whatif_js_1.whatifCommand)(thesisId, { set: opts.set, json: opts.json, apiKey: g.apiKey, apiUrl: g.apiUrl }));
321
+ });
299
322
  // ── sf schedule ──────────────────────────────────────────────────────────────
300
323
  program
301
324
  .command('schedule')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.1.6",
3
+ "version": "1.1.8",
4
4
  "description": "Prediction market intelligence CLI. Causal thesis model, 24/7 Kalshi/Polymarket scan, live orderbook, edge detection. Interactive agent mode with tool calling.",
5
5
  "bin": {
6
6
  "sf": "./dist/index.js"