@spfunctions/cli 1.7.11 → 1.7.13
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.
- package/dist/client.d.ts +2 -0
- package/dist/client.js +6 -0
- package/dist/commands/agent.js +111 -56
- package/dist/commands/context.js +44 -54
- package/dist/commands/login.d.ts +9 -0
- package/dist/commands/login.js +98 -0
- package/dist/commands/setup.js +9 -3
- package/dist/index.js +16 -5
- package/dist/telegram/agent-bridge.js +26 -8
- package/package.json +1 -1
package/dist/client.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export declare class SFClient {
|
|
|
17
17
|
createThesis(rawThesis: string, sync?: boolean): Promise<any>;
|
|
18
18
|
injectSignal(id: string, type: string, content: string, source?: string): Promise<any>;
|
|
19
19
|
evaluate(id: string): Promise<any>;
|
|
20
|
+
getHeartbeatConfig(id: string): Promise<any>;
|
|
21
|
+
updateHeartbeatConfig(id: string, config: Record<string, unknown>): Promise<any>;
|
|
20
22
|
getFeed(hours?: number, limit?: number): Promise<any>;
|
|
21
23
|
getChanges(id: string, since: string): Promise<any>;
|
|
22
24
|
updateThesis(id: string, data: Record<string, unknown>): Promise<any>;
|
package/dist/client.js
CHANGED
|
@@ -74,6 +74,12 @@ class SFClient {
|
|
|
74
74
|
async evaluate(id) {
|
|
75
75
|
return this.request('POST', `/api/thesis/${id}/evaluate`);
|
|
76
76
|
}
|
|
77
|
+
async getHeartbeatConfig(id) {
|
|
78
|
+
return this.request('GET', `/api/thesis/${id}/heartbeat`);
|
|
79
|
+
}
|
|
80
|
+
async updateHeartbeatConfig(id, config) {
|
|
81
|
+
return this.request('PATCH', `/api/thesis/${id}/heartbeat`, config);
|
|
82
|
+
}
|
|
77
83
|
async getFeed(hours = 24, limit = 200) {
|
|
78
84
|
return this.request('GET', `/api/feed?hours=${hours}&limit=${limit}`);
|
|
79
85
|
}
|
package/dist/commands/agent.js
CHANGED
|
@@ -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
|
|
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
|
|
476
|
+
console.error('Need an API key to power the agent LLM.');
|
|
471
477
|
console.error('');
|
|
472
|
-
console.error(' 1
|
|
473
|
-
console.error(' 2
|
|
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
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
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
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
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
|
-
|
|
568
|
+
m = getModel('openrouter', name);
|
|
557
569
|
}
|
|
558
570
|
catch {
|
|
559
|
-
|
|
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
|
|
962
|
-
|
|
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: '
|
|
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
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
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
|
|
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
|
-
|
|
2637
|
+
m = getModel('openrouter', name);
|
|
2596
2638
|
}
|
|
2597
2639
|
catch {
|
|
2598
|
-
|
|
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
|
|
2743
|
-
|
|
2744
|
-
|
|
2745
|
-
|
|
2746
|
-
|
|
2747
|
-
|
|
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();
|
package/dist/commands/context.js
CHANGED
|
@@ -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
|
-
|
|
23
|
-
|
|
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
|
-
// ──
|
|
36
|
-
const
|
|
37
|
-
if (
|
|
38
|
-
console.log(`${utils_js_1.c.bold}
|
|
39
|
-
for (const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
|
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,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
|
+
}
|
package/dist/commands/setup.js
CHANGED
|
@@ -210,10 +210,13 @@ async function showCheck() {
|
|
|
210
210
|
}
|
|
211
211
|
// OpenRouter
|
|
212
212
|
if (config.openrouterKey) {
|
|
213
|
-
ok(`
|
|
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(`
|
|
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[
|
|
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.
|
|
256
|
-
console.log(' 1. \x1b[36msf
|
|
257
|
-
console.log(' 2. \x1b[36msf
|
|
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
|
-
|
|
246
|
-
|
|
247
|
-
const
|
|
248
|
-
|
|
249
|
-
|
|
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
|
|
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('
|
|
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.
|
|
3
|
+
"version": "1.7.13",
|
|
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"
|