@spfunctions/cli 1.1.2 → 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.
@@ -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;