@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.
- package/dist/commands/agent.js +233 -31
- package/dist/commands/edges.d.ts +21 -0
- package/dist/commands/edges.js +205 -0
- package/dist/commands/setup.d.ts +20 -0
- package/dist/commands/setup.js +569 -0
- package/dist/config.d.ts +50 -0
- package/dist/config.js +117 -0
- package/dist/index.d.ts +6 -3
- package/dist/index.js +54 -11
- package/package.json +1 -1
package/dist/commands/agent.js
CHANGED
|
@@ -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:
|
|
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.
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
-
|
|
942
|
-
|
|
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 {};
|