@spfunctions/cli 1.1.3 → 1.1.4

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.
@@ -520,7 +520,7 @@ async function agentCommand(thesisId, opts) {
520
520
  const series = await (0, client_js_1.kalshiFetchAllSeries)();
521
521
  const keywords = params.query.toLowerCase().split(/\s+/);
522
522
  const matched = series
523
- .filter((s) => keywords.some((kw) => (s.title || '').toLowerCase().includes(kw) ||
523
+ .filter((s) => keywords.every((kw) => (s.title || '').toLowerCase().includes(kw) ||
524
524
  (s.ticker || '').toLowerCase().includes(kw)))
525
525
  .slice(0, 15);
526
526
  result = matched;
@@ -0,0 +1,21 @@
1
+ /**
2
+ * sf edges — Top edges across all active theses
3
+ *
4
+ * The most important output of the entire system: "what to trade now."
5
+ *
6
+ * Flow:
7
+ * 1. GET /api/thesis → all active theses
8
+ * 2. For each: GET /api/thesis/:id/context → edges with orderbook
9
+ * 3. Optional: getPositions() → Kalshi positions with live prices
10
+ * 4. Merge edges, dedupe by marketId (keep highest edge, note source thesis)
11
+ * 5. Sort by executableEdge descending
12
+ * 6. Display table with position overlay + summary
13
+ */
14
+ interface EdgesOpts {
15
+ json?: boolean;
16
+ limit?: string;
17
+ apiKey?: string;
18
+ apiUrl?: string;
19
+ }
20
+ export declare function edgesCommand(opts: EdgesOpts): Promise<void>;
21
+ export {};
@@ -0,0 +1,205 @@
1
+ "use strict";
2
+ /**
3
+ * sf edges — Top edges across all active theses
4
+ *
5
+ * The most important output of the entire system: "what to trade now."
6
+ *
7
+ * Flow:
8
+ * 1. GET /api/thesis → all active theses
9
+ * 2. For each: GET /api/thesis/:id/context → edges with orderbook
10
+ * 3. Optional: getPositions() → Kalshi positions with live prices
11
+ * 4. Merge edges, dedupe by marketId (keep highest edge, note source thesis)
12
+ * 5. Sort by executableEdge descending
13
+ * 6. Display table with position overlay + summary
14
+ */
15
+ Object.defineProperty(exports, "__esModule", { value: true });
16
+ exports.edgesCommand = edgesCommand;
17
+ const client_js_1 = require("../client.js");
18
+ const kalshi_js_1 = require("../kalshi.js");
19
+ const utils_js_1 = require("../utils.js");
20
+ async function edgesCommand(opts) {
21
+ const client = new client_js_1.SFClient(opts.apiKey, opts.apiUrl);
22
+ const limit = parseInt(opts.limit || '20');
23
+ // ── Step 1: Fetch all active theses ────────────────────────────────────────
24
+ console.log(`${utils_js_1.c.dim}Fetching theses...${utils_js_1.c.reset}`);
25
+ const data = await client.listTheses();
26
+ const rawTheses = data.theses || data;
27
+ const theses = (Array.isArray(rawTheses) ? rawTheses : []).filter((t) => t.status === 'active');
28
+ if (theses.length === 0) {
29
+ console.log(`${utils_js_1.c.yellow}No active theses found.${utils_js_1.c.reset} Create one: sf create "your thesis"`);
30
+ return;
31
+ }
32
+ // ── 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}`);
34
+ const allEdges = [];
35
+ const contextPromises = theses.map(async (t) => {
36
+ try {
37
+ const ctx = await client.getContext(t.id);
38
+ return { thesisId: t.id, edges: ctx.edges || [] };
39
+ }
40
+ catch {
41
+ return { thesisId: t.id, edges: [] };
42
+ }
43
+ });
44
+ const results = await Promise.all(contextPromises);
45
+ for (const { thesisId, edges } of results) {
46
+ for (const e of edges) {
47
+ allEdges.push({
48
+ marketId: e.marketId || '',
49
+ market: e.market || e.marketTitle || e.marketId || '',
50
+ venue: e.venue || 'kalshi',
51
+ direction: e.direction || 'yes',
52
+ marketPrice: typeof e.marketPrice === 'number' ? e.marketPrice : 0,
53
+ thesisPrice: typeof e.thesisPrice === 'number' ? e.thesisPrice : 0,
54
+ edge: typeof e.edge === 'number' ? e.edge : 0,
55
+ executableEdge: typeof e.executableEdge === 'number' ? e.executableEdge : null,
56
+ spread: e.orderbook?.spread ?? null,
57
+ liquidityScore: e.orderbook?.liquidityScore ?? null,
58
+ thesisId,
59
+ position: null,
60
+ });
61
+ }
62
+ }
63
+ if (allEdges.length === 0) {
64
+ console.log(`${utils_js_1.c.yellow}No edges found across ${theses.length} theses.${utils_js_1.c.reset}`);
65
+ return;
66
+ }
67
+ // ── Step 3: Dedupe by marketId — keep highest absolute edge ────────────────
68
+ const deduped = new Map();
69
+ for (const edge of allEdges) {
70
+ const key = edge.marketId;
71
+ if (!key)
72
+ continue;
73
+ const existing = deduped.get(key);
74
+ if (!existing || Math.abs(edge.edge) > Math.abs(existing.edge)) {
75
+ deduped.set(key, edge);
76
+ }
77
+ }
78
+ let merged = Array.from(deduped.values());
79
+ // ── Step 4: Fetch positions (optional) ─────────────────────────────────────
80
+ let positions = null;
81
+ if ((0, kalshi_js_1.isKalshiConfigured)()) {
82
+ console.log(`${utils_js_1.c.dim}Fetching Kalshi positions...${utils_js_1.c.reset}`);
83
+ positions = await (0, kalshi_js_1.getPositions)();
84
+ if (positions) {
85
+ // Enrich with live prices
86
+ for (const pos of positions) {
87
+ const livePrice = await (0, kalshi_js_1.getMarketPrice)(pos.ticker);
88
+ if (livePrice !== null) {
89
+ pos.current_value = livePrice;
90
+ pos.unrealized_pnl = Math.round((livePrice - pos.average_price_paid) * pos.quantity);
91
+ }
92
+ }
93
+ // Match positions to edges
94
+ for (const edge of merged) {
95
+ const pos = positions.find(p => p.ticker === edge.marketId ||
96
+ (edge.marketId && p.ticker?.includes(edge.marketId)));
97
+ if (pos) {
98
+ edge.position = {
99
+ side: pos.side || 'yes',
100
+ quantity: pos.quantity,
101
+ avgPrice: pos.average_price_paid,
102
+ currentValue: pos.current_value,
103
+ pnl: pos.unrealized_pnl || 0,
104
+ totalCost: pos.total_cost || Math.round(pos.average_price_paid * pos.quantity),
105
+ };
106
+ }
107
+ }
108
+ }
109
+ }
110
+ // ── Step 5: Sort by executableEdge (or edge) descending ────────────────────
111
+ merged.sort((a, b) => {
112
+ const aVal = a.executableEdge !== null ? a.executableEdge : a.edge;
113
+ const bVal = b.executableEdge !== null ? b.executableEdge : b.edge;
114
+ return Math.abs(bVal) - Math.abs(aVal);
115
+ });
116
+ // Apply limit
117
+ const display = merged.slice(0, limit);
118
+ // ── Step 6: JSON output ────────────────────────────────────────────────────
119
+ if (opts.json) {
120
+ console.log(JSON.stringify({
121
+ totalEdges: merged.length,
122
+ displayed: display.length,
123
+ thesesScanned: theses.length,
124
+ edges: display,
125
+ }, null, 2));
126
+ return;
127
+ }
128
+ // ── Step 6: Pretty output ──────────────────────────────────────────────────
129
+ console.log();
130
+ (0, utils_js_1.header)(`Top Edges Across ${theses.length} Theses`);
131
+ console.log();
132
+ // Header row
133
+ const hdr = [
134
+ (0, utils_js_1.pad)('Market', 32),
135
+ (0, utils_js_1.rpad)('Mkt', 5),
136
+ (0, utils_js_1.rpad)('Thesis', 7),
137
+ (0, utils_js_1.rpad)('Edge', 6),
138
+ (0, utils_js_1.rpad)('Exec', 6),
139
+ (0, utils_js_1.rpad)('Sprd', 5),
140
+ (0, utils_js_1.pad)('Liq', 5),
141
+ (0, utils_js_1.pad)('Thesis', 10),
142
+ (0, utils_js_1.pad)('Position', 20),
143
+ ].join(' ');
144
+ console.log(`${utils_js_1.c.dim}${hdr}${utils_js_1.c.reset}`);
145
+ (0, utils_js_1.hr)(100);
146
+ for (const edge of display) {
147
+ const name = (0, utils_js_1.trunc)(edge.market, 31);
148
+ const mktStr = `${edge.marketPrice}¢`;
149
+ const thesisStr = `${edge.thesisPrice}¢`;
150
+ const edgeStr = edge.edge > 0 ? `+${edge.edge}` : `${edge.edge}`;
151
+ const execStr = edge.executableEdge !== null ? (edge.executableEdge > 0 ? `+${edge.executableEdge}` : `${edge.executableEdge}`) : '—';
152
+ const spreadStr = edge.spread !== null ? `${edge.spread}¢` : '—';
153
+ const liqStr = edge.liquidityScore || '—';
154
+ const thesisIdStr = (0, utils_js_1.shortId)(edge.thesisId);
155
+ // Color the edge values
156
+ const edgeColor = edge.edge > 0 ? utils_js_1.c.green : edge.edge < 0 ? utils_js_1.c.red : utils_js_1.c.dim;
157
+ const execColor = edge.executableEdge !== null ? (edge.executableEdge > 0 ? utils_js_1.c.green : utils_js_1.c.red) : utils_js_1.c.dim;
158
+ const liqColor = liqStr === 'high' ? utils_js_1.c.green : liqStr === 'medium' ? utils_js_1.c.yellow : utils_js_1.c.dim;
159
+ // Position string
160
+ let posStr = `${utils_js_1.c.dim}—${utils_js_1.c.reset}`;
161
+ if (edge.position) {
162
+ const p = edge.position;
163
+ const pnlStr = p.pnl >= 0 ? `${utils_js_1.c.green}+$${(p.pnl / 100).toFixed(0)}${utils_js_1.c.reset}` : `${utils_js_1.c.red}-$${(Math.abs(p.pnl) / 100).toFixed(0)}${utils_js_1.c.reset}`;
164
+ posStr = `${utils_js_1.c.green}${p.quantity}@${p.avgPrice}¢${utils_js_1.c.reset} ${pnlStr}`;
165
+ }
166
+ const row = [
167
+ edge.position ? `${utils_js_1.c.green}${(0, utils_js_1.pad)(name, 32)}${utils_js_1.c.reset}` : (0, utils_js_1.pad)(name, 32),
168
+ (0, utils_js_1.rpad)(mktStr, 5),
169
+ (0, utils_js_1.rpad)(thesisStr, 7),
170
+ `${edgeColor}${(0, utils_js_1.rpad)(edgeStr, 6)}${utils_js_1.c.reset}`,
171
+ `${execColor}${(0, utils_js_1.rpad)(execStr, 6)}${utils_js_1.c.reset}`,
172
+ (0, utils_js_1.rpad)(spreadStr, 5),
173
+ `${liqColor}${(0, utils_js_1.pad)(liqStr, 5)}${utils_js_1.c.reset}`,
174
+ `${utils_js_1.c.dim}${(0, utils_js_1.pad)(thesisIdStr, 10)}${utils_js_1.c.reset}`,
175
+ posStr,
176
+ ].join(' ');
177
+ console.log(row);
178
+ }
179
+ // ── Summary ────────────────────────────────────────────────────────────────
180
+ (0, utils_js_1.hr)(100);
181
+ // Positioned summary
182
+ const positioned = display.filter(e => e.position);
183
+ if (positioned.length > 0) {
184
+ let totalCost = 0;
185
+ let totalPnl = 0;
186
+ for (const e of positioned) {
187
+ totalCost += e.position.totalCost;
188
+ totalPnl += e.position.pnl;
189
+ }
190
+ const costStr = `$${(totalCost / 100).toFixed(0)}`;
191
+ const pnlColor = totalPnl >= 0 ? utils_js_1.c.green : utils_js_1.c.red;
192
+ const pnlSign = totalPnl >= 0 ? '+' : '-';
193
+ const pnlStr = `${pnlColor}${pnlSign}$${(Math.abs(totalPnl) / 100).toFixed(0)}${utils_js_1.c.reset}`;
194
+ console.log(`${utils_js_1.c.bold}Total positioned:${utils_js_1.c.reset} ${costStr} cost | P&L: ${pnlStr}`);
195
+ }
196
+ // Top unpositioned
197
+ const unpositioned = display.filter(e => !e.position && e.edge > 0);
198
+ if (unpositioned.length > 0) {
199
+ const top = unpositioned[0];
200
+ const execLabel = top.executableEdge !== null ? `exec +${top.executableEdge}` : `edge +${top.edge}`;
201
+ const liq = top.liquidityScore ? `, ${top.liquidityScore} liq` : '';
202
+ console.log(`${utils_js_1.c.bold}Top unpositioned:${utils_js_1.c.reset} ${(0, utils_js_1.trunc)(top.market, 30)} @ ${top.marketPrice}¢ (${execLabel}${liq})`);
203
+ }
204
+ console.log();
205
+ }
@@ -0,0 +1,20 @@
1
+ /**
2
+ * sf setup — Interactive configuration wizard
3
+ *
4
+ * Walks user through:
5
+ * 1. SF API key (required)
6
+ * 2. OpenRouter API key (optional, for agent)
7
+ * 3. Kalshi exchange credentials (optional, for positions)
8
+ * 4. Tavily API key (optional, for web search)
9
+ * 5. First thesis creation (if none exist)
10
+ *
11
+ * Each key is validated in real-time.
12
+ * Config is saved to ~/.sf/config.json.
13
+ */
14
+ interface SetupOpts {
15
+ check?: boolean;
16
+ reset?: boolean;
17
+ key?: string;
18
+ }
19
+ export declare function setupCommand(opts: SetupOpts): Promise<void>;
20
+ export {};
@@ -0,0 +1,569 @@
1
+ "use strict";
2
+ /**
3
+ * sf setup — Interactive configuration wizard
4
+ *
5
+ * Walks user through:
6
+ * 1. SF API key (required)
7
+ * 2. OpenRouter API key (optional, for agent)
8
+ * 3. Kalshi exchange credentials (optional, for positions)
9
+ * 4. Tavily API key (optional, for web search)
10
+ * 5. First thesis creation (if none exist)
11
+ *
12
+ * Each key is validated in real-time.
13
+ * Config is saved to ~/.sf/config.json.
14
+ */
15
+ var __importDefault = (this && this.__importDefault) || function (mod) {
16
+ return (mod && mod.__esModule) ? mod : { "default": mod };
17
+ };
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.setupCommand = setupCommand;
20
+ const readline_1 = __importDefault(require("readline"));
21
+ const child_process_1 = require("child_process");
22
+ const config_js_1 = require("../config.js");
23
+ const client_js_1 = require("../client.js");
24
+ const kalshi_js_1 = require("../kalshi.js");
25
+ const agent_js_1 = require("./agent.js");
26
+ // ─── ANSI helpers ────────────────────────────────────────────────────────────
27
+ const green = (s) => `\x1b[32m${s}\x1b[39m`;
28
+ const red = (s) => `\x1b[31m${s}\x1b[39m`;
29
+ const dim = (s) => `\x1b[2m${s}\x1b[22m`;
30
+ const bold = (s) => `\x1b[1m${s}\x1b[22m`;
31
+ const cyan = (s) => `\x1b[36m${s}\x1b[39m`;
32
+ function ok(msg) { console.log(` ${green('✓')} ${msg}`); }
33
+ function fail(msg) { console.log(` ${red('✗')} ${msg}`); }
34
+ function info(msg) { console.log(` ${msg}`); }
35
+ function blank() { console.log(); }
36
+ // ─── Prompt helper ───────────────────────────────────────────────────────────
37
+ function prompt(question) {
38
+ const rl = readline_1.default.createInterface({
39
+ input: process.stdin,
40
+ output: process.stdout,
41
+ terminal: true,
42
+ });
43
+ return new Promise(resolve => {
44
+ rl.question(question, answer => {
45
+ rl.close();
46
+ resolve(answer.trim());
47
+ });
48
+ });
49
+ }
50
+ function promptYN(question, defaultYes = true) {
51
+ return prompt(question).then(ans => {
52
+ if (!ans)
53
+ return defaultYes;
54
+ return ans.toLowerCase().startsWith('y');
55
+ });
56
+ }
57
+ function openBrowser(url) {
58
+ const cmd = process.platform === 'darwin' ? 'open' :
59
+ process.platform === 'win32' ? 'start' : 'xdg-open';
60
+ (0, child_process_1.exec)(`${cmd} ${url}`);
61
+ }
62
+ function mask(s) {
63
+ if (!s || s.length <= 12)
64
+ return s;
65
+ return s.slice(0, 8) + '...' + s.slice(-4);
66
+ }
67
+ // ─── Validators ──────────────────────────────────────────────────────────────
68
+ async function validateSFKey(key, apiUrl) {
69
+ try {
70
+ const res = await fetch(`${apiUrl}/api/thesis`, {
71
+ headers: { 'Authorization': `Bearer ${key}` },
72
+ });
73
+ if (res.ok)
74
+ return { valid: true, msg: `API Key 有效 — 连接到 ${apiUrl.replace('https://', '')}` };
75
+ if (res.status === 401)
76
+ return { valid: false, msg: '无效 key,请重试' };
77
+ return { valid: false, msg: `服务器返回 ${res.status}` };
78
+ }
79
+ catch (err) {
80
+ return { valid: false, msg: `连接失败: ${err.message}` };
81
+ }
82
+ }
83
+ async function validateOpenRouterKey(key) {
84
+ try {
85
+ const res = await fetch('https://openrouter.ai/api/v1/models', {
86
+ headers: { 'Authorization': `Bearer ${key}` },
87
+ });
88
+ if (res.ok)
89
+ return { valid: true, msg: 'OpenRouter 连接正常 — 可用模型: claude-sonnet-4.6' };
90
+ return { valid: false, msg: `OpenRouter 返回 ${res.status}` };
91
+ }
92
+ catch (err) {
93
+ return { valid: false, msg: `连接失败: ${err.message}` };
94
+ }
95
+ }
96
+ async function validateKalshi() {
97
+ try {
98
+ const positions = await (0, kalshi_js_1.getPositions)();
99
+ if (positions === null)
100
+ return { valid: false, msg: 'Kalshi 认证失败', posCount: 0 };
101
+ return { valid: true, msg: `Kalshi 认证成功 — 发现 ${positions.length} 个持仓`, posCount: positions.length };
102
+ }
103
+ catch (err) {
104
+ return { valid: false, msg: `Kalshi 连接失败: ${err.message}`, posCount: 0 };
105
+ }
106
+ }
107
+ async function validateTavily(key) {
108
+ try {
109
+ const res = await fetch('https://api.tavily.com/search', {
110
+ method: 'POST',
111
+ headers: { 'Content-Type': 'application/json' },
112
+ body: JSON.stringify({ api_key: key, query: 'test', max_results: 1 }),
113
+ });
114
+ if (res.ok)
115
+ return { valid: true, msg: 'Tavily 连接正常' };
116
+ return { valid: false, msg: `Tavily 返回 ${res.status}` };
117
+ }
118
+ catch (err) {
119
+ return { valid: false, msg: `连接失败: ${err.message}` };
120
+ }
121
+ }
122
+ async function setupCommand(opts) {
123
+ // ── sf setup --check ──────────────────────────────────────────────────────
124
+ if (opts.check) {
125
+ return showCheck();
126
+ }
127
+ // ── sf setup --reset ──────────────────────────────────────────────────────
128
+ if (opts.reset) {
129
+ (0, config_js_1.resetConfig)();
130
+ ok('配置已重置');
131
+ blank();
132
+ info('运行 sf setup 重新配置');
133
+ blank();
134
+ return;
135
+ }
136
+ // ── sf setup --key <key> (non-interactive) ────────────────────────────────
137
+ if (opts.key) {
138
+ const apiUrl = process.env.SF_API_URL || 'https://simplefunctions.dev';
139
+ const result = await validateSFKey(opts.key, apiUrl);
140
+ if (!result.valid) {
141
+ fail(result.msg);
142
+ process.exit(1);
143
+ }
144
+ const existing = (0, config_js_1.loadFileConfig)();
145
+ (0, config_js_1.saveConfig)({ ...existing, apiKey: opts.key, apiUrl });
146
+ ok(result.msg);
147
+ ok(`保存到 ${(0, config_js_1.getConfigPath)()}`);
148
+ return;
149
+ }
150
+ // ── Full interactive wizard ───────────────────────────────────────────────
151
+ return runWizard();
152
+ }
153
+ // ─── Check command ───────────────────────────────────────────────────────────
154
+ async function showCheck() {
155
+ const config = (0, config_js_1.loadConfig)();
156
+ blank();
157
+ console.log(` ${bold('SimpleFunctions 配置状态')}`);
158
+ console.log(` ${dim('─'.repeat(35))}`);
159
+ blank();
160
+ // SF API Key
161
+ if (config.apiKey) {
162
+ ok(`SF_API_KEY ${dim(mask(config.apiKey))}`);
163
+ }
164
+ else {
165
+ fail('SF_API_KEY 未配置(必须)');
166
+ }
167
+ // OpenRouter
168
+ if (config.openrouterKey) {
169
+ ok(`OPENROUTER_KEY ${dim(mask(config.openrouterKey))}`);
170
+ }
171
+ else {
172
+ fail(`OPENROUTER_KEY 未配置(agent 不可用)`);
173
+ }
174
+ // Kalshi
175
+ if (config.kalshiKeyId && config.kalshiPrivateKeyPath) {
176
+ ok(`KALSHI ${dim(mask(config.kalshiKeyId))}`);
177
+ }
178
+ else {
179
+ info(`${dim('○')} KALSHI ${dim('跳过')}`);
180
+ }
181
+ // Tavily
182
+ if (config.tavilyKey) {
183
+ ok(`TAVILY ${dim(mask(config.tavilyKey))}`);
184
+ }
185
+ else {
186
+ info(`${dim('○')} TAVILY ${dim('跳过')}`);
187
+ }
188
+ blank();
189
+ console.log(` ${dim('配置文件: ' + (0, config_js_1.getConfigPath)())}`);
190
+ blank();
191
+ }
192
+ // ─── Interactive Wizard ──────────────────────────────────────────────────────
193
+ async function runWizard() {
194
+ blank();
195
+ console.log(` ${bold('SimpleFunctions Setup')}`);
196
+ console.log(` ${dim('─'.repeat(25))}`);
197
+ blank();
198
+ const config = (0, config_js_1.loadFileConfig)();
199
+ const apiUrl = config.apiUrl || 'https://simplefunctions.dev';
200
+ // ════════════════════════════════════════════════════════════════════════════
201
+ // Step 1: SF API Key
202
+ // ════════════════════════════════════════════════════════════════════════════
203
+ console.log(` ${bold('第 1 步:API Key')}`);
204
+ blank();
205
+ const existingSfKey = process.env.SF_API_KEY || config.apiKey;
206
+ if (existingSfKey) {
207
+ const result = await validateSFKey(existingSfKey, apiUrl);
208
+ if (result.valid) {
209
+ ok(`已检测到 SF_API_KEY — ${dim(mask(existingSfKey))}`);
210
+ info(dim('跳过。'));
211
+ config.apiKey = existingSfKey;
212
+ blank();
213
+ }
214
+ else {
215
+ fail(`已有 key 无效: ${result.msg}`);
216
+ config.apiKey = await promptForSFKey(apiUrl);
217
+ }
218
+ }
219
+ else {
220
+ config.apiKey = await promptForSFKey(apiUrl);
221
+ }
222
+ // Save after each step (so partial progress is preserved)
223
+ config.apiUrl = apiUrl;
224
+ (0, config_js_1.saveConfig)(config);
225
+ // Also apply so subsequent validation calls can use it
226
+ process.env.SF_API_KEY = config.apiKey;
227
+ // ════════════════════════════════════════════════════════════════════════════
228
+ // Step 2: OpenRouter API Key
229
+ // ════════════════════════════════════════════════════════════════════════════
230
+ console.log(` ${bold('第 2 步:AI 模型(用于 sf agent)')}`);
231
+ blank();
232
+ const existingOrKey = process.env.OPENROUTER_API_KEY || config.openrouterKey;
233
+ if (existingOrKey) {
234
+ const result = await validateOpenRouterKey(existingOrKey);
235
+ if (result.valid) {
236
+ ok(`已检测到 OPENROUTER_API_KEY — ${dim(mask(existingOrKey))}`);
237
+ info(dim('跳过。'));
238
+ config.openrouterKey = existingOrKey;
239
+ blank();
240
+ }
241
+ else {
242
+ fail(`已有 key 无效: ${result.msg}`);
243
+ config.openrouterKey = await promptForOpenRouterKey();
244
+ }
245
+ }
246
+ else {
247
+ config.openrouterKey = await promptForOpenRouterKey();
248
+ }
249
+ (0, config_js_1.saveConfig)(config);
250
+ if (config.openrouterKey)
251
+ process.env.OPENROUTER_API_KEY = config.openrouterKey;
252
+ // ════════════════════════════════════════════════════════════════════════════
253
+ // Step 3: Kalshi Exchange
254
+ // ════════════════════════════════════════════════════════════════════════════
255
+ console.log(` ${bold('第 3 步:Kalshi 交易所(可选)')}`);
256
+ blank();
257
+ const existingKalshiId = process.env.KALSHI_API_KEY_ID || config.kalshiKeyId;
258
+ const existingKalshiPath = process.env.KALSHI_PRIVATE_KEY_PATH || config.kalshiPrivateKeyPath;
259
+ if (existingKalshiId && existingKalshiPath) {
260
+ // Temporarily apply for validation
261
+ process.env.KALSHI_API_KEY_ID = existingKalshiId;
262
+ process.env.KALSHI_PRIVATE_KEY_PATH = existingKalshiPath;
263
+ const result = await validateKalshi();
264
+ if (result.valid) {
265
+ ok(`已检测到 Kalshi — ${dim(mask(existingKalshiId))} (${result.posCount} 个持仓)`);
266
+ info(dim('跳过。'));
267
+ config.kalshiKeyId = existingKalshiId;
268
+ config.kalshiPrivateKeyPath = existingKalshiPath;
269
+ blank();
270
+ }
271
+ else {
272
+ fail(`已有凭证无效: ${result.msg}`);
273
+ await promptForKalshi(config);
274
+ }
275
+ }
276
+ else {
277
+ await promptForKalshi(config);
278
+ }
279
+ (0, config_js_1.saveConfig)(config);
280
+ // ════════════════════════════════════════════════════════════════════════════
281
+ // Step 4: Tavily
282
+ // ════════════════════════════════════════════════════════════════════════════
283
+ console.log(` ${bold('第 4 步:新闻搜索(可选)')}`);
284
+ blank();
285
+ const existingTavily = process.env.TAVILY_API_KEY || config.tavilyKey;
286
+ if (existingTavily) {
287
+ const result = await validateTavily(existingTavily);
288
+ if (result.valid) {
289
+ ok(`已检测到 TAVILY_API_KEY — ${dim(mask(existingTavily))}`);
290
+ info(dim('跳过。'));
291
+ config.tavilyKey = existingTavily;
292
+ blank();
293
+ }
294
+ else {
295
+ fail(`已有 key 无效: ${result.msg}`);
296
+ config.tavilyKey = await promptForTavily();
297
+ }
298
+ }
299
+ else {
300
+ config.tavilyKey = await promptForTavily();
301
+ }
302
+ (0, config_js_1.saveConfig)(config);
303
+ if (config.tavilyKey)
304
+ process.env.TAVILY_API_KEY = config.tavilyKey;
305
+ // ════════════════════════════════════════════════════════════════════════════
306
+ // Summary
307
+ // ════════════════════════════════════════════════════════════════════════════
308
+ console.log(` ${dim('─'.repeat(25))}`);
309
+ info(`配置保存到 ${dim((0, config_js_1.getConfigPath)())}`);
310
+ blank();
311
+ if (config.apiKey)
312
+ ok('SF_API_KEY 已配置');
313
+ else
314
+ fail('SF_API_KEY 未配置');
315
+ if (config.openrouterKey)
316
+ ok('OPENROUTER_KEY 已配置');
317
+ else
318
+ fail('OPENROUTER_KEY 跳过');
319
+ if (config.kalshiKeyId)
320
+ ok('KALSHI 已配置');
321
+ else
322
+ info(`${dim('○')} KALSHI 跳过`);
323
+ if (config.tavilyKey)
324
+ ok('TAVILY 已配置');
325
+ else
326
+ info(`${dim('○')} TAVILY 跳过`);
327
+ blank();
328
+ // ════════════════════════════════════════════════════════════════════════════
329
+ // Step 5: Thesis creation
330
+ // ════════════════════════════════════════════════════════════════════════════
331
+ if (config.apiKey) {
332
+ await handleThesisStep(config);
333
+ }
334
+ }
335
+ // ─── Step prompt helpers ─────────────────────────────────────────────────────
336
+ async function promptForSFKey(apiUrl) {
337
+ info(`还没有 key?去 ${cyan('https://simplefunctions.dev/dashboard')} 注册获取。`);
338
+ info('按 Enter 打开浏览器,或直接粘贴你的 key:');
339
+ blank();
340
+ while (true) {
341
+ const answer = await prompt(' > ');
342
+ if (!answer) {
343
+ // Open browser
344
+ openBrowser('https://simplefunctions.dev/dashboard');
345
+ info(dim('浏览器已打开。获取 key 后粘贴到这里:'));
346
+ continue;
347
+ }
348
+ info(dim('验证中...'));
349
+ const result = await validateSFKey(answer, apiUrl);
350
+ if (result.valid) {
351
+ ok(result.msg);
352
+ blank();
353
+ return answer;
354
+ }
355
+ else {
356
+ fail(result.msg);
357
+ }
358
+ }
359
+ }
360
+ async function promptForOpenRouterKey() {
361
+ info(`需要 OpenRouter API key。去 ${cyan('https://openrouter.ai/settings/keys')} 获取。`);
362
+ info('按 Enter 跳过(agent 功能不可用),或粘贴 key:');
363
+ blank();
364
+ const answer = await prompt(' > ');
365
+ if (!answer) {
366
+ info(dim('跳过。'));
367
+ blank();
368
+ return undefined;
369
+ }
370
+ info(dim('验证中...'));
371
+ const result = await validateOpenRouterKey(answer);
372
+ if (result.valid) {
373
+ ok(result.msg);
374
+ }
375
+ else {
376
+ fail(result.msg);
377
+ info(dim('已保存,之后可以重新运行 sf setup 修正。'));
378
+ }
379
+ blank();
380
+ return answer;
381
+ }
382
+ async function promptForKalshi(config) {
383
+ info(`连接 Kalshi 查看你的持仓和盈亏。`);
384
+ info(`需要 API Key ID 和私钥文件。`);
385
+ info(`${cyan('https://kalshi.com/account/api-keys')} 获取。`);
386
+ info('按 Enter 跳过,或粘贴 Key ID:');
387
+ blank();
388
+ const keyId = await prompt(' > ');
389
+ if (!keyId) {
390
+ info(dim('跳过。'));
391
+ blank();
392
+ return;
393
+ }
394
+ info('私钥文件路径(默认 ~/.kalshi/private.pem):');
395
+ const keyPathInput = await prompt(' > ');
396
+ const keyPath = keyPathInput || '~/.kalshi/private.pem';
397
+ config.kalshiKeyId = keyId;
398
+ config.kalshiPrivateKeyPath = keyPath;
399
+ // Temporarily set for validation
400
+ process.env.KALSHI_API_KEY_ID = keyId;
401
+ process.env.KALSHI_PRIVATE_KEY_PATH = keyPath;
402
+ info(dim('验证中...'));
403
+ const result = await validateKalshi();
404
+ if (result.valid) {
405
+ ok(result.msg);
406
+ }
407
+ else {
408
+ fail(result.msg);
409
+ info(dim('已保存,之后可以重新运行 sf setup 修正。'));
410
+ }
411
+ blank();
412
+ }
413
+ async function promptForTavily() {
414
+ info(`Tavily API 用于 agent 的 web_search 功能。`);
415
+ info(`${cyan('https://tavily.com')} 获取免费 key。`);
416
+ info('按 Enter 跳过:');
417
+ blank();
418
+ const answer = await prompt(' > ');
419
+ if (!answer) {
420
+ info(dim('跳过。'));
421
+ blank();
422
+ return undefined;
423
+ }
424
+ info(dim('验证中...'));
425
+ const result = await validateTavily(answer);
426
+ if (result.valid) {
427
+ ok(result.msg);
428
+ }
429
+ else {
430
+ fail(result.msg);
431
+ info(dim('已保存,之后可以重新运行 sf setup 修正。'));
432
+ }
433
+ blank();
434
+ return answer;
435
+ }
436
+ // ─── Step 5: Thesis ──────────────────────────────────────────────────────────
437
+ async function handleThesisStep(config) {
438
+ try {
439
+ const client = new client_js_1.SFClient(config.apiKey, config.apiUrl);
440
+ const data = await client.listTheses();
441
+ const theses = data.theses || [];
442
+ const activeTheses = theses.filter((t) => t.status === 'active');
443
+ if (activeTheses.length > 0) {
444
+ console.log(` ${bold('第 5 步:论文')}`);
445
+ blank();
446
+ ok(`已有 ${activeTheses.length} 个活跃论文:`);
447
+ for (const t of activeTheses.slice(0, 5)) {
448
+ const conf = typeof t.confidence === 'number' ? Math.round(t.confidence * 100) : 0;
449
+ const thesis = (t.rawThesis || t.thesis || t.title || '').slice(0, 60);
450
+ info(` ${dim(t.id.slice(0, 8))} — ${thesis} — ${conf}%`);
451
+ }
452
+ info(dim('跳过创建。'));
453
+ blank();
454
+ // Offer to launch agent
455
+ if (config.openrouterKey) {
456
+ console.log(` ${dim('─'.repeat(25))}`);
457
+ console.log(` ${bold('全部就绪!')}`);
458
+ blank();
459
+ info(` ${cyan('sf agent')} 和你的论文对话`);
460
+ info(` ${cyan('sf context <id>')} 查看论文快照`);
461
+ info(` ${cyan('sf positions')} 查看持仓`);
462
+ info(` ${cyan('sf setup --check')} 检查配置`);
463
+ blank();
464
+ const shouldLaunch = await promptYN(` 要不要现在启动 agent?(Y/n) `);
465
+ if (shouldLaunch) {
466
+ blank();
467
+ info('启动中...');
468
+ blank();
469
+ await (0, agent_js_1.agentCommand)(activeTheses[0].id, { model: config.model });
470
+ }
471
+ }
472
+ else {
473
+ blank();
474
+ console.log(` ${bold('全部就绪!')}`);
475
+ blank();
476
+ info(` ${cyan('sf list')} 查看所有论文`);
477
+ info(` ${cyan('sf context <id>')} 查看论文快照`);
478
+ info(` ${cyan('sf positions')} 查看持仓`);
479
+ info(` ${cyan('sf setup --check')} 检查配置`);
480
+ blank();
481
+ }
482
+ return;
483
+ }
484
+ // No theses — offer to create one
485
+ console.log(` ${bold('第 5 步:创建你的第一个论文')}`);
486
+ blank();
487
+ info('论文是你对市场的一个核心判断。系统会基于它构建因果模型,');
488
+ info('然后持续扫描预测市场寻找被错误定价的合约。');
489
+ blank();
490
+ info('比如:');
491
+ info(` ${dim('"美联储2026年不会降息,通胀因油价持续高企"')}`);
492
+ info(` ${dim('"AI裁员潮导致消费萎缩,标普年底跌20%"')}`);
493
+ info(` ${dim('"Trump无法退出伊朗战争,油价维持$100以上六个月"')}`);
494
+ blank();
495
+ const thesis = await prompt(' 输入你的论文(按 Enter 跳过,之后用 sf create):\n > ');
496
+ if (!thesis) {
497
+ blank();
498
+ info(dim('跳过。之后用 sf create "你的论文" 创建。'));
499
+ blank();
500
+ showFinalHints(config);
501
+ return;
502
+ }
503
+ blank();
504
+ info('构建因果模型中...(约30秒)');
505
+ blank();
506
+ try {
507
+ const result = await client.createThesis(thesis, true);
508
+ if (result.id) {
509
+ const nodeCount = result.causalTree?.nodes?.length || 0;
510
+ const edgeCount = result.edgeAnalysis?.edges?.length || 0;
511
+ const totalMarkets = result.edgeAnalysis?.totalMarketsAnalyzed || 0;
512
+ const confidence = Math.round((parseFloat(result.confidence) || 0.5) * 100);
513
+ ok(`因果树:${nodeCount} 个节点`);
514
+ ok(`扫描 ${totalMarkets} 个市场,找到 ${edgeCount} 个有边际的合约`);
515
+ ok(`置信度:${confidence}%`);
516
+ ok(`论文 ID:${result.id.slice(0, 8)}`);
517
+ blank();
518
+ // Offer to launch agent
519
+ if (config.openrouterKey) {
520
+ console.log(` ${dim('─'.repeat(25))}`);
521
+ console.log(` ${bold('全部就绪!')}`);
522
+ blank();
523
+ const shouldLaunch = await promptYN(` 要不要现在启动 agent?(Y/n) `);
524
+ if (shouldLaunch) {
525
+ blank();
526
+ info('启动中...');
527
+ blank();
528
+ await (0, agent_js_1.agentCommand)(result.id, { model: config.model });
529
+ }
530
+ else {
531
+ blank();
532
+ showFinalHints(config);
533
+ }
534
+ }
535
+ else {
536
+ showFinalHints(config);
537
+ }
538
+ }
539
+ else {
540
+ fail(`创建失败:${result.error || '未知错误'}`);
541
+ info(dim('之后可以用 sf create "你的论文" 重试'));
542
+ blank();
543
+ showFinalHints(config);
544
+ }
545
+ }
546
+ catch (err) {
547
+ fail(`创建失败:${err.message}`);
548
+ info(dim('之后可以用 sf create "你的论文" 重试'));
549
+ blank();
550
+ showFinalHints(config);
551
+ }
552
+ }
553
+ catch {
554
+ // Can't connect to API, skip thesis step
555
+ blank();
556
+ showFinalHints(config);
557
+ }
558
+ }
559
+ function showFinalHints(config) {
560
+ console.log(` ${dim('─'.repeat(25))}`);
561
+ console.log(` ${bold('全部就绪!')}`);
562
+ blank();
563
+ info(` ${cyan('sf agent')} 和你的论文对话`);
564
+ info(` ${cyan('sf list')} 查看所有论文`);
565
+ info(` ${cyan('sf context <id>')} 查看论文快照`);
566
+ info(` ${cyan('sf positions')} 查看持仓`);
567
+ info(` ${cyan('sf setup --check')} 检查配置`);
568
+ blank();
569
+ }
@@ -0,0 +1,50 @@
1
+ /**
2
+ * CLI Configuration — ~/.sf/config.json
3
+ *
4
+ * Priority: env vars > config file > defaults
5
+ *
6
+ * After `sf setup`, all keys are stored in config.json.
7
+ * `applyConfig()` sets process.env from config so all existing code
8
+ * (client.ts, kalshi.ts, agent.ts) keeps working without changes.
9
+ */
10
+ export interface SFConfig {
11
+ apiKey?: string;
12
+ apiUrl?: string;
13
+ openrouterKey?: string;
14
+ kalshiKeyId?: string;
15
+ kalshiPrivateKeyPath?: string;
16
+ tavilyKey?: string;
17
+ model?: string;
18
+ configuredAt?: string;
19
+ }
20
+ /**
21
+ * Load config from file. Does NOT apply env overrides — use resolveConfig() for that.
22
+ */
23
+ export declare function loadFileConfig(): SFConfig;
24
+ /**
25
+ * Resolve final config: env vars > config file > defaults.
26
+ */
27
+ export declare function loadConfig(): SFConfig;
28
+ /**
29
+ * Save config to ~/.sf/config.json.
30
+ */
31
+ export declare function saveConfig(config: SFConfig): void;
32
+ /**
33
+ * Delete config file (for --reset).
34
+ */
35
+ export declare function resetConfig(): void;
36
+ /**
37
+ * Apply config to process.env.
38
+ *
39
+ * Call this ONCE at CLI startup, before any command runs.
40
+ * This means client.ts, kalshi.ts, agent.ts etc. keep reading process.env
41
+ * and just work — no code changes needed in those files.
42
+ *
43
+ * Env vars already set by the user take priority (we only fill gaps).
44
+ */
45
+ export declare function applyConfig(): void;
46
+ /**
47
+ * Check if SF API key is configured (from any source).
48
+ */
49
+ export declare function isConfigured(): boolean;
50
+ export declare function getConfigPath(): string;
package/dist/config.js ADDED
@@ -0,0 +1,117 @@
1
+ "use strict";
2
+ /**
3
+ * CLI Configuration — ~/.sf/config.json
4
+ *
5
+ * Priority: env vars > config file > defaults
6
+ *
7
+ * After `sf setup`, all keys are stored in config.json.
8
+ * `applyConfig()` sets process.env from config so all existing code
9
+ * (client.ts, kalshi.ts, agent.ts) keeps working without changes.
10
+ */
11
+ var __importDefault = (this && this.__importDefault) || function (mod) {
12
+ return (mod && mod.__esModule) ? mod : { "default": mod };
13
+ };
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.loadFileConfig = loadFileConfig;
16
+ exports.loadConfig = loadConfig;
17
+ exports.saveConfig = saveConfig;
18
+ exports.resetConfig = resetConfig;
19
+ exports.applyConfig = applyConfig;
20
+ exports.isConfigured = isConfigured;
21
+ exports.getConfigPath = getConfigPath;
22
+ const fs_1 = __importDefault(require("fs"));
23
+ const path_1 = __importDefault(require("path"));
24
+ const os_1 = __importDefault(require("os"));
25
+ const CONFIG_DIR = path_1.default.join(os_1.default.homedir(), '.sf');
26
+ const CONFIG_PATH = path_1.default.join(CONFIG_DIR, 'config.json');
27
+ const DEFAULT_API_URL = 'https://simplefunctions.dev';
28
+ const DEFAULT_MODEL = 'anthropic/claude-sonnet-4.6';
29
+ /**
30
+ * Load config from file. Does NOT apply env overrides — use resolveConfig() for that.
31
+ */
32
+ function loadFileConfig() {
33
+ try {
34
+ if (fs_1.default.existsSync(CONFIG_PATH)) {
35
+ return JSON.parse(fs_1.default.readFileSync(CONFIG_PATH, 'utf-8'));
36
+ }
37
+ }
38
+ catch { /* corrupt file, ignore */ }
39
+ return {};
40
+ }
41
+ /**
42
+ * Resolve final config: env vars > config file > defaults.
43
+ */
44
+ function loadConfig() {
45
+ const file = loadFileConfig();
46
+ return {
47
+ apiKey: process.env.SF_API_KEY || file.apiKey,
48
+ apiUrl: process.env.SF_API_URL || file.apiUrl || DEFAULT_API_URL,
49
+ openrouterKey: process.env.OPENROUTER_API_KEY || file.openrouterKey,
50
+ kalshiKeyId: process.env.KALSHI_API_KEY_ID || file.kalshiKeyId,
51
+ kalshiPrivateKeyPath: process.env.KALSHI_PRIVATE_KEY_PATH || file.kalshiPrivateKeyPath,
52
+ tavilyKey: process.env.TAVILY_API_KEY || file.tavilyKey,
53
+ model: process.env.SF_MODEL || file.model || DEFAULT_MODEL,
54
+ };
55
+ }
56
+ /**
57
+ * Save config to ~/.sf/config.json.
58
+ */
59
+ function saveConfig(config) {
60
+ fs_1.default.mkdirSync(CONFIG_DIR, { recursive: true });
61
+ fs_1.default.writeFileSync(CONFIG_PATH, JSON.stringify({ ...config, configuredAt: new Date().toISOString() }, null, 2));
62
+ }
63
+ /**
64
+ * Delete config file (for --reset).
65
+ */
66
+ function resetConfig() {
67
+ try {
68
+ if (fs_1.default.existsSync(CONFIG_PATH)) {
69
+ fs_1.default.unlinkSync(CONFIG_PATH);
70
+ }
71
+ }
72
+ catch { /* ignore */ }
73
+ }
74
+ /**
75
+ * Apply config to process.env.
76
+ *
77
+ * Call this ONCE at CLI startup, before any command runs.
78
+ * This means client.ts, kalshi.ts, agent.ts etc. keep reading process.env
79
+ * and just work — no code changes needed in those files.
80
+ *
81
+ * Env vars already set by the user take priority (we only fill gaps).
82
+ */
83
+ function applyConfig() {
84
+ const file = loadFileConfig();
85
+ // Only set process.env if not already set (env vars > config file)
86
+ if (!process.env.SF_API_KEY && file.apiKey) {
87
+ process.env.SF_API_KEY = file.apiKey;
88
+ }
89
+ if (!process.env.SF_API_URL && file.apiUrl) {
90
+ process.env.SF_API_URL = file.apiUrl;
91
+ }
92
+ if (!process.env.OPENROUTER_API_KEY && file.openrouterKey) {
93
+ process.env.OPENROUTER_API_KEY = file.openrouterKey;
94
+ }
95
+ if (!process.env.KALSHI_API_KEY_ID && file.kalshiKeyId) {
96
+ process.env.KALSHI_API_KEY_ID = file.kalshiKeyId;
97
+ }
98
+ if (!process.env.KALSHI_PRIVATE_KEY_PATH && file.kalshiPrivateKeyPath) {
99
+ process.env.KALSHI_PRIVATE_KEY_PATH = file.kalshiPrivateKeyPath;
100
+ }
101
+ if (!process.env.TAVILY_API_KEY && file.tavilyKey) {
102
+ process.env.TAVILY_API_KEY = file.tavilyKey;
103
+ }
104
+ if (!process.env.SF_MODEL && file.model) {
105
+ process.env.SF_MODEL = file.model;
106
+ }
107
+ }
108
+ /**
109
+ * Check if SF API key is configured (from any source).
110
+ */
111
+ function isConfigured() {
112
+ const config = loadConfig();
113
+ return !!config.apiKey;
114
+ }
115
+ function getConfigPath() {
116
+ return CONFIG_PATH;
117
+ }
package/dist/index.d.ts CHANGED
@@ -6,12 +6,15 @@
6
6
  * Zero heavy dependencies: commander + native fetch.
7
7
  *
8
8
  * Usage:
9
- * sf list
10
- * sf get <id>
11
- * sf context <id> [--json]
9
+ * sf setup — Interactive configuration wizard
10
+ * sf list — List all theses
11
+ * sf get <id> Full thesis details
12
+ * sf context <id> [--json] — Thesis context snapshot
12
13
  * sf create "thesis text" [--async]
13
14
  * sf signal <id> "content" [--type news|user_note|external]
14
15
  * sf evaluate <id>
15
16
  * sf scan "keywords" [--series TICKER] [--market TICKER] [--json]
17
+ * sf positions — Kalshi positions with edge overlay
18
+ * sf agent [thesisId] — Interactive agent mode
16
19
  */
17
20
  export {};
package/dist/index.js CHANGED
@@ -7,16 +7,20 @@
7
7
  * Zero heavy dependencies: commander + native fetch.
8
8
  *
9
9
  * Usage:
10
- * sf list
11
- * sf get <id>
12
- * sf context <id> [--json]
10
+ * sf setup — Interactive configuration wizard
11
+ * sf list — List all theses
12
+ * sf get <id> Full thesis details
13
+ * sf context <id> [--json] — Thesis context snapshot
13
14
  * sf create "thesis text" [--async]
14
15
  * sf signal <id> "content" [--type news|user_note|external]
15
16
  * sf evaluate <id>
16
17
  * sf scan "keywords" [--series TICKER] [--market TICKER] [--json]
18
+ * sf positions — Kalshi positions with edge overlay
19
+ * sf agent [thesisId] — Interactive agent mode
17
20
  */
18
21
  Object.defineProperty(exports, "__esModule", { value: true });
19
22
  const commander_1 = require("commander");
23
+ const config_js_1 = require("./config.js");
20
24
  const list_js_1 = require("./commands/list.js");
21
25
  const get_js_1 = require("./commands/get.js");
22
26
  const context_js_1 = require("./commands/context.js");
@@ -25,8 +29,13 @@ const signal_js_1 = require("./commands/signal.js");
25
29
  const evaluate_js_1 = require("./commands/evaluate.js");
26
30
  const scan_js_1 = require("./commands/scan.js");
27
31
  const positions_js_1 = require("./commands/positions.js");
32
+ const edges_js_1 = require("./commands/edges.js");
28
33
  const agent_js_1 = require("./commands/agent.js");
34
+ const setup_js_1 = require("./commands/setup.js");
29
35
  const utils_js_1 = require("./utils.js");
36
+ // ── Apply ~/.sf/config.json to process.env BEFORE any command ────────────────
37
+ // This means client.ts, kalshi.ts, agent.ts keep reading process.env and just work.
38
+ (0, config_js_1.applyConfig)();
30
39
  const program = new commander_1.Command();
31
40
  program
32
41
  .name('sf')
@@ -34,14 +43,33 @@ program
34
43
  .version('0.1.0')
35
44
  .option('--api-key <key>', 'API key (or set SF_API_KEY env var)')
36
45
  .option('--api-url <url>', 'API base URL (or set SF_API_URL env var)');
37
- // Helper to extract global opts from any command's opts chain
38
- function globalOpts(opts) {
39
- // Commander merges parent opts into child opts when using .optsWithGlobals()
40
- return {
41
- apiKey: opts.apiKey || opts.parent?.apiKey,
42
- apiUrl: opts.apiUrl || opts.parent?.apiUrl,
43
- };
44
- }
46
+ // ── Pre-action guard: check configuration ────────────────────────────────────
47
+ const NO_CONFIG_COMMANDS = new Set(['setup', 'help', 'scan']);
48
+ program.hook('preAction', (thisCommand) => {
49
+ const cmdName = thisCommand.name();
50
+ if (NO_CONFIG_COMMANDS.has(cmdName))
51
+ return;
52
+ // --api-key flag overrides config check
53
+ const g = thisCommand.optsWithGlobals?.() || thisCommand.opts();
54
+ if (g.apiKey)
55
+ return;
56
+ if (!(0, config_js_1.isConfigured)()) {
57
+ console.log();
58
+ console.log(' SimpleFunctions 未配置。运行 \x1b[36msf setup\x1b[39m 开始。');
59
+ console.log();
60
+ process.exit(1);
61
+ }
62
+ });
63
+ // ── sf setup ──────────────────────────────────────────────────────────────────
64
+ program
65
+ .command('setup')
66
+ .description('Interactive configuration wizard')
67
+ .option('--check', 'Show current configuration status')
68
+ .option('--reset', 'Delete config and start over')
69
+ .option('--key <key>', 'Set SF API key (non-interactive, for CI)')
70
+ .action(async (opts) => {
71
+ await run(() => (0, setup_js_1.setupCommand)({ check: opts.check, reset: opts.reset, key: opts.key }));
72
+ });
45
73
  // ── sf list ──────────────────────────────────────────────────────────────────
46
74
  program
47
75
  .command('list')
@@ -116,6 +144,21 @@ program
116
144
  apiUrl: g.apiUrl,
117
145
  }));
118
146
  });
147
+ // ── sf edges ──────────────────────────────────────────────────────────────────
148
+ program
149
+ .command('edges')
150
+ .description('Top edges across all theses — what to trade now')
151
+ .option('--json', 'JSON output for agents')
152
+ .option('--limit <n>', 'Max edges to show', '20')
153
+ .action(async (opts, cmd) => {
154
+ const g = cmd.optsWithGlobals();
155
+ await run(() => (0, edges_js_1.edgesCommand)({
156
+ json: opts.json,
157
+ limit: opts.limit,
158
+ apiKey: g.apiKey,
159
+ apiUrl: g.apiUrl,
160
+ }));
161
+ });
119
162
  // ── sf positions ──────────────────────────────────────────────────────────────
120
163
  program
121
164
  .command('positions')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
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"