@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.
- package/dist/commands/agent.js +111 -56
- package/dist/commands/context.js +101 -57
- package/dist/commands/delta.d.ts +16 -0
- package/dist/commands/delta.js +115 -0
- package/dist/commands/edges.d.ts +4 -0
- package/dist/commands/edges.js +38 -12
- 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 +38 -5
- package/dist/telegram/agent-bridge.js +26 -8
- package/package.json +1 -1
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
|
@@ -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
|
|
9
|
+
// ── Mode 1: No thesis ID → global market intelligence ─────────────────────
|
|
9
10
|
if (!id) {
|
|
10
|
-
const res = await fetch(
|
|
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
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
console.log(` ${
|
|
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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
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
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
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
|
|
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}
|
|
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
|
+
}
|
package/dist/commands/edges.d.ts
CHANGED
package/dist/commands/edges.js
CHANGED
|
@@ -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
|
-
|
|
24
|
-
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
const
|
|
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
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
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,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
|
@@ -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[
|
|
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.
|
|
254
|
-
console.log(' 1. \x1b[36msf
|
|
255
|
-
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');
|
|
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
|
-
|
|
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.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"
|