@spfunctions/cli 1.1.9 → 1.3.0

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.
@@ -1007,6 +1007,73 @@ async function agentCommand(thesisId, opts) {
1007
1007
  }
1008
1008
  },
1009
1009
  },
1010
+ {
1011
+ name: 'create_thesis',
1012
+ label: 'Create Thesis',
1013
+ description: 'Create a new thesis from a raw thesis statement. Returns the thesis ID, confidence, node count, and edge count.',
1014
+ parameters: Type.Object({
1015
+ rawThesis: Type.String({ description: 'The raw thesis statement to create' }),
1016
+ webhookUrl: Type.Optional(Type.String({ description: 'Optional webhook URL for notifications' })),
1017
+ }),
1018
+ execute: async (_toolCallId, params) => {
1019
+ const result = await sfClient.createThesis(params.rawThesis, true);
1020
+ const thesis = result.thesis || result;
1021
+ const nodeCount = thesis.causalTree?.nodes?.length || 0;
1022
+ const edgeCount = (thesis.edges || []).length;
1023
+ const confidence = typeof thesis.confidence === 'number' ? Math.round(thesis.confidence * 100) : 0;
1024
+ return {
1025
+ content: [{ type: 'text', text: `Thesis created.\nID: ${thesis.id}\nConfidence: ${confidence}%\nNodes: ${nodeCount}\nEdges: ${edgeCount}` }],
1026
+ details: {},
1027
+ };
1028
+ },
1029
+ },
1030
+ {
1031
+ name: 'get_edges',
1032
+ label: 'Get Edges',
1033
+ description: 'Get top edges across all active theses. Returns the top 10 edges sorted by absolute edge size with ticker, market name, edge size, direction, and venue.',
1034
+ parameters: emptyParams,
1035
+ execute: async () => {
1036
+ const { theses } = await sfClient.listTheses();
1037
+ const activeTheses = (theses || []).filter((t) => t.status === 'active' || t.status === 'monitoring');
1038
+ const allEdges = [];
1039
+ for (const t of activeTheses) {
1040
+ try {
1041
+ const ctx = await sfClient.getContext(t.id);
1042
+ for (const edge of (ctx.edges || [])) {
1043
+ allEdges.push({ ...edge, thesisId: t.id });
1044
+ }
1045
+ }
1046
+ catch { /* skip inaccessible theses */ }
1047
+ }
1048
+ allEdges.sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0));
1049
+ const top10 = allEdges.slice(0, 10).map((e) => ({
1050
+ ticker: e.marketId || e.ticker || '-',
1051
+ market: e.market || e.marketTitle || '-',
1052
+ edge: e.edge || e.edgeSize || 0,
1053
+ direction: e.direction || 'yes',
1054
+ venue: e.venue || 'kalshi',
1055
+ }));
1056
+ return {
1057
+ content: [{ type: 'text', text: JSON.stringify(top10, null, 2) }],
1058
+ details: {},
1059
+ };
1060
+ },
1061
+ },
1062
+ {
1063
+ name: 'get_feed',
1064
+ label: 'Get Feed',
1065
+ description: 'Get evaluation history / activity feed. Shows recent evaluations, signals, and changes across theses.',
1066
+ parameters: Type.Object({
1067
+ hours: Type.Optional(Type.Number({ description: 'Hours of history to fetch (default 24)' })),
1068
+ }),
1069
+ execute: async (_toolCallId, params) => {
1070
+ const result = await sfClient.getFeed(params.hours || 24);
1071
+ return {
1072
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
1073
+ details: {},
1074
+ };
1075
+ },
1076
+ },
1010
1077
  ];
1011
1078
  // ── What-if tool (always available) ────────────────────────────────────────
1012
1079
  tools.push({
@@ -1100,8 +1167,8 @@ async function agentCommand(thesisId, opts) {
1100
1167
  }),
1101
1168
  execute: async (_toolCallId, params) => {
1102
1169
  const { createOrder } = await import('../kalshi.js');
1103
- const priceDollars = params.price_cents ? (params.price_cents / 100).toFixed(2) : undefined;
1104
- const maxCost = ((params.price_cents || 99) * params.count / 100).toFixed(2);
1170
+ const priceCents = params.price_cents ? Math.round(Number(params.price_cents)) : undefined;
1171
+ const maxCost = ((priceCents || 99) * params.count / 100).toFixed(2);
1105
1172
  // Show preview
1106
1173
  const preview = [
1107
1174
  C.zinc200(bold('ORDER PREVIEW')),
@@ -1110,7 +1177,7 @@ async function agentCommand(thesisId, opts) {
1110
1177
  ` Action: ${params.action.toUpperCase()}`,
1111
1178
  ` Quantity: ${params.count}`,
1112
1179
  ` Type: ${params.type}`,
1113
- params.price_cents ? ` Price: ${params.price_cents}\u00A2` : '',
1180
+ priceCents ? ` Price: ${priceCents}\u00A2` : '',
1114
1181
  ` Max cost: $${maxCost}`,
1115
1182
  ].filter(Boolean).join('\n');
1116
1183
  addSystemText(preview);
@@ -1128,7 +1195,7 @@ async function agentCommand(thesisId, opts) {
1128
1195
  action: params.action,
1129
1196
  type: params.type,
1130
1197
  count: params.count,
1131
- ...(priceDollars ? { yes_price: priceDollars } : {}),
1198
+ ...(priceCents ? { yes_price: priceCents } : {}),
1132
1199
  });
1133
1200
  const order = result.order || result;
1134
1201
  return {
@@ -1217,6 +1284,10 @@ When the conversation produces a concrete trade idea (specific contract, directi
1217
1284
  - After creating, confirm the strategy details and mention that sf runtime --dangerous can execute it.
1218
1285
  - If the user says "change the stop loss on T150 to 30", use update_strategy.
1219
1286
 
1287
+ ## Trading status
1288
+
1289
+ ${config.tradingEnabled ? 'Trading is ENABLED. You have place_order and cancel_order tools.' : 'Trading is DISABLED. You cannot place or cancel orders. Tell the user to run `sf setup --enable-trading` to unlock trading.'}
1290
+
1220
1291
  ## Current thesis state
1221
1292
 
1222
1293
  Thesis: ${ctx.thesis || ctx.rawThesis || 'N/A'}
@@ -1762,7 +1833,7 @@ Output a structured summary. Be concise but preserve every important detail —
1762
1833
  const result = await createOrder({
1763
1834
  ticker, side: 'yes', action: 'buy', type: 'limit',
1764
1835
  count: parseInt(qtyStr),
1765
- yes_price: (parseInt(priceStr) / 100).toFixed(2),
1836
+ yes_price: parseInt(priceStr),
1766
1837
  });
1767
1838
  addSystemText(C.emerald('\u2713 Order placed: ' + ((result.order || result).order_id || 'OK')));
1768
1839
  }
@@ -1794,7 +1865,7 @@ Output a structured summary. Be concise but preserve every important detail —
1794
1865
  const result = await createOrder({
1795
1866
  ticker, side: 'yes', action: 'sell', type: 'limit',
1796
1867
  count: parseInt(qtyStr),
1797
- yes_price: (parseInt(priceStr) / 100).toFixed(2),
1868
+ yes_price: parseInt(priceStr),
1798
1869
  });
1799
1870
  addSystemText(C.emerald('\u2713 Order placed: ' + ((result.order || result).order_id || 'OK')));
1800
1871
  }
@@ -2299,7 +2370,305 @@ async function runPlainTextAgent(params) {
2299
2370
  }
2300
2371
  },
2301
2372
  },
2373
+ {
2374
+ name: 'create_thesis',
2375
+ label: 'Create Thesis',
2376
+ description: 'Create a new thesis from a raw thesis statement. Returns the thesis ID, confidence, node count, and edge count.',
2377
+ parameters: Type.Object({
2378
+ rawThesis: Type.String({ description: 'The raw thesis statement to create' }),
2379
+ webhookUrl: Type.Optional(Type.String({ description: 'Optional webhook URL for notifications' })),
2380
+ }),
2381
+ execute: async (_id, p) => {
2382
+ const result = await sfClient.createThesis(p.rawThesis, true);
2383
+ const thesis = result.thesis || result;
2384
+ const nodeCount = thesis.causalTree?.nodes?.length || 0;
2385
+ const edgeCount = (thesis.edges || []).length;
2386
+ const confidence = typeof thesis.confidence === 'number' ? Math.round(thesis.confidence * 100) : 0;
2387
+ return {
2388
+ content: [{ type: 'text', text: `Thesis created.\nID: ${thesis.id}\nConfidence: ${confidence}%\nNodes: ${nodeCount}\nEdges: ${edgeCount}` }],
2389
+ details: {},
2390
+ };
2391
+ },
2392
+ },
2393
+ {
2394
+ name: 'get_edges',
2395
+ label: 'Get Edges',
2396
+ description: 'Get top edges across all active theses. Returns the top 10 edges sorted by absolute edge size with ticker, market name, edge size, direction, and venue.',
2397
+ parameters: emptyParams,
2398
+ execute: async () => {
2399
+ const { theses } = await sfClient.listTheses();
2400
+ const activeTheses = (theses || []).filter((t) => t.status === 'active' || t.status === 'monitoring');
2401
+ const allEdges = [];
2402
+ for (const t of activeTheses) {
2403
+ try {
2404
+ const ctx = await sfClient.getContext(t.id);
2405
+ for (const edge of (ctx.edges || [])) {
2406
+ allEdges.push({ ...edge, thesisId: t.id });
2407
+ }
2408
+ }
2409
+ catch { /* skip inaccessible theses */ }
2410
+ }
2411
+ allEdges.sort((a, b) => Math.abs(b.edge || b.edgeSize || 0) - Math.abs(a.edge || a.edgeSize || 0));
2412
+ const top10 = allEdges.slice(0, 10).map((e) => ({
2413
+ ticker: e.marketId || e.ticker || '-',
2414
+ market: e.market || e.marketTitle || '-',
2415
+ edge: e.edge || e.edgeSize || 0,
2416
+ direction: e.direction || 'yes',
2417
+ venue: e.venue || 'kalshi',
2418
+ }));
2419
+ return {
2420
+ content: [{ type: 'text', text: JSON.stringify(top10, null, 2) }],
2421
+ details: {},
2422
+ };
2423
+ },
2424
+ },
2425
+ {
2426
+ name: 'get_feed',
2427
+ label: 'Get Feed',
2428
+ description: 'Get evaluation history / activity feed. Shows recent evaluations, signals, and changes across theses.',
2429
+ parameters: Type.Object({
2430
+ hours: Type.Optional(Type.Number({ description: 'Hours of history to fetch (default 24)' })),
2431
+ }),
2432
+ execute: async (_id, p) => {
2433
+ const result = await sfClient.getFeed(p.hours || 24);
2434
+ return {
2435
+ content: [{ type: 'text', text: JSON.stringify(result, null, 2) }],
2436
+ details: {},
2437
+ };
2438
+ },
2439
+ },
2440
+ {
2441
+ name: 'explore_public',
2442
+ label: 'Explore Public Theses',
2443
+ description: 'Browse public theses from other users. No auth required. Pass a slug to get details, or omit to list all.',
2444
+ parameters: Type.Object({
2445
+ slug: Type.Optional(Type.String({ description: 'Specific thesis slug, or empty to list all' })),
2446
+ }),
2447
+ execute: async (_id, p) => {
2448
+ const base = 'https://simplefunctions.dev';
2449
+ if (p.slug) {
2450
+ const res = await fetch(`${base}/api/public/thesis/${p.slug}`);
2451
+ if (!res.ok)
2452
+ return { content: [{ type: 'text', text: `Not found: ${p.slug}` }], details: {} };
2453
+ const data = await res.json();
2454
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
2455
+ }
2456
+ const res = await fetch(`${base}/api/public/theses`);
2457
+ if (!res.ok)
2458
+ return { content: [{ type: 'text', text: 'Failed to fetch public theses' }], details: {} };
2459
+ const data = await res.json();
2460
+ return { content: [{ type: 'text', text: JSON.stringify(data, null, 2) }], details: {} };
2461
+ },
2462
+ },
2463
+ {
2464
+ name: 'create_strategy',
2465
+ label: 'Create Strategy',
2466
+ description: 'Create a trading strategy for a thesis. Extract hard conditions (entryBelow/stopLoss/takeProfit as cents) and soft conditions from conversation. Called when user mentions specific trade ideas.',
2467
+ parameters: Type.Object({
2468
+ thesisId: Type.String({ description: 'Thesis ID' }),
2469
+ marketId: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T150' }),
2470
+ market: Type.String({ description: 'Human-readable market name' }),
2471
+ direction: Type.String({ description: 'yes or no' }),
2472
+ horizon: Type.Optional(Type.String({ description: 'short, medium, or long. Default: medium' })),
2473
+ entryBelow: Type.Optional(Type.Number({ description: 'Entry trigger: ask <= this value (cents)' })),
2474
+ entryAbove: Type.Optional(Type.Number({ description: 'Entry trigger: ask >= this value (cents, for NO direction)' })),
2475
+ stopLoss: Type.Optional(Type.Number({ description: 'Stop loss: bid <= this value (cents)' })),
2476
+ takeProfit: Type.Optional(Type.Number({ description: 'Take profit: bid >= this value (cents)' })),
2477
+ maxQuantity: Type.Optional(Type.Number({ description: 'Max total contracts. Default: 500' })),
2478
+ perOrderQuantity: Type.Optional(Type.Number({ description: 'Contracts per order. Default: 50' })),
2479
+ softConditions: Type.Optional(Type.String({ description: 'LLM-evaluated conditions e.g. "only enter when n3 > 60%"' })),
2480
+ rationale: Type.Optional(Type.String({ description: 'Full logic description' })),
2481
+ }),
2482
+ execute: async (_id, p) => {
2483
+ const result = await sfClient.createStrategyAPI(p.thesisId, {
2484
+ marketId: p.marketId,
2485
+ market: p.market,
2486
+ direction: p.direction,
2487
+ horizon: p.horizon,
2488
+ entryBelow: p.entryBelow,
2489
+ entryAbove: p.entryAbove,
2490
+ stopLoss: p.stopLoss,
2491
+ takeProfit: p.takeProfit,
2492
+ maxQuantity: p.maxQuantity,
2493
+ perOrderQuantity: p.perOrderQuantity,
2494
+ softConditions: p.softConditions,
2495
+ rationale: p.rationale,
2496
+ createdBy: 'agent',
2497
+ });
2498
+ return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
2499
+ },
2500
+ },
2501
+ {
2502
+ name: 'list_strategies',
2503
+ label: 'List Strategies',
2504
+ description: 'List strategies for a thesis. Filter by status (active/watching/executed/cancelled/review) or omit for all.',
2505
+ parameters: Type.Object({
2506
+ thesisId: Type.String({ description: 'Thesis ID' }),
2507
+ status: Type.Optional(Type.String({ description: 'Filter by status. Omit for all.' })),
2508
+ }),
2509
+ execute: async (_id, p) => {
2510
+ const result = await sfClient.getStrategies(p.thesisId, p.status);
2511
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
2512
+ },
2513
+ },
2514
+ {
2515
+ name: 'update_strategy',
2516
+ label: 'Update Strategy',
2517
+ description: 'Update a strategy (change stop loss, take profit, status, priority, etc.)',
2518
+ parameters: Type.Object({
2519
+ thesisId: Type.String({ description: 'Thesis ID' }),
2520
+ strategyId: Type.String({ description: 'Strategy ID (UUID)' }),
2521
+ stopLoss: Type.Optional(Type.Number({ description: 'New stop loss (cents)' })),
2522
+ takeProfit: Type.Optional(Type.Number({ description: 'New take profit (cents)' })),
2523
+ entryBelow: Type.Optional(Type.Number({ description: 'New entry below trigger (cents)' })),
2524
+ entryAbove: Type.Optional(Type.Number({ description: 'New entry above trigger (cents)' })),
2525
+ status: Type.Optional(Type.String({ description: 'New status: active|watching|executed|cancelled|review' })),
2526
+ priority: Type.Optional(Type.Number({ description: 'New priority' })),
2527
+ softConditions: Type.Optional(Type.String({ description: 'Updated soft conditions' })),
2528
+ rationale: Type.Optional(Type.String({ description: 'Updated rationale' })),
2529
+ }),
2530
+ execute: async (_id, p) => {
2531
+ const { thesisId, strategyId, ...updates } = p;
2532
+ const result = await sfClient.updateStrategyAPI(thesisId, strategyId, updates);
2533
+ return { content: [{ type: 'text', text: JSON.stringify(result) }], details: {} };
2534
+ },
2535
+ },
2536
+ {
2537
+ name: 'what_if',
2538
+ label: 'What-If',
2539
+ description: 'Run a what-if scenario: override causal tree node probabilities and see how edges and confidence change. Zero LLM cost — pure computation.',
2540
+ parameters: Type.Object({
2541
+ overrides: Type.Array(Type.Object({
2542
+ nodeId: Type.String({ description: 'Causal tree node ID (e.g. n1, n3.1)' }),
2543
+ newProbability: Type.Number({ description: 'New probability 0-1' }),
2544
+ }), { description: 'Node probability overrides' }),
2545
+ }),
2546
+ execute: async (_id, p) => {
2547
+ const ctx = latestContext;
2548
+ const allNodes = [];
2549
+ function flatten(nodes) {
2550
+ for (const n of nodes) {
2551
+ allNodes.push(n);
2552
+ if (n.children?.length)
2553
+ flatten(n.children);
2554
+ }
2555
+ }
2556
+ const rawNodes = ctx.causalTree?.nodes || [];
2557
+ flatten(rawNodes);
2558
+ const treeNodes = rawNodes.filter((n) => n.depth === 0 || (n.depth === undefined && !n.id.includes('.')));
2559
+ const overrideMap = new Map(p.overrides.map((o) => [o.nodeId, o.newProbability]));
2560
+ const oldConf = treeNodes.reduce((s, n) => s + (n.probability || 0) * (n.importance || 0), 0);
2561
+ const newConf = treeNodes.reduce((s, n) => {
2562
+ const prob = overrideMap.get(n.id) ?? n.probability ?? 0;
2563
+ return s + prob * (n.importance || 0);
2564
+ }, 0);
2565
+ const nodeScales = new Map();
2566
+ for (const [nid, np] of overrideMap.entries()) {
2567
+ const nd = allNodes.find((n) => n.id === nid);
2568
+ if (nd && nd.probability > 0)
2569
+ nodeScales.set(nid, Math.max(0, Math.min(2, np / nd.probability)));
2570
+ }
2571
+ const edges = (ctx.edges || []).map((edge) => {
2572
+ const relNode = edge.relatedNodeId;
2573
+ let scaleFactor = 1;
2574
+ if (relNode) {
2575
+ const candidates = [relNode, relNode.split('.').slice(0, -1).join('.'), relNode.split('.')[0]].filter(Boolean);
2576
+ for (const cid of candidates) {
2577
+ if (nodeScales.has(cid)) {
2578
+ scaleFactor = nodeScales.get(cid);
2579
+ break;
2580
+ }
2581
+ }
2582
+ }
2583
+ const mkt = edge.marketPrice || 0;
2584
+ const oldTP = edge.thesisPrice || edge.thesisImpliedPrice || mkt;
2585
+ const oldEdge = edge.edge || edge.edgeSize || 0;
2586
+ const newTP = Math.round((mkt + (oldTP - mkt) * scaleFactor) * 100) / 100;
2587
+ const dir = edge.direction || 'yes';
2588
+ const newEdge = Math.round((dir === 'yes' ? newTP - mkt : mkt - newTP) * 100) / 100;
2589
+ return {
2590
+ market: edge.market || edge.marketTitle || edge.marketId,
2591
+ marketPrice: mkt,
2592
+ oldEdge,
2593
+ newEdge,
2594
+ delta: newEdge - oldEdge,
2595
+ signal: Math.abs(newEdge - oldEdge) < 1 ? 'unchanged' : (oldEdge > 0 && newEdge < 0) || (oldEdge < 0 && newEdge > 0) ? 'REVERSED' : Math.abs(newEdge) < 2 ? 'GONE' : 'reduced',
2596
+ };
2597
+ }).filter((e) => e.signal !== 'unchanged');
2598
+ const result = {
2599
+ overrides: p.overrides.map((o) => {
2600
+ const node = allNodes.find((n) => n.id === o.nodeId);
2601
+ return { nodeId: o.nodeId, label: node?.label || o.nodeId, oldProb: node?.probability, newProb: o.newProbability };
2602
+ }),
2603
+ confidence: { old: Math.round(oldConf * 100), new: Math.round(newConf * 100), delta: Math.round((newConf - oldConf) * 100) },
2604
+ affectedEdges: edges,
2605
+ };
2606
+ return { content: [{ type: 'text', text: JSON.stringify(result, null, 2) }], details: {} };
2607
+ },
2608
+ },
2302
2609
  ];
2610
+ // ── Trading tools (conditional on tradingEnabled) for plain mode ──────────
2611
+ const config = (0, config_js_1.loadConfig)();
2612
+ if (config.tradingEnabled) {
2613
+ tools.push({
2614
+ name: 'place_order',
2615
+ label: 'Place Order',
2616
+ description: 'Place a buy or sell order on Kalshi. Executes directly (no confirmation prompt in plain mode).',
2617
+ parameters: Type.Object({
2618
+ ticker: Type.String({ description: 'Market ticker e.g. KXWTIMAX-26DEC31-T135' }),
2619
+ side: Type.String({ description: 'yes or no' }),
2620
+ action: Type.String({ description: 'buy or sell' }),
2621
+ type: Type.String({ description: 'limit or market' }),
2622
+ count: Type.Number({ description: 'Number of contracts' }),
2623
+ price_cents: Type.Optional(Type.Number({ description: 'Limit price in cents (1-99). Required for limit orders.' })),
2624
+ }),
2625
+ execute: async (_id, p) => {
2626
+ const { createOrder } = await import('../kalshi.js');
2627
+ const priceCents = p.price_cents ? Math.round(Number(p.price_cents)) : undefined;
2628
+ const maxCost = ((priceCents || 99) * p.count / 100).toFixed(2);
2629
+ process.stderr.write(` Order: ${p.action.toUpperCase()} ${p.count}x ${p.ticker} ${p.side.toUpperCase()} @ ${priceCents ? priceCents + '\u00A2' : 'market'} (max $${maxCost})\n`);
2630
+ try {
2631
+ const result = await createOrder({
2632
+ ticker: p.ticker,
2633
+ side: p.side,
2634
+ action: p.action,
2635
+ type: p.type,
2636
+ count: p.count,
2637
+ ...(priceCents ? { yes_price: priceCents } : {}),
2638
+ });
2639
+ const order = result.order || result;
2640
+ return {
2641
+ content: [{ type: 'text', text: `Order placed: ${order.order_id || 'OK'}\nStatus: ${order.status || '-'}\nFilled: ${order.fill_count_fp || 0}/${order.initial_count_fp || p.count}` }],
2642
+ details: {},
2643
+ };
2644
+ }
2645
+ catch (err) {
2646
+ const msg = err.message || String(err);
2647
+ if (msg.includes('403')) {
2648
+ return { content: [{ type: 'text', text: '403 Forbidden \u2014 your Kalshi key lacks write permission. Get a read+write key at kalshi.com/account/api-keys' }], details: {} };
2649
+ }
2650
+ return { content: [{ type: 'text', text: `Order failed: ${msg}` }], details: {} };
2651
+ }
2652
+ },
2653
+ }, {
2654
+ name: 'cancel_order',
2655
+ label: 'Cancel Order',
2656
+ description: 'Cancel a resting order by order ID. Executes directly (no confirmation prompt in plain mode).',
2657
+ parameters: Type.Object({
2658
+ order_id: Type.String({ description: 'Order ID to cancel' }),
2659
+ }),
2660
+ execute: async (_id, p) => {
2661
+ const { cancelOrder } = await import('../kalshi.js');
2662
+ try {
2663
+ await cancelOrder(p.order_id);
2664
+ return { content: [{ type: 'text', text: `Order ${p.order_id} cancelled.` }], details: {} };
2665
+ }
2666
+ catch (err) {
2667
+ return { content: [{ type: 'text', text: `Cancel failed: ${err.message}` }], details: {} };
2668
+ }
2669
+ },
2670
+ });
2671
+ }
2303
2672
  // ── System prompt ─────────────────────────────────────────────────────────
2304
2673
  const ctx = latestContext;
2305
2674
  const edgesSummary = ctx.edges
@@ -17,6 +17,7 @@ interface SetupOpts {
17
17
  key?: string;
18
18
  enableTrading?: boolean;
19
19
  disableTrading?: boolean;
20
+ kalshi?: boolean;
20
21
  }
21
22
  export declare function setupCommand(opts: SetupOpts): Promise<void>;
22
23
  export {};
@@ -71,13 +71,13 @@ async function validateSFKey(key, apiUrl) {
71
71
  headers: { 'Authorization': `Bearer ${key}` },
72
72
  });
73
73
  if (res.ok)
74
- return { valid: true, msg: `API Key 有效连接到 ${apiUrl.replace('https://', '')}` };
74
+ return { valid: true, msg: `API key validconnected to ${apiUrl.replace('https://', '')}` };
75
75
  if (res.status === 401)
76
- return { valid: false, msg: '无效 key,请重试' };
77
- return { valid: false, msg: `服务器返回 ${res.status}` };
76
+ return { valid: false, msg: 'Invalid key, please try again' };
77
+ return { valid: false, msg: `Server returned ${res.status}` };
78
78
  }
79
79
  catch (err) {
80
- return { valid: false, msg: `连接失败: ${err.message}` };
80
+ return { valid: false, msg: `Connection failed: ${err.message}` };
81
81
  }
82
82
  }
83
83
  async function validateOpenRouterKey(key) {
@@ -86,22 +86,22 @@ async function validateOpenRouterKey(key) {
86
86
  headers: { 'Authorization': `Bearer ${key}` },
87
87
  });
88
88
  if (res.ok)
89
- return { valid: true, msg: 'OpenRouter 连接正常可用模型: claude-sonnet-4.6' };
90
- return { valid: false, msg: `OpenRouter 返回 ${res.status}` };
89
+ return { valid: true, msg: 'OpenRouter connectedavailable model: claude-sonnet-4.6' };
90
+ return { valid: false, msg: `OpenRouter returned ${res.status}` };
91
91
  }
92
92
  catch (err) {
93
- return { valid: false, msg: `连接失败: ${err.message}` };
93
+ return { valid: false, msg: `Connection failed: ${err.message}` };
94
94
  }
95
95
  }
96
96
  async function validateKalshi() {
97
97
  try {
98
98
  const positions = await (0, kalshi_js_1.getPositions)();
99
99
  if (positions === null)
100
- return { valid: false, msg: 'Kalshi 认证失败', posCount: 0 };
101
- return { valid: true, msg: `Kalshi 认证成功发现 ${positions.length} 个持仓`, posCount: positions.length };
100
+ return { valid: false, msg: 'Kalshi authentication failed', posCount: 0 };
101
+ return { valid: true, msg: `Kalshi authenticatedfound ${positions.length} position(s)`, posCount: positions.length };
102
102
  }
103
103
  catch (err) {
104
- return { valid: false, msg: `Kalshi 连接失败: ${err.message}`, posCount: 0 };
104
+ return { valid: false, msg: `Kalshi connection failed: ${err.message}`, posCount: 0 };
105
105
  }
106
106
  }
107
107
  async function validateTavily(key) {
@@ -112,11 +112,11 @@ async function validateTavily(key) {
112
112
  body: JSON.stringify({ api_key: key, query: 'test', max_results: 1 }),
113
113
  });
114
114
  if (res.ok)
115
- return { valid: true, msg: 'Tavily 连接正常' };
116
- return { valid: false, msg: `Tavily 返回 ${res.status}` };
115
+ return { valid: true, msg: 'Tavily connected' };
116
+ return { valid: false, msg: `Tavily returned ${res.status}` };
117
117
  }
118
118
  catch (err) {
119
- return { valid: false, msg: `连接失败: ${err.message}` };
119
+ return { valid: false, msg: `Connection failed: ${err.message}` };
120
120
  }
121
121
  }
122
122
  async function setupCommand(opts) {
@@ -127,9 +127,9 @@ async function setupCommand(opts) {
127
127
  // ── sf setup --reset ──────────────────────────────────────────────────────
128
128
  if (opts.reset) {
129
129
  (0, config_js_1.resetConfig)();
130
- ok('配置已重置');
130
+ ok('Config reset');
131
131
  blank();
132
- info('运行 sf setup 重新配置');
132
+ info('Run sf setup to reconfigure');
133
133
  blank();
134
134
  return;
135
135
  }
@@ -144,7 +144,25 @@ async function setupCommand(opts) {
144
144
  const existing = (0, config_js_1.loadFileConfig)();
145
145
  (0, config_js_1.saveConfig)({ ...existing, apiKey: opts.key, apiUrl });
146
146
  ok(result.msg);
147
- ok(`保存到 ${(0, config_js_1.getConfigPath)()}`);
147
+ ok(`Saved to ${(0, config_js_1.getConfigPath)()}`);
148
+ return;
149
+ }
150
+ // ── sf setup --kalshi (reconfigure Kalshi credentials) ──────────────────
151
+ if (opts.kalshi) {
152
+ const existing = (0, config_js_1.loadFileConfig)();
153
+ blank();
154
+ console.log(` ${bold('Reconfigure Kalshi Credentials')}`);
155
+ blank();
156
+ info('Go to https://kalshi.com/account/api-keys to generate a new API key.');
157
+ info('If you need trading, make sure to enable read+write permissions.');
158
+ blank();
159
+ await promptForKalshi(existing);
160
+ (0, config_js_1.saveConfig)(existing);
161
+ if (existing.kalshiKeyId) {
162
+ process.env.KALSHI_API_KEY_ID = existing.kalshiKeyId;
163
+ process.env.KALSHI_PRIVATE_KEY_PATH = existing.kalshiPrivateKeyPath;
164
+ }
165
+ blank();
148
166
  return;
149
167
  }
150
168
  // ── sf setup --enable-trading / --disable-trading ────────────────────────
@@ -169,7 +187,7 @@ async function setupCommand(opts) {
169
187
  async function showCheck() {
170
188
  const config = (0, config_js_1.loadConfig)();
171
189
  blank();
172
- console.log(` ${bold('SimpleFunctions 配置状态')}`);
190
+ console.log(` ${bold('SimpleFunctions Config Status')}`);
173
191
  console.log(` ${dim('─'.repeat(35))}`);
174
192
  blank();
175
193
  // SF API Key
@@ -177,38 +195,38 @@ async function showCheck() {
177
195
  ok(`SF_API_KEY ${dim(mask(config.apiKey))}`);
178
196
  }
179
197
  else {
180
- fail('SF_API_KEY 未配置(必须)');
198
+ fail('SF_API_KEY not configured (required)');
181
199
  }
182
200
  // OpenRouter
183
201
  if (config.openrouterKey) {
184
202
  ok(`OPENROUTER_KEY ${dim(mask(config.openrouterKey))}`);
185
203
  }
186
204
  else {
187
- fail(`OPENROUTER_KEY 未配置(agent 不可用)`);
205
+ fail(`OPENROUTER_KEY not configured (agent unavailable)`);
188
206
  }
189
207
  // Kalshi
190
208
  if (config.kalshiKeyId && config.kalshiPrivateKeyPath) {
191
209
  ok(`KALSHI ${dim(mask(config.kalshiKeyId))}`);
192
210
  }
193
211
  else {
194
- info(`${dim('○')} KALSHI ${dim('跳过')}`);
212
+ info(`${dim('○')} KALSHI ${dim('skipped')}`);
195
213
  }
196
214
  // Tavily
197
215
  if (config.tavilyKey) {
198
216
  ok(`TAVILY ${dim(mask(config.tavilyKey))}`);
199
217
  }
200
218
  else {
201
- info(`${dim('○')} TAVILY ${dim('跳过')}`);
219
+ info(`${dim('○')} TAVILY ${dim('skipped')}`);
202
220
  }
203
221
  // Trading
204
222
  if (config.tradingEnabled) {
205
- ok('TRADING 已启用');
223
+ ok('TRADING enabled');
206
224
  }
207
225
  else {
208
- info(`${dim('○')} TRADING ${dim('未启用 — sf setup --enable-trading')}`);
226
+ info(`${dim('○')} TRADING ${dim('disabled — sf setup --enable-trading')}`);
209
227
  }
210
228
  blank();
211
- console.log(` ${dim('配置文件: ' + (0, config_js_1.getConfigPath)())}`);
229
+ console.log(` ${dim('Config file: ' + (0, config_js_1.getConfigPath)())}`);
212
230
  blank();
213
231
  }
214
232
  // ─── Interactive Wizard ──────────────────────────────────────────────────────
@@ -222,19 +240,19 @@ async function runWizard() {
222
240
  // ════════════════════════════════════════════════════════════════════════════
223
241
  // Step 1: SF API Key
224
242
  // ════════════════════════════════════════════════════════════════════════════
225
- console.log(` ${bold(' 1 步:API Key')}`);
243
+ console.log(` ${bold('Step 1: API Key')}`);
226
244
  blank();
227
245
  const existingSfKey = process.env.SF_API_KEY || config.apiKey;
228
246
  if (existingSfKey) {
229
247
  const result = await validateSFKey(existingSfKey, apiUrl);
230
248
  if (result.valid) {
231
- ok(`已检测到 SF_API_KEY — ${dim(mask(existingSfKey))}`);
232
- info(dim('跳过。'));
249
+ ok(`Detected SF_API_KEY — ${dim(mask(existingSfKey))}`);
250
+ info(dim('Skipping.'));
233
251
  config.apiKey = existingSfKey;
234
252
  blank();
235
253
  }
236
254
  else {
237
- fail(`已有 key 无效: ${result.msg}`);
255
+ fail(`Existing key invalid: ${result.msg}`);
238
256
  config.apiKey = await promptForSFKey(apiUrl);
239
257
  }
240
258
  }
@@ -249,19 +267,19 @@ async function runWizard() {
249
267
  // ════════════════════════════════════════════════════════════════════════════
250
268
  // Step 2: OpenRouter API Key
251
269
  // ════════════════════════════════════════════════════════════════════════════
252
- console.log(` ${bold(' 2 步:AI 模型(用于 sf agent')}`);
270
+ console.log(` ${bold('Step 2: AI Model (for sf agent)')}`);
253
271
  blank();
254
272
  const existingOrKey = process.env.OPENROUTER_API_KEY || config.openrouterKey;
255
273
  if (existingOrKey) {
256
274
  const result = await validateOpenRouterKey(existingOrKey);
257
275
  if (result.valid) {
258
- ok(`已检测到 OPENROUTER_API_KEY — ${dim(mask(existingOrKey))}`);
259
- info(dim('跳过。'));
276
+ ok(`Detected OPENROUTER_API_KEY — ${dim(mask(existingOrKey))}`);
277
+ info(dim('Skipping.'));
260
278
  config.openrouterKey = existingOrKey;
261
279
  blank();
262
280
  }
263
281
  else {
264
- fail(`已有 key 无效: ${result.msg}`);
282
+ fail(`Existing key invalid: ${result.msg}`);
265
283
  config.openrouterKey = await promptForOpenRouterKey();
266
284
  }
267
285
  }
@@ -274,7 +292,7 @@ async function runWizard() {
274
292
  // ════════════════════════════════════════════════════════════════════════════
275
293
  // Step 3: Kalshi Exchange
276
294
  // ════════════════════════════════════════════════════════════════════════════
277
- console.log(` ${bold(' 3 步:Kalshi 交易所(可选)')}`);
295
+ console.log(` ${bold('Step 3: Kalshi Exchange (optional)')}`);
278
296
  blank();
279
297
  const existingKalshiId = process.env.KALSHI_API_KEY_ID || config.kalshiKeyId;
280
298
  const existingKalshiPath = process.env.KALSHI_PRIVATE_KEY_PATH || config.kalshiPrivateKeyPath;
@@ -284,14 +302,14 @@ async function runWizard() {
284
302
  process.env.KALSHI_PRIVATE_KEY_PATH = existingKalshiPath;
285
303
  const result = await validateKalshi();
286
304
  if (result.valid) {
287
- ok(`已检测到 Kalshi — ${dim(mask(existingKalshiId))} (${result.posCount} 个持仓)`);
288
- info(dim('跳过。'));
305
+ ok(`Detected Kalshi — ${dim(mask(existingKalshiId))} (${result.posCount} position(s))`);
306
+ info(dim('Skipping.'));
289
307
  config.kalshiKeyId = existingKalshiId;
290
308
  config.kalshiPrivateKeyPath = existingKalshiPath;
291
309
  blank();
292
310
  }
293
311
  else {
294
- fail(`已有凭证无效: ${result.msg}`);
312
+ fail(`Existing credentials invalid: ${result.msg}`);
295
313
  await promptForKalshi(config);
296
314
  }
297
315
  }
@@ -302,19 +320,19 @@ async function runWizard() {
302
320
  // ════════════════════════════════════════════════════════════════════════════
303
321
  // Step 4: Tavily
304
322
  // ════════════════════════════════════════════════════════════════════════════
305
- console.log(` ${bold(' 4 步:新闻搜索(可选)')}`);
323
+ console.log(` ${bold('Step 4: News Search (optional)')}`);
306
324
  blank();
307
325
  const existingTavily = process.env.TAVILY_API_KEY || config.tavilyKey;
308
326
  if (existingTavily) {
309
327
  const result = await validateTavily(existingTavily);
310
328
  if (result.valid) {
311
- ok(`已检测到 TAVILY_API_KEY — ${dim(mask(existingTavily))}`);
312
- info(dim('跳过。'));
329
+ ok(`Detected TAVILY_API_KEY — ${dim(mask(existingTavily))}`);
330
+ info(dim('Skipping.'));
313
331
  config.tavilyKey = existingTavily;
314
332
  blank();
315
333
  }
316
334
  else {
317
- fail(`已有 key 无效: ${result.msg}`);
335
+ fail(`Existing key invalid: ${result.msg}`);
318
336
  config.tavilyKey = await promptForTavily();
319
337
  }
320
338
  }
@@ -328,18 +346,18 @@ async function runWizard() {
328
346
  // Step 5: Trading
329
347
  // ════════════════════════════════════════════════════════════════════════════
330
348
  if (config.kalshiKeyId) {
331
- console.log(` ${bold(' 5 步:交易功能(可选)')}`);
349
+ console.log(` ${bold('Step 5: Trading (optional)')}`);
332
350
  blank();
333
- info('⚠️ 启用后 sf buy / sf sell / sf cancel 可用。');
334
- info('你的 Kalshi API key 必须有 read+write 权限。');
351
+ info('Warning: enabling this unlocks sf buy / sf sell / sf cancel.');
352
+ info('Your Kalshi API key must have read+write permissions.');
335
353
  blank();
336
- const enableTrading = await promptYN(' 启用交易功能?(y/N) ', false);
354
+ const enableTrading = await promptYN(' Enable trading? (y/N) ', false);
337
355
  config.tradingEnabled = enableTrading;
338
356
  if (enableTrading) {
339
- ok('交易功能已启用');
357
+ ok('Trading enabled');
340
358
  }
341
359
  else {
342
- info(dim('跳过。之后可以 sf setup --enable-trading 启用。'));
360
+ info(dim('Skipped. You can enable later with sf setup --enable-trading.'));
343
361
  }
344
362
  blank();
345
363
  (0, config_js_1.saveConfig)(config);
@@ -348,24 +366,24 @@ async function runWizard() {
348
366
  // Summary
349
367
  // ════════════════════════════════════════════════════════════════════════════
350
368
  console.log(` ${dim('─'.repeat(25))}`);
351
- info(`配置保存到 ${dim((0, config_js_1.getConfigPath)())}`);
369
+ info(`Config saved to ${dim((0, config_js_1.getConfigPath)())}`);
352
370
  blank();
353
371
  if (config.apiKey)
354
- ok('SF_API_KEY 已配置');
372
+ ok('SF_API_KEY configured');
355
373
  else
356
- fail('SF_API_KEY 未配置');
374
+ fail('SF_API_KEY not configured');
357
375
  if (config.openrouterKey)
358
- ok('OPENROUTER_KEY 已配置');
376
+ ok('OPENROUTER_KEY configured');
359
377
  else
360
- fail('OPENROUTER_KEY 跳过');
378
+ fail('OPENROUTER_KEY skipped');
361
379
  if (config.kalshiKeyId)
362
- ok('KALSHI 已配置');
380
+ ok('KALSHI configured');
363
381
  else
364
- info(`${dim('○')} KALSHI 跳过`);
382
+ info(`${dim('○')} KALSHI skipped`);
365
383
  if (config.tavilyKey)
366
- ok('TAVILY 已配置');
384
+ ok('TAVILY configured');
367
385
  else
368
- info(`${dim('○')} TAVILY 跳过`);
386
+ info(`${dim('○')} TAVILY skipped`);
369
387
  blank();
370
388
  // ════════════════════════════════════════════════════════════════════════════
371
389
  // Step 5: Thesis creation
@@ -376,18 +394,18 @@ async function runWizard() {
376
394
  }
377
395
  // ─── Step prompt helpers ─────────────────────────────────────────────────────
378
396
  async function promptForSFKey(apiUrl) {
379
- info(`还没有 key?去 ${cyan('https://simplefunctions.dev/dashboard')} 注册获取。`);
380
- info(' Enter 打开浏览器,或直接粘贴你的 key');
397
+ info(`Don't have a key? Sign up at ${cyan('https://simplefunctions.dev/dashboard')}.`);
398
+ info('Press Enter to open browser, or paste your key:');
381
399
  blank();
382
400
  while (true) {
383
401
  const answer = await prompt(' > ');
384
402
  if (!answer) {
385
403
  // Open browser
386
404
  openBrowser('https://simplefunctions.dev/dashboard');
387
- info(dim('浏览器已打开。获取 key 后粘贴到这里:'));
405
+ info(dim('Browser opened. Paste your key here once you have it:'));
388
406
  continue;
389
407
  }
390
- info(dim('验证中...'));
408
+ info(dim('Validating...'));
391
409
  const result = await validateSFKey(answer, apiUrl);
392
410
  if (result.valid) {
393
411
  ok(result.msg);
@@ -400,40 +418,40 @@ async function promptForSFKey(apiUrl) {
400
418
  }
401
419
  }
402
420
  async function promptForOpenRouterKey() {
403
- info(`需要 OpenRouter API key。去 ${cyan('https://openrouter.ai/settings/keys')} 获取。`);
404
- info(' Enter 跳过(agent 功能不可用),或粘贴 key');
421
+ info(`Requires an OpenRouter API key. Get one at ${cyan('https://openrouter.ai/settings/keys')}.`);
422
+ info('Press Enter to skip (agent unavailable), or paste key:');
405
423
  blank();
406
424
  const answer = await prompt(' > ');
407
425
  if (!answer) {
408
- info(dim('跳过。'));
426
+ info(dim('Skipped.'));
409
427
  blank();
410
428
  return undefined;
411
429
  }
412
- info(dim('验证中...'));
430
+ info(dim('Validating...'));
413
431
  const result = await validateOpenRouterKey(answer);
414
432
  if (result.valid) {
415
433
  ok(result.msg);
416
434
  }
417
435
  else {
418
436
  fail(result.msg);
419
- info(dim('已保存,之后可以重新运行 sf setup 修正。'));
437
+ info(dim('Saved. You can re-run sf setup later to fix this.'));
420
438
  }
421
439
  blank();
422
440
  return answer;
423
441
  }
424
442
  async function promptForKalshi(config) {
425
- info(`连接 Kalshi 查看你的持仓和盈亏。`);
426
- info(`需要 API Key ID 和私钥文件。`);
427
- info(`${cyan('https://kalshi.com/account/api-keys')} 获取。`);
428
- info(' Enter 跳过,或粘贴 Key ID');
443
+ info(`Connect Kalshi to view your positions and P&L.`);
444
+ info(`Requires an API Key ID and private key file.`);
445
+ info(`Get them at ${cyan('https://kalshi.com/account/api-keys')}.`);
446
+ info('Press Enter to skip, or paste Key ID:');
429
447
  blank();
430
448
  const keyId = await prompt(' > ');
431
449
  if (!keyId) {
432
- info(dim('跳过。'));
450
+ info(dim('Skipped.'));
433
451
  blank();
434
452
  return;
435
453
  }
436
- info('私钥文件路径(默认 ~/.kalshi/private.pem):');
454
+ info('Private key file path (default ~/.kalshi/private.pem):');
437
455
  const keyPathInput = await prompt(' > ');
438
456
  const keyPath = keyPathInput || '~/.kalshi/private.pem';
439
457
  config.kalshiKeyId = keyId;
@@ -441,36 +459,36 @@ async function promptForKalshi(config) {
441
459
  // Temporarily set for validation
442
460
  process.env.KALSHI_API_KEY_ID = keyId;
443
461
  process.env.KALSHI_PRIVATE_KEY_PATH = keyPath;
444
- info(dim('验证中...'));
462
+ info(dim('Validating...'));
445
463
  const result = await validateKalshi();
446
464
  if (result.valid) {
447
465
  ok(result.msg);
448
466
  }
449
467
  else {
450
468
  fail(result.msg);
451
- info(dim('已保存,之后可以重新运行 sf setup 修正。'));
469
+ info(dim('Saved. You can re-run sf setup later to fix this.'));
452
470
  }
453
471
  blank();
454
472
  }
455
473
  async function promptForTavily() {
456
- info(`Tavily API 用于 agent web_search 功能。`);
457
- info(`${cyan('https://tavily.com')} 获取免费 key。`);
458
- info(' Enter 跳过:');
474
+ info(`Tavily API powers the agent's web_search tool.`);
475
+ info(`Get a free key at ${cyan('https://tavily.com')}.`);
476
+ info('Press Enter to skip:');
459
477
  blank();
460
478
  const answer = await prompt(' > ');
461
479
  if (!answer) {
462
- info(dim('跳过。'));
480
+ info(dim('Skipped.'));
463
481
  blank();
464
482
  return undefined;
465
483
  }
466
- info(dim('验证中...'));
484
+ info(dim('Validating...'));
467
485
  const result = await validateTavily(answer);
468
486
  if (result.valid) {
469
487
  ok(result.msg);
470
488
  }
471
489
  else {
472
490
  fail(result.msg);
473
- info(dim('已保存,之后可以重新运行 sf setup 修正。'));
491
+ info(dim('Saved. You can re-run sf setup later to fix this.'));
474
492
  }
475
493
  blank();
476
494
  return answer;
@@ -483,67 +501,67 @@ async function handleThesisStep(config) {
483
501
  const theses = data.theses || [];
484
502
  const activeTheses = theses.filter((t) => t.status === 'active');
485
503
  if (activeTheses.length > 0) {
486
- console.log(` ${bold(' 6 步:论文')}`);
504
+ console.log(` ${bold('Step 6: Theses')}`);
487
505
  blank();
488
- ok(`已有 ${activeTheses.length} 个活跃论文:`);
506
+ ok(`Found ${activeTheses.length} active thesis(es):`);
489
507
  for (const t of activeTheses.slice(0, 5)) {
490
508
  const conf = typeof t.confidence === 'number' ? Math.round(t.confidence * 100) : 0;
491
509
  const thesis = (t.rawThesis || t.thesis || t.title || '').slice(0, 60);
492
510
  info(` ${dim(t.id.slice(0, 8))} — ${thesis} — ${conf}%`);
493
511
  }
494
- info(dim('跳过创建。'));
512
+ info(dim('Skipping creation.'));
495
513
  blank();
496
514
  // Offer to launch agent
497
515
  if (config.openrouterKey) {
498
516
  console.log(` ${dim('─'.repeat(25))}`);
499
- console.log(` ${bold('全部就绪!')}`);
517
+ console.log(` ${bold('All set!')}`);
500
518
  blank();
501
- info(` ${cyan('sf agent')} 和你的论文对话`);
502
- info(` ${cyan('sf context <id>')} 查看论文快照`);
503
- info(` ${cyan('sf positions')} 查看持仓`);
504
- info(` ${cyan('sf setup --check')} 检查配置`);
519
+ info(` ${cyan('sf agent')} Chat with your thesis`);
520
+ info(` ${cyan('sf context <id>')} View thesis snapshot`);
521
+ info(` ${cyan('sf positions')} View positions`);
522
+ info(` ${cyan('sf setup --check')} Check config`);
505
523
  blank();
506
- const shouldLaunch = await promptYN(` 要不要现在启动 agent(Y/n) `);
524
+ const shouldLaunch = await promptYN(` Launch agent now? (Y/n) `);
507
525
  if (shouldLaunch) {
508
526
  blank();
509
- info('启动中...');
527
+ info('Launching...');
510
528
  blank();
511
529
  await (0, agent_js_1.agentCommand)(activeTheses[0].id, { model: config.model });
512
530
  }
513
531
  }
514
532
  else {
515
533
  blank();
516
- console.log(` ${bold('全部就绪!')}`);
534
+ console.log(` ${bold('All set!')}`);
517
535
  blank();
518
- info(` ${cyan('sf list')} 查看所有论文`);
519
- info(` ${cyan('sf context <id>')} 查看论文快照`);
520
- info(` ${cyan('sf positions')} 查看持仓`);
521
- info(` ${cyan('sf setup --check')} 检查配置`);
536
+ info(` ${cyan('sf list')} List all theses`);
537
+ info(` ${cyan('sf context <id>')} View thesis snapshot`);
538
+ info(` ${cyan('sf positions')} View positions`);
539
+ info(` ${cyan('sf setup --check')} Check config`);
522
540
  blank();
523
541
  }
524
542
  return;
525
543
  }
526
544
  // No theses — offer to create one
527
- console.log(` ${bold(' 6 步:创建你的第一个论文')}`);
545
+ console.log(` ${bold('Step 6: Create Your First Thesis')}`);
528
546
  blank();
529
- info('论文是你对市场的一个核心判断。系统会基于它构建因果模型,');
530
- info('然后持续扫描预测市场寻找被错误定价的合约。');
547
+ info('A thesis is your core market conviction. The system builds a causal model');
548
+ info('from it, then continuously scans prediction markets for mispriced contracts.');
531
549
  blank();
532
- info('比如:');
533
- info(` ${dim('"美联储2026年不会降息,通胀因油价持续高企"')}`);
534
- info(` ${dim('"AI裁员潮导致消费萎缩,标普年底跌20%"')}`);
535
- info(` ${dim('"Trump无法退出伊朗战争,油价维持$100以上六个月"')}`);
550
+ info('Examples:');
551
+ info(` ${dim('"The Fed won\'t cut rates in 2026 — inflation stays elevated due to oil prices"')}`);
552
+ info(` ${dim('"AI-driven layoffs cause consumer spending to contract, S&P drops 20% by year-end"')}`);
553
+ info(` ${dim('"Trump can\'t exit the Iran conflict — oil stays above $100 for six months"')}`);
536
554
  blank();
537
- const thesis = await prompt(' 输入你的论文(按 Enter 跳过,之后用 sf create):\n > ');
555
+ const thesis = await prompt(' Enter your thesis (press Enter to skip, use sf create later):\n > ');
538
556
  if (!thesis) {
539
557
  blank();
540
- info(dim('跳过。之后用 sf create "你的论文" 创建。'));
558
+ info(dim('Skipped. Use sf create "your thesis" to create one later.'));
541
559
  blank();
542
560
  showFinalHints(config);
543
561
  return;
544
562
  }
545
563
  blank();
546
- info('构建因果模型中...(约30秒)');
564
+ info('Building causal model... (~30s)');
547
565
  blank();
548
566
  try {
549
567
  const result = await client.createThesis(thesis, true);
@@ -552,20 +570,20 @@ async function handleThesisStep(config) {
552
570
  const edgeCount = result.edgeAnalysis?.edges?.length || 0;
553
571
  const totalMarkets = result.edgeAnalysis?.totalMarketsAnalyzed || 0;
554
572
  const confidence = Math.round((parseFloat(result.confidence) || 0.5) * 100);
555
- ok(`因果树:${nodeCount} 个节点`);
556
- ok(`扫描 ${totalMarkets} 个市场,找到 ${edgeCount} 个有边际的合约`);
557
- ok(`置信度:${confidence}%`);
558
- ok(`论文 ID:${result.id.slice(0, 8)}`);
573
+ ok(`Causal tree: ${nodeCount} node(s)`);
574
+ ok(`Scanned ${totalMarkets} markets, found ${edgeCount} contract(s) with edge`);
575
+ ok(`Confidence: ${confidence}%`);
576
+ ok(`Thesis ID: ${result.id.slice(0, 8)}`);
559
577
  blank();
560
578
  // Offer to launch agent
561
579
  if (config.openrouterKey) {
562
580
  console.log(` ${dim('─'.repeat(25))}`);
563
- console.log(` ${bold('全部就绪!')}`);
581
+ console.log(` ${bold('All set!')}`);
564
582
  blank();
565
- const shouldLaunch = await promptYN(` 要不要现在启动 agent(Y/n) `);
583
+ const shouldLaunch = await promptYN(` Launch agent now? (Y/n) `);
566
584
  if (shouldLaunch) {
567
585
  blank();
568
- info('启动中...');
586
+ info('Launching...');
569
587
  blank();
570
588
  await (0, agent_js_1.agentCommand)(result.id, { model: config.model });
571
589
  }
@@ -579,15 +597,15 @@ async function handleThesisStep(config) {
579
597
  }
580
598
  }
581
599
  else {
582
- fail(`创建失败:${result.error || '未知错误'}`);
583
- info(dim('之后可以用 sf create "你的论文" 重试'));
600
+ fail(`Creation failed: ${result.error || 'unknown error'}`);
601
+ info(dim('You can retry later with sf create "your thesis"'));
584
602
  blank();
585
603
  showFinalHints(config);
586
604
  }
587
605
  }
588
606
  catch (err) {
589
- fail(`创建失败:${err.message}`);
590
- info(dim('之后可以用 sf create "你的论文" 重试'));
607
+ fail(`Creation failed: ${err.message}`);
608
+ info(dim('You can retry later with sf create "your thesis"'));
591
609
  blank();
592
610
  showFinalHints(config);
593
611
  }
@@ -600,12 +618,12 @@ async function handleThesisStep(config) {
600
618
  }
601
619
  function showFinalHints(config) {
602
620
  console.log(` ${dim('─'.repeat(25))}`);
603
- console.log(` ${bold('全部就绪!')}`);
621
+ console.log(` ${bold('All set!')}`);
604
622
  blank();
605
- info(` ${cyan('sf agent')} 和你的论文对话`);
606
- info(` ${cyan('sf list')} 查看所有论文`);
607
- info(` ${cyan('sf context <id>')} 查看论文快照`);
608
- info(` ${cyan('sf positions')} 查看持仓`);
609
- info(` ${cyan('sf setup --check')} 检查配置`);
623
+ info(` ${cyan('sf agent')} Chat with your thesis`);
624
+ info(` ${cyan('sf list')} List all theses`);
625
+ info(` ${cyan('sf context <id>')} View thesis snapshot`);
626
+ info(` ${cyan('sf positions')} View positions`);
627
+ info(` ${cyan('sf setup --check')} Check config`);
610
628
  blank();
611
629
  }
@@ -26,7 +26,6 @@ async function executeOrder(ticker, qty, action, opts) {
26
26
  if (priceCents !== undefined && (priceCents < 1 || priceCents > 99)) {
27
27
  throw new Error('Price must be 1-99 cents.');
28
28
  }
29
- const priceDollars = priceCents ? (priceCents / 100).toFixed(2) : undefined;
30
29
  const maxCost = ((priceCents || 99) * quantity / 100).toFixed(2);
31
30
  console.log();
32
31
  console.log(` ${utils_js_1.c.bold}${utils_js_1.c.cyan}${action.toUpperCase()} Order${utils_js_1.c.reset}`);
@@ -35,7 +34,7 @@ async function executeOrder(ticker, qty, action, opts) {
35
34
  console.log(` Side: ${side === 'yes' ? utils_js_1.c.green + 'YES' + utils_js_1.c.reset : utils_js_1.c.red + 'NO' + utils_js_1.c.reset}`);
36
35
  console.log(` Quantity: ${quantity}`);
37
36
  console.log(` Type: ${orderType}`);
38
- if (priceDollars)
37
+ if (priceCents)
39
38
  console.log(` Price: ${priceCents}¢`);
40
39
  console.log(` Max cost: $${maxCost}`);
41
40
  console.log();
@@ -53,7 +52,7 @@ async function executeOrder(ticker, qty, action, opts) {
53
52
  action,
54
53
  type: orderType,
55
54
  count: quantity,
56
- ...(priceDollars ? { yes_price: priceDollars } : {}),
55
+ ...(priceCents ? { yes_price: priceCents } : {}),
57
56
  });
58
57
  const order = result.order || result;
59
58
  console.log();
package/dist/index.js CHANGED
@@ -63,8 +63,8 @@ program
63
63
  .option('--api-url <url>', 'API base URL (or set SF_API_URL env var)');
64
64
  // ── Pre-action guard: check configuration ────────────────────────────────────
65
65
  const NO_CONFIG_COMMANDS = new Set(['setup', 'help', 'scan', 'explore', 'milestones', 'forecast', 'settlements', 'balance', 'orders', 'fills', 'schedule', 'announcements', 'history']);
66
- program.hook('preAction', (thisCommand) => {
67
- const cmdName = thisCommand.name();
66
+ program.hook('preAction', (thisCommand, actionCommand) => {
67
+ const cmdName = actionCommand.name();
68
68
  if (NO_CONFIG_COMMANDS.has(cmdName))
69
69
  return;
70
70
  // --api-key flag overrides config check
@@ -87,8 +87,9 @@ program
87
87
  .option('--key <key>', 'Set SF API key (non-interactive, for CI)')
88
88
  .option('--enable-trading', 'Enable trading (sf buy/sell/cancel)')
89
89
  .option('--disable-trading', 'Disable trading')
90
+ .option('--kalshi', 'Reconfigure Kalshi API credentials')
90
91
  .action(async (opts) => {
91
- await run(() => (0, setup_js_1.setupCommand)({ check: opts.check, reset: opts.reset, key: opts.key, enableTrading: opts.enableTrading, disableTrading: opts.disableTrading }));
92
+ 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 }));
92
93
  });
93
94
  // ── sf list ──────────────────────────────────────────────────────────────────
94
95
  program
package/dist/kalshi.d.ts CHANGED
@@ -103,7 +103,7 @@ export declare function createOrder(params: {
103
103
  action: 'buy' | 'sell';
104
104
  type: 'limit' | 'market';
105
105
  count: number;
106
- yes_price?: string;
106
+ yes_price?: number;
107
107
  client_order_id?: string;
108
108
  }): Promise<{
109
109
  order: any;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@spfunctions/cli",
3
- "version": "1.1.9",
3
+ "version": "1.3.0",
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"
@@ -39,6 +39,6 @@
39
39
  "license": "MIT",
40
40
  "repository": {
41
41
  "type": "git",
42
- "url": "https://github.com/simplefunctions/simplefunctions"
42
+ "url": "https://github.com/spfunctions/simplefunctions-cli"
43
43
  }
44
44
  }