@spfunctions/cli 0.1.7 → 1.1.2

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.
@@ -15,4 +15,5 @@
15
15
  export declare function agentCommand(thesisId?: string, opts?: {
16
16
  model?: string;
17
17
  modelKey?: string;
18
+ newSession?: boolean;
18
19
  }): Promise<void>;
@@ -13,10 +13,43 @@
13
13
  * Slash commands (bypass LLM):
14
14
  * /help /tree /edges /pos /eval /model /clear /exit
15
15
  */
16
+ var __importDefault = (this && this.__importDefault) || function (mod) {
17
+ return (mod && mod.__esModule) ? mod : { "default": mod };
18
+ };
16
19
  Object.defineProperty(exports, "__esModule", { value: true });
17
20
  exports.agentCommand = agentCommand;
21
+ const fs_1 = __importDefault(require("fs"));
22
+ const path_1 = __importDefault(require("path"));
23
+ const os_1 = __importDefault(require("os"));
18
24
  const client_js_1 = require("../client.js");
19
25
  const kalshi_js_1 = require("../kalshi.js");
26
+ // ─── Session persistence ─────────────────────────────────────────────────────
27
+ function getSessionDir() {
28
+ return path_1.default.join(os_1.default.homedir(), '.sf', 'sessions');
29
+ }
30
+ function getSessionPath(thesisId) {
31
+ return path_1.default.join(getSessionDir(), `${thesisId}.json`);
32
+ }
33
+ function loadSession(thesisId) {
34
+ const p = getSessionPath(thesisId);
35
+ try {
36
+ if (fs_1.default.existsSync(p)) {
37
+ return JSON.parse(fs_1.default.readFileSync(p, 'utf-8'));
38
+ }
39
+ }
40
+ catch { /* corrupt file, ignore */ }
41
+ return null;
42
+ }
43
+ function saveSession(thesisId, model, messages) {
44
+ const dir = getSessionDir();
45
+ fs_1.default.mkdirSync(dir, { recursive: true });
46
+ fs_1.default.writeFileSync(getSessionPath(thesisId), JSON.stringify({
47
+ thesisId,
48
+ model,
49
+ updatedAt: new Date().toISOString(),
50
+ messages,
51
+ }, null, 2));
52
+ }
20
53
  // ─── ANSI 24-bit color helpers (no chalk dependency) ─────────────────────────
21
54
  const rgb = (r, g, b) => (s) => `\x1b[38;2;${r};${g};${b}m${s}\x1b[39m`;
22
55
  const bgRgb = (r, g, b) => (s) => `\x1b[48;2;${r};${g};${b}m${s}\x1b[49m`;
@@ -368,9 +401,12 @@ async function agentCommand(thesisId, opts) {
368
401
  { name: 'edges', description: 'Display edge/spread table' },
369
402
  { name: 'pos', description: 'Display Kalshi positions' },
370
403
  { name: 'eval', description: 'Trigger deep evaluation' },
404
+ { name: 'switch', description: 'Switch thesis (e.g. /switch f582bf76)' },
405
+ { name: 'compact', description: 'Compress conversation history' },
406
+ { name: 'new', description: 'Start fresh session' },
371
407
  { name: 'model', description: 'Switch model (e.g. /model anthropic/claude-sonnet-4)' },
372
- { name: 'clear', description: 'Clear chat' },
373
- { name: 'exit', description: 'Exit agent' },
408
+ { name: 'clear', description: 'Clear screen (keeps history)' },
409
+ { name: 'exit', description: 'Exit agent (auto-saves)' },
374
410
  ], process.cwd());
375
411
  editor.setAutocompleteProvider(autocompleteProvider);
376
412
  // Assemble TUI tree
@@ -535,17 +571,60 @@ async function agentCommand(thesisId, opts) {
535
571
  },
536
572
  },
537
573
  ];
538
- // ── System prompt ──────────────────────────────────────────────────────────
539
- const systemPrompt = `You are a SimpleFunctions prediction market trading assistant.
574
+ // ── System prompt builder ──────────────────────────────────────────────────
575
+ function buildSystemPrompt(ctx) {
576
+ const edgesSummary = ctx.edges
577
+ ?.sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
578
+ .slice(0, 5)
579
+ .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 || '?'}`)
580
+ .join('\n') || ' (no edge data)';
581
+ const nodesSummary = ctx.causalTree?.nodes
582
+ ?.filter((n) => n.depth === 0)
583
+ .map((n) => ` ${n.id} ${(n.label || '').slice(0, 40)} \u2014 ${Math.round(n.probability * 100)}%`)
584
+ .join('\n') || ' (no causal tree)';
585
+ const conf = typeof ctx.confidence === 'number'
586
+ ? Math.round(ctx.confidence * 100)
587
+ : (typeof ctx.confidence === 'string' ? parseInt(ctx.confidence) : 0);
588
+ 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.
589
+
590
+ ## Your analytical framework
591
+
592
+ 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.
593
+
594
+ 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.
595
+
596
+ 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.
597
+
598
+ 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.
599
+
600
+ ## Your behavioral rules
601
+
602
+ - Think before calling tools. If the data is already in context, don't re-fetch.
603
+ - If the user asks about positions, check if Kalshi is configured first. If not, say so directly.
604
+ - If the user says "note this" or mentions a news event, inject a signal. Don't ask "should I note this?"
605
+ - If the user says "evaluate" or "run it", trigger immediately. Don't confirm.
606
+ - Don't end every response with "anything else?" \u2014 the user will ask when they want to.
607
+ - If you notice an edge narrowing or disappearing, say so proactively. Don't only report good news.
608
+ - If a causal tree node probability seriously contradicts the market price, point it out.
609
+ - Use Chinese if the user writes in Chinese, English if they write in English.
610
+ - Align tables. Be precise with numbers to the cent.
540
611
 
541
- Current thesis: ${latestContext.thesis || latestContext.rawThesis || 'N/A'}
542
- Confidence: ${confidencePct}%
543
- Status: ${latestContext.status}
544
- Thesis ID: ${latestContext.thesisId || resolvedThesisId}
612
+ ## Current thesis state
545
613
 
546
- You have six tools available. Use them when you need real-time data. Answer directly when you don't.
547
- Be concise. Use Chinese if the user writes in Chinese, English if they write in English.
548
- Do NOT make up data. Always call tools to get current state.`;
614
+ Thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
615
+ ID: ${ctx.thesisId || resolvedThesisId}
616
+ Confidence: ${conf}%
617
+ Status: ${ctx.status}
618
+
619
+ Top-level causal tree nodes:
620
+ ${nodesSummary}
621
+
622
+ Top 5 edges by magnitude:
623
+ ${edgesSummary}
624
+
625
+ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation.summary.slice(0, 300)}` : ''}`;
626
+ }
627
+ const systemPrompt = buildSystemPrompt(latestContext);
549
628
  // ── Create Agent ───────────────────────────────────────────────────────────
550
629
  const agent = new Agent({
551
630
  initialState: {
@@ -561,6 +640,30 @@ Do NOT make up data. Always call tools to get current state.`;
561
640
  return undefined;
562
641
  },
563
642
  });
643
+ // ── Session restore ────────────────────────────────────────────────────────
644
+ let sessionRestored = false;
645
+ if (!opts?.newSession) {
646
+ const saved = loadSession(resolvedThesisId);
647
+ if (saved?.messages?.length > 0) {
648
+ try {
649
+ agent.replaceMessages(saved.messages);
650
+ // Always update system prompt with fresh context
651
+ agent.setSystemPrompt(systemPrompt);
652
+ sessionRestored = true;
653
+ }
654
+ catch { /* corrupt session, start fresh */ }
655
+ }
656
+ }
657
+ // Helper to persist session after each turn
658
+ function persistSession() {
659
+ try {
660
+ const msgs = agent.state.messages;
661
+ if (msgs.length > 0) {
662
+ saveSession(resolvedThesisId, currentModelName, msgs);
663
+ }
664
+ }
665
+ catch { /* best-effort save */ }
666
+ }
564
667
  // ── Subscribe to agent events → update TUI ────────────────────────────────
565
668
  let currentAssistantMd = null;
566
669
  let currentAssistantText = '';
@@ -633,6 +736,7 @@ Do NOT make up data. Always call tools to get current state.`;
633
736
  if (event.type === 'agent_end') {
634
737
  // Agent turn fully complete — safe to accept new input
635
738
  isProcessing = false;
739
+ persistSession();
636
740
  flushRender();
637
741
  }
638
742
  if (event.type === 'tool_execution_start') {
@@ -671,14 +775,17 @@ Do NOT make up data. Always call tools to get current state.`;
671
775
  case '/help': {
672
776
  addSpacer();
673
777
  addSystemText(C.zinc200(bold('Commands')) + '\n' +
674
- C.emerald('/help ') + C.zinc400(' Show this help') + '\n' +
675
- C.emerald('/tree ') + C.zinc400(' Display causal tree') + '\n' +
676
- C.emerald('/edges ') + C.zinc400(' Display edge/spread table') + '\n' +
677
- C.emerald('/pos ') + C.zinc400(' Display Kalshi positions') + '\n' +
678
- C.emerald('/eval ') + C.zinc400(' Trigger deep evaluation') + '\n' +
679
- C.emerald('/model ') + C.zinc400(' Switch model (e.g. /model anthropic/claude-sonnet-4)') + '\n' +
680
- C.emerald('/clear ') + C.zinc400(' Clear chat') + '\n' +
681
- C.emerald('/exit ') + C.zinc400(' Exit agent'));
778
+ C.emerald('/help ') + C.zinc400('Show this help') + '\n' +
779
+ C.emerald('/tree ') + C.zinc400('Display causal tree') + '\n' +
780
+ C.emerald('/edges ') + C.zinc400('Display edge/spread table') + '\n' +
781
+ C.emerald('/pos ') + C.zinc400('Display Kalshi positions') + '\n' +
782
+ C.emerald('/eval ') + C.zinc400('Trigger deep evaluation') + '\n' +
783
+ C.emerald('/switch <id>') + C.zinc400(' Switch thesis') + '\n' +
784
+ C.emerald('/compact ') + C.zinc400('Compress conversation history') + '\n' +
785
+ C.emerald('/new ') + C.zinc400('Start fresh session') + '\n' +
786
+ C.emerald('/model <m> ') + C.zinc400('Switch model') + '\n' +
787
+ C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
788
+ C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
682
789
  addSpacer();
683
790
  return true;
684
791
  }
@@ -766,6 +873,98 @@ Do NOT make up data. Always call tools to get current state.`;
766
873
  tui.requestRender();
767
874
  return true;
768
875
  }
876
+ case '/switch': {
877
+ const newId = parts[1]?.trim();
878
+ if (!newId) {
879
+ addSystemText(C.zinc400('Usage: /switch <thesisId>'));
880
+ return true;
881
+ }
882
+ addSpacer();
883
+ try {
884
+ // Save current session
885
+ persistSession();
886
+ // Load new thesis context
887
+ const newContext = await sfClient.getContext(newId);
888
+ resolvedThesisId = newContext.thesisId || newId;
889
+ latestContext = newContext;
890
+ // Build new system prompt using the rich builder
891
+ const newSysPrompt = buildSystemPrompt(newContext);
892
+ const newConf = typeof newContext.confidence === 'number'
893
+ ? Math.round(newContext.confidence * 100) : 0;
894
+ // Load saved session or start fresh
895
+ const saved = loadSession(resolvedThesisId);
896
+ if (saved?.messages?.length > 0) {
897
+ agent.replaceMessages(saved.messages);
898
+ agent.setSystemPrompt(newSysPrompt);
899
+ addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(` (resumed ${saved.messages.length} messages)`));
900
+ }
901
+ else {
902
+ agent.clearMessages();
903
+ agent.setSystemPrompt(newSysPrompt);
904
+ addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
905
+ }
906
+ // Update header
907
+ headerBar.update(C.emerald(bold('SF Agent')) + C.zinc600(` \u2014 ${resolvedThesisId.slice(0, 8)}`), newConf > 0 ? C.zinc200(`${newConf}%`) : '', undefined);
908
+ chatContainer.clear();
909
+ const thText = (newContext.thesis || newContext.rawThesis || '').slice(0, 120);
910
+ addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
911
+ C.zinc200(bold(thText)) + '\n' +
912
+ C.zinc600(`${newContext.status || 'active'} ${newConf > 0 ? newConf + '%' : ''} ${(newContext.edges || []).length} edges`) + '\n' +
913
+ C.zinc600('\u2500'.repeat(50)));
914
+ }
915
+ catch (err) {
916
+ addSystemText(C.red(`Switch failed: ${err.message}`));
917
+ }
918
+ addSpacer();
919
+ tui.requestRender();
920
+ return true;
921
+ }
922
+ case '/compact': {
923
+ 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)}`);
940
+ }
941
+ else if (m.role === 'assistant' && content) {
942
+ bulletPoints.push(`- Assistant: ${content.slice(0, 150)}`);
943
+ }
944
+ }
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
+ return true;
956
+ }
957
+ case '/new': {
958
+ addSpacer();
959
+ persistSession(); // save current before clearing
960
+ agent.clearMessages();
961
+ agent.setSystemPrompt(systemPrompt);
962
+ chatContainer.clear();
963
+ addSystemText(C.emerald('Session cleared') + C.zinc400(' \u2014 fresh start'));
964
+ addSpacer();
965
+ tui.requestRender();
966
+ return true;
967
+ }
769
968
  case '/clear': {
770
969
  chatContainer.clear();
771
970
  tui.requestRender();
@@ -821,6 +1020,7 @@ Do NOT make up data. Always call tools to get current state.`;
821
1020
  function cleanup() {
822
1021
  if (currentLoader)
823
1022
  currentLoader.stop();
1023
+ persistSession();
824
1024
  tui.stop();
825
1025
  process.exit(0);
826
1026
  }
@@ -839,9 +1039,12 @@ Do NOT make up data. Always call tools to get current state.`;
839
1039
  // ── Show initial welcome ───────────────────────────────────────────────────
840
1040
  const thesisText = latestContext.thesis || latestContext.rawThesis || 'N/A';
841
1041
  const truncatedThesis = thesisText.length > 120 ? thesisText.slice(0, 120) + '...' : thesisText;
1042
+ const sessionStatus = sessionRestored
1043
+ ? C.zinc600(` resumed (${agent.state.messages.length} messages)`)
1044
+ : C.zinc600(' new session');
842
1045
  addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
843
1046
  C.zinc200(bold(truncatedThesis)) + '\n' +
844
- C.zinc600(`${latestContext.status || 'active'} ${confidencePct > 0 ? confidencePct + '%' : ''} ${(latestContext.edges || []).length} edges`) + '\n' +
1047
+ C.zinc600(`${latestContext.status || 'active'} ${confidencePct > 0 ? confidencePct + '%' : ''} ${(latestContext.edges || []).length} edges`) + sessionStatus + '\n' +
845
1048
  C.zinc600('\u2500'.repeat(50)));
846
1049
  addSpacer();
847
1050
  // ── Start TUI ──────────────────────────────────────────────────────────────
package/dist/index.js CHANGED
@@ -135,11 +135,12 @@ program
135
135
  program
136
136
  .command('agent [thesisId]')
137
137
  .description('Interactive agent mode — natural language interface to SimpleFunctions')
138
- .option('--model <model>', 'Model via OpenRouter (default: anthropic/claude-sonnet-4-20250514)')
138
+ .option('--model <model>', 'Model via OpenRouter (default: anthropic/claude-sonnet-4.6)')
139
139
  .option('--model-key <key>', 'OpenRouter API key (or set OPENROUTER_API_KEY)')
140
+ .option('--new', 'Start a fresh session (default: continue last session)')
140
141
  .action(async (thesisId, opts, cmd) => {
141
142
  const g = cmd.optsWithGlobals();
142
- await run(() => (0, agent_js_1.agentCommand)(thesisId, { model: opts.model, modelKey: opts.modelKey }));
143
+ await run(() => (0, agent_js_1.agentCommand)(thesisId, { model: opts.model, modelKey: opts.modelKey, newSession: opts.new }));
143
144
  });
144
145
  // ── Error wrapper ─────────────────────────────────────────────────────────────
145
146
  async function run(fn) {
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "0.1.7",
4
- "description": "CLI for SimpleFunctions prediction market thesis agent",
3
+ "version": "1.1.2",
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": {