@spfunctions/cli 1.1.0 → 1.1.3

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;
@@ -570,18 +574,102 @@ 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
- // ── System prompt ──────────────────────────────────────────────────────────
575
- const systemPrompt = `You are a SimpleFunctions prediction market trading assistant.
617
+ // ── System prompt builder ──────────────────────────────────────────────────
618
+ function buildSystemPrompt(ctx) {
619
+ const edgesSummary = ctx.edges
620
+ ?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
621
+ .slice(0, 5)
622
+ .map((e) => ` ${(e.market || '').slice(0, 40)} | ${e.venue || 'kalshi'} | mkt ${e.marketPrice}\u00A2 \u2192 thesis ${e.thesisPrice}\u00A2 | edge ${e.edge > 0 ? '+' : ''}${e.edge} | ${e.orderbook?.liquidityScore || '?'}`)
623
+ .join('\n') || ' (no edge data)';
624
+ const nodesSummary = ctx.causalTree?.nodes
625
+ ?.filter((n) => n.depth === 0)
626
+ .map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
627
+ .join('\n') || ' (no causal tree)';
628
+ const conf = typeof ctx.confidence === 'number'
629
+ ? Math.round(ctx.confidence * 100)
630
+ : (typeof ctx.confidence === 'string' ? parseInt(ctx.confidence) : 0);
631
+ return `You are a prediction market trading assistant. Your job is not to please the user \u2014 it is to help them see reality clearly and make correct trading decisions.
632
+
633
+ ## Your analytical framework
634
+
635
+ Each thesis has a causal tree. Every node is a causal hypothesis with a probability. Nodes have causal relationships \u2014 when upstream nodes change, downstream nodes follow.
636
+
637
+ Edge = thesis-implied price - actual market price. Positive edge means the market underprices this event. Negative edge means overpriced. Contracts with large edges AND good liquidity are the most tradeable.
638
+
639
+ executableEdge is the real edge after subtracting the bid-ask spread. A contract with a big theoretical edge but wide spread may not be worth entering.
640
+
641
+ Short-term markets (weekly/monthly contracts) settle into hard data that calibrates the long-term thesis. Don't use them to bet (outcomes are nearly known) \u2014 use them to verify whether causal tree node probabilities are accurate.
642
+
643
+ ## Your behavioral rules
644
+
645
+ - Think before calling tools. If the data is already in context, don't re-fetch.
646
+ - If the user asks about positions, check if Kalshi is configured first. If not, say so directly.
647
+ - If the user says "note this" or mentions a news event, inject a signal. Don't ask "should I note this?"
648
+ - If the user says "evaluate" or "run it", trigger immediately. Don't confirm.
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.
651
+ - If you notice an edge narrowing or disappearing, say so proactively. Don't only report good news.
652
+ - If a causal tree node probability seriously contradicts the market price, point it out.
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.
655
+ - Align tables. Be precise with numbers to the cent.
576
656
 
577
- Current thesis: ${latestContext.thesis || latestContext.rawThesis || 'N/A'}
578
- Confidence: ${confidencePct}%
579
- Status: ${latestContext.status}
580
- Thesis ID: ${latestContext.thesisId || resolvedThesisId}
657
+ ## Current thesis state
581
658
 
582
- You have six tools available. Use them when you need real-time data. Answer directly when you don't.
583
- Be concise. Use Chinese if the user writes in Chinese, English if they write in English.
584
- Do NOT make up data. Always call tools to get current state.`;
659
+ Thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
660
+ ID: ${ctx.thesisId || resolvedThesisId}
661
+ Confidence: ${conf}%
662
+ Status: ${ctx.status}
663
+
664
+ Top-level causal tree nodes:
665
+ ${nodesSummary}
666
+
667
+ Top 5 edges by magnitude:
668
+ ${edgesSummary}
669
+
670
+ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
671
+ }
672
+ const systemPrompt = buildSystemPrompt(latestContext);
585
673
  // ── Create Agent ───────────────────────────────────────────────────────────
586
674
  const agent = new Agent({
587
675
  initialState: {
@@ -741,6 +829,7 @@ Do NOT make up data. Always call tools to get current state.`;
741
829
  C.emerald('/compact ') + C.zinc400('Compress conversation history') + '\n' +
742
830
  C.emerald('/new ') + C.zinc400('Start fresh session') + '\n' +
743
831
  C.emerald('/model <m> ') + C.zinc400('Switch model') + '\n' +
832
+ C.emerald('/env ') + C.zinc400('Show environment variable status') + '\n' +
744
833
  C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
745
834
  C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
746
835
  addSpacer();
@@ -844,10 +933,14 @@ Do NOT make up data. Always call tools to get current state.`;
844
933
  const newContext = await sfClient.getContext(newId);
845
934
  resolvedThesisId = newContext.thesisId || newId;
846
935
  latestContext = newContext;
847
- // Build new system prompt
936
+ // Build new system prompt using the rich builder
937
+ const newSysPrompt = buildSystemPrompt(newContext);
848
938
  const newConf = typeof newContext.confidence === 'number'
849
939
  ? Math.round(newContext.confidence * 100) : 0;
850
- const newSysPrompt = `You are a SimpleFunctions prediction market trading assistant.\n\nCurrent thesis: ${newContext.thesis || newContext.rawThesis || 'N/A'}\nConfidence: ${newConf}%\nStatus: ${newContext.status}\nThesis ID: ${resolvedThesisId}\n\nYou have six tools available. Use them when you need real-time data. Answer directly when you don't.\nBe concise. Use Chinese if the user writes in Chinese, English if they write in English.\nDo NOT make up data. Always call tools to get current state.`;
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();
851
944
  // Load saved session or start fresh
852
945
  const saved = loadSession(resolvedThesisId);
853
946
  if (saved?.messages?.length > 0) {
@@ -856,7 +949,6 @@ Do NOT make up data. Always call tools to get current state.`;
856
949
  addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(` (resumed ${saved.messages.length} messages)`));
857
950
  }
858
951
  else {
859
- agent.clearMessages();
860
952
  agent.setSystemPrompt(newSysPrompt);
861
953
  addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
862
954
  }
@@ -873,42 +965,168 @@ Do NOT make up data. Always call tools to get current state.`;
873
965
  addSystemText(C.red(`Switch failed: ${err.message}`));
874
966
  }
875
967
  addSpacer();
968
+ // Force re-focus editor so input stays responsive
969
+ tui.setFocus(editor);
876
970
  tui.requestRender();
877
971
  return true;
878
972
  }
879
973
  case '/compact': {
880
974
  addSpacer();
881
- const msgs = agent.state.messages;
882
- if (msgs.length <= 10) {
883
- addSystemText(C.zinc400('Conversation too short to compact'));
884
- addSpacer();
885
- return true;
886
- }
887
- // Keep recent 6 messages (3 turns) + create summary of the rest
888
- const recentCount = 6;
889
- const toCompress = msgs.slice(0, -recentCount);
890
- const toKeep = msgs.slice(-recentCount);
891
- // Extract text for summary (no LLM, just bullet points)
892
- const bulletPoints = [];
893
- for (const m of toCompress) {
894
- const content = typeof m.content === 'string' ? m.content : '';
895
- if (m.role === 'user' && content) {
896
- 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
+ }
897
997
  }
898
- else if (m.role === 'assistant' && content) {
899
- bulletPoints.push(`- Assistant: ${content.slice(0, 150)}`);
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;
900
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
+ }
1078
+ }
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')}`;
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();
901
1129
  }
902
- const summary = bulletPoints.slice(-20).join('\n');
903
- // Replace messages: summary + recent
904
- const compactedMessages = [
905
- { role: 'assistant', content: `[Conversation summary - ${toCompress.length} messages compressed]\n${summary}` },
906
- ...toKeep,
907
- ];
908
- agent.replaceMessages(compactedMessages);
909
- persistSession();
910
- addSystemText(C.emerald(`Compacted: ${toCompress.length} messages \u2192 summary + ${toKeep.length} recent`));
911
- addSpacer();
912
1130
  return true;
913
1131
  }
914
1132
  case '/new': {
@@ -922,6 +1140,33 @@ Do NOT make up data. Always call tools to get current state.`;
922
1140
  tui.requestRender();
923
1141
  return true;
924
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
+ }
925
1170
  case '/clear': {
926
1171
  chatContainer.clear();
927
1172
  tui.requestRender();
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.1.0",
4
- "description": "CLI for SimpleFunctions prediction market thesis agent",
3
+ "version": "1.1.3",
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"
7
7
  },
@@ -26,11 +26,15 @@
26
26
  "dist"
27
27
  ],
28
28
  "keywords": [
29
- "prediction-markets",
30
- "thesis-agent",
29
+ "prediction-market",
31
30
  "kalshi",
32
31
  "polymarket",
33
- "cli"
32
+ "trading",
33
+ "cli",
34
+ "agent",
35
+ "orderbook",
36
+ "market-intelligence",
37
+ "edge-detection"
34
38
  ],
35
39
  "license": "MIT",
36
40
  "repository": {