@spfunctions/cli 1.1.6 → 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.
package/dist/client.d.ts CHANGED
@@ -17,6 +17,7 @@ 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>;
20
21
  updateThesis(id: string, data: Record<string, unknown>): Promise<any>;
21
22
  publish(id: string, slug: string, description?: string): Promise<any>;
22
23
  unpublish(id: string): Promise<any>;
package/dist/client.js CHANGED
@@ -62,6 +62,9 @@ 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
+ }
65
68
  async updateThesis(id, data) {
66
69
  return this.request('PATCH', `/api/thesis/${id}`, data);
67
70
  }
@@ -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));
@@ -405,9 +460,9 @@ async function agentCommand(thesisId, opts) {
405
460
  const mdDefaultStyle = {
406
461
  color: (s) => C.zinc400(s),
407
462
  };
408
- // Editor theme
463
+ // Editor theme — use dim zinc borders instead of default green
409
464
  const editorTheme = {
410
- borderColor: (s) => C.zinc800(s),
465
+ borderColor: (s) => `\x1b[38;2;50;50;55m${s}\x1b[39m`,
411
466
  selectList: {
412
467
  selectedPrefix: (s) => C.emerald(s),
413
468
  selectedText: (s) => C.zinc200(s),
@@ -417,12 +472,31 @@ async function agentCommand(thesisId, opts) {
417
472
  },
418
473
  };
419
474
  // ── 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));
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);
425
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(() => { });
426
500
  const topSpacer = new Spacer(1);
427
501
  const bottomSpacer = new Spacer(1);
428
502
  const chatContainer = new Container();
@@ -517,11 +591,7 @@ async function agentCommand(thesisId, opts) {
517
591
  execute: async (_toolCallId, params) => {
518
592
  const ctx = await sfClient.getContext(params.thesisId);
519
593
  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);
594
+ headerBar.setFromContext(ctx, initialPositions || undefined);
525
595
  tui.requestRender();
526
596
  return {
527
597
  content: [{ type: 'text', text: JSON.stringify(ctx, null, 2) }],
@@ -549,6 +619,26 @@ async function agentCommand(thesisId, opts) {
549
619
  parameters: thesisIdParam,
550
620
  execute: async (_toolCallId, params) => {
551
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 { }
552
642
  return {
553
643
  content: [{ type: 'text', text: JSON.stringify(result) }],
554
644
  details: {},
@@ -901,6 +991,81 @@ async function agentCommand(thesisId, opts) {
901
991
  },
902
992
  },
903
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
+ });
904
1069
  // ── Trading tools (conditional on tradingEnabled) ──────────────────────────
905
1070
  const config = (0, config_js_1.loadConfig)();
906
1071
  if (config.tradingEnabled) {
@@ -1298,7 +1463,8 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
1298
1463
  model = resolveModel(currentModelName);
1299
1464
  // Update agent model
1300
1465
  agent.setModel(model);
1301
- headerBar.update(undefined, undefined, C.zinc600(currentModelName.split('/').pop() || currentModelName));
1466
+ footerBar.modelName = currentModelName;
1467
+ footerBar.update();
1302
1468
  addSystemText(C.emerald(`Model switched to ${currentModelName}`));
1303
1469
  addSpacer();
1304
1470
  tui.requestRender();
@@ -1338,13 +1504,9 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
1338
1504
  addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
1339
1505
  }
1340
1506
  // Update header
1341
- 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);
1342
1508
  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)));
1509
+ addSystemText(buildWelcomeDashboard(newContext, initialPositions));
1348
1510
  }
1349
1511
  catch (err) {
1350
1512
  addSystemText(C.red(`Switch failed: ${err.message}`));
@@ -1721,16 +1883,71 @@ Output a structured summary. Be concise but preserve every important detail —
1721
1883
  // Also handle SIGINT
1722
1884
  process.on('SIGINT', cleanup);
1723
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
+ }
1724
1945
  // ── Show initial welcome ───────────────────────────────────────────────────
1725
- const thesisText = latestContext.thesis || latestContext.rawThesis || 'N/A';
1726
- const truncatedThesis = thesisText.length > 120 ? thesisText.slice(0, 120) + '...' : thesisText;
1727
1946
  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)));
1947
+ ? C.zinc600(`resumed (${agent.state.messages.length} messages)`)
1948
+ : C.zinc600('new session');
1949
+ addSystemText(buildWelcomeDashboard(latestContext, initialPositions));
1950
+ addSystemText(' ' + sessionStatus);
1734
1951
  addSpacer();
1735
1952
  // ── Start TUI ──────────────────────────────────────────────────────────────
1736
1953
  tui.start();
@@ -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.7",
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"