@spfunctions/cli 1.4.4 → 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 +245 -67
  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
@@ -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 {};