@spfunctions/cli 1.4.3 → 1.4.5

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.
Files changed (71) hide show
  1. package/README.md +205 -48
  2. package/dist/cache.d.ts +6 -0
  3. package/dist/cache.js +31 -0
  4. package/dist/cache.test.d.ts +1 -0
  5. package/dist/cache.test.js +73 -0
  6. package/dist/client.test.d.ts +1 -0
  7. package/dist/client.test.js +89 -0
  8. package/dist/commands/agent.js +257 -70
  9. package/dist/commands/dashboard.d.ts +6 -3
  10. package/dist/commands/dashboard.js +28 -26
  11. package/dist/commands/performance.js +9 -2
  12. package/dist/commands/telegram.d.ts +15 -0
  13. package/dist/commands/telegram.js +125 -0
  14. package/dist/config.d.ts +1 -0
  15. package/dist/config.js +1 -0
  16. package/dist/config.test.d.ts +1 -0
  17. package/dist/config.test.js +138 -0
  18. package/dist/index.js +16 -2
  19. package/dist/telegram/agent-bridge.d.ts +15 -0
  20. package/dist/telegram/agent-bridge.js +368 -0
  21. package/dist/telegram/bot.d.ts +10 -0
  22. package/dist/telegram/bot.js +297 -0
  23. package/dist/telegram/commands.d.ts +11 -0
  24. package/dist/telegram/commands.js +120 -0
  25. package/dist/telegram/format.d.ts +11 -0
  26. package/dist/telegram/format.js +51 -0
  27. package/dist/telegram/format.test.d.ts +1 -0
  28. package/dist/telegram/format.test.js +73 -0
  29. package/dist/telegram/poller.d.ts +6 -0
  30. package/dist/telegram/poller.js +32 -0
  31. package/dist/topics.test.d.ts +1 -0
  32. package/dist/topics.test.js +54 -0
  33. package/dist/tui/border.d.ts +33 -0
  34. package/dist/tui/border.js +87 -0
  35. package/dist/tui/chart.d.ts +19 -0
  36. package/dist/tui/chart.js +117 -0
  37. package/dist/tui/dashboard.d.ts +9 -0
  38. package/dist/tui/dashboard.js +779 -0
  39. package/dist/tui/layout.d.ts +16 -0
  40. package/dist/tui/layout.js +41 -0
  41. package/dist/tui/screen.d.ts +33 -0
  42. package/dist/tui/screen.js +102 -0
  43. package/dist/tui/state.d.ts +40 -0
  44. package/dist/tui/state.js +36 -0
  45. package/dist/tui/widgets/commandbar.d.ts +8 -0
  46. package/dist/tui/widgets/commandbar.js +82 -0
  47. package/dist/tui/widgets/detail.d.ts +9 -0
  48. package/dist/tui/widgets/detail.js +151 -0
  49. package/dist/tui/widgets/edges.d.ts +4 -0
  50. package/dist/tui/widgets/edges.js +33 -0
  51. package/dist/tui/widgets/liquidity.d.ts +9 -0
  52. package/dist/tui/widgets/liquidity.js +142 -0
  53. package/dist/tui/widgets/orders.d.ts +4 -0
  54. package/dist/tui/widgets/orders.js +37 -0
  55. package/dist/tui/widgets/portfolio.d.ts +4 -0
  56. package/dist/tui/widgets/portfolio.js +58 -0
  57. package/dist/tui/widgets/signals.d.ts +4 -0
  58. package/dist/tui/widgets/signals.js +31 -0
  59. package/dist/tui/widgets/statusbar.d.ts +8 -0
  60. package/dist/tui/widgets/statusbar.js +72 -0
  61. package/dist/tui/widgets/thesis.d.ts +4 -0
  62. package/dist/tui/widgets/thesis.js +66 -0
  63. package/dist/tui/widgets/trade.d.ts +9 -0
  64. package/dist/tui/widgets/trade.js +117 -0
  65. package/dist/tui/widgets/upcoming.d.ts +4 -0
  66. package/dist/tui/widgets/upcoming.js +41 -0
  67. package/dist/tui/widgets/whatif.d.ts +7 -0
  68. package/dist/tui/widgets/whatif.js +113 -0
  69. package/dist/utils.test.d.ts +1 -0
  70. package/dist/utils.test.js +111 -0
  71. package/package.json +6 -2
@@ -1,17 +1,19 @@
1
1
  "use strict";
2
2
  /**
3
- * sf dashboard — Portfolio overview
3
+ * sf dashboard — Commander entry point
4
4
  *
5
- * One-screen summary: theses, positions, risk exposure, top unpositioned edges.
6
- * Uses existing APIs + local Kalshi positions.
5
+ * Three modes:
6
+ * --json → dump current state as JSON
7
+ * --once → one-time formatted print (no interactive TUI)
8
+ * default → launch interactive TUI dashboard
7
9
  */
8
10
  Object.defineProperty(exports, "__esModule", { value: true });
9
11
  exports.dashboardCommand = dashboardCommand;
10
12
  const client_js_1 = require("../client.js");
11
13
  const kalshi_js_1 = require("../kalshi.js");
12
14
  const topics_js_1 = require("../topics.js");
15
+ const dashboard_js_1 = require("../tui/dashboard.js");
13
16
  function categorize(ticker) {
14
- // Match longest prefix first
15
17
  const sorted = Object.keys(topics_js_1.RISK_CATEGORIES).sort((a, b) => b.length - a.length);
16
18
  for (const prefix of sorted) {
17
19
  if (ticker.startsWith(prefix))
@@ -31,8 +33,13 @@ function timeAgo(dateStr) {
31
33
  return `${days}d ago`;
32
34
  }
33
35
  async function dashboardCommand(opts) {
36
+ // ── Default: interactive TUI ──
37
+ if (!opts?.json && !opts?.once) {
38
+ await (0, dashboard_js_1.startDashboard)();
39
+ return;
40
+ }
41
+ // ── JSON or one-time print modes (legacy behavior) ──
34
42
  const client = new client_js_1.SFClient(opts?.apiKey, opts?.apiUrl);
35
- // ── Fetch data in parallel ─────────────────────────────────────────────────
36
43
  const [thesesResult, positions] = await Promise.all([
37
44
  client.listTheses(),
38
45
  (0, kalshi_js_1.getPositions)().catch(() => null),
@@ -59,7 +66,7 @@ async function dashboardCommand(opts) {
59
66
  }
60
67
  }
61
68
  }
62
- // ── Collect all edges across all theses ────────────────────────────────────
69
+ // Collect all edges across all theses
63
70
  const allEdges = [];
64
71
  for (const ctx of contexts) {
65
72
  if (!ctx?.edges)
@@ -78,26 +85,22 @@ async function dashboardCommand(opts) {
78
85
  }
79
86
  // Find positioned tickers
80
87
  const positionedTickers = new Set(positions?.map((p) => p.ticker) || []);
81
- // Unpositioned edges = edges where no position exists on that marketId
88
+ // Unpositioned edges
82
89
  const unpositionedEdges = [...edgeMap.values()]
83
90
  .filter(e => !positionedTickers.has(e.marketId))
84
91
  .sort((a, b) => Math.abs(b.edge) - Math.abs(a.edge))
85
92
  .slice(0, 10);
86
- // ── JSON output ────────────────────────────────────────────────────────────
93
+ // ── JSON output ──
87
94
  if (opts?.json) {
88
- console.log(JSON.stringify({
89
- theses,
90
- positions,
91
- unpositionedEdges,
92
- }, null, 2));
95
+ console.log(JSON.stringify({ theses, positions, unpositionedEdges }, null, 2));
93
96
  return;
94
97
  }
95
- // ── Formatted output ───────────────────────────────────────────────────────
98
+ // ── One-time formatted output ──
96
99
  console.log();
97
100
  console.log(' SimpleFunctions Dashboard');
98
- console.log(' ' + ''.repeat(50));
101
+ console.log(' ' + '\u2500'.repeat(50));
99
102
  console.log();
100
- // ── Theses ─────────────────────────────────────────────────────────────────
103
+ // Theses
101
104
  console.log(' Theses');
102
105
  if (theses.length === 0) {
103
106
  console.log(' (none)');
@@ -115,7 +118,7 @@ async function dashboardCommand(opts) {
115
118
  }
116
119
  }
117
120
  console.log();
118
- // ── Positions ──────────────────────────────────────────────────────────────
121
+ // Positions
119
122
  console.log(' Positions');
120
123
  if (!positions || positions.length === 0) {
121
124
  console.log(' (no Kalshi positions or Kalshi not configured)');
@@ -126,8 +129,8 @@ async function dashboardCommand(opts) {
126
129
  for (const p of positions) {
127
130
  const ticker = (p.ticker || '').padEnd(22);
128
131
  const qty = String(p.quantity || 0).padStart(5);
129
- const avg = `${p.average_price_paid || 0}¢`;
130
- const now = typeof p.current_value === 'number' ? `${p.current_value}¢` : '';
132
+ const avg = `${p.average_price_paid || 0}\u00A2`;
133
+ const now = typeof p.current_value === 'number' ? `${p.current_value}\u00A2` : '?\u00A2';
131
134
  const pnlCents = p.unrealized_pnl || 0;
132
135
  const pnlDollars = (pnlCents / 100).toFixed(2);
133
136
  const pnlStr = pnlCents >= 0 ? `+$${pnlDollars}` : `-$${Math.abs(parseFloat(pnlDollars)).toFixed(2)}`;
@@ -136,14 +139,14 @@ async function dashboardCommand(opts) {
136
139
  totalPnl += pnlCents;
137
140
  console.log(` ${ticker} ${qty} @ ${avg.padEnd(5)} now ${now.padEnd(5)} ${pnlStr}`);
138
141
  }
139
- console.log(' ' + ''.repeat(45));
142
+ console.log(' ' + '\u2500'.repeat(45));
140
143
  const totalCostDollars = (totalCost / 100).toFixed(0);
141
144
  const totalPnlDollars = (totalPnl / 100).toFixed(2);
142
145
  const pnlDisplay = totalPnl >= 0 ? `+$${totalPnlDollars}` : `-$${Math.abs(parseFloat(totalPnlDollars)).toFixed(2)}`;
143
146
  console.log(` Total cost: $${totalCostDollars} | P&L: ${pnlDisplay}`);
144
147
  }
145
148
  console.log();
146
- // ── Risk Exposure ──────────────────────────────────────────────────────────
149
+ // Risk Exposure
147
150
  if (positions && positions.length > 0) {
148
151
  console.log(' Risk Exposure');
149
152
  const riskGroups = new Map();
@@ -157,7 +160,6 @@ async function dashboardCommand(opts) {
157
160
  existing.tickers.push(p.ticker);
158
161
  riskGroups.set(cat, existing);
159
162
  }
160
- // Sort by cost descending
161
163
  const sorted = [...riskGroups.entries()].sort((a, b) => b[1].cost - a[1].cost);
162
164
  for (const [category, data] of sorted) {
163
165
  const costDollars = `$${(data.cost / 100).toFixed(0)}`;
@@ -168,16 +170,16 @@ async function dashboardCommand(opts) {
168
170
  }
169
171
  console.log();
170
172
  }
171
- // ── Top Unpositioned Edges ─────────────────────────────────────────────────
173
+ // Top Unpositioned Edges
172
174
  if (unpositionedEdges.length > 0) {
173
175
  console.log(' Top Unpositioned Edges');
174
176
  for (const e of unpositionedEdges) {
175
177
  const name = (e.market || e.marketId || '').slice(0, 25).padEnd(25);
176
- const mkt = `${e.marketPrice}¢`;
177
- const thesis = `${e.thesisPrice}¢`;
178
+ const mkt = `${e.marketPrice}\u00A2`;
179
+ const thesis = `${e.thesisPrice}\u00A2`;
178
180
  const edge = e.edge > 0 ? `+${e.edge}` : `${e.edge}`;
179
181
  const liq = e.orderbook?.liquidityScore || '?';
180
- console.log(` ${name} ${mkt.padStart(5)} ${thesis.padStart(5)} edge ${edge.padStart(4)} ${liq}`);
182
+ console.log(` ${name} ${mkt.padStart(5)} \u2192 ${thesis.padStart(5)} edge ${edge.padStart(4)} ${liq}`);
181
183
  }
182
184
  console.log();
183
185
  }
@@ -56,15 +56,22 @@ async function performanceCommand(opts) {
56
56
  if (!ticker)
57
57
  continue;
58
58
  const action = fill.action || 'buy';
59
+ const side = fill.side || 'yes';
59
60
  const count = Math.round(parseFloat(fill.count_fp || fill.count || '0'));
60
61
  const yesPrice = Math.round(parseFloat(fill.yes_price_dollars || '0') * 100);
62
+ // buy yes = +count, sell yes = -count
63
+ // buy no = -count (short yes), sell no = +count (close short)
61
64
  let delta = count;
62
65
  if (action === 'sell')
63
- delta = -count;
66
+ delta = -delta;
67
+ if (side === 'no')
68
+ delta = -delta;
64
69
  const info = tickerMap.get(ticker) || { ticker, netQty: 0, totalCostCents: 0, totalContracts: 0, earliestFillTs: Infinity };
65
70
  info.netQty += delta;
66
71
  if (delta > 0) {
67
- info.totalCostCents += yesPrice * count;
72
+ // Entering a position — accumulate cost
73
+ const costPerContract = side === 'no' ? (100 - yesPrice) : yesPrice;
74
+ info.totalCostCents += costPerContract * count;
68
75
  info.totalContracts += count;
69
76
  }
70
77
  const fillTime = fill.created_time || fill.ts || fill.created_at;
@@ -0,0 +1,15 @@
1
+ /**
2
+ * sf telegram — Start Telegram bot
3
+ *
4
+ * Token is saved to ~/.sf/config.json on first use.
5
+ * --daemon: fork to background, write PID to ~/.sf/telegram.pid
6
+ * --stop: kill running daemon
7
+ * --status: check if daemon is running
8
+ */
9
+ export declare function telegramCommand(opts: {
10
+ token?: string;
11
+ chatId?: string;
12
+ daemon?: boolean;
13
+ stop?: boolean;
14
+ status?: boolean;
15
+ }): Promise<void>;
@@ -0,0 +1,125 @@
1
+ "use strict";
2
+ /**
3
+ * sf telegram — Start Telegram bot
4
+ *
5
+ * Token is saved to ~/.sf/config.json on first use.
6
+ * --daemon: fork to background, write PID to ~/.sf/telegram.pid
7
+ * --stop: kill running daemon
8
+ * --status: check if daemon is running
9
+ */
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.telegramCommand = telegramCommand;
15
+ const child_process_1 = require("child_process");
16
+ const fs_1 = __importDefault(require("fs"));
17
+ const path_1 = __importDefault(require("path"));
18
+ const os_1 = __importDefault(require("os"));
19
+ const config_js_1 = require("../config.js");
20
+ const PID_FILE = path_1.default.join(os_1.default.homedir(), '.sf', 'telegram.pid');
21
+ const LOG_FILE = path_1.default.join(os_1.default.homedir(), '.sf', 'telegram.log');
22
+ function readPid() {
23
+ try {
24
+ const pid = parseInt(fs_1.default.readFileSync(PID_FILE, 'utf-8').trim());
25
+ if (isNaN(pid))
26
+ return null;
27
+ try {
28
+ process.kill(pid, 0);
29
+ return pid;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ /** Resolve token: --token flag > config > env var. Save to config if new. */
40
+ function resolveToken(flagToken) {
41
+ const config = (0, config_js_1.loadConfig)();
42
+ // Flag takes priority
43
+ if (flagToken) {
44
+ // Save to config for future use
45
+ const file = (0, config_js_1.loadFileConfig)();
46
+ if (file.telegramBotToken !== flagToken) {
47
+ (0, config_js_1.saveConfig)({ ...file, telegramBotToken: flagToken });
48
+ }
49
+ return flagToken;
50
+ }
51
+ return config.telegramBotToken || null;
52
+ }
53
+ async function telegramCommand(opts) {
54
+ // ── sf telegram --stop ──
55
+ if (opts.stop) {
56
+ const pid = readPid();
57
+ if (pid) {
58
+ process.kill(pid, 'SIGTERM');
59
+ try {
60
+ fs_1.default.unlinkSync(PID_FILE);
61
+ }
62
+ catch { }
63
+ console.log(` Telegram bot stopped (PID ${pid})`);
64
+ }
65
+ else {
66
+ console.log(' No running Telegram bot found.');
67
+ }
68
+ return;
69
+ }
70
+ // ── sf telegram --status ──
71
+ if (opts.status) {
72
+ const pid = readPid();
73
+ if (pid) {
74
+ console.log(` Telegram bot running (PID ${pid})`);
75
+ console.log(` Log: ${LOG_FILE}`);
76
+ }
77
+ else {
78
+ console.log(' Telegram bot not running.');
79
+ }
80
+ return;
81
+ }
82
+ // Resolve token (saves to config on first use)
83
+ const token = resolveToken(opts.token);
84
+ if (!token) {
85
+ console.log(' No Telegram bot token configured.\n');
86
+ console.log(' Setup:');
87
+ console.log(' 1. Message @BotFather on Telegram → /newbot');
88
+ console.log(' 2. Copy the token');
89
+ console.log(' 3. Run: sf telegram --token YOUR_TOKEN\n');
90
+ console.log(' The token will be saved to ~/.sf/config.json for future use.');
91
+ return;
92
+ }
93
+ // ── sf telegram --daemon ──
94
+ if (opts.daemon) {
95
+ const existing = readPid();
96
+ if (existing) {
97
+ console.log(` Bot already running (PID ${existing}). Use --stop first.`);
98
+ return;
99
+ }
100
+ // Daemon doesn't need --token since it's in config now
101
+ const args = ['telegram'];
102
+ if (opts.chatId)
103
+ args.push('--chat-id', opts.chatId);
104
+ const sfBin = process.argv[1];
105
+ fs_1.default.mkdirSync(path_1.default.dirname(PID_FILE), { recursive: true });
106
+ const logStream = fs_1.default.openSync(LOG_FILE, 'a');
107
+ const child = (0, child_process_1.spawn)(process.execPath, [sfBin, ...args], {
108
+ detached: true,
109
+ stdio: ['ignore', logStream, logStream],
110
+ env: { ...process.env },
111
+ });
112
+ child.unref();
113
+ fs_1.default.writeFileSync(PID_FILE, String(child.pid));
114
+ console.log(` Telegram bot started in background (PID ${child.pid})`);
115
+ console.log(` Log: ${LOG_FILE}`);
116
+ console.log(` Stop: sf telegram --stop`);
117
+ return;
118
+ }
119
+ // ── sf telegram (foreground) ──
120
+ const { startBot } = await import('../telegram/bot.js');
121
+ await startBot({
122
+ token,
123
+ chatId: opts.chatId ? parseInt(opts.chatId) : undefined,
124
+ });
125
+ }
package/dist/config.d.ts CHANGED
@@ -16,6 +16,7 @@ export interface SFConfig {
16
16
  tavilyKey?: string;
17
17
  model?: string;
18
18
  tradingEnabled?: boolean;
19
+ telegramBotToken?: string;
19
20
  configuredAt?: string;
20
21
  }
21
22
  /**
package/dist/config.js CHANGED
@@ -53,6 +53,7 @@ function loadConfig() {
53
53
  tavilyKey: process.env.TAVILY_API_KEY || file.tavilyKey,
54
54
  model: process.env.SF_MODEL || file.model || DEFAULT_MODEL,
55
55
  tradingEnabled: file.tradingEnabled || false,
56
+ telegramBotToken: process.env.TELEGRAM_BOT_TOKEN || file.telegramBotToken,
56
57
  };
57
58
  }
58
59
  /**
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ const vitest_1 = require("vitest");
7
+ vitest_1.vi.mock('fs', () => ({
8
+ default: {
9
+ existsSync: vitest_1.vi.fn(),
10
+ readFileSync: vitest_1.vi.fn(),
11
+ writeFileSync: vitest_1.vi.fn(),
12
+ mkdirSync: vitest_1.vi.fn(),
13
+ unlinkSync: vitest_1.vi.fn(),
14
+ },
15
+ existsSync: vitest_1.vi.fn(),
16
+ readFileSync: vitest_1.vi.fn(),
17
+ writeFileSync: vitest_1.vi.fn(),
18
+ mkdirSync: vitest_1.vi.fn(),
19
+ unlinkSync: vitest_1.vi.fn(),
20
+ }));
21
+ const fs_1 = __importDefault(require("fs"));
22
+ const config_js_1 = require("./config.js");
23
+ (0, vitest_1.beforeEach)(() => {
24
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReset();
25
+ vitest_1.vi.mocked(fs_1.default.readFileSync).mockReset();
26
+ vitest_1.vi.mocked(fs_1.default.writeFileSync).mockReset();
27
+ vitest_1.vi.mocked(fs_1.default.mkdirSync).mockReset();
28
+ vitest_1.vi.mocked(fs_1.default.unlinkSync).mockReset();
29
+ // Clear env vars
30
+ delete process.env.SF_API_KEY;
31
+ delete process.env.SF_API_URL;
32
+ delete process.env.OPENROUTER_API_KEY;
33
+ delete process.env.KALSHI_API_KEY_ID;
34
+ delete process.env.KALSHI_PRIVATE_KEY_PATH;
35
+ delete process.env.TAVILY_API_KEY;
36
+ delete process.env.SF_MODEL;
37
+ delete process.env.TELEGRAM_BOT_TOKEN;
38
+ });
39
+ (0, vitest_1.describe)('loadFileConfig', () => {
40
+ (0, vitest_1.it)('returns empty object when no config file', () => {
41
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(false);
42
+ (0, vitest_1.expect)((0, config_js_1.loadFileConfig)()).toEqual({});
43
+ });
44
+ (0, vitest_1.it)('reads and parses config file', () => {
45
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(true);
46
+ vitest_1.vi.mocked(fs_1.default.readFileSync).mockReturnValue(JSON.stringify({ apiKey: 'sf_live_test' }));
47
+ (0, vitest_1.expect)((0, config_js_1.loadFileConfig)()).toEqual({ apiKey: 'sf_live_test' });
48
+ });
49
+ (0, vitest_1.it)('returns empty object on corrupt file', () => {
50
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(true);
51
+ vitest_1.vi.mocked(fs_1.default.readFileSync).mockReturnValue('not json');
52
+ (0, vitest_1.expect)((0, config_js_1.loadFileConfig)()).toEqual({});
53
+ });
54
+ });
55
+ (0, vitest_1.describe)('loadConfig', () => {
56
+ (0, vitest_1.it)('returns defaults when no file and no env', () => {
57
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(false);
58
+ const config = (0, config_js_1.loadConfig)();
59
+ (0, vitest_1.expect)(config.apiUrl).toBe('https://simplefunctions.dev');
60
+ (0, vitest_1.expect)(config.model).toBe('anthropic/claude-sonnet-4.6');
61
+ (0, vitest_1.expect)(config.tradingEnabled).toBe(false);
62
+ });
63
+ (0, vitest_1.it)('env vars override file values', () => {
64
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(true);
65
+ vitest_1.vi.mocked(fs_1.default.readFileSync).mockReturnValue(JSON.stringify({
66
+ apiKey: 'file_key',
67
+ model: 'file_model',
68
+ }));
69
+ process.env.SF_API_KEY = 'env_key';
70
+ const config = (0, config_js_1.loadConfig)();
71
+ (0, vitest_1.expect)(config.apiKey).toBe('env_key');
72
+ (0, vitest_1.expect)(config.model).toBe('file_model');
73
+ });
74
+ (0, vitest_1.it)('file values fill gaps', () => {
75
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(true);
76
+ vitest_1.vi.mocked(fs_1.default.readFileSync).mockReturnValue(JSON.stringify({
77
+ apiKey: 'file_key',
78
+ tavilyKey: 'tavily123',
79
+ }));
80
+ const config = (0, config_js_1.loadConfig)();
81
+ (0, vitest_1.expect)(config.apiKey).toBe('file_key');
82
+ (0, vitest_1.expect)(config.tavilyKey).toBe('tavily123');
83
+ });
84
+ });
85
+ (0, vitest_1.describe)('saveConfig', () => {
86
+ (0, vitest_1.it)('writes JSON with configuredAt', () => {
87
+ (0, config_js_1.saveConfig)({ apiKey: 'test_key' });
88
+ (0, vitest_1.expect)(vitest_1.vi.mocked(fs_1.default.mkdirSync)).toHaveBeenCalled();
89
+ (0, vitest_1.expect)(vitest_1.vi.mocked(fs_1.default.writeFileSync)).toHaveBeenCalled();
90
+ const written = JSON.parse(vitest_1.vi.mocked(fs_1.default.writeFileSync).mock.calls[0][1]);
91
+ (0, vitest_1.expect)(written.apiKey).toBe('test_key');
92
+ (0, vitest_1.expect)(written.configuredAt).toBeDefined();
93
+ });
94
+ });
95
+ (0, vitest_1.describe)('applyConfig', () => {
96
+ (0, vitest_1.it)('sets env vars from file when not already set', () => {
97
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(true);
98
+ vitest_1.vi.mocked(fs_1.default.readFileSync).mockReturnValue(JSON.stringify({
99
+ apiKey: 'file_key',
100
+ openrouterKey: 'or_key',
101
+ }));
102
+ (0, config_js_1.applyConfig)();
103
+ (0, vitest_1.expect)(process.env.SF_API_KEY).toBe('file_key');
104
+ (0, vitest_1.expect)(process.env.OPENROUTER_API_KEY).toBe('or_key');
105
+ });
106
+ (0, vitest_1.it)('does not override existing env vars', () => {
107
+ process.env.SF_API_KEY = 'existing_key';
108
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(true);
109
+ vitest_1.vi.mocked(fs_1.default.readFileSync).mockReturnValue(JSON.stringify({
110
+ apiKey: 'file_key',
111
+ }));
112
+ (0, config_js_1.applyConfig)();
113
+ (0, vitest_1.expect)(process.env.SF_API_KEY).toBe('existing_key');
114
+ });
115
+ });
116
+ (0, vitest_1.describe)('isConfigured', () => {
117
+ (0, vitest_1.it)('returns true when apiKey is set', () => {
118
+ process.env.SF_API_KEY = 'test';
119
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(false);
120
+ (0, vitest_1.expect)((0, config_js_1.isConfigured)()).toBe(true);
121
+ });
122
+ (0, vitest_1.it)('returns false when no apiKey', () => {
123
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(false);
124
+ (0, vitest_1.expect)((0, config_js_1.isConfigured)()).toBe(false);
125
+ });
126
+ });
127
+ (0, vitest_1.describe)('resetConfig', () => {
128
+ (0, vitest_1.it)('deletes config file if exists', () => {
129
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(true);
130
+ (0, config_js_1.resetConfig)();
131
+ (0, vitest_1.expect)(vitest_1.vi.mocked(fs_1.default.unlinkSync)).toHaveBeenCalled();
132
+ });
133
+ (0, vitest_1.it)('does nothing if no config file', () => {
134
+ vitest_1.vi.mocked(fs_1.default.existsSync).mockReturnValue(false);
135
+ (0, config_js_1.resetConfig)();
136
+ (0, vitest_1.expect)(vitest_1.vi.mocked(fs_1.default.unlinkSync)).not.toHaveBeenCalled();
137
+ });
138
+ });
package/dist/index.js CHANGED
@@ -52,6 +52,7 @@ const announcements_js_1 = require("./commands/announcements.js");
52
52
  const history_js_1 = require("./commands/history.js");
53
53
  const performance_js_1 = require("./commands/performance.js");
54
54
  const liquidity_js_1 = require("./commands/liquidity.js");
55
+ const telegram_js_1 = require("./commands/telegram.js");
55
56
  const utils_js_1 = require("./utils.js");
56
57
  // ── Apply ~/.sf/config.json to process.env BEFORE any command ────────────────
57
58
  // This means client.ts, kalshi.ts, agent.ts keep reading process.env and just work.
@@ -238,11 +239,12 @@ program
238
239
  // ── sf dashboard ──────────────────────────────────────────────────────────────
239
240
  program
240
241
  .command('dashboard')
241
- .description('Portfolio overview — theses, positions, risk, unpositioned edges')
242
+ .description('Portfolio overview — interactive TUI (default), or one-shot with --once/--json')
242
243
  .option('--json', 'JSON output')
244
+ .option('--once', 'One-time print (no interactive mode)')
243
245
  .action(async (opts, cmd) => {
244
246
  const g = cmd.optsWithGlobals();
245
- await run(() => (0, dashboard_js_1.dashboardCommand)({ json: opts.json, apiKey: g.apiKey, apiUrl: g.apiUrl }));
247
+ await run(() => (0, dashboard_js_1.dashboardCommand)({ json: opts.json, once: opts.once, apiKey: g.apiKey, apiUrl: g.apiUrl }));
246
248
  });
247
249
  // ── sf milestones ────────────────────────────────────────────────────────────
248
250
  program
@@ -409,6 +411,18 @@ program
409
411
  .action(async (opts) => {
410
412
  await run(() => (0, liquidity_js_1.liquidityCommand)(opts));
411
413
  });
414
+ // ── sf telegram ──────────────────────────────────────────────────────────────
415
+ program
416
+ .command('telegram')
417
+ .description('Start Telegram bot for monitoring and trading')
418
+ .option('--token <token>', 'Telegram bot token (or set TELEGRAM_BOT_TOKEN)')
419
+ .option('--chat-id <id>', 'Restrict to specific chat ID')
420
+ .option('--daemon', 'Run in background')
421
+ .option('--stop', 'Stop background bot')
422
+ .option('--status', 'Check if bot is running')
423
+ .action(async (opts) => {
424
+ await run(() => (0, telegram_js_1.telegramCommand)(opts));
425
+ });
412
426
  // ── sf strategies ─────────────────────────────────────────────────────────────
413
427
  (0, strategies_js_1.registerStrategies)(program);
414
428
  // ── Error wrapper ─────────────────────────────────────────────────────────────
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Agent bridge — connects Telegram to pi-agent-core
3
+ *
4
+ * Uses the SAME tools as sf agent --plain. Multi-turn tool calling
5
+ * is handled by pi-agent-core's Agent class (not manual OpenRouter calls).
6
+ */
7
+ import { SFClient } from '../client.js';
8
+ interface SessionState {
9
+ thesisId: string | null;
10
+ agentMessages: any[];
11
+ agent?: any;
12
+ }
13
+ export declare function getOrCreateAgent(sfClient: SFClient, session: SessionState): Promise<any>;
14
+ export declare function runAgentMessage(client: SFClient, session: SessionState, userMessage: string): Promise<string>;
15
+ export {};