@spfunctions/cli 1.7.10 → 1.7.12

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.
@@ -465,35 +465,46 @@ async function selectThesis(theses) {
465
465
  // ─── Main command ────────────────────────────────────────────────────────────
466
466
  async function agentCommand(thesisId, opts) {
467
467
  // ── Validate API keys ──────────────────────────────────────────────────────
468
- const openrouterKey = opts?.modelKey || process.env.OPENROUTER_API_KEY;
468
+ const directOrKey = opts?.modelKey || process.env.OPENROUTER_API_KEY;
469
+ const sfApiKey = process.env.SF_API_KEY;
470
+ const sfApiUrl = process.env.SF_API_URL || 'https://simplefunctions.dev';
471
+ // Proxy mode: no local OpenRouter key, but have SF API key → route through server
472
+ const useProxy = !directOrKey && !!sfApiKey;
473
+ const openrouterKey = directOrKey || sfApiKey; // SF key used as Bearer for proxy
474
+ const llmBaseUrl = useProxy ? `${sfApiUrl}/api/proxy` : 'https://openrouter.ai/api/v1';
469
475
  if (!openrouterKey) {
470
- console.error('Need OpenRouter API key to power the agent LLM.');
476
+ console.error('Need an API key to power the agent LLM.');
471
477
  console.error('');
472
- console.error(' 1. Get a key at https://openrouter.ai/keys');
473
- console.error(' 2. Then either:');
478
+ console.error(' Option 1 (recommended): sf login');
479
+ console.error(' Option 2: Get an OpenRouter key at https://openrouter.ai/keys');
474
480
  console.error(' export OPENROUTER_API_KEY=sk-or-v1-...');
475
481
  console.error(' sf agent --model-key sk-or-v1-...');
476
482
  console.error(' sf setup (saves to ~/.sf/config.json)');
477
483
  process.exit(1);
478
484
  }
479
- // Pre-flight: validate OpenRouter key
480
- try {
481
- const checkRes = await fetch('https://openrouter.ai/api/v1/auth/key', {
482
- headers: { 'Authorization': `Bearer ${openrouterKey}` },
483
- signal: AbortSignal.timeout(8000),
484
- });
485
- if (!checkRes.ok) {
486
- console.error('OpenRouter API key is invalid or expired.');
487
- console.error('Get a new key at https://openrouter.ai/keys');
488
- process.exit(1);
485
+ // Pre-flight: validate key (skip for proxy mode — server validates)
486
+ if (!useProxy) {
487
+ try {
488
+ const checkRes = await fetch('https://openrouter.ai/api/v1/auth/key', {
489
+ headers: { 'Authorization': `Bearer ${openrouterKey}` },
490
+ signal: AbortSignal.timeout(8000),
491
+ });
492
+ if (!checkRes.ok) {
493
+ console.error('OpenRouter API key is invalid or expired.');
494
+ console.error('Get a new key at https://openrouter.ai/keys');
495
+ process.exit(1);
496
+ }
489
497
  }
490
- }
491
- catch (err) {
492
- const msg = err instanceof Error ? err.message : String(err);
493
- if (!msg.includes('timeout')) {
494
- console.warn(`Warning: Could not verify OpenRouter key (${msg}). Continuing anyway.`);
498
+ catch (err) {
499
+ const msg = err instanceof Error ? err.message : String(err);
500
+ if (!msg.includes('timeout')) {
501
+ console.warn(`Warning: Could not verify OpenRouter key (${msg}). Continuing anyway.`);
502
+ }
495
503
  }
496
504
  }
505
+ else {
506
+ console.log(' \x1b[2mUsing SimpleFunctions LLM proxy (no OpenRouter key needed)\x1b[22m');
507
+ }
497
508
  const sfClient = new client_js_1.SFClient();
498
509
  // ── Resolve thesis ID (interactive selection if needed) ─────────────────────
499
510
  let resolvedThesisId = thesisId;
@@ -536,7 +547,7 @@ async function agentCommand(thesisId, opts) {
536
547
  let latestContext = await sfClient.getContext(resolvedThesisId);
537
548
  // ── Branch: plain-text mode ────────────────────────────────────────────────
538
549
  if (opts?.noTui) {
539
- return runPlainTextAgent({ openrouterKey, sfClient, resolvedThesisId: resolvedThesisId, latestContext, opts });
550
+ return runPlainTextAgent({ openrouterKey, sfClient, resolvedThesisId: resolvedThesisId, latestContext, useProxy, llmBaseUrl, sfApiKey, sfApiUrl, opts });
540
551
  }
541
552
  // ── Dynamic imports (all ESM-only packages) ────────────────────────────────
542
553
  const piTui = await import('@mariozechner/pi-tui');
@@ -552,11 +563,12 @@ async function agentCommand(thesisId, opts) {
552
563
  const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
553
564
  let currentModelName = rawModelName.replace(/^openrouter\//, '');
554
565
  function resolveModel(name) {
566
+ let m;
555
567
  try {
556
- return getModel('openrouter', name);
568
+ m = getModel('openrouter', name);
557
569
  }
558
570
  catch {
559
- return {
571
+ m = {
560
572
  modelId: name,
561
573
  provider: 'openrouter',
562
574
  api: 'openai-completions',
@@ -570,6 +582,10 @@ async function agentCommand(thesisId, opts) {
570
582
  supportsTools: true,
571
583
  };
572
584
  }
585
+ // Override baseUrl in proxy mode
586
+ if (useProxy)
587
+ m.baseUrl = llmBaseUrl;
588
+ return m;
573
589
  }
574
590
  let model = resolveModel(currentModelName);
575
591
  // ── Tracking state ─────────────────────────────────────────────────────────
@@ -958,24 +974,45 @@ async function agentCommand(thesisId, opts) {
958
974
  description: 'Search latest news and information. Use for real-time info not yet covered by the causal tree or heartbeat engine.',
959
975
  parameters: webSearchParams,
960
976
  execute: async (_toolCallId, params) => {
961
- const apiKey = process.env.TAVILY_API_KEY;
962
- if (!apiKey) {
977
+ const tavilyKey = process.env.TAVILY_API_KEY;
978
+ const canProxy = !tavilyKey && sfApiKey;
979
+ if (!tavilyKey && !canProxy) {
963
980
  return {
964
- 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.' }],
981
+ content: [{ type: 'text', text: 'Web search not available. Run sf login (proxied search) or set TAVILY_API_KEY.' }],
965
982
  details: {},
966
983
  };
967
984
  }
968
- const res = await fetch('https://api.tavily.com/search', {
969
- method: 'POST',
970
- headers: { 'Content-Type': 'application/json' },
971
- body: JSON.stringify({
972
- api_key: apiKey,
973
- query: params.query,
974
- max_results: 5,
975
- search_depth: 'basic',
976
- include_answer: true,
977
- }),
978
- });
985
+ let res;
986
+ if (tavilyKey) {
987
+ // Direct mode
988
+ res = await fetch('https://api.tavily.com/search', {
989
+ method: 'POST',
990
+ headers: { 'Content-Type': 'application/json' },
991
+ body: JSON.stringify({
992
+ api_key: tavilyKey,
993
+ query: params.query,
994
+ max_results: 5,
995
+ search_depth: 'basic',
996
+ include_answer: true,
997
+ }),
998
+ });
999
+ }
1000
+ else {
1001
+ // Proxy mode
1002
+ res = await fetch(`${sfApiUrl}/api/proxy/search`, {
1003
+ method: 'POST',
1004
+ headers: {
1005
+ 'Content-Type': 'application/json',
1006
+ 'Authorization': `Bearer ${sfApiKey}`,
1007
+ },
1008
+ body: JSON.stringify({
1009
+ query: params.query,
1010
+ max_results: 5,
1011
+ search_depth: 'basic',
1012
+ include_answer: true,
1013
+ }),
1014
+ });
1015
+ }
979
1016
  if (!res.ok) {
980
1017
  return {
981
1018
  content: [{ type: 'text', text: `Search failed: ${res.status}` }],
@@ -2152,7 +2189,19 @@ ${ctx.lastEvaluation?.summary ? `Latest evaluation summary: ${ctx.lastEvaluation
2152
2189
  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.`;
2153
2190
  let summaryText;
2154
2191
  try {
2155
- const orRes = await fetch('https://openrouter.ai/api/v1/chat/completions', {
2192
+ const compactUrl = useProxy
2193
+ ? `${sfApiUrl}/api/proxy/llm`
2194
+ : 'https://openrouter.ai/api/v1/chat/completions';
2195
+ const compactBody = {
2196
+ model: summaryModel,
2197
+ messages: [
2198
+ { role: 'system', content: summarySystemPrompt },
2199
+ { role: 'user', content: `Summarize this conversation (${toCompress.length} messages):\n\n${conversationDump}` },
2200
+ ],
2201
+ max_tokens: 2000,
2202
+ temperature: 0.2,
2203
+ };
2204
+ const orRes = await fetch(compactUrl, {
2156
2205
  method: 'POST',
2157
2206
  headers: {
2158
2207
  'Content-Type': 'application/json',
@@ -2160,15 +2209,7 @@ Output a structured summary. Be concise but preserve every important detail —
2160
2209
  'HTTP-Referer': 'https://simplefunctions.com',
2161
2210
  'X-Title': 'SF Agent Compact',
2162
2211
  },
2163
- body: JSON.stringify({
2164
- model: summaryModel,
2165
- messages: [
2166
- { role: 'system', content: summarySystemPrompt },
2167
- { role: 'user', content: `Summarize this conversation (${toCompress.length} messages):\n\n${conversationDump}` },
2168
- ],
2169
- max_tokens: 2000,
2170
- temperature: 0.2,
2171
- }),
2212
+ body: JSON.stringify(compactBody),
2172
2213
  });
2173
2214
  if (!orRes.ok) {
2174
2215
  const errText = await orRes.text().catch(() => '');
@@ -2581,7 +2622,7 @@ Output a structured summary. Be concise but preserve every important detail —
2581
2622
  // PLAIN-TEXT MODE (--no-tui)
2582
2623
  // ============================================================================
2583
2624
  async function runPlainTextAgent(params) {
2584
- const { openrouterKey, sfClient, resolvedThesisId, opts } = params;
2625
+ const { openrouterKey, sfClient, resolvedThesisId, opts, useProxy, llmBaseUrl, sfApiKey, sfApiUrl } = params;
2585
2626
  let latestContext = params.latestContext;
2586
2627
  const readline = await import('readline');
2587
2628
  const piAi = await import('@mariozechner/pi-ai');
@@ -2591,17 +2632,21 @@ async function runPlainTextAgent(params) {
2591
2632
  const rawModelName = opts?.model || 'anthropic/claude-sonnet-4.6';
2592
2633
  let currentModelName = rawModelName.replace(/^openrouter\//, '');
2593
2634
  function resolveModel(name) {
2635
+ let m;
2594
2636
  try {
2595
- return getModel('openrouter', name);
2637
+ m = getModel('openrouter', name);
2596
2638
  }
2597
2639
  catch {
2598
- return {
2640
+ m = {
2599
2641
  modelId: name, provider: 'openrouter', api: 'openai-completions',
2600
2642
  baseUrl: 'https://openrouter.ai/api/v1', id: name, name,
2601
2643
  inputPrice: 0, outputPrice: 0, contextWindow: 200000,
2602
2644
  supportsImages: true, supportsTools: true,
2603
2645
  };
2604
2646
  }
2647
+ if (useProxy)
2648
+ m.baseUrl = llmBaseUrl;
2649
+ return m;
2605
2650
  }
2606
2651
  let model = resolveModel(currentModelName);
2607
2652
  // ── Tools (same definitions as TUI mode) ──────────────────────────────────
@@ -2739,13 +2784,23 @@ async function runPlainTextAgent(params) {
2739
2784
  description: 'Search latest news and information',
2740
2785
  parameters: webSearchParams,
2741
2786
  execute: async (_id, p) => {
2742
- const apiKey = process.env.TAVILY_API_KEY;
2743
- if (!apiKey)
2744
- return { content: [{ type: 'text', text: 'Tavily not configured. Set TAVILY_API_KEY.' }], details: {} };
2745
- const res = await fetch('https://api.tavily.com/search', {
2746
- method: 'POST', headers: { 'Content-Type': 'application/json' },
2747
- body: JSON.stringify({ api_key: apiKey, query: p.query, max_results: 5, search_depth: 'basic', include_answer: true }),
2748
- });
2787
+ const tavilyKey2 = process.env.TAVILY_API_KEY;
2788
+ const canProxy2 = !tavilyKey2 && sfApiKey;
2789
+ if (!tavilyKey2 && !canProxy2)
2790
+ return { content: [{ type: 'text', text: 'Web search not available. Run sf login or set TAVILY_API_KEY.' }], details: {} };
2791
+ let res;
2792
+ if (tavilyKey2) {
2793
+ res = await fetch('https://api.tavily.com/search', {
2794
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
2795
+ body: JSON.stringify({ api_key: tavilyKey2, query: p.query, max_results: 5, search_depth: 'basic', include_answer: true }),
2796
+ });
2797
+ }
2798
+ else {
2799
+ res = await fetch(`${sfApiUrl}/api/proxy/search`, {
2800
+ method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${sfApiKey}` },
2801
+ body: JSON.stringify({ query: p.query, max_results: 5, search_depth: 'basic', include_answer: true }),
2802
+ });
2803
+ }
2749
2804
  if (!res.ok)
2750
2805
  return { content: [{ type: 'text', text: `Search failed: ${res.status}` }], details: {} };
2751
2806
  const data = await res.json();
@@ -4,10 +4,11 @@ exports.contextCommand = contextCommand;
4
4
  const client_js_1 = require("../client.js");
5
5
  const kalshi_js_1 = require("../kalshi.js");
6
6
  const utils_js_1 = require("../utils.js");
7
+ const SF_API_URL = process.env.SF_API_URL || 'https://simplefunctions.dev';
7
8
  async function contextCommand(id, opts) {
8
- // No thesis ID → global market snapshot
9
+ // ── Mode 1: No thesis ID → global market intelligence ─────────────────────
9
10
  if (!id) {
10
- const res = await fetch('https://simplefunctions.dev/api/public/context');
11
+ const res = await fetch(`${opts.apiUrl || SF_API_URL}/api/public/context`);
11
12
  if (!res.ok) {
12
13
  console.error(` Error: ${res.status} ${await res.text()}`);
13
14
  return;
@@ -17,63 +18,109 @@ async function contextCommand(id, opts) {
17
18
  console.log(JSON.stringify(data, null, 2));
18
19
  return;
19
20
  }
20
- console.log(`\n${utils_js_1.c.bold}Market Snapshot${utils_js_1.c.reset} ${(0, utils_js_1.shortDate)(data.snapshotAt)}\n`);
21
- if (data.edges?.length > 0) {
22
- console.log(`${utils_js_1.c.bold}Thesis Edges${utils_js_1.c.reset}`);
23
- for (const m of data.edges.slice(0, 10)) {
24
- const venue = m.venue === 'kalshi' ? `${utils_js_1.c.cyan}K${utils_js_1.c.reset}` : `${utils_js_1.c.magenta}P${utils_js_1.c.reset}`;
25
- const edgeStr = m.edge > 0 ? `${utils_js_1.c.green}+${m.edge}¢${utils_js_1.c.reset}` : `${utils_js_1.c.red}${m.edge}¢${utils_js_1.c.reset}`;
26
- console.log(` ${venue} ${String(m.price).padStart(3)}¢ edge ${edgeStr.padStart(14)} ${m.title.slice(0, 50)}`);
27
- }
21
+ const scanTime = data.scannedAt ? (0, utils_js_1.shortDate)(data.scannedAt) : 'no scan yet';
22
+ const meta = data.meta || {};
23
+ console.log();
24
+ console.log(`${utils_js_1.c.bold}Markets${utils_js_1.c.reset} ${utils_js_1.c.dim}${meta.totalMarkets || 0} markets (K:${meta.kalshiMarkets || 0} P:${meta.polymarketMarkets || 0}) · scan: ${scanTime}${utils_js_1.c.reset}`);
25
+ console.log();
26
+ // ── Traditional markets ────────────────────────────────────────────────
27
+ const trad = data.traditional || [];
28
+ if (trad.length > 0) {
29
+ const line = trad.map((m) => {
30
+ const ch = m.changePct || 0;
31
+ const color = ch > 0 ? utils_js_1.c.green : ch < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
32
+ const sign = ch > 0 ? '+' : '';
33
+ return `${utils_js_1.c.bold}${m.symbol}${utils_js_1.c.reset} ${m.price} ${color}${sign}${ch.toFixed(1)}%${utils_js_1.c.reset}`;
34
+ }).join(' ');
35
+ console.log(` ${line}`);
28
36
  console.log();
29
37
  }
30
- if (data.movers?.length > 0) {
31
- console.log(`${utils_js_1.c.bold}Movers (24h)${utils_js_1.c.reset}`);
32
- for (const m of data.movers.slice(0, 8)) {
33
- const venue = m.venue === 'kalshi' ? `${utils_js_1.c.cyan}K${utils_js_1.c.reset}` : `${utils_js_1.c.magenta}P${utils_js_1.c.reset}`;
34
- const ch = m.change24h > 0 ? `${utils_js_1.c.green}+${m.change24h}¢${utils_js_1.c.reset}` : `${utils_js_1.c.red}${m.change24h}¢${utils_js_1.c.reset}`;
35
- console.log(` ${venue} ${String(m.price).padStart(3)}¢ ${ch.padStart(16)} ${m.title.slice(0, 55)}`);
38
+ // ── Highlights (cross-category stories) ────────────────────────────────
39
+ const highlights = data.highlights || [];
40
+ if (highlights.length > 0) {
41
+ console.log(`${utils_js_1.c.bold}${utils_js_1.c.cyan}Highlights${utils_js_1.c.reset}`);
42
+ for (const h of highlights) {
43
+ console.log(` ${utils_js_1.c.bold}${h.title}${utils_js_1.c.reset}`);
44
+ console.log(` ${utils_js_1.c.dim}${h.detail}${utils_js_1.c.reset}`);
45
+ if (h.relatedTickers?.length > 0) {
46
+ console.log(` ${utils_js_1.c.dim}tickers: ${h.relatedTickers.join(', ')}${utils_js_1.c.reset}`);
47
+ }
48
+ console.log(` ${utils_js_1.c.cyan}→ ${h.suggestedAction}${utils_js_1.c.reset}`);
49
+ console.log();
36
50
  }
37
- console.log();
38
51
  }
39
- if (data.milestones?.length > 0) {
40
- console.log(`${utils_js_1.c.bold}Upcoming (48h)${utils_js_1.c.reset}`);
41
- for (const m of data.milestones.slice(0, 8)) {
42
- console.log(` ${utils_js_1.c.dim}${String(m.hoursUntil).padStart(3)}h${utils_js_1.c.reset} ${m.title.slice(0, 55)} ${utils_js_1.c.dim}${m.category}${utils_js_1.c.reset}`);
52
+ // ── Categories ─────────────────────────────────────────────────────────
53
+ const cats = data.categories || [];
54
+ if (cats.length > 0) {
55
+ console.log(`${utils_js_1.c.bold}Categories${utils_js_1.c.reset} ${utils_js_1.c.dim}(${cats.length} total)${utils_js_1.c.reset}`);
56
+ console.log(`${utils_js_1.c.dim}${'─'.repeat(70)}${utils_js_1.c.reset}`);
57
+ for (const cat of cats) {
58
+ const desc = cat.description ? ` ${utils_js_1.c.dim}${cat.description}${utils_js_1.c.reset}` : '';
59
+ console.log(`\n ${utils_js_1.c.bold}${cat.name}${utils_js_1.c.reset} ${utils_js_1.c.dim}${cat.marketCount} mkts · vol ${(0, utils_js_1.vol)(cat.totalVolume24h)}${utils_js_1.c.reset}${desc}`);
60
+ // Top movers in category
61
+ const movers = cat.topMovers || [];
62
+ if (movers.length > 0) {
63
+ for (const m of movers) {
64
+ const venue = m.venue === 'kalshi' ? `${utils_js_1.c.cyan}K${utils_js_1.c.reset}` : `${utils_js_1.c.magenta}P${utils_js_1.c.reset}`;
65
+ const ch = m.change24h || 0;
66
+ const chColor = ch > 0 ? utils_js_1.c.green : ch < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
67
+ const chStr = ch !== 0 ? `${chColor}${ch > 0 ? '+' : ''}${ch}¢${utils_js_1.c.reset}` : '';
68
+ console.log(` ${venue} ${(0, utils_js_1.rpad)(`${m.price}¢`, 5)} ${(0, utils_js_1.rpad)(chStr, 14)} ${(0, utils_js_1.trunc)(m.title, 45)} ${utils_js_1.c.dim}${m.ticker.slice(0, 18)}${utils_js_1.c.reset}`);
69
+ if (m.whyInteresting) {
70
+ console.log(` ${utils_js_1.c.dim}${m.whyInteresting}${utils_js_1.c.reset}`);
71
+ }
72
+ }
73
+ }
74
+ // Most liquid
75
+ const liquid = cat.mostLiquid || [];
76
+ if (liquid.length > 0) {
77
+ for (const m of liquid) {
78
+ const venue = m.venue === 'kalshi' ? `${utils_js_1.c.cyan}K${utils_js_1.c.reset}` : `${utils_js_1.c.magenta}P${utils_js_1.c.reset}`;
79
+ console.log(` ${venue} ${(0, utils_js_1.rpad)(`${m.price}¢`, 5)} spread ${m.spread}¢ vol ${(0, utils_js_1.vol)(Math.round(m.volume24h))} ${utils_js_1.c.dim}${(0, utils_js_1.trunc)(m.title, 35)}${utils_js_1.c.reset}`);
80
+ }
81
+ }
43
82
  }
44
83
  console.log();
45
84
  }
46
- if (data.liquid?.length > 0) {
47
- console.log(`${utils_js_1.c.bold}Liquid${utils_js_1.c.reset}`);
48
- for (const m of data.liquid.slice(0, 8)) {
49
- const venue = m.venue === 'kalshi' ? `${utils_js_1.c.cyan}K${utils_js_1.c.reset}` : `${utils_js_1.c.magenta}P${utils_js_1.c.reset}`;
50
- console.log(` ${venue} ${String(m.price).padStart(3)}¢ spread ${m.spread}¢ ${m.title.slice(0, 55)}`);
85
+ // ── Thesis edges ───────────────────────────────────────────────────────
86
+ const edges = data.edges || [];
87
+ if (edges.length > 0) {
88
+ console.log(`${utils_js_1.c.bold}Thesis Edges${utils_js_1.c.reset}`);
89
+ for (const e of edges.slice(0, 6)) {
90
+ const venue = e.venue === 'kalshi' ? `${utils_js_1.c.cyan}K${utils_js_1.c.reset}` : `${utils_js_1.c.magenta}P${utils_js_1.c.reset}`;
91
+ const edgeColor = e.edge > 0 ? utils_js_1.c.green : utils_js_1.c.red;
92
+ console.log(` ${venue} ${(0, utils_js_1.rpad)(`${e.price}¢`, 5)} ${edgeColor}edge ${e.edge > 0 ? '+' : ''}${e.edge}¢${utils_js_1.c.reset} ${(0, utils_js_1.trunc)(e.title, 40)} ${utils_js_1.c.dim}${e.thesisSlug || ''}${utils_js_1.c.reset}`);
51
93
  }
52
94
  console.log();
53
95
  }
54
- if (data.signals?.length > 0) {
55
- console.log(`${utils_js_1.c.bold}Recent Signals${utils_js_1.c.reset}`);
56
- for (const s of data.signals.slice(0, 5)) {
57
- const d = s.confidenceDelta > 0 ? `${utils_js_1.c.green}▲${s.confidenceDelta}${utils_js_1.c.reset}` : s.confidenceDelta < 0 ? `${utils_js_1.c.red}▼${s.confidenceDelta}${utils_js_1.c.reset}` : `${s.confidenceDelta}`;
58
- console.log(` ${d} ${s.confidence}% ${s.summary.slice(0, 60)}`);
59
- console.log(` ${utils_js_1.c.dim}${s.thesisTitle.slice(0, 40)} ${(0, utils_js_1.shortDate)(s.evaluatedAt)}${utils_js_1.c.reset}`);
96
+ // ── Recent evaluations ─────────────────────────────────────────────────
97
+ const signals = data.signals || [];
98
+ if (signals.length > 0) {
99
+ console.log(`${utils_js_1.c.bold}Recent Evaluations${utils_js_1.c.reset}`);
100
+ for (const s of signals.slice(0, 5)) {
101
+ const d = s.confidenceDelta;
102
+ const dStr = d > 0 ? `${utils_js_1.c.green}+${d}%${utils_js_1.c.reset}` : d < 0 ? `${utils_js_1.c.red}${d}%${utils_js_1.c.reset}` : `${utils_js_1.c.dim}0%${utils_js_1.c.reset}`;
103
+ const ago = timeAgo(s.evaluatedAt);
104
+ console.log(` ${(0, utils_js_1.rpad)(ago, 5)} ${(0, utils_js_1.rpad)(s.thesisSlug || '?', 18)} ${(0, utils_js_1.rpad)(dStr, 14)} ${(0, utils_js_1.trunc)(s.summary, 50)}`);
60
105
  }
61
106
  console.log();
62
107
  }
63
- console.log(` ${utils_js_1.c.dim}Create a thesis for focused context: sf create "your market view"${utils_js_1.c.reset}\n`);
108
+ // ── Actions ────────────────────────────────────────────────────────────
109
+ console.log(`${utils_js_1.c.dim}${'─'.repeat(60)}${utils_js_1.c.reset}`);
110
+ console.log(` ${utils_js_1.c.cyan}sf query${utils_js_1.c.reset} "topic" ${utils_js_1.c.dim}ask anything${utils_js_1.c.reset}`);
111
+ console.log(` ${utils_js_1.c.cyan}sf scan${utils_js_1.c.reset} "keywords" ${utils_js_1.c.dim}search markets${utils_js_1.c.reset}`);
112
+ console.log(` ${utils_js_1.c.cyan}sf explore${utils_js_1.c.reset} ${utils_js_1.c.dim}public theses${utils_js_1.c.reset}`);
113
+ console.log(` ${utils_js_1.c.cyan}sf create${utils_js_1.c.reset} "thesis" ${utils_js_1.c.dim}start monitoring${utils_js_1.c.reset}`);
114
+ console.log();
64
115
  return;
65
116
  }
117
+ // ── Mode 2: Thesis-specific context (requires API key) ────────────────────
66
118
  const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
67
119
  const ctx = await client.getContext(id);
68
120
  if (opts.json) {
69
121
  console.log(JSON.stringify(ctx, null, 2));
70
122
  return;
71
123
  }
72
- // Context API shape:
73
- // { thesisId, thesis, title, status, confidence (number|null),
74
- // causalTree: { nodes: FlatNode[] }, edges: Edge[], edgeMeta,
75
- // lastEvaluation: { summary, confidenceDelta, ... }, updatedAt, ... }
76
- // Thesis header
77
124
  console.log(`\n${utils_js_1.c.bold}Thesis:${utils_js_1.c.reset} ${ctx.thesis || ctx.rawThesis || '(unknown)'}`);
78
125
  const confStr = ctx.confidence !== null && ctx.confidence !== undefined ? (0, utils_js_1.pct)(ctx.confidence) : '-';
79
126
  const confDelta = ctx.lastEvaluation?.confidenceDelta;
@@ -81,7 +128,6 @@ async function contextCommand(id, opts) {
81
128
  console.log(`${utils_js_1.c.bold}Confidence:${utils_js_1.c.reset} ${confStr}${deltaStr}`);
82
129
  console.log(`${utils_js_1.c.bold}Status:${utils_js_1.c.reset} ${ctx.status}`);
83
130
  console.log(`${utils_js_1.c.bold}Last Updated:${utils_js_1.c.reset} ${(0, utils_js_1.shortDate)(ctx.updatedAt)}`);
84
- // Causal tree nodes (flat array from API)
85
131
  const nodes = ctx.causalTree?.nodes;
86
132
  if (nodes && nodes.length > 0) {
87
133
  (0, utils_js_1.header)('Causal Tree');
@@ -92,53 +138,42 @@ async function contextCommand(id, opts) {
92
138
  console.log(`${indent}${utils_js_1.c.cyan}${node.id}${utils_js_1.c.reset} ${(0, utils_js_1.pad)(label, 40)} ${(0, utils_js_1.rpad)(prob, 5)}`);
93
139
  }
94
140
  }
95
- // Fetch positions if Kalshi is configured (local only, no server)
96
141
  let positions = null;
97
142
  if ((0, kalshi_js_1.isKalshiConfigured)()) {
98
143
  try {
99
144
  positions = await (0, kalshi_js_1.getPositions)();
100
145
  }
101
- catch {
102
- // silently skip — positions are optional
103
- }
146
+ catch { /* optional */ }
104
147
  }
105
148
  const posMap = new Map();
106
149
  if (positions) {
107
- for (const p of positions) {
150
+ for (const p of positions)
108
151
  posMap.set(p.ticker, p);
109
- }
110
152
  }
111
- // Top edges (sorted by absolute edge size)
112
153
  const edges = ctx.edges;
113
154
  if (edges && edges.length > 0) {
114
- (0, utils_js_1.header)('Top Edges (by edge size)');
155
+ (0, utils_js_1.header)('Top Edges');
115
156
  const sorted = [...edges].sort((a, b) => Math.abs(b.edge ?? b.edgeSize ?? 0) - Math.abs(a.edge ?? a.edgeSize ?? 0));
116
157
  for (const edge of sorted.slice(0, 10)) {
117
158
  const edgeSize = edge.edge ?? edge.edgeSize ?? 0;
118
159
  const edgeColor = edgeSize > 10 ? utils_js_1.c.green : edgeSize > 0 ? utils_js_1.c.yellow : utils_js_1.c.red;
119
160
  const mktPrice = edge.marketPrice ?? edge.currentPrice ?? 0;
120
161
  const title = edge.market || edge.marketTitle || edge.marketId || '?';
121
- // Orderbook info (if enriched by rescan)
122
162
  const ob = edge.orderbook;
123
163
  const obStr = ob ? ` ${utils_js_1.c.dim}spread ${ob.spread}¢ ${ob.liquidityScore}${utils_js_1.c.reset}` : '';
124
- // Position overlay (if user has Kalshi positions)
125
164
  const pos = posMap.get(edge.marketId);
126
165
  let posStr = '';
127
166
  if (pos) {
128
167
  const pnl = pos.unrealized_pnl || 0;
129
168
  const pnlColor = pnl > 0 ? utils_js_1.c.green : pnl < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
130
169
  const pnlFmt = pnl >= 0 ? `+$${(pnl / 100).toFixed(0)}` : `-$${(Math.abs(pnl) / 100).toFixed(0)}`;
131
- posStr = ` ${utils_js_1.c.cyan}← ${pos.quantity}张 @ ${pos.average_price_paid}¢ ${pnlColor}${pnlFmt}${utils_js_1.c.reset}`;
170
+ posStr = ` ${utils_js_1.c.cyan}← ${pos.quantity}@${pos.average_price_paid}¢ ${pnlColor}${pnlFmt}${utils_js_1.c.reset}`;
132
171
  }
133
- console.log(` ${(0, utils_js_1.pad)(title, 35)}` +
134
- ` ${(0, utils_js_1.rpad)(mktPrice.toFixed(0) + '¢', 5)}` +
172
+ console.log(` ${(0, utils_js_1.pad)(title, 35)} ${(0, utils_js_1.rpad)(mktPrice.toFixed(0) + '¢', 5)}` +
135
173
  ` ${edgeColor}edge ${edgeSize > 0 ? '+' : ''}${edgeSize.toFixed(1)}${utils_js_1.c.reset}` +
136
- ` ${utils_js_1.c.dim}${edge.venue || ''}${utils_js_1.c.reset}` +
137
- obStr +
138
- posStr);
174
+ ` ${utils_js_1.c.dim}${edge.venue || ''}${utils_js_1.c.reset}` + obStr + posStr);
139
175
  }
140
176
  }
141
- // Last evaluation summary
142
177
  if (ctx.lastEvaluation?.summary) {
143
178
  (0, utils_js_1.header)('Last Evaluation');
144
179
  console.log(` ${utils_js_1.c.dim}${(0, utils_js_1.shortDate)(ctx.lastEvaluation.evaluatedAt)} | model: ${ctx.lastEvaluation.model || ''}${utils_js_1.c.reset}`);
@@ -151,9 +186,18 @@ async function contextCommand(id, opts) {
151
186
  }
152
187
  }
153
188
  }
154
- // Edge meta (last rescan time)
155
189
  if (ctx.edgeMeta?.lastRescanAt) {
156
190
  console.log(`\n${utils_js_1.c.dim}Last rescan: ${(0, utils_js_1.shortDate)(ctx.edgeMeta.lastRescanAt)}${utils_js_1.c.reset}`);
157
191
  }
158
192
  console.log('');
159
193
  }
194
+ function timeAgo(iso) {
195
+ const ms = Date.now() - new Date(iso).getTime();
196
+ const min = Math.round(ms / 60000);
197
+ if (min < 60)
198
+ return `${min}m`;
199
+ const hr = Math.round(min / 60);
200
+ if (hr < 24)
201
+ return `${hr}h`;
202
+ return `${Math.round(hr / 24)}d`;
203
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * sf delta <thesisId> — Changes since a timestamp.
3
+ *
4
+ * Calls GET /api/thesis/:id/changes?since=<ts>
5
+ * Shows: confidence delta, new signals, node probability changes, edge movements.
6
+ */
7
+ interface DeltaOpts {
8
+ since?: string;
9
+ hours?: string;
10
+ json?: boolean;
11
+ watch?: boolean;
12
+ apiKey?: string;
13
+ apiUrl?: string;
14
+ }
15
+ export declare function deltaCommand(thesisId: string, opts: DeltaOpts): Promise<void>;
16
+ export {};
@@ -0,0 +1,115 @@
1
+ "use strict";
2
+ /**
3
+ * sf delta <thesisId> — Changes since a timestamp.
4
+ *
5
+ * Calls GET /api/thesis/:id/changes?since=<ts>
6
+ * Shows: confidence delta, new signals, node probability changes, edge movements.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.deltaCommand = deltaCommand;
10
+ const client_js_1 = require("../client.js");
11
+ const utils_js_1 = require("../utils.js");
12
+ async function deltaCommand(thesisId, opts) {
13
+ const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
14
+ // Resolve "since" — either explicit timestamp or --hours offset
15
+ const sinceDate = opts.since
16
+ ? new Date(opts.since)
17
+ : new Date(Date.now() - (parseInt(opts.hours || '6') * 3600000));
18
+ if (isNaN(sinceDate.getTime())) {
19
+ throw new Error(`Invalid timestamp: "${opts.since}". Use ISO 8601 format (e.g., 2026-03-28T14:00:00Z)`);
20
+ }
21
+ const run = async () => {
22
+ const since = sinceDate.toISOString();
23
+ const data = await client.getChanges(thesisId, since);
24
+ if (opts.json) {
25
+ console.log(JSON.stringify(data, null, 2));
26
+ return;
27
+ }
28
+ if (!data.changed) {
29
+ console.log(`${utils_js_1.c.dim}No changes since ${sinceDate.toLocaleString()}.${utils_js_1.c.reset}`);
30
+ return;
31
+ }
32
+ // Header
33
+ const id = (0, utils_js_1.shortId)(thesisId);
34
+ console.log();
35
+ console.log(`${utils_js_1.c.bold}${utils_js_1.c.cyan}Delta: ${id}${utils_js_1.c.reset}${utils_js_1.c.dim} — since ${sinceDate.toLocaleString()}${utils_js_1.c.reset}`);
36
+ console.log(`${utils_js_1.c.dim}${'─'.repeat(65)}${utils_js_1.c.reset}`);
37
+ // Confidence change
38
+ if (data.confidence !== undefined) {
39
+ const conf = Math.round(data.confidence * 100);
40
+ const prev = data.previousConfidence !== undefined ? Math.round(data.previousConfidence * 100) : null;
41
+ const delta = prev !== null ? conf - prev : null;
42
+ let deltaStr = '';
43
+ if (delta !== null) {
44
+ deltaStr = delta > 0 ? `${utils_js_1.c.green} (+${delta}%)${utils_js_1.c.reset}` : delta < 0 ? `${utils_js_1.c.red} (${delta}%)${utils_js_1.c.reset}` : `${utils_js_1.c.dim} (0%)${utils_js_1.c.reset}`;
45
+ }
46
+ console.log(` Confidence: ${utils_js_1.c.bold}${conf}%${utils_js_1.c.reset}${deltaStr}`);
47
+ }
48
+ // Evaluation count
49
+ if (data.evaluationCount) {
50
+ console.log(` Evaluations: ${data.evaluationCount} cycle(s)`);
51
+ }
52
+ // Updated nodes
53
+ const nodes = data.updatedNodes || [];
54
+ if (nodes.length > 0) {
55
+ console.log();
56
+ console.log(` ${utils_js_1.c.bold}Node Changes (${nodes.length}):${utils_js_1.c.reset}`);
57
+ for (const n of nodes) {
58
+ const prob = Math.round((n.newProbability ?? n.newProb ?? 0) * 100);
59
+ const prev = n.previousProbability ?? n.prevProb;
60
+ let changeStr = '';
61
+ if (prev !== undefined && prev !== null) {
62
+ const prevPct = Math.round(prev * 100);
63
+ const d = prob - prevPct;
64
+ changeStr = d > 0 ? ` ${utils_js_1.c.green}+${d}%${utils_js_1.c.reset}` : d < 0 ? ` ${utils_js_1.c.red}${d}%${utils_js_1.c.reset}` : '';
65
+ }
66
+ const label = n.label || n.nodeId || '?';
67
+ console.log(` ${label.slice(0, 40).padEnd(40)} ${prob}%${changeStr}`);
68
+ }
69
+ }
70
+ // New signals
71
+ const signals = data.newSignals || [];
72
+ if (signals.length > 0) {
73
+ console.log();
74
+ console.log(` ${utils_js_1.c.bold}New Signals (${signals.length}):${utils_js_1.c.reset}`);
75
+ for (const s of signals) {
76
+ const type = s.type || 'signal';
77
+ const content = (s.content || s.title || '').replace(/\n/g, ' ').slice(0, 70);
78
+ console.log(` ${utils_js_1.c.dim}[${type}]${utils_js_1.c.reset} ${content}`);
79
+ }
80
+ }
81
+ // Edge changes
82
+ const edges = data.edgeChanges || data.edges || [];
83
+ if (edges.length > 0) {
84
+ console.log();
85
+ console.log(` ${utils_js_1.c.bold}Edge Movements (${edges.length}):${utils_js_1.c.reset}`);
86
+ for (const e of edges.slice(0, 15)) {
87
+ const market = (e.market || e.marketId || '').slice(0, 35).padEnd(35);
88
+ const edge = e.edge ?? 0;
89
+ const prev = e.previousEdge ?? null;
90
+ const edgeStr = edge > 0 ? `+${edge}` : `${edge}`;
91
+ let changeStr = '';
92
+ if (prev !== null) {
93
+ const d = edge - prev;
94
+ changeStr = d > 0 ? ` ${utils_js_1.c.green}(+${d})${utils_js_1.c.reset}` : d < 0 ? ` ${utils_js_1.c.red}(${d})${utils_js_1.c.reset}` : '';
95
+ }
96
+ const color = edge > 0 ? utils_js_1.c.green : edge < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
97
+ console.log(` ${market} ${color}${edgeStr}${utils_js_1.c.reset}${changeStr}`);
98
+ }
99
+ }
100
+ console.log(`${utils_js_1.c.dim}${'─'.repeat(65)}${utils_js_1.c.reset}`);
101
+ console.log();
102
+ };
103
+ if (opts.watch) {
104
+ console.log(`${utils_js_1.c.dim}Watching for changes every 60s... (Ctrl+C to stop)${utils_js_1.c.reset}`);
105
+ // eslint-disable-next-line no-constant-condition
106
+ while (true) {
107
+ await run();
108
+ await new Promise(r => setTimeout(r, 60_000));
109
+ console.clear();
110
+ }
111
+ }
112
+ else {
113
+ await run();
114
+ }
115
+ }
@@ -14,6 +14,10 @@
14
14
  interface EdgesOpts {
15
15
  json?: boolean;
16
16
  limit?: string;
17
+ thesis?: string;
18
+ minEdge?: string;
19
+ minLiquidity?: string;
20
+ sort?: string;
17
21
  apiKey?: string;
18
22
  apiUrl?: string;
19
23
  }
@@ -20,17 +20,30 @@ const utils_js_1 = require("../utils.js");
20
20
  async function edgesCommand(opts) {
21
21
  const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
22
22
  const limit = parseInt(opts.limit || '20');
23
- // ── Step 1: Fetch all active theses ────────────────────────────────────────
24
- console.log(`${utils_js_1.c.dim}Fetching theses...${utils_js_1.c.reset}`);
25
- const data = await client.listTheses();
26
- const rawTheses = data.theses || data;
27
- const theses = (Array.isArray(rawTheses) ? rawTheses : []).filter((t) => t.status === 'active');
23
+ const minEdge = opts.minEdge ? parseInt(opts.minEdge) : 0;
24
+ const minLiq = opts.minLiquidity?.toLowerCase() || '';
25
+ const sortBy = opts.sort || 'edge';
26
+ // Support both letter grades (A/B/C/D) and word grades (high/medium/low)
27
+ const liqRank = { a: 4, high: 4, b: 3, medium: 3, c: 2, low: 2, d: 1 };
28
+ // ── Step 1: Fetch theses (or single thesis) ────────────────────────────────
29
+ let theses;
30
+ if (opts.thesis) {
31
+ // Single thesis mode — skip listing, fetch context directly
32
+ console.log(`${utils_js_1.c.dim}Fetching edges for thesis ${opts.thesis}...${utils_js_1.c.reset}`);
33
+ theses = [{ id: opts.thesis }];
34
+ }
35
+ else {
36
+ console.log(`${utils_js_1.c.dim}Fetching theses...${utils_js_1.c.reset}`);
37
+ const data = await client.listTheses();
38
+ const rawTheses = data.theses || data;
39
+ theses = (Array.isArray(rawTheses) ? rawTheses : []).filter((t) => t.status === 'active');
40
+ }
28
41
  if (theses.length === 0) {
29
42
  console.log(`${utils_js_1.c.yellow}No active theses found.${utils_js_1.c.reset} Create one: sf create "your thesis"`);
30
43
  return;
31
44
  }
32
45
  // ── Step 2: Fetch context for each thesis (parallel) ───────────────────────
33
- console.log(`${utils_js_1.c.dim}Fetching edges from ${theses.length} theses...${utils_js_1.c.reset}`);
46
+ console.log(`${utils_js_1.c.dim}Fetching edges from ${theses.length} ${theses.length === 1 ? 'thesis' : 'theses'}...${utils_js_1.c.reset}`);
34
47
  const allEdges = [];
35
48
  const contextPromises = theses.map(async (t) => {
36
49
  try {
@@ -76,6 +89,13 @@ async function edgesCommand(opts) {
76
89
  }
77
90
  }
78
91
  let merged = Array.from(deduped.values());
92
+ // ── Step 3b: Apply filters ─────────────────────────────────────────────────
93
+ if (minEdge > 0) {
94
+ merged = merged.filter(e => Math.abs(e.edge) >= minEdge);
95
+ }
96
+ if (minLiq && liqRank[minLiq]) {
97
+ merged = merged.filter(e => e.liquidityScore && (liqRank[e.liquidityScore.toLowerCase()] || 0) >= liqRank[minLiq]);
98
+ }
79
99
  // ── Step 4: Fetch positions (optional) ─────────────────────────────────────
80
100
  let positions = null;
81
101
  if ((0, kalshi_js_1.isKalshiConfigured)()) {
@@ -107,12 +127,18 @@ async function edgesCommand(opts) {
107
127
  }
108
128
  }
109
129
  }
110
- // ── Step 5: Sort by executableEdge (or edge) descending ────────────────────
111
- merged.sort((a, b) => {
112
- const aVal = a.executableEdge !== null ? a.executableEdge : a.edge;
113
- const bVal = b.executableEdge !== null ? b.executableEdge : b.edge;
114
- return Math.abs(bVal) - Math.abs(aVal);
115
- });
130
+ // ── Step 5: Sort ────────────────────────────────────────────────────────────
131
+ if (sortBy === 'spread') {
132
+ merged.sort((a, b) => (a.spread ?? 999) - (b.spread ?? 999));
133
+ }
134
+ else {
135
+ // Default: sort by executableEdge (or edge) descending
136
+ merged.sort((a, b) => {
137
+ const aVal = a.executableEdge !== null ? a.executableEdge : a.edge;
138
+ const bVal = b.executableEdge !== null ? b.executableEdge : b.edge;
139
+ return Math.abs(bVal) - Math.abs(aVal);
140
+ });
141
+ }
116
142
  // Apply limit
117
143
  const display = merged.slice(0, limit);
118
144
  // ── Step 6: JSON output ────────────────────────────────────────────────────
@@ -0,0 +1,9 @@
1
+ /**
2
+ * sf login — Browser-based authentication.
3
+ *
4
+ * Opens the browser, user logs in via magic link, CLI receives an API key.
5
+ * Zero manual key configuration needed.
6
+ */
7
+ export declare function loginCommand(opts: {
8
+ apiUrl?: string;
9
+ }): Promise<void>;
@@ -0,0 +1,98 @@
1
+ "use strict";
2
+ /**
3
+ * sf login — Browser-based authentication.
4
+ *
5
+ * Opens the browser, user logs in via magic link, CLI receives an API key.
6
+ * Zero manual key configuration needed.
7
+ */
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.loginCommand = loginCommand;
10
+ const crypto_1 = require("crypto");
11
+ const config_js_1 = require("../config.js");
12
+ const utils_js_1 = require("../utils.js");
13
+ const SF_API_URL = process.env.SF_API_URL || 'https://simplefunctions.dev';
14
+ async function loginCommand(opts) {
15
+ const apiUrl = opts.apiUrl || (0, config_js_1.loadConfig)().apiUrl || SF_API_URL;
16
+ // Only block if the CONFIG FILE has an API key (not env vars)
17
+ const fileConfig = (0, config_js_1.loadFileConfig)();
18
+ if (fileConfig.apiKey) {
19
+ console.log(`\n ${utils_js_1.c.dim}Already configured with API key ${fileConfig.apiKey.slice(0, 12)}...${utils_js_1.c.reset}`);
20
+ console.log(` ${utils_js_1.c.dim}Run ${utils_js_1.c.cyan}sf setup --reset${utils_js_1.c.dim} first to reconfigure.${utils_js_1.c.reset}\n`);
21
+ return;
22
+ }
23
+ // Generate session token
24
+ const sessionToken = (0, crypto_1.randomBytes)(32).toString('base64url');
25
+ // Register session on server
26
+ console.log(`\n ${utils_js_1.c.dim}Registering login session...${utils_js_1.c.reset}`);
27
+ try {
28
+ const res = await fetch(`${apiUrl}/api/auth/cli`, {
29
+ method: 'POST',
30
+ headers: { 'Content-Type': 'application/json' },
31
+ body: JSON.stringify({ sessionToken }),
32
+ });
33
+ if (!res.ok) {
34
+ const data = await res.json().catch(() => ({}));
35
+ console.error(` ${utils_js_1.c.red}Failed to create session: ${data.error || res.status}${utils_js_1.c.reset}`);
36
+ return;
37
+ }
38
+ }
39
+ catch (err) {
40
+ console.error(` ${utils_js_1.c.red}Could not reach ${apiUrl}${utils_js_1.c.reset}`);
41
+ return;
42
+ }
43
+ // Open browser
44
+ const loginUrl = `${apiUrl}/auth/cli?token=${sessionToken}`;
45
+ console.log(`\n ${utils_js_1.c.bold}Opening browser...${utils_js_1.c.reset}`);
46
+ console.log(` ${utils_js_1.c.dim}${loginUrl}${utils_js_1.c.reset}\n`);
47
+ try {
48
+ const { exec } = await import('child_process');
49
+ const platform = process.platform;
50
+ const cmd = platform === 'darwin' ? 'open'
51
+ : platform === 'win32' ? 'start'
52
+ : 'xdg-open';
53
+ exec(`${cmd} "${loginUrl}"`);
54
+ }
55
+ catch {
56
+ console.log(` ${utils_js_1.c.dim}Could not open browser. Visit the URL above manually.${utils_js_1.c.reset}`);
57
+ }
58
+ // Poll for completion
59
+ console.log(` ${utils_js_1.c.dim}Waiting for login...${utils_js_1.c.reset}`);
60
+ const maxWait = 5 * 60 * 1000; // 5 minutes
61
+ const pollInterval = 2000;
62
+ const startTime = Date.now();
63
+ while (Date.now() - startTime < maxWait) {
64
+ await new Promise(r => setTimeout(r, pollInterval));
65
+ try {
66
+ const res = await fetch(`${apiUrl}/api/auth/cli/poll?token=${sessionToken}`);
67
+ const data = await res.json();
68
+ if (data.status === 'ready' && data.apiKey) {
69
+ // Save to config
70
+ (0, config_js_1.saveConfig)({
71
+ ...fileConfig,
72
+ apiKey: data.apiKey,
73
+ apiUrl: apiUrl !== SF_API_URL ? apiUrl : undefined,
74
+ });
75
+ console.log(`\n ${utils_js_1.c.green}✓${utils_js_1.c.reset} ${utils_js_1.c.bold}Authenticated!${utils_js_1.c.reset}`);
76
+ console.log(` ${utils_js_1.c.dim}API key saved to ~/.sf/config.json${utils_js_1.c.reset}`);
77
+ console.log(`\n ${utils_js_1.c.dim}OpenRouter & Tavily are proxied through ${apiUrl}${utils_js_1.c.reset}`);
78
+ console.log(` ${utils_js_1.c.dim}No additional API keys needed.${utils_js_1.c.reset}`);
79
+ console.log(`\n ${utils_js_1.c.cyan}sf context${utils_js_1.c.reset} ${utils_js_1.c.dim}market intelligence${utils_js_1.c.reset}`);
80
+ console.log(` ${utils_js_1.c.cyan}sf agent${utils_js_1.c.reset} ${utils_js_1.c.dim}<thesis>${utils_js_1.c.reset} ${utils_js_1.c.dim}start the agent${utils_js_1.c.reset}`);
81
+ console.log(` ${utils_js_1.c.cyan}sf setup --check${utils_js_1.c.reset} ${utils_js_1.c.dim}verify config${utils_js_1.c.reset}`);
82
+ console.log();
83
+ return;
84
+ }
85
+ if (data.status === 'expired') {
86
+ console.error(`\n ${utils_js_1.c.red}Session expired. Run ${utils_js_1.c.cyan}sf login${utils_js_1.c.red} again.${utils_js_1.c.reset}\n`);
87
+ return;
88
+ }
89
+ // status === 'pending' — keep polling
90
+ process.stdout.write('.');
91
+ }
92
+ catch {
93
+ // Network error — keep trying
94
+ process.stdout.write('x');
95
+ }
96
+ }
97
+ console.error(`\n ${utils_js_1.c.red}Timed out waiting for login. Run ${utils_js_1.c.cyan}sf login${utils_js_1.c.red} again.${utils_js_1.c.reset}\n`);
98
+ }
@@ -210,10 +210,13 @@ async function showCheck() {
210
210
  }
211
211
  // OpenRouter
212
212
  if (config.openrouterKey) {
213
- ok(`OPENROUTER_KEY ${dim(mask(config.openrouterKey))}`);
213
+ ok(`OPENROUTER ${dim(mask(config.openrouterKey))} (direct)`);
214
+ }
215
+ else if (config.apiKey) {
216
+ ok(`OPENROUTER ${dim('proxied via SimpleFunctions')}`);
214
217
  }
215
218
  else {
216
- fail(`OPENROUTER_KEY not configured (agent unavailable)`);
219
+ fail(`OPENROUTER not configured (run sf login or set key)`);
217
220
  }
218
221
  // Kalshi
219
222
  if (config.kalshiKeyId && config.kalshiPrivateKeyPath) {
@@ -231,7 +234,10 @@ async function showCheck() {
231
234
  }
232
235
  // Tavily
233
236
  if (config.tavilyKey) {
234
- ok(`TAVILY ${dim(mask(config.tavilyKey))}`);
237
+ ok(`TAVILY ${dim(mask(config.tavilyKey))} (direct)`);
238
+ }
239
+ else if (config.apiKey) {
240
+ ok(`TAVILY ${dim('proxied via SimpleFunctions')}`);
235
241
  }
236
242
  else {
237
243
  info(`${dim('○')} TAVILY ${dim('skipped')}`);
package/dist/index.js CHANGED
@@ -57,6 +57,8 @@ const book_js_1 = require("./commands/book.js");
57
57
  const prompt_js_1 = require("./commands/prompt.js");
58
58
  const augment_js_1 = require("./commands/augment.js");
59
59
  const telegram_js_1 = require("./commands/telegram.js");
60
+ const delta_js_1 = require("./commands/delta.js");
61
+ const login_js_1 = require("./commands/login.js");
60
62
  const query_js_1 = require("./commands/query.js");
61
63
  const markets_js_1 = require("./commands/markets.js");
62
64
  const x_js_1 = require("./commands/x.js");
@@ -72,7 +74,8 @@ const GROUPED_HELP = `
72
74
  sf <command> --help for detailed options
73
75
 
74
76
  \x1b[1mSetup\x1b[22m
75
- \x1b[36msetup\x1b[39m Interactive config wizard
77
+ \x1b[36mlogin\x1b[39m Browser login (recommended)
78
+ \x1b[36msetup\x1b[39m Interactive config wizard (power users)
76
79
  \x1b[36msetup --check\x1b[39m Show config status
77
80
  \x1b[36msetup --polymarket\x1b[39m Configure Polymarket wallet
78
81
 
@@ -128,6 +131,7 @@ const GROUPED_HELP = `
128
131
 
129
132
  \x1b[1mInfo\x1b[22m
130
133
  \x1b[36mfeed\x1b[39m Evaluation history stream
134
+ \x1b[36mdelta\x1b[39m <id> Changes since timestamp
131
135
  \x1b[36mmilestones\x1b[39m Upcoming Kalshi events
132
136
  \x1b[36mschedule\x1b[39m Exchange status
133
137
  \x1b[36mannouncements\x1b[39m Exchange announcements
@@ -234,7 +238,7 @@ async function interactiveEntry() {
234
238
  console.log();
235
239
  }
236
240
  // ── Pre-action guard: check configuration ────────────────────────────────────
237
- const NO_CONFIG_COMMANDS = new Set(['setup', 'help', 'scan', 'explore', 'query', 'context', 'markets', 'milestones', 'forecast', 'settlements', 'balance', 'orders', 'fills', 'schedule', 'announcements', 'history', 'liquidity', 'book', 'prompt', 'sf']);
241
+ const NO_CONFIG_COMMANDS = new Set(['setup', 'login', 'help', 'scan', 'explore', 'query', 'context', 'markets', 'milestones', 'forecast', 'settlements', 'balance', 'orders', 'fills', 'schedule', 'announcements', 'history', 'liquidity', 'book', 'prompt', 'sf']);
238
242
  program.hook('preAction', (thisCommand, actionCommand) => {
239
243
  const cmdName = actionCommand.name();
240
244
  if (NO_CONFIG_COMMANDS.has(cmdName))
@@ -250,9 +254,10 @@ program.hook('preAction', (thisCommand, actionCommand) => {
250
254
  }
251
255
  else {
252
256
  console.log();
253
- console.log(' This command needs an API key. Two options:');
254
- console.log(' 1. \x1b[36msf setup\x1b[39m Interactive wizard (2 min)');
255
- console.log(' 2. \x1b[36msf --api-key KEY\x1b[39m Pass inline');
257
+ console.log(' This command needs an API key. Three options:');
258
+ console.log(' 1. \x1b[36msf login\x1b[39m Browser login (30 sec, recommended)');
259
+ console.log(' 2. \x1b[36msf setup\x1b[39m Interactive wizard (2 min)');
260
+ console.log(' 3. \x1b[36msf --api-key KEY\x1b[39m Pass inline');
256
261
  console.log();
257
262
  console.log(' \x1b[2mDon\'t have a key? Try \x1b[36msf scan "oil"\x1b[22m\x1b[2m — works without login.\x1b[22m');
258
263
  }
@@ -274,6 +279,14 @@ program
274
279
  .action(async (opts) => {
275
280
  await run(() => (0, setup_js_1.setupCommand)({ check: opts.check, reset: opts.reset, key: opts.key, enableTrading: opts.enableTrading, disableTrading: opts.disableTrading, kalshi: opts.kalshi, polymarket: opts.polymarket }));
276
281
  });
282
+ // ── sf login ─────────────────────────────────────────────────────────────────
283
+ program
284
+ .command('login')
285
+ .description('Browser-based login (recommended — no API keys needed)')
286
+ .action(async (_opts, cmd) => {
287
+ const g = cmd.optsWithGlobals();
288
+ await run(() => (0, login_js_1.loginCommand)({ apiUrl: g.apiUrl }));
289
+ });
277
290
  // ── sf list ──────────────────────────────────────────────────────────────────
278
291
  program
279
292
  .command('list')
@@ -359,11 +372,19 @@ program
359
372
  .description('Top edges across all theses — what to trade now')
360
373
  .option('--json', 'JSON output for agents')
361
374
  .option('--limit <n>', 'Max edges to show', '20')
375
+ .option('--thesis <id>', 'Filter to a single thesis')
376
+ .option('--min-edge <cents>', 'Minimum absolute edge size in cents')
377
+ .option('--min-liquidity <grade>', 'Minimum liquidity (high/medium/low or A/B/C)')
378
+ .option('--sort <by>', 'Sort by: edge (default), spread')
362
379
  .action(async (opts, cmd) => {
363
380
  const g = cmd.optsWithGlobals();
364
381
  await run(() => (0, edges_js_1.edgesCommand)({
365
382
  json: opts.json,
366
383
  limit: opts.limit,
384
+ thesis: opts.thesis,
385
+ minEdge: opts.minEdge,
386
+ minLiquidity: opts.minLiquidity,
387
+ sort: opts.sort,
367
388
  apiKey: g.apiKey,
368
389
  apiUrl: g.apiUrl,
369
390
  }));
@@ -499,6 +520,18 @@ program
499
520
  const g = cmd.optsWithGlobals();
500
521
  await run(() => (0, feed_js_1.feedCommand)({ ...opts, apiKey: g.apiKey, apiUrl: g.apiUrl }));
501
522
  });
523
+ // ── sf delta <thesisId> ───────────────────────────────────────────────────────
524
+ program
525
+ .command('delta <thesisId>')
526
+ .description('Changes since a timestamp — confidence, nodes, signals, edges')
527
+ .option('--since <timestamp>', 'ISO 8601 start time (e.g., 2026-03-28T14:00:00Z)')
528
+ .option('--hours <n>', 'Hours to look back (default 6)', '6')
529
+ .option('--watch', 'Continuously poll every 60s')
530
+ .option('--json', 'JSON output')
531
+ .action(async (thesisId, opts, cmd) => {
532
+ const g = cmd.optsWithGlobals();
533
+ await run(() => (0, delta_js_1.deltaCommand)(thesisId, { ...opts, apiKey: g.apiKey, apiUrl: g.apiUrl }));
534
+ });
502
535
  // ── sf whatif <thesisId> ──────────────────────────────────────────────────────
503
536
  program
504
537
  .command('whatif <thesisId>')
@@ -242,12 +242,24 @@ async function buildTools(sfClient, thesisId, latestContext) {
242
242
  parameters: Type.Object({ query: Type.String({ description: 'Search query' }) }),
243
243
  execute: async (_id, p) => {
244
244
  const tavilyKey = process.env.TAVILY_API_KEY || config.tavilyKey;
245
- if (!tavilyKey)
246
- return { content: [{ type: 'text', text: 'Tavily not configured.' }], details: {} };
247
- const res = await fetch('https://api.tavily.com/search', {
248
- method: 'POST', headers: { 'Content-Type': 'application/json' },
249
- body: JSON.stringify({ api_key: tavilyKey, query: p.query, max_results: 3, search_depth: 'basic', include_answer: true }),
250
- });
245
+ const tgSfKey = config.apiKey || process.env.SF_API_KEY;
246
+ const tgSfUrl = config.apiUrl || process.env.SF_API_URL || 'https://simplefunctions.dev';
247
+ const canProxySearch = !tavilyKey && tgSfKey;
248
+ if (!tavilyKey && !canProxySearch)
249
+ return { content: [{ type: 'text', text: 'Web search not available. Run sf login or set TAVILY_API_KEY.' }], details: {} };
250
+ let res;
251
+ if (tavilyKey) {
252
+ res = await fetch('https://api.tavily.com/search', {
253
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
254
+ body: JSON.stringify({ api_key: tavilyKey, query: p.query, max_results: 3, search_depth: 'basic', include_answer: true }),
255
+ });
256
+ }
257
+ else {
258
+ res = await fetch(`${tgSfUrl}/api/proxy/search`, {
259
+ method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${tgSfKey}` },
260
+ body: JSON.stringify({ query: p.query, max_results: 3, search_depth: 'basic', include_answer: true }),
261
+ });
262
+ }
251
263
  if (!res.ok)
252
264
  return { content: [{ type: 'text', text: `Search failed: ${res.status}` }], details: {} };
253
265
  const data = await res.json();
@@ -407,9 +419,13 @@ async function getOrCreateAgent(sfClient, session) {
407
419
  const piAi = await import('@mariozechner/pi-ai');
408
420
  const { getModel } = piAi;
409
421
  const config = (0, config_js_1.loadConfig)();
410
- const openrouterKey = config.openrouterKey || process.env.OPENROUTER_API_KEY;
422
+ const directOrKey = config.openrouterKey || process.env.OPENROUTER_API_KEY;
423
+ const sfApiKey = config.apiKey || process.env.SF_API_KEY;
424
+ const sfApiUrl = config.apiUrl || process.env.SF_API_URL || 'https://simplefunctions.dev';
425
+ const tgUseProxy = !directOrKey && !!sfApiKey;
426
+ const openrouterKey = directOrKey || sfApiKey;
411
427
  if (!openrouterKey)
412
- throw new Error('OpenRouter not configured. Use slash commands or run sf setup.');
428
+ throw new Error('Need API key. Run sf login or sf setup.');
413
429
  const ctx = await sfClient.getContext(session.thesisId);
414
430
  const conf = typeof ctx.confidence === 'number' ? Math.round(ctx.confidence * 100) : 50;
415
431
  const tools = await buildTools(sfClient, session.thesisId, ctx);
@@ -427,6 +443,8 @@ async function getOrCreateAgent(sfClient, session) {
427
443
  supportsImages: true, supportsTools: true,
428
444
  };
429
445
  }
446
+ if (tgUseProxy)
447
+ model.baseUrl = `${sfApiUrl}/api/proxy`;
430
448
  const edgesSummary = (ctx.edges || [])
431
449
  .sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
432
450
  .slice(0, 5)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.7.10",
3
+ "version": "1.7.12",
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"