@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.
- package/README.md +205 -48
- package/dist/cache.d.ts +6 -0
- package/dist/cache.js +31 -0
- package/dist/cache.test.d.ts +1 -0
- package/dist/cache.test.js +73 -0
- package/dist/client.test.d.ts +1 -0
- package/dist/client.test.js +89 -0
- package/dist/commands/agent.js +257 -70
- package/dist/commands/dashboard.d.ts +6 -3
- package/dist/commands/dashboard.js +28 -26
- package/dist/commands/performance.js +9 -2
- package/dist/commands/telegram.d.ts +15 -0
- package/dist/commands/telegram.js +125 -0
- package/dist/config.d.ts +1 -0
- package/dist/config.js +1 -0
- package/dist/config.test.d.ts +1 -0
- package/dist/config.test.js +138 -0
- package/dist/index.js +16 -2
- package/dist/telegram/agent-bridge.d.ts +15 -0
- package/dist/telegram/agent-bridge.js +368 -0
- package/dist/telegram/bot.d.ts +10 -0
- package/dist/telegram/bot.js +297 -0
- package/dist/telegram/commands.d.ts +11 -0
- package/dist/telegram/commands.js +120 -0
- package/dist/telegram/format.d.ts +11 -0
- package/dist/telegram/format.js +51 -0
- package/dist/telegram/format.test.d.ts +1 -0
- package/dist/telegram/format.test.js +73 -0
- package/dist/telegram/poller.d.ts +6 -0
- package/dist/telegram/poller.js +32 -0
- package/dist/topics.test.d.ts +1 -0
- package/dist/topics.test.js +54 -0
- package/dist/tui/border.d.ts +33 -0
- package/dist/tui/border.js +87 -0
- package/dist/tui/chart.d.ts +19 -0
- package/dist/tui/chart.js +117 -0
- package/dist/tui/dashboard.d.ts +9 -0
- package/dist/tui/dashboard.js +779 -0
- package/dist/tui/layout.d.ts +16 -0
- package/dist/tui/layout.js +41 -0
- package/dist/tui/screen.d.ts +33 -0
- package/dist/tui/screen.js +102 -0
- package/dist/tui/state.d.ts +40 -0
- package/dist/tui/state.js +36 -0
- package/dist/tui/widgets/commandbar.d.ts +8 -0
- package/dist/tui/widgets/commandbar.js +82 -0
- package/dist/tui/widgets/detail.d.ts +9 -0
- package/dist/tui/widgets/detail.js +151 -0
- package/dist/tui/widgets/edges.d.ts +4 -0
- package/dist/tui/widgets/edges.js +33 -0
- package/dist/tui/widgets/liquidity.d.ts +9 -0
- package/dist/tui/widgets/liquidity.js +142 -0
- package/dist/tui/widgets/orders.d.ts +4 -0
- package/dist/tui/widgets/orders.js +37 -0
- package/dist/tui/widgets/portfolio.d.ts +4 -0
- package/dist/tui/widgets/portfolio.js +58 -0
- package/dist/tui/widgets/signals.d.ts +4 -0
- package/dist/tui/widgets/signals.js +31 -0
- package/dist/tui/widgets/statusbar.d.ts +8 -0
- package/dist/tui/widgets/statusbar.js +72 -0
- package/dist/tui/widgets/thesis.d.ts +4 -0
- package/dist/tui/widgets/thesis.js +66 -0
- package/dist/tui/widgets/trade.d.ts +9 -0
- package/dist/tui/widgets/trade.js +117 -0
- package/dist/tui/widgets/upcoming.d.ts +4 -0
- package/dist/tui/widgets/upcoming.js +41 -0
- package/dist/tui/widgets/whatif.d.ts +7 -0
- package/dist/tui/widgets/whatif.js +113 -0
- package/dist/utils.test.d.ts +1 -0
- package/dist/utils.test.js +111 -0
- package/package.json +6 -2
|
@@ -1,17 +1,19 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
/**
|
|
3
|
-
* sf dashboard —
|
|
3
|
+
* sf dashboard — Commander entry point
|
|
4
4
|
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
-
//
|
|
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
|
|
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
|
-
// ──
|
|
98
|
+
// ── One-time formatted output ──
|
|
96
99
|
console.log();
|
|
97
100
|
console.log(' SimpleFunctions Dashboard');
|
|
98
|
-
console.log(' ' + '
|
|
101
|
+
console.log(' ' + '\u2500'.repeat(50));
|
|
99
102
|
console.log();
|
|
100
|
-
//
|
|
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
|
-
//
|
|
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(' ' + '
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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)}
|
|
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 = -
|
|
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
|
-
|
|
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
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 —
|
|
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 {};
|