@spfunctions/cli 0.1.7 → 1.1.0

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
@@ -561,6 +597,30 @@ Do NOT make up data. Always call tools to get current state.`;
561
597
  return undefined;
562
598
  },
563
599
  });
600
+ // ── Session restore ────────────────────────────────────────────────────────
601
+ let sessionRestored = false;
602
+ if (!opts?.newSession) {
603
+ const saved = loadSession(resolvedThesisId);
604
+ if (saved?.messages?.length > 0) {
605
+ try {
606
+ agent.replaceMessages(saved.messages);
607
+ // Always update system prompt with fresh context
608
+ agent.setSystemPrompt(systemPrompt);
609
+ sessionRestored = true;
610
+ }
611
+ catch { /* corrupt session, start fresh */ }
612
+ }
613
+ }
614
+ // Helper to persist session after each turn
615
+ function persistSession() {
616
+ try {
617
+ const msgs = agent.state.messages;
618
+ if (msgs.length > 0) {
619
+ saveSession(resolvedThesisId, currentModelName, msgs);
620
+ }
621
+ }
622
+ catch { /* best-effort save */ }
623
+ }
564
624
  // ── Subscribe to agent events → update TUI ────────────────────────────────
565
625
  let currentAssistantMd = null;
566
626
  let currentAssistantText = '';
@@ -633,6 +693,7 @@ Do NOT make up data. Always call tools to get current state.`;
633
693
  if (event.type === 'agent_end') {
634
694
  // Agent turn fully complete — safe to accept new input
635
695
  isProcessing = false;
696
+ persistSession();
636
697
  flushRender();
637
698
  }
638
699
  if (event.type === 'tool_execution_start') {
@@ -671,14 +732,17 @@ Do NOT make up data. Always call tools to get current state.`;
671
732
  case '/help': {
672
733
  addSpacer();
673
734
  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'));
735
+ C.emerald('/help ') + C.zinc400('Show this help') + '\n' +
736
+ C.emerald('/tree ') + C.zinc400('Display causal tree') + '\n' +
737
+ C.emerald('/edges ') + C.zinc400('Display edge/spread table') + '\n' +
738
+ C.emerald('/pos ') + C.zinc400('Display Kalshi positions') + '\n' +
739
+ C.emerald('/eval ') + C.zinc400('Trigger deep evaluation') + '\n' +
740
+ C.emerald('/switch <id>') + C.zinc400(' Switch thesis') + '\n' +
741
+ C.emerald('/compact ') + C.zinc400('Compress conversation history') + '\n' +
742
+ C.emerald('/new ') + C.zinc400('Start fresh session') + '\n' +
743
+ C.emerald('/model <m> ') + C.zinc400('Switch model') + '\n' +
744
+ C.emerald('/clear ') + C.zinc400('Clear screen (keeps history)') + '\n' +
745
+ C.emerald('/exit ') + C.zinc400('Exit (auto-saves)'));
682
746
  addSpacer();
683
747
  return true;
684
748
  }
@@ -766,6 +830,98 @@ Do NOT make up data. Always call tools to get current state.`;
766
830
  tui.requestRender();
767
831
  return true;
768
832
  }
833
+ case '/switch': {
834
+ const newId = parts[1]?.trim();
835
+ if (!newId) {
836
+ addSystemText(C.zinc400('Usage: /switch <thesisId>'));
837
+ return true;
838
+ }
839
+ addSpacer();
840
+ try {
841
+ // Save current session
842
+ persistSession();
843
+ // Load new thesis context
844
+ const newContext = await sfClient.getContext(newId);
845
+ resolvedThesisId = newContext.thesisId || newId;
846
+ latestContext = newContext;
847
+ // Build new system prompt
848
+ const newConf = typeof newContext.confidence === 'number'
849
+ ? 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.`;
851
+ // Load saved session or start fresh
852
+ const saved = loadSession(resolvedThesisId);
853
+ if (saved?.messages?.length > 0) {
854
+ agent.replaceMessages(saved.messages);
855
+ agent.setSystemPrompt(newSysPrompt);
856
+ addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(` (resumed ${saved.messages.length} messages)`));
857
+ }
858
+ else {
859
+ agent.clearMessages();
860
+ agent.setSystemPrompt(newSysPrompt);
861
+ addSystemText(C.emerald(`Switched to ${resolvedThesisId.slice(0, 8)}`) + C.zinc400(' (new session)'));
862
+ }
863
+ // Update header
864
+ headerBar.update(C.emerald(bold('SF Agent')) + C.zinc600(` \u2014 ${resolvedThesisId.slice(0, 8)}`), newConf > 0 ? C.zinc200(`${newConf}%`) : '', undefined);
865
+ chatContainer.clear();
866
+ const thText = (newContext.thesis || newContext.rawThesis || '').slice(0, 120);
867
+ addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
868
+ C.zinc200(bold(thText)) + '\n' +
869
+ C.zinc600(`${newContext.status || 'active'} ${newConf > 0 ? newConf + '%' : ''} ${(newContext.edges || []).length} edges`) + '\n' +
870
+ C.zinc600('\u2500'.repeat(50)));
871
+ }
872
+ catch (err) {
873
+ addSystemText(C.red(`Switch failed: ${err.message}`));
874
+ }
875
+ addSpacer();
876
+ tui.requestRender();
877
+ return true;
878
+ }
879
+ case '/compact': {
880
+ 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)}`);
897
+ }
898
+ else if (m.role === 'assistant' && content) {
899
+ bulletPoints.push(`- Assistant: ${content.slice(0, 150)}`);
900
+ }
901
+ }
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
+ return true;
913
+ }
914
+ case '/new': {
915
+ addSpacer();
916
+ persistSession(); // save current before clearing
917
+ agent.clearMessages();
918
+ agent.setSystemPrompt(systemPrompt);
919
+ chatContainer.clear();
920
+ addSystemText(C.emerald('Session cleared') + C.zinc400(' \u2014 fresh start'));
921
+ addSpacer();
922
+ tui.requestRender();
923
+ return true;
924
+ }
769
925
  case '/clear': {
770
926
  chatContainer.clear();
771
927
  tui.requestRender();
@@ -821,6 +977,7 @@ Do NOT make up data. Always call tools to get current state.`;
821
977
  function cleanup() {
822
978
  if (currentLoader)
823
979
  currentLoader.stop();
980
+ persistSession();
824
981
  tui.stop();
825
982
  process.exit(0);
826
983
  }
@@ -839,9 +996,12 @@ Do NOT make up data. Always call tools to get current state.`;
839
996
  // ── Show initial welcome ───────────────────────────────────────────────────
840
997
  const thesisText = latestContext.thesis || latestContext.rawThesis || 'N/A';
841
998
  const truncatedThesis = thesisText.length > 120 ? thesisText.slice(0, 120) + '...' : thesisText;
999
+ const sessionStatus = sessionRestored
1000
+ ? C.zinc600(` resumed (${agent.state.messages.length} messages)`)
1001
+ : C.zinc600(' new session');
842
1002
  addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
843
1003
  C.zinc200(bold(truncatedThesis)) + '\n' +
844
- C.zinc600(`${latestContext.status || 'active'} ${confidencePct > 0 ? confidencePct + '%' : ''} ${(latestContext.edges || []).length} edges`) + '\n' +
1004
+ C.zinc600(`${latestContext.status || 'active'} ${confidencePct > 0 ? confidencePct + '%' : ''} ${(latestContext.edges || []).length} edges`) + sessionStatus + '\n' +
845
1005
  C.zinc600('\u2500'.repeat(50)));
846
1006
  addSpacer();
847
1007
  // ── 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,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "0.1.7",
3
+ "version": "1.1.0",
4
4
  "description": "CLI for SimpleFunctions prediction market thesis agent",
5
5
  "bin": {
6
6
  "sf": "./dist/index.js"