@spfunctions/cli 0.1.6 → 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`;
@@ -197,16 +230,19 @@ function renderEdges(context, piTui) {
197
230
  const positions = context._positions || [];
198
231
  const lines = [];
199
232
  for (const e of edges) {
200
- const name = (e.marketTitle || e.ticker || '').slice(0, 18).padEnd(18);
201
- const market = typeof e.marketPrice === 'number' ? `${e.marketPrice}\u00A2` : '?';
202
- const thesis = typeof e.thesisImpliedPrice === 'number' ? `${e.thesisImpliedPrice}\u00A2` : '?';
203
- const edge = typeof e.edgeSize === 'number' ? (e.edgeSize > 0 ? `+${e.edgeSize}` : `${e.edgeSize}`) : '?';
204
- const spread = typeof e.spread === 'number' ? `${e.spread}\u00A2` : '?';
205
- const liq = e.liquidityScore || 'low';
233
+ // Context API field names: market, marketId, thesisPrice, edge, orderbook.spread, orderbook.liquidityScore
234
+ const name = (e.market || e.marketId || '').slice(0, 18).padEnd(18);
235
+ const marketStr = typeof e.marketPrice === 'number' ? `${e.marketPrice}\u00A2` : '?';
236
+ const thesisStr = typeof e.thesisPrice === 'number' ? `${e.thesisPrice}\u00A2` : '?';
237
+ const edgeVal = typeof e.edge === 'number' ? (e.edge > 0 ? `+${e.edge}` : `${e.edge}`) : '?';
238
+ const ob = e.orderbook || {};
239
+ const spreadStr = typeof ob.spread === 'number' ? `${ob.spread}\u00A2` : '?';
240
+ const liq = ob.liquidityScore || 'low';
206
241
  const liqBars = liq === 'high' ? '\u25A0\u25A0\u25A0' : liq === 'medium' ? '\u25A0\u25A0 ' : '\u25A0 ';
207
242
  const liqColor = liq === 'high' ? C.emerald : liq === 'medium' ? C.amber : C.red;
208
- // Check if we have a position on this edge
209
- const pos = positions.find((p) => p.ticker === e.ticker);
243
+ // Check if we have a position on this edge (match by marketId prefix in ticker)
244
+ const pos = positions.find((p) => p.ticker === e.marketId ||
245
+ (e.marketId && p.ticker?.includes(e.marketId)));
210
246
  let posStr = C.zinc600('\u2014');
211
247
  if (pos) {
212
248
  const side = pos.side?.toUpperCase() || 'YES';
@@ -215,7 +251,7 @@ function renderEdges(context, piTui) {
215
251
  : '';
216
252
  posStr = C.emerald(`${side} (${pos.quantity}@${pos.average_price_paid}\u00A2 ${pnl})`);
217
253
  }
218
- lines.push(` ${C.zinc200(name)} ${C.zinc400(market)} \u2192 ${C.zinc400(thesis)} edge ${edge.includes('+') ? C.emerald(edge) : C.red(edge)} spread ${C.zinc600(spread)} ${liqColor(liqBars)} ${liqColor(liq.padEnd(4))} ${posStr}`);
254
+ lines.push(` ${C.zinc200(name)} ${C.zinc400(marketStr)} \u2192 ${C.zinc400(thesisStr)} edge ${edgeVal.includes('+') ? C.emerald(edgeVal) : C.red(edgeVal)} spread ${C.zinc600(spreadStr)} ${liqColor(liqBars)} ${liqColor(liq.padEnd(4))} ${posStr}`);
219
255
  }
220
256
  return lines.join('\n');
221
257
  }
@@ -283,7 +319,7 @@ async function agentCommand(thesisId, opts) {
283
319
  // ── Fetch initial context ──────────────────────────────────────────────────
284
320
  let latestContext = await sfClient.getContext(resolvedThesisId);
285
321
  // ── Model setup ────────────────────────────────────────────────────────────
286
- const rawModelName = opts?.model || 'anthropic/claude-sonnet-4-20250514';
322
+ const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
287
323
  let currentModelName = rawModelName.replace(/^openrouter\//, '');
288
324
  function resolveModel(name) {
289
325
  try {
@@ -365,9 +401,12 @@ async function agentCommand(thesisId, opts) {
365
401
  { name: 'edges', description: 'Display edge/spread table' },
366
402
  { name: 'pos', description: 'Display Kalshi positions' },
367
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' },
368
407
  { name: 'model', description: 'Switch model (e.g. /model anthropic/claude-sonnet-4)' },
369
- { name: 'clear', description: 'Clear chat' },
370
- { name: 'exit', description: 'Exit agent' },
408
+ { name: 'clear', description: 'Clear screen (keeps history)' },
409
+ { name: 'exit', description: 'Exit agent (auto-saves)' },
371
410
  ], process.cwd());
372
411
  editor.setAutocompleteProvider(autocompleteProvider);
373
412
  // Assemble TUI tree
@@ -558,12 +597,53 @@ Do NOT make up data. Always call tools to get current state.`;
558
597
  return undefined;
559
598
  },
560
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
+ }
561
624
  // ── Subscribe to agent events → update TUI ────────────────────────────────
562
625
  let currentAssistantMd = null;
563
626
  let currentAssistantText = '';
564
627
  let currentLoader = null;
565
628
  const toolStartTimes = new Map();
566
629
  const toolLines = new Map();
630
+ // Throttle renders during streaming to prevent flicker (max ~15fps)
631
+ let renderTimer = null;
632
+ function throttledRender() {
633
+ if (renderTimer)
634
+ return;
635
+ renderTimer = setTimeout(() => {
636
+ renderTimer = null;
637
+ tui.requestRender();
638
+ }, 66);
639
+ }
640
+ function flushRender() {
641
+ if (renderTimer) {
642
+ clearTimeout(renderTimer);
643
+ renderTimer = null;
644
+ }
645
+ tui.requestRender();
646
+ }
567
647
  agent.subscribe((event) => {
568
648
  if (event.type === 'message_start') {
569
649
  // Show loader while waiting for first text
@@ -590,30 +670,31 @@ Do NOT make up data. Always call tools to get current state.`;
590
670
  if (currentAssistantMd) {
591
671
  currentAssistantMd.setText(currentAssistantText);
592
672
  }
593
- tui.requestRender();
673
+ // Throttled render to prevent flicker during fast token streaming
674
+ throttledRender();
594
675
  }
595
676
  }
596
- if (event.type === 'message_complete') {
677
+ if (event.type === 'message_end') {
597
678
  // Clean up loader if still present (no text was generated)
598
679
  if (currentLoader) {
599
680
  currentLoader.stop();
600
681
  chatContainer.removeChild(currentLoader);
601
682
  currentLoader = null;
602
683
  }
603
- // Add spacer after message
684
+ // Final render of the complete message
685
+ if (currentAssistantMd && currentAssistantText) {
686
+ currentAssistantMd.setText(currentAssistantText);
687
+ }
604
688
  addSpacer();
605
689
  currentAssistantMd = null;
606
690
  currentAssistantText = '';
691
+ flushRender();
692
+ }
693
+ if (event.type === 'agent_end') {
694
+ // Agent turn fully complete — safe to accept new input
607
695
  isProcessing = false;
608
- tui.requestRender();
609
- // Update token/cost tracking from event if available
610
- if (event.usage) {
611
- totalTokens += (event.usage.inputTokens || 0) + (event.usage.outputTokens || 0);
612
- totalCost += event.usage.totalCost || 0;
613
- footerBar.tokens = totalTokens;
614
- footerBar.cost = totalCost;
615
- footerBar.update();
616
- }
696
+ persistSession();
697
+ flushRender();
617
698
  }
618
699
  if (event.type === 'tool_execution_start') {
619
700
  const toolLine = new MutableLine(C.zinc600(` \u26A1 ${event.toolName}...`));
@@ -651,14 +732,17 @@ Do NOT make up data. Always call tools to get current state.`;
651
732
  case '/help': {
652
733
  addSpacer();
653
734
  addSystemText(C.zinc200(bold('Commands')) + '\n' +
654
- C.emerald('/help ') + C.zinc400(' Show this help') + '\n' +
655
- C.emerald('/tree ') + C.zinc400(' Display causal tree') + '\n' +
656
- C.emerald('/edges ') + C.zinc400(' Display edge/spread table') + '\n' +
657
- C.emerald('/pos ') + C.zinc400(' Display Kalshi positions') + '\n' +
658
- C.emerald('/eval ') + C.zinc400(' Trigger deep evaluation') + '\n' +
659
- C.emerald('/model ') + C.zinc400(' Switch model (e.g. /model anthropic/claude-sonnet-4)') + '\n' +
660
- C.emerald('/clear ') + C.zinc400(' Clear chat') + '\n' +
661
- 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)'));
662
746
  addSpacer();
663
747
  return true;
664
748
  }
@@ -746,6 +830,98 @@ Do NOT make up data. Always call tools to get current state.`;
746
830
  tui.requestRender();
747
831
  return true;
748
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
+ }
749
925
  case '/clear': {
750
926
  chatContainer.clear();
751
927
  tui.requestRender();
@@ -801,6 +977,7 @@ Do NOT make up data. Always call tools to get current state.`;
801
977
  function cleanup() {
802
978
  if (currentLoader)
803
979
  currentLoader.stop();
980
+ persistSession();
804
981
  tui.stop();
805
982
  process.exit(0);
806
983
  }
@@ -819,9 +996,12 @@ Do NOT make up data. Always call tools to get current state.`;
819
996
  // ── Show initial welcome ───────────────────────────────────────────────────
820
997
  const thesisText = latestContext.thesis || latestContext.rawThesis || 'N/A';
821
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');
822
1002
  addSystemText(C.zinc600('\u2500'.repeat(50)) + '\n' +
823
1003
  C.zinc200(bold(truncatedThesis)) + '\n' +
824
- 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' +
825
1005
  C.zinc600('\u2500'.repeat(50)));
826
1006
  addSpacer();
827
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.6",
3
+ "version": "1.1.0",
4
4
  "description": "CLI for SimpleFunctions prediction market thesis agent",
5
5
  "bin": {
6
6
  "sf": "./dist/index.js"