@spfunctions/cli 1.7.11 → 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();
@@ -19,8 +19,11 @@ async function contextCommand(id, opts) {
19
19
  return;
20
20
  }
21
21
  const scanTime = data.scannedAt ? (0, utils_js_1.shortDate)(data.scannedAt) : 'no scan yet';
22
- console.log(`\n${utils_js_1.c.bold}Markets${utils_js_1.c.reset} ${utils_js_1.c.dim}scan: ${scanTime}${utils_js_1.c.reset}\n`);
23
- // ── Traditional markets ticker bar ──────────────────────────────────────
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 ────────────────────────────────────────────────
24
27
  const trad = data.traditional || [];
25
28
  if (trad.length > 0) {
26
29
  const line = trad.map((m) => {
@@ -32,51 +35,54 @@ async function contextCommand(id, opts) {
32
35
  console.log(` ${line}`);
33
36
  console.log();
34
37
  }
35
- // ── Movers ─────────────────────────────────────────────────────────────
36
- const movers = data.movers || [];
37
- if (movers.length > 0) {
38
- console.log(`${utils_js_1.c.bold}Movers (24h)${utils_js_1.c.reset}`);
39
- for (const m of movers.slice(0, 8)) {
40
- 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}`;
41
- const ch = m.change24h || 0;
42
- const chColor = ch > 0 ? utils_js_1.c.green : utils_js_1.c.red;
43
- const chStr = `${chColor}${ch > 0 ? '+' : ''}${ch}¢${utils_js_1.c.reset}`;
44
- const cat = m.category ? `${utils_js_1.c.dim}[${m.category}]${utils_js_1.c.reset}` : '';
45
- console.log(` ${venue} ${(0, utils_js_1.rpad)(`${m.price}¢`, 5)} ${(0, utils_js_1.rpad)(chStr, 16)} ${(0, utils_js_1.trunc)(m.title, 50)} ${cat}`);
46
- // Actionable: show command to dig deeper
47
- if (m.venue === 'kalshi' && m.ticker) {
48
- console.log(` ${utils_js_1.c.dim}→ sf book ${m.ticker}${utils_js_1.c.reset}`);
49
- }
50
- else {
51
- console.log(` ${utils_js_1.c.dim}→ sf query "${extractQuery(m.title)}"${utils_js_1.c.reset}`);
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}`);
52
47
  }
48
+ console.log(` ${utils_js_1.c.cyan}→ ${h.suggestedAction}${utils_js_1.c.reset}`);
49
+ console.log();
53
50
  }
54
- console.log();
55
- }
56
- // ── Liquid markets ─────────────────────────────────────────────────────
57
- const liquid = data.liquid || [];
58
- if (liquid.length > 0) {
59
- console.log(`${utils_js_1.c.bold}Most Liquid${utils_js_1.c.reset}`);
60
- for (const m of liquid.slice(0, 6)) {
61
- 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}`;
62
- console.log(` ${venue} ${(0, utils_js_1.rpad)(`${m.price}¢`, 5)} spread ${m.spread}¢ vol ${(0, utils_js_1.vol)(Math.round(m.volume24h))} ${(0, utils_js_1.trunc)(m.title, 45)}`);
63
- }
64
- console.log();
65
51
  }
66
52
  // ── Categories ─────────────────────────────────────────────────────────
67
53
  const cats = data.categories || [];
68
54
  if (cats.length > 0) {
69
- console.log(`${utils_js_1.c.bold}Categories${utils_js_1.c.reset}`);
70
- for (const cat of cats.slice(0, 8)) {
71
- const mover = cat.topMover;
72
- const moverStr = mover
73
- ? `${utils_js_1.c.dim}top: ${(0, utils_js_1.trunc)(mover.title, 30)} ${mover.change24h > 0 ? utils_js_1.c.green + '+' : utils_js_1.c.red}${mover.change24h}¢${utils_js_1.c.reset}`
74
- : '';
75
- console.log(` ${(0, utils_js_1.pad)(cat.name, 14)} ${(0, utils_js_1.rpad)(String(cat.marketCount) + ' mkts', 10)} vol ${(0, utils_js_1.vol)(cat.totalVolume24h)} ${moverStr}`);
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
+ }
76
82
  }
77
83
  console.log();
78
84
  }
79
- // ── Thesis edges (from public theses) ──────────────────────────────────
85
+ // ── Thesis edges ───────────────────────────────────────────────────────
80
86
  const edges = data.edges || [];
81
87
  if (edges.length > 0) {
82
88
  console.log(`${utils_js_1.c.bold}Thesis Edges${utils_js_1.c.reset}`);
@@ -122,7 +128,6 @@ async function contextCommand(id, opts) {
122
128
  console.log(`${utils_js_1.c.bold}Confidence:${utils_js_1.c.reset} ${confStr}${deltaStr}`);
123
129
  console.log(`${utils_js_1.c.bold}Status:${utils_js_1.c.reset} ${ctx.status}`);
124
130
  console.log(`${utils_js_1.c.bold}Last Updated:${utils_js_1.c.reset} ${(0, utils_js_1.shortDate)(ctx.updatedAt)}`);
125
- // Causal tree nodes
126
131
  const nodes = ctx.causalTree?.nodes;
127
132
  if (nodes && nodes.length > 0) {
128
133
  (0, utils_js_1.header)('Causal Tree');
@@ -133,7 +138,6 @@ async function contextCommand(id, opts) {
133
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)}`);
134
139
  }
135
140
  }
136
- // Fetch positions if Kalshi is configured
137
141
  let positions = null;
138
142
  if ((0, kalshi_js_1.isKalshiConfigured)()) {
139
143
  try {
@@ -146,7 +150,6 @@ async function contextCommand(id, opts) {
146
150
  for (const p of positions)
147
151
  posMap.set(p.ticker, p);
148
152
  }
149
- // Top edges
150
153
  const edges = ctx.edges;
151
154
  if (edges && edges.length > 0) {
152
155
  (0, utils_js_1.header)('Top Edges');
@@ -171,7 +174,6 @@ async function contextCommand(id, opts) {
171
174
  ` ${utils_js_1.c.dim}${edge.venue || ''}${utils_js_1.c.reset}` + obStr + posStr);
172
175
  }
173
176
  }
174
- // Last evaluation
175
177
  if (ctx.lastEvaluation?.summary) {
176
178
  (0, utils_js_1.header)('Last Evaluation');
177
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}`);
@@ -189,18 +191,6 @@ async function contextCommand(id, opts) {
189
191
  }
190
192
  console.log('');
191
193
  }
192
- /** Extract a short search query from a market title */
193
- function extractQuery(title) {
194
- return title
195
- .replace(/^Will\s+/i, '')
196
- .replace(/\?.*$/, '')
197
- .replace(/by\s+\w+\s+\d{1,2}.*$/i, '')
198
- .replace(/\*\*/g, '')
199
- .trim()
200
- .slice(0, 40)
201
- .trim();
202
- }
203
- /** Human-readable time ago */
204
194
  function timeAgo(iso) {
205
195
  const ms = Date.now() - new Date(iso).getTime();
206
196
  const min = Math.round(ms / 60000);
@@ -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
@@ -58,6 +58,7 @@ 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
60
  const delta_js_1 = require("./commands/delta.js");
61
+ const login_js_1 = require("./commands/login.js");
61
62
  const query_js_1 = require("./commands/query.js");
62
63
  const markets_js_1 = require("./commands/markets.js");
63
64
  const x_js_1 = require("./commands/x.js");
@@ -73,7 +74,8 @@ const GROUPED_HELP = `
73
74
  sf <command> --help for detailed options
74
75
 
75
76
  \x1b[1mSetup\x1b[22m
76
- \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)
77
79
  \x1b[36msetup --check\x1b[39m Show config status
78
80
  \x1b[36msetup --polymarket\x1b[39m Configure Polymarket wallet
79
81
 
@@ -236,7 +238,7 @@ async function interactiveEntry() {
236
238
  console.log();
237
239
  }
238
240
  // ── Pre-action guard: check configuration ────────────────────────────────────
239
- 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']);
240
242
  program.hook('preAction', (thisCommand, actionCommand) => {
241
243
  const cmdName = actionCommand.name();
242
244
  if (NO_CONFIG_COMMANDS.has(cmdName))
@@ -252,9 +254,10 @@ program.hook('preAction', (thisCommand, actionCommand) => {
252
254
  }
253
255
  else {
254
256
  console.log();
255
- console.log(' This command needs an API key. Two options:');
256
- console.log(' 1. \x1b[36msf setup\x1b[39m Interactive wizard (2 min)');
257
- 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');
258
261
  console.log();
259
262
  console.log(' \x1b[2mDon\'t have a key? Try \x1b[36msf scan "oil"\x1b[22m\x1b[2m — works without login.\x1b[22m');
260
263
  }
@@ -276,6 +279,14 @@ program
276
279
  .action(async (opts) => {
277
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 }));
278
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
+ });
279
290
  // ── sf list ──────────────────────────────────────────────────────────────────
280
291
  program
281
292
  .command('list')
@@ -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.11",
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"