@spfunctions/cli 1.1.2 → 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,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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
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"