@spfunctions/cli 1.1.2 → 1.1.4

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.
@@ -405,6 +405,7 @@ async function agentCommand(thesisId, opts) {
405
405
  { name: 'compact', description: 'Compress conversation history' },
406
406
  { name: 'new', description: 'Start fresh session' },
407
407
  { name: 'model', description: 'Switch model (e.g. /model anthropic/claude-sonnet-4)' },
408
+ { name: 'env', description: 'Show environment variable status' },
408
409
  { name: 'clear', description: 'Clear screen (keeps history)' },
409
410
  { name: 'exit', description: 'Exit agent (auto-saves)' },
410
411
  ], process.cwd());
@@ -451,6 +452,9 @@ async function agentCommand(thesisId, opts) {
451
452
  series: Type.Optional(Type.String({ description: 'Kalshi series ticker (e.g. KXWTIMAX)' })),
452
453
  market: Type.Optional(Type.String({ description: 'Specific market ticker' })),
453
454
  });
455
+ const webSearchParams = Type.Object({
456
+ query: Type.String({ description: 'Search keywords' }),
457
+ });
454
458
  const emptyParams = Type.Object({});
455
459
  const tools = [
456
460
  {
@@ -502,7 +506,7 @@ async function agentCommand(thesisId, opts) {
502
506
  {
503
507
  name: 'scan_markets',
504
508
  label: 'Scan Markets',
505
- description: 'Search Kalshi prediction markets: by keywords, series ticker, or specific market ticker',
509
+ description: 'Search Kalshi prediction markets. Provide exactly one of: query (keyword search), series (series ticker), or market (specific ticker). If multiple are provided, priority is: market > series > query.',
506
510
  parameters: scanParams,
507
511
  execute: async (_toolCallId, params) => {
508
512
  let result;
@@ -516,7 +520,7 @@ async function agentCommand(thesisId, opts) {
516
520
  const series = await (0, client_js_1.kalshiFetchAllSeries)();
517
521
  const keywords = params.query.toLowerCase().split(/\s+/);
518
522
  const matched = series
519
- .filter((s) => keywords.some((kw) => (s.title || '').toLowerCase().includes(kw) ||
523
+ .filter((s) => keywords.every((kw) => (s.title || '').toLowerCase().includes(kw) ||
520
524
  (s.ticker || '').toLowerCase().includes(kw)))
521
525
  .slice(0, 15);
522
526
  result = matched;
@@ -570,6 +574,45 @@ async function agentCommand(thesisId, opts) {
570
574
  };
571
575
  },
572
576
  },
577
+ {
578
+ name: 'web_search',
579
+ label: 'Web Search',
580
+ description: 'Search latest news and information. Use for real-time info not yet covered by the causal tree or heartbeat engine.',
581
+ parameters: webSearchParams,
582
+ execute: async (_toolCallId, params) => {
583
+ const apiKey = process.env.TAVILY_API_KEY;
584
+ if (!apiKey) {
585
+ return {
586
+ content: [{ type: 'text', text: 'Tavily not configured. Set TAVILY_API_KEY to enable web search. You can also manually inject a signal and let the heartbeat engine search.' }],
587
+ details: {},
588
+ };
589
+ }
590
+ const res = await fetch('https://api.tavily.com/search', {
591
+ method: 'POST',
592
+ headers: { 'Content-Type': 'application/json' },
593
+ body: JSON.stringify({
594
+ api_key: apiKey,
595
+ query: params.query,
596
+ max_results: 5,
597
+ search_depth: 'basic',
598
+ include_answer: true,
599
+ }),
600
+ });
601
+ if (!res.ok) {
602
+ return {
603
+ content: [{ type: 'text', text: `Search failed: ${res.status}` }],
604
+ details: {},
605
+ };
606
+ }
607
+ const data = await res.json();
608
+ const results = (data.results || []).map((r) => `[${r.title}](${r.url})\n${r.content?.slice(0, 200)}`).join('\n\n');
609
+ const answer = data.answer ? `Summary: ${data.answer}\n\n---\n\n` : '';
610
+ return {
611
+ content: [{ type: 'text', text: `${answer}${results}` }],
612
+ details: {},
613
+ };
614
+ },
615
+ },
573
616
  ];
574
617
  // ── System prompt builder ──────────────────────────────────────────────────
575
618
  function buildSystemPrompt(ctx) {
@@ -604,9 +647,11 @@ Short-term markets (weekly/monthly contracts) settle into hard data that calibra
604
647
  - If the user says "note this" or mentions a news event, inject a signal. Don't ask "should I note this?"
605
648
  - If the user says "evaluate" or "run it", trigger immediately. Don't confirm.
606
649
  - Don't end every response with "anything else?" \u2014 the user will ask when they want to.
650
+ - If the user asks about latest news or real-time events, use web_search first, then answer based on results. If you find important information, suggest injecting it as a signal.
607
651
  - If you notice an edge narrowing or disappearing, say so proactively. Don't only report good news.
608
652
  - If a causal tree node probability seriously contradicts the market price, point it out.
609
653
  - Use Chinese if the user writes in Chinese, English if they write in English.
654
+ - 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.
610
655
  - Align tables. Be precise with numbers to the cent.
611
656
 
612
657
  ## Current thesis state
@@ -784,6 +829,7 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
784
829
  C.emerald('/compact ') + C.zinc400('Compress conversation history') + '\n' +
785
830
  C.emerald('/new ') + C.zinc400('Start fresh session') + '\n' +
786
831
  C.emerald('/model <m> ') + C.zinc400('Switch model') + '\n' +
832
+ C.emerald('/env ') + C.zinc400('Show environment variable status') + '\n' +
787
833
  C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
788
834
  C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
789
835
  addSpacer();
@@ -891,6 +937,10 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
891
937
  const newSysPrompt = buildSystemPrompt(newContext);
892
938
  const newConf = typeof newContext.confidence === 'number'
893
939
  ? Math.round(newContext.confidence * 100) : 0;
940
+ // CRITICAL: Always clearMessages() first to reset agent internal state.
941
+ // replaceMessages() on a mid-conversation agent corrupts pi-agent-core's
942
+ // state machine, causing the TUI to freeze.
943
+ agent.clearMessages();
894
944
  // Load saved session or start fresh
895
945
  const saved = loadSession(resolvedThesisId);
896
946
  if (saved?.messages?.length > 0) {
@@ -899,7 +949,6 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
899
949
  addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(` (resumed ${saved.messages.length} messages)`));
900
950
  }
901
951
  else {
902
- agent.clearMessages();
903
952
  agent.setSystemPrompt(newSysPrompt);
904
953
  addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
905
954
  }
@@ -916,42 +965,168 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
916
965
  addSystemText(C.red(`Switch failed: ${err.message}`));
917
966
  }
918
967
  addSpacer();
968
+ // Force re-focus editor so input stays responsive
969
+ tui.setFocus(editor);
919
970
  tui.requestRender();
920
971
  return true;
921
972
  }
922
973
  case '/compact': {
923
974
  addSpacer();
924
- const msgs = agent.state.messages;
925
- if (msgs.length <= 10) {
926
- addSystemText(C.zinc400('Conversation too short to compact'));
927
- addSpacer();
928
- return true;
929
- }
930
- // Keep recent 6 messages (3 turns) + create summary of the rest
931
- const recentCount = 6;
932
- const toCompress = msgs.slice(0, -recentCount);
933
- const toKeep = msgs.slice(-recentCount);
934
- // Extract text for summary (no LLM, just bullet points)
935
- const bulletPoints = [];
936
- for (const m of toCompress) {
937
- const content = typeof m.content === 'string' ? m.content : '';
938
- if (m.role === 'user' && content) {
939
- bulletPoints.push(`- User: ${content.slice(0, 100)}`);
975
+ try {
976
+ const msgs = agent.state.messages;
977
+ if (msgs.length <= 10) {
978
+ addSystemText(C.zinc400('Conversation too short to compact'));
979
+ addSpacer();
980
+ tui.setFocus(editor);
981
+ return true;
982
+ }
983
+ // ── Find clean cut point ──────────────────────────────────────
984
+ // Walk backwards counting user messages as turn starts.
985
+ // Keep 3 complete turns. Never split a tool_call/tool_result pair.
986
+ const turnsToKeep = 3;
987
+ let turnsSeen = 0;
988
+ let cutIndex = msgs.length;
989
+ for (let i = msgs.length - 1; i >= 0; i--) {
990
+ if (msgs[i].role === 'user') {
991
+ turnsSeen++;
992
+ if (turnsSeen >= turnsToKeep) {
993
+ cutIndex = i;
994
+ break;
995
+ }
996
+ }
997
+ }
998
+ if (cutIndex <= 2) {
999
+ addSystemText(C.zinc400('Not enough complete turns to compact'));
1000
+ addSpacer();
1001
+ tui.setFocus(editor);
1002
+ return true;
1003
+ }
1004
+ const toCompress = msgs.slice(0, cutIndex);
1005
+ const toKeep = msgs.slice(cutIndex);
1006
+ // ── Show loader ───────────────────────────────────────────────
1007
+ const compactLoader = new Loader(tui, (s) => C.emerald(s), (s) => C.zinc600(s), 'compacting with LLM...');
1008
+ compactLoader.start();
1009
+ chatContainer.addChild(compactLoader);
1010
+ tui.requestRender();
1011
+ // ── Serialize messages for the summarizer ─────────────────────
1012
+ // Strip tool results to raw text, cap total length to ~12k chars
1013
+ const serialized = [];
1014
+ let totalLen = 0;
1015
+ const MAX_CHARS = 12000;
1016
+ for (const m of toCompress) {
1017
+ if (totalLen >= MAX_CHARS)
1018
+ break;
1019
+ let text = '';
1020
+ if (typeof m.content === 'string') {
1021
+ text = m.content;
1022
+ }
1023
+ else if (Array.isArray(m.content)) {
1024
+ // OpenAI format: content blocks
1025
+ text = m.content
1026
+ .filter((b) => b.type === 'text')
1027
+ .map((b) => b.text)
1028
+ .join('\n');
1029
+ }
1030
+ if (!text)
1031
+ continue;
1032
+ const role = (m.role || 'unknown').toUpperCase();
1033
+ const truncated = text.slice(0, 800);
1034
+ const line = `[${role}]: ${truncated}`;
1035
+ serialized.push(line);
1036
+ totalLen += line.length;
1037
+ }
1038
+ const conversationDump = serialized.join('\n\n');
1039
+ // ── Call OpenRouter for LLM summary ───────────────────────────
1040
+ // Use a cheap/fast model — gemini flash
1041
+ const summaryModel = 'google/gemini-2.0-flash-001';
1042
+ const summarySystemPrompt = `You are a conversation compressor. Given a conversation between a user and a prediction-market trading assistant, produce a dense summary that preserves:
1043
+ 1. All factual conclusions, numbers, prices, and probabilities mentioned
1044
+ 2. Key trading decisions, positions taken or discussed
1045
+ 3. Signals injected, evaluations triggered, and their outcomes
1046
+ 4. Any action items or pending questions
1047
+
1048
+ Output a structured summary. Be concise but preserve every important detail — this summary replaces the original messages for continued conversation. Do NOT add commentary or meta-text. Just the summary.`;
1049
+ let summaryText;
1050
+ try {
1051
+ const orRes = await fetch('https://openrouter.ai/api/v1/chat/completions', {
1052
+ method: 'POST',
1053
+ headers: {
1054
+ 'Content-Type': 'application/json',
1055
+ 'Authorization': `Bearer ${openrouterKey}`,
1056
+ 'HTTP-Referer': 'https://simplefunctions.com',
1057
+ 'X-Title': 'SF Agent Compact',
1058
+ },
1059
+ body: JSON.stringify({
1060
+ model: summaryModel,
1061
+ messages: [
1062
+ { role: 'system', content: summarySystemPrompt },
1063
+ { role: 'user', content: `Summarize this conversation (${toCompress.length} messages):\n\n${conversationDump}` },
1064
+ ],
1065
+ max_tokens: 2000,
1066
+ temperature: 0.2,
1067
+ }),
1068
+ });
1069
+ if (!orRes.ok) {
1070
+ const errText = await orRes.text().catch(() => '');
1071
+ throw new Error(`OpenRouter ${orRes.status}: ${errText.slice(0, 200)}`);
1072
+ }
1073
+ const orData = await orRes.json();
1074
+ summaryText = orData.choices?.[0]?.message?.content || '';
1075
+ if (!summaryText) {
1076
+ throw new Error('Empty summary from LLM');
1077
+ }
940
1078
  }
941
- else if (m.role === 'assistant' && content) {
942
- bulletPoints.push(`- Assistant: ${content.slice(0, 150)}`);
1079
+ catch (llmErr) {
1080
+ // LLM failed — fall back to bullet-point extraction
1081
+ const bulletPoints = [];
1082
+ for (const m of toCompress) {
1083
+ const content = typeof m.content === 'string' ? m.content : '';
1084
+ if (m.role === 'user' && content) {
1085
+ bulletPoints.push(`- User: ${content.slice(0, 100)}`);
1086
+ }
1087
+ else if (m.role === 'assistant' && content) {
1088
+ bulletPoints.push(`- Assistant: ${content.slice(0, 150)}`);
1089
+ }
1090
+ }
1091
+ summaryText = `[LLM summary failed: ${llmErr.message}. Fallback bullet points:]\n\n${bulletPoints.slice(-20).join('\n')}`;
943
1092
  }
1093
+ // ── Remove loader ─────────────────────────────────────────────
1094
+ compactLoader.stop();
1095
+ chatContainer.removeChild(compactLoader);
1096
+ // ── Build compacted message array ──────────────────────────────
1097
+ // user(summary) → assistant(ack) → ...toKeep
1098
+ // This maintains valid user→assistant alternation.
1099
+ // toKeep starts with a user message (guaranteed by our cut logic).
1100
+ const compactedMessages = [
1101
+ {
1102
+ role: 'user',
1103
+ content: `[Conversation summary — ${toCompress.length} messages compressed]\n\n${summaryText}`,
1104
+ },
1105
+ {
1106
+ role: 'assistant',
1107
+ content: 'Understood. I have the full conversation context from the summary above. Continuing from where we left off.',
1108
+ },
1109
+ ...toKeep,
1110
+ ];
1111
+ // ── Replace agent state ───────────────────────────────────────
1112
+ // Clear first to reset internal state, then load compacted messages
1113
+ agent.clearMessages();
1114
+ agent.replaceMessages(compactedMessages);
1115
+ agent.setSystemPrompt(systemPrompt);
1116
+ persistSession();
1117
+ addSystemText(C.emerald(`Compacted: ${toCompress.length} messages \u2192 summary + ${toKeep.length} recent`) +
1118
+ C.zinc600(` (via ${summaryModel.split('/').pop()})`));
1119
+ addSpacer();
1120
+ // Force re-focus and render so editor stays responsive
1121
+ tui.setFocus(editor);
1122
+ tui.requestRender();
1123
+ }
1124
+ catch (err) {
1125
+ addSystemText(C.red(`Compact failed: ${err.message || err}`));
1126
+ addSpacer();
1127
+ tui.setFocus(editor);
1128
+ tui.requestRender();
944
1129
  }
945
- const summary = bulletPoints.slice(-20).join('\n');
946
- // Replace messages: summary + recent
947
- const compactedMessages = [
948
- { role: 'assistant', content: `[Conversation summary - ${toCompress.length} messages compressed]\n${summary}` },
949
- ...toKeep,
950
- ];
951
- agent.replaceMessages(compactedMessages);
952
- persistSession();
953
- addSystemText(C.emerald(`Compacted: ${toCompress.length} messages \u2192 summary + ${toKeep.length} recent`));
954
- addSpacer();
955
1130
  return true;
956
1131
  }
957
1132
  case '/new': {
@@ -965,6 +1140,33 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
965
1140
  tui.requestRender();
966
1141
  return true;
967
1142
  }
1143
+ case '/env': {
1144
+ addSpacer();
1145
+ const envVars = [
1146
+ { name: 'SF_API_KEY', key: 'SF_API_KEY', required: true, mask: true },
1147
+ { name: 'SF_API_URL', key: 'SF_API_URL', required: false, mask: false },
1148
+ { name: 'OPENROUTER_KEY', key: 'OPENROUTER_API_KEY', required: true, mask: true },
1149
+ { name: 'KALSHI_KEY_ID', key: 'KALSHI_API_KEY_ID', required: false, mask: true },
1150
+ { name: 'KALSHI_PEM_PATH', key: 'KALSHI_PRIVATE_KEY_PATH', required: false, mask: false },
1151
+ { name: 'TAVILY_API_KEY', key: 'TAVILY_API_KEY', required: false, mask: true },
1152
+ ];
1153
+ const lines = envVars.map(v => {
1154
+ const val = process.env[v.key];
1155
+ if (val) {
1156
+ const display = v.mask
1157
+ ? val.slice(0, Math.min(8, val.length)) + '...' + val.slice(-4)
1158
+ : val;
1159
+ return ` ${v.name.padEnd(18)} ${C.emerald('\u2713')} ${C.zinc400(display)}`;
1160
+ }
1161
+ else {
1162
+ const note = v.required ? '\u5FC5\u987B' : '\u53EF\u9009';
1163
+ return ` ${v.name.padEnd(18)} ${C.red('\u2717')} ${C.zinc600(`\u672A\u914D\u7F6E\uFF08${note}\uFF09`)}`;
1164
+ }
1165
+ });
1166
+ addSystemText(C.zinc200(bold('Environment')) + '\n' + lines.join('\n'));
1167
+ addSpacer();
1168
+ return true;
1169
+ }
968
1170
  case '/clear': {
969
1171
  chatContainer.clear();
970
1172
  tui.requestRender();
@@ -0,0 +1,21 @@
1
+ /**
2
+ * sf edges — Top edges across all active theses
3
+ *
4
+ * The most important output of the entire system: "what to trade now."
5
+ *
6
+ * Flow:
7
+ * 1. GET /api/thesis → all active theses
8
+ * 2. For each: GET /api/thesis/:id/context → edges with orderbook
9
+ * 3. Optional: getPositions() → Kalshi positions with live prices
10
+ * 4. Merge edges, dedupe by marketId (keep highest edge, note source thesis)
11
+ * 5. Sort by executableEdge descending
12
+ * 6. Display table with position overlay + summary
13
+ */
14
+ interface EdgesOpts {
15
+ json?: boolean;
16
+ limit?: string;
17
+ apiKey?: string;
18
+ apiUrl?: string;
19
+ }
20
+ export declare function edgesCommand(opts: EdgesOpts): Promise<void>;
21
+ export {};
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ /**
3
+ * sf edges — Top edges across all active theses
4
+ *
5
+ * The most important output of the entire system: "what to trade now."
6
+ *
7
+ * Flow:
8
+ * 1. GET /api/thesis → all active theses
9
+ * 2. For each: GET /api/thesis/:id/context → edges with orderbook
10
+ * 3. Optional: getPositions() → Kalshi positions with live prices
11
+ * 4. Merge edges, dedupe by marketId (keep highest edge, note source thesis)
12
+ * 5. Sort by executableEdge descending
13
+ * 6. Display table with position overlay + summary
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.edgesCommand = edgesCommand;
17
+ const client_js_1 = require("../client.js");
18
+ const kalshi_js_1 = require("../kalshi.js");
19
+ const utils_js_1 = require("../utils.js");
20
+ async function edgesCommand(opts) {
21
+ const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
22
+ const limit = parseInt(opts.limit || '20');
23
+ // ── Step 1: Fetch all active theses ────────────────────────────────────────
24
+ console.log(`${utils_js_1.c.dim}Fetching theses...${utils_js_1.c.reset}`);
25
+ const data = await client.listTheses();
26
+ const rawTheses = data.theses || data;
27
+ const theses = (Array.isArray(rawTheses) ? rawTheses : []).filter((t) => t.status === 'active');
28
+ if (theses.length === 0) {
29
+ console.log(`${utils_js_1.c.yellow}No active theses found.${utils_js_1.c.reset} Create one: sf create "your thesis"`);
30
+ return;
31
+ }
32
+ // ── Step 2: Fetch context for each thesis (parallel) ───────────────────────
33
+ console.log(`${utils_js_1.c.dim}Fetching edges from ${theses.length} theses...${utils_js_1.c.reset}`);
34
+ const allEdges = [];
35
+ const contextPromises = theses.map(async (t) => {
36
+ try {
37
+ const ctx = await client.getContext(t.id);
38
+ return { thesisId: t.id, edges: ctx.edges || [] };
39
+ }
40
+ catch {
41
+ return { thesisId: t.id, edges: [] };
42
+ }
43
+ });
44
+ const results = await Promise.all(contextPromises);
45
+ for (const { thesisId, edges } of results) {
46
+ for (const e of edges) {
47
+ allEdges.push({
48
+ marketId: e.marketId || '',
49
+ market: e.market || e.marketTitle || e.marketId || '',
50
+ venue: e.venue || 'kalshi',
51
+ direction: e.direction || 'yes',
52
+ marketPrice: typeof e.marketPrice === 'number' ? e.marketPrice : 0,
53
+ thesisPrice: typeof e.thesisPrice === 'number' ? e.thesisPrice : 0,
54
+ edge: typeof e.edge === 'number' ? e.edge : 0,
55
+ executableEdge: typeof e.executableEdge === 'number' ? e.executableEdge : null,
56
+ spread: e.orderbook?.spread ?? null,
57
+ liquidityScore: e.orderbook?.liquidityScore ?? null,
58
+ thesisId,
59
+ position: null,
60
+ });
61
+ }
62
+ }
63
+ if (allEdges.length === 0) {
64
+ console.log(`${utils_js_1.c.yellow}No edges found across ${theses.length} theses.${utils_js_1.c.reset}`);
65
+ return;
66
+ }
67
+ // ── Step 3: Dedupe by marketId — keep highest absolute edge ────────────────
68
+ const deduped = new Map();
69
+ for (const edge of allEdges) {
70
+ const key = edge.marketId;
71
+ if (!key)
72
+ continue;
73
+ const existing = deduped.get(key);
74
+ if (!existing || Math.abs(edge.edge) > Math.abs(existing.edge)) {
75
+ deduped.set(key, edge);
76
+ }
77
+ }
78
+ let merged = Array.from(deduped.values());
79
+ // ── Step 4: Fetch positions (optional) ─────────────────────────────────────
80
+ let positions = null;
81
+ if ((0, kalshi_js_1.isKalshiConfigured)()) {
82
+ console.log(`${utils_js_1.c.dim}Fetching Kalshi positions...${utils_js_1.c.reset}`);
83
+ positions = await (0, kalshi_js_1.getPositions)();
84
+ if (positions) {
85
+ // Enrich with live prices
86
+ for (const pos of positions) {
87
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
88
+ if (livePrice !== null) {
89
+ pos.current_value = livePrice;
90
+ pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
91
+ }
92
+ }
93
+ // Match positions to edges
94
+ for (const edge of merged) {
95
+ const pos = positions.find(p => p.ticker === edge.marketId ||
96
+ (edge.marketId && p.ticker?.includes(edge.marketId)));
97
+ if (pos) {
98
+ edge.position = {
99
+ side: pos.side || 'yes',
100
+ quantity: pos.quantity,
101
+ avgPrice: pos.average_price_paid,
102
+ currentValue: pos.current_value,
103
+ pnl: pos.unrealized_pnl || 0,
104
+ totalCost: pos.total_cost || Math.round(pos.average_price_paid * pos.quantity),
105
+ };
106
+ }
107
+ }
108
+ }
109
+ }
110
+ // ── Step 5: Sort by executableEdge (or edge) descending ────────────────────
111
+ merged.sort((a, b) => {
112
+ const aVal = a.executableEdge !== null ? a.executableEdge : a.edge;
113
+ const bVal = b.executableEdge !== null ? b.executableEdge : b.edge;
114
+ return Math.abs(bVal) - Math.abs(aVal);
115
+ });
116
+ // Apply limit
117
+ const display = merged.slice(0, limit);
118
+ // ── Step 6: JSON output ────────────────────────────────────────────────────
119
+ if (opts.json) {
120
+ console.log(JSON.stringify({
121
+ totalEdges: merged.length,
122
+ displayed: display.length,
123
+ thesesScanned: theses.length,
124
+ edges: display,
125
+ }, null, 2));
126
+ return;
127
+ }
128
+ // ── Step 6: Pretty output ──────────────────────────────────────────────────
129
+ console.log();
130
+ (0, utils_js_1.header)(`Top Edges Across ${theses.length} Theses`);
131
+ console.log();
132
+ // Header row
133
+ const hdr = [
134
+ (0, utils_js_1.pad)('Market', 32),
135
+ (0, utils_js_1.rpad)('Mkt', 5),
136
+ (0, utils_js_1.rpad)('Thesis', 7),
137
+ (0, utils_js_1.rpad)('Edge', 6),
138
+ (0, utils_js_1.rpad)('Exec', 6),
139
+ (0, utils_js_1.rpad)('Sprd', 5),
140
+ (0, utils_js_1.pad)('Liq', 5),
141
+ (0, utils_js_1.pad)('Thesis', 10),
142
+ (0, utils_js_1.pad)('Position', 20),
143
+ ].join(' ');
144
+ console.log(`${utils_js_1.c.dim}${hdr}${utils_js_1.c.reset}`);
145
+ (0, utils_js_1.hr)(100);
146
+ for (const edge of display) {
147
+ const name = (0, utils_js_1.trunc)(edge.market, 31);
148
+ const mktStr = `${edge.marketPrice}¢`;
149
+ const thesisStr = `${edge.thesisPrice}¢`;
150
+ const edgeStr = edge.edge > 0 ? `+${edge.edge}` : `${edge.edge}`;
151
+ const execStr = edge.executableEdge !== null ? (edge.executableEdge > 0 ? `+${edge.executableEdge}` : `${edge.executableEdge}`) : '—';
152
+ const spreadStr = edge.spread !== null ? `${edge.spread}¢` : '—';
153
+ const liqStr = edge.liquidityScore || '—';
154
+ const thesisIdStr = (0, utils_js_1.shortId)(edge.thesisId);
155
+ // Color the edge values
156
+ const edgeColor = edge.edge > 0 ? utils_js_1.c.green : edge.edge < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
157
+ const execColor = edge.executableEdge !== null ? (edge.executableEdge > 0 ? utils_js_1.c.green : utils_js_1.c.red) : utils_js_1.c.dim;
158
+ const liqColor = liqStr === 'high' ? utils_js_1.c.green : liqStr === 'medium' ? utils_js_1.c.yellow : utils_js_1.c.dim;
159
+ // Position string
160
+ let posStr = `${utils_js_1.c.dim}—${utils_js_1.c.reset}`;
161
+ if (edge.position) {
162
+ const p = edge.position;
163
+ const pnlStr = p.pnl >= 0 ? `${utils_js_1.c.green}+$${(p.pnl / 100).toFixed(0)}${utils_js_1.c.reset}` : `${utils_js_1.c.red}-$${(Math.abs(p.pnl) / 100).toFixed(0)}${utils_js_1.c.reset}`;
164
+ posStr = `${utils_js_1.c.green}${p.quantity}@${p.avgPrice}¢${utils_js_1.c.reset} ${pnlStr}`;
165
+ }
166
+ const row = [
167
+ edge.position ? `${utils_js_1.c.green}${(0, utils_js_1.pad)(name, 32)}${utils_js_1.c.reset}` : (0, utils_js_1.pad)(name, 32),
168
+ (0, utils_js_1.rpad)(mktStr, 5),
169
+ (0, utils_js_1.rpad)(thesisStr, 7),
170
+ `${edgeColor}${(0, utils_js_1.rpad)(edgeStr, 6)}${utils_js_1.c.reset}`,
171
+ `${execColor}${(0, utils_js_1.rpad)(execStr, 6)}${utils_js_1.c.reset}`,
172
+ (0, utils_js_1.rpad)(spreadStr, 5),
173
+ `${liqColor}${(0, utils_js_1.pad)(liqStr, 5)}${utils_js_1.c.reset}`,
174
+ `${utils_js_1.c.dim}${(0, utils_js_1.pad)(thesisIdStr, 10)}${utils_js_1.c.reset}`,
175
+ posStr,
176
+ ].join(' ');
177
+ console.log(row);
178
+ }
179
+ // ── Summary ────────────────────────────────────────────────────────────────
180
+ (0, utils_js_1.hr)(100);
181
+ // Positioned summary
182
+ const positioned = display.filter(e => e.position);
183
+ if (positioned.length > 0) {
184
+ let totalCost = 0;
185
+ let totalPnl = 0;
186
+ for (const e of positioned) {
187
+ totalCost += e.position.totalCost;
188
+ totalPnl += e.position.pnl;
189
+ }
190
+ const costStr = `$${(totalCost / 100).toFixed(0)}`;
191
+ const pnlColor = totalPnl >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
192
+ const pnlSign = totalPnl >= 0 ? '+' : '-';
193
+ const pnlStr = `${pnlColor}${pnlSign}$${(Math.abs(totalPnl) / 100).toFixed(0)}${utils_js_1.c.reset}`;
194
+ console.log(`${utils_js_1.c.bold}Total positioned:${utils_js_1.c.reset} ${costStr} cost | P&L: ${pnlStr}`);
195
+ }
196
+ // Top unpositioned
197
+ const unpositioned = display.filter(e => !e.position && e.edge > 0);
198
+ if (unpositioned.length > 0) {
199
+ const top = unpositioned[0];
200
+ const execLabel = top.executableEdge !== null ? `exec +${top.executableEdge}` : `edge +${top.edge}`;
201
+ const liq = top.liquidityScore ? `, ${top.liquidityScore} liq` : '';
202
+ console.log(`${utils_js_1.c.bold}Top unpositioned:${utils_js_1.c.reset} ${(0, utils_js_1.trunc)(top.market, 30)} @ ${top.marketPrice}¢ (${execLabel}${liq})`);
203
+ }
204
+ console.log();
205
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * sf setup — Interactive configuration wizard
3
+ *
4
+ * Walks user through:
5
+ * 1. SF API key (required)
6
+ * 2. OpenRouter API key (optional, for agent)
7
+ * 3. Kalshi exchange credentials (optional, for positions)
8
+ * 4. Tavily API key (optional, for web search)
9
+ * 5. First thesis creation (if none exist)
10
+ *
11
+ * Each key is validated in real-time.
12
+ * Config is saved to ~/.sf/config.json.
13
+ */
14
+ interface SetupOpts {
15
+ check?: boolean;
16
+ reset?: boolean;
17
+ key?: string;
18
+ }
19
+ export declare function setupCommand(opts: SetupOpts): Promise<void>;
20
+ export {};