perp-cli 0.5.3 → 0.6.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.
package/README.md CHANGED
@@ -1,5 +1,9 @@
1
1
  # perp-cli
2
2
 
3
+ [![npm version](https://img.shields.io/npm/v/perp-cli.svg)](https://www.npmjs.com/package/perp-cli)
4
+ [![npm downloads](https://img.shields.io/npm/dw/perp-cli.svg)](https://www.npmjs.com/package/perp-cli)
5
+ [![license](https://img.shields.io/npm/l/perp-cli.svg)](https://github.com/hypurrquant/perp-cli/blob/main/LICENSE)
6
+
3
7
  Multi-DEX perpetual futures CLI — **Pacifica** (Solana), **Hyperliquid** (HyperEVM), **Lighter** (Ethereum).
4
8
 
5
9
  ```bash
@@ -58,6 +62,7 @@ Same EVM key works for both Hyperliquid and Lighter.
58
62
  | `plan` | Multi-step composite execution plans |
59
63
  | `rebalance` | Cross-exchange balance management |
60
64
  | `jobs` | Background job management (tmux) |
65
+ | `alerts` | Telegram funding rate alerts with background daemon |
61
66
  | `agent` | Schema introspection, capabilities, health check |
62
67
 
63
68
  ## Core Commands
@@ -125,6 +130,30 @@ perp --json bridge quote --from solana --to arbitrum --amount 100
125
130
  perp --json bridge send --from solana --to arbitrum --amount 100
126
131
  ```
127
132
 
133
+ ## Telegram Alerts
134
+
135
+ Funding rate alerts via Telegram with background daemon support.
136
+
137
+ ```bash
138
+ # Interactive setup (BotFather token + chat ID auto-detection)
139
+ perp alerts setup
140
+
141
+ # Add alert rules
142
+ perp alerts add ETH 30 # alert when ETH funding > 30% annualized
143
+ perp alerts add --all 50 # alert for any symbol > 50%
144
+
145
+ # Test & manage
146
+ perp alerts test # send test message
147
+ perp alerts list # show active rules
148
+
149
+ # Run daemon
150
+ perp alerts start # foreground
151
+ perp alerts start --background # tmux background daemon
152
+ perp alerts stop # stop background daemon
153
+ ```
154
+
155
+ Setup flow: BotFather token → bot validation → send `/start` to bot → auto-detect chat ID → test message.
156
+
128
157
  Exchange flag: `-e hyperliquid` / `-e pacifica` / `-e lighter` (aliases: `hl`, `pac`, `lt`).
129
158
 
130
159
  Global flags: `--json`, `--fields <f>`, `--ndjson`, `--dry-run`, `--dex <name>` (HIP-3), `-w, --wallet <name>`.
@@ -216,8 +216,10 @@ export function registerAgentCommands(program, getAdapter, isJson) {
216
216
  category: "analytics",
217
217
  commands: [
218
218
  "perp analytics summary --json",
219
- "perp analytics pnl --json",
220
- "perp analytics funding --json",
219
+ "perp analytics pnl --period 30d --json",
220
+ "perp analytics funding --period 30d --json",
221
+ "perp analytics funding --period 7d --daily --json",
222
+ "perp analytics compare --json",
221
223
  "perp portfolio --json",
222
224
  "perp health --json",
223
225
  "perp history list --json",
@@ -0,0 +1,2 @@
1
+ import { Command } from "commander";
2
+ export declare function registerAlertCommands(program: Command, isJson: () => boolean): void;
@@ -0,0 +1,487 @@
1
+ import chalk from "chalk";
2
+ import { createInterface } from "readline";
3
+ import { loadSettings, saveSettings } from "../settings.js";
4
+ import { validateBotToken as validateDiscordBot, sendDiscordDM, openDMChannel } from "../discord.js";
5
+ import { validateTelegramBot, sendTelegramMessage, getRecentChatId } from "../telegram.js";
6
+ import { printJson, jsonOk } from "../utils.js";
7
+ import { fetchAllFundingRates } from "../funding-rates.js";
8
+ function ask(rl, question) {
9
+ return new Promise((resolve) => rl.question(question, (answer) => resolve(answer.trim())));
10
+ }
11
+ function askChoice(rl, question, choices) {
12
+ return new Promise((resolve) => {
13
+ const choiceStr = choices.map((c, i) => `${i + 1}) ${c}`).join(" ");
14
+ rl.question(`${question} [${choiceStr}]: `, (answer) => {
15
+ const trimmed = answer.trim();
16
+ const idx = parseInt(trimmed) - 1;
17
+ if (idx >= 0 && idx < choices.length)
18
+ return resolve(choices[idx]);
19
+ const match = choices.find((c) => c.toLowerCase().startsWith(trimmed.toLowerCase()));
20
+ resolve(match || choices[0]);
21
+ });
22
+ });
23
+ }
24
+ /** Send alert message to all configured channels (Telegram and/or Discord) */
25
+ async function sendAlert(alerts, message) {
26
+ const errors = [];
27
+ if (alerts.telegram?.botToken && alerts.telegram?.chatId) {
28
+ try {
29
+ await sendTelegramMessage(alerts.telegram, message);
30
+ }
31
+ catch (err) {
32
+ errors.push(`Telegram: ${err instanceof Error ? err.message : err}`);
33
+ }
34
+ }
35
+ if (alerts.discord?.botToken && alerts.discord?.userId) {
36
+ try {
37
+ await sendDiscordDM(alerts.discord, message);
38
+ }
39
+ catch (err) {
40
+ errors.push(`Discord: ${err instanceof Error ? err.message : err}`);
41
+ }
42
+ }
43
+ if (errors.length > 0 && !alerts.telegram && !alerts.discord) {
44
+ throw new Error("No messaging service configured");
45
+ }
46
+ }
47
+ function hasAnyService(alerts) {
48
+ return !!(alerts.telegram?.botToken || alerts.discord?.botToken);
49
+ }
50
+ function serviceStatus(alerts) {
51
+ const parts = [];
52
+ if (alerts.telegram?.botToken)
53
+ parts.push(`Telegram (${alerts.telegram.chatId})`);
54
+ if (alerts.discord?.botToken)
55
+ parts.push(`Discord (${alerts.discord.userId})`);
56
+ return parts;
57
+ }
58
+ export function registerAlertCommands(program, isJson) {
59
+ const alerts = program.command("alerts").description("Funding rate alerts (Telegram / Discord)");
60
+ // ── alerts setup ──
61
+ alerts
62
+ .command("setup")
63
+ .description("Interactive alert setup (Telegram or Discord)")
64
+ .action(async () => {
65
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
66
+ try {
67
+ const settings = loadSettings();
68
+ console.log(chalk.cyan.bold("\n Alert Setup\n"));
69
+ // Show existing config
70
+ const services = serviceStatus(settings.alerts);
71
+ if (services.length > 0) {
72
+ console.log(chalk.gray(` Configured: ${services.join(", ")}\n`));
73
+ }
74
+ const service = await askChoice(rl, " Service", ["Telegram", "Discord"]);
75
+ if (service === "Telegram") {
76
+ await setupTelegram(rl, settings);
77
+ }
78
+ else {
79
+ await setupDiscord(rl, settings);
80
+ }
81
+ saveSettings(settings);
82
+ console.log(chalk.green.bold("\n Setup complete!"));
83
+ console.log(chalk.gray(" Config saved to ~/.perp/settings.json\n"));
84
+ console.log(chalk.white(" Next steps:"));
85
+ console.log(chalk.gray(` perp alerts add ETH 30 # ETH funding > 30% alert`));
86
+ console.log(chalk.gray(` perp alerts add --all 50 # Any symbol > 50%`));
87
+ console.log(chalk.gray(` perp alerts start # Start monitoring\n`));
88
+ rl.close();
89
+ }
90
+ catch (err) {
91
+ rl.close();
92
+ throw err;
93
+ }
94
+ });
95
+ // ── alerts test ──
96
+ alerts
97
+ .command("test")
98
+ .description("Send a test message to verify connection")
99
+ .action(async () => {
100
+ const settings = loadSettings();
101
+ if (!hasAnyService(settings.alerts)) {
102
+ console.log(chalk.red(" Not configured. Run: perp alerts setup\n"));
103
+ return;
104
+ }
105
+ const msg = `[TEST] perp-cli alert system is working. ${new Date().toISOString()}`;
106
+ try {
107
+ await sendAlert(settings.alerts, msg);
108
+ if (isJson())
109
+ return printJson(jsonOk({ status: "sent", services: serviceStatus(settings.alerts) }));
110
+ console.log(chalk.green(` Test message sent! (${serviceStatus(settings.alerts).join(", ")})\n`));
111
+ }
112
+ catch (err) {
113
+ if (isJson())
114
+ return printJson(jsonOk({ error: String(err) }));
115
+ console.log(chalk.red(` Failed: ${err instanceof Error ? err.message : err}\n`));
116
+ }
117
+ });
118
+ // ── alerts add ──
119
+ alerts
120
+ .command("add")
121
+ .description("Add a funding rate alert rule")
122
+ .argument("[symbol]", "Symbol to monitor (e.g. ETH, BTC)")
123
+ .argument("[threshold]", "Annual funding rate % threshold")
124
+ .option("--all", "Monitor all symbols")
125
+ .option("--exchange <ex>", "Filter by exchange (hl, pac, lt)")
126
+ .option("--below", "Alert when rate drops below threshold (default: above)")
127
+ .action((symbol, thresholdStr, opts) => {
128
+ const sym = opts.all ? "*" : symbol?.toUpperCase();
129
+ const rawThreshold = opts.all && symbol && !thresholdStr ? symbol : thresholdStr;
130
+ const threshold = parseFloat(rawThreshold || "30");
131
+ if (!sym) {
132
+ console.log(chalk.red(" Specify a symbol or use --all\n"));
133
+ return;
134
+ }
135
+ if (isNaN(threshold) || threshold <= 0) {
136
+ console.log(chalk.red(" Threshold must be a positive number (annual %)\n"));
137
+ return;
138
+ }
139
+ const aliasMap = { hl: "hyperliquid", pac: "pacifica", lt: "lighter" };
140
+ const exchange = opts.exchange ? (aliasMap[opts.exchange.toLowerCase()] || opts.exchange.toLowerCase()) : "*";
141
+ const settings = loadSettings();
142
+ const rule = {
143
+ id: `${sym}-${exchange}-${Date.now()}`,
144
+ symbol: sym,
145
+ exchange,
146
+ threshold,
147
+ direction: opts.below ? "below" : "above",
148
+ enabled: true,
149
+ };
150
+ settings.alerts.rules.push(rule);
151
+ saveSettings(settings);
152
+ if (isJson())
153
+ return printJson(jsonOk(rule));
154
+ const exLabel = exchange === "*" ? "all exchanges" : exchange;
155
+ const dirLabel = opts.below ? "below" : "above";
156
+ console.log(chalk.green(` Alert added: ${sym === "*" ? "ALL" : sym} on ${exLabel} — ${dirLabel} ${threshold}% annual\n`));
157
+ });
158
+ // ── alerts list ──
159
+ alerts
160
+ .command("list")
161
+ .description("List configured alert rules")
162
+ .action(() => {
163
+ const settings = loadSettings();
164
+ const rules = settings.alerts.rules;
165
+ const tg = settings.alerts.telegram;
166
+ const dc = settings.alerts.discord;
167
+ if (isJson()) {
168
+ return printJson(jsonOk({
169
+ telegram: tg ? { configured: true, chatId: tg.chatId } : { configured: false },
170
+ discord: dc ? { configured: true, userId: dc.userId } : { configured: false },
171
+ rules,
172
+ intervalSec: settings.alerts.intervalSec,
173
+ cooldownMin: settings.alerts.cooldownMin,
174
+ }));
175
+ }
176
+ console.log(chalk.cyan.bold("\n Alert Configuration\n"));
177
+ if (tg?.botToken) {
178
+ console.log(` Telegram: ${chalk.green("configured")} (Chat: ${tg.chatId})`);
179
+ }
180
+ else {
181
+ console.log(` Telegram: ${chalk.gray("not configured")}`);
182
+ }
183
+ if (dc?.botToken) {
184
+ console.log(` Discord: ${chalk.green("configured")} (User: ${dc.userId})`);
185
+ }
186
+ else {
187
+ console.log(` Discord: ${chalk.gray("not configured")}`);
188
+ }
189
+ if (!tg?.botToken && !dc?.botToken) {
190
+ console.log(chalk.yellow(` Run: perp alerts setup`));
191
+ }
192
+ console.log(` Interval: ${settings.alerts.intervalSec}s | Cooldown: ${settings.alerts.cooldownMin}min\n`);
193
+ if (rules.length === 0) {
194
+ console.log(chalk.gray(" No alert rules. Add one: perp alerts add ETH 30\n"));
195
+ return;
196
+ }
197
+ console.log(chalk.white(" Rules:"));
198
+ for (const r of rules) {
199
+ const sym = r.symbol === "*" ? "ALL" : r.symbol;
200
+ const ex = r.exchange === "*" ? "all" : r.exchange;
201
+ const dir = r.direction === "above" ? ">" : "<";
202
+ const status = r.enabled ? chalk.green("ON") : chalk.gray("OFF");
203
+ console.log(` ${status} ${chalk.white.bold(sym.padEnd(6))} ${ex.padEnd(12)} ${dir} ${r.threshold}% annual`);
204
+ }
205
+ console.log();
206
+ });
207
+ // ── alerts remove ──
208
+ alerts
209
+ .command("remove")
210
+ .description("Remove an alert rule")
211
+ .argument("<symbol>", "Symbol to remove (or 'all')")
212
+ .action((symbol) => {
213
+ const settings = loadSettings();
214
+ const sym = symbol.toUpperCase();
215
+ const before = settings.alerts.rules.length;
216
+ if (sym === "ALL") {
217
+ settings.alerts.rules = [];
218
+ }
219
+ else {
220
+ settings.alerts.rules = settings.alerts.rules.filter(r => r.symbol !== sym);
221
+ }
222
+ const removed = before - settings.alerts.rules.length;
223
+ saveSettings(settings);
224
+ if (isJson())
225
+ return printJson(jsonOk({ removed, remaining: settings.alerts.rules.length }));
226
+ if (removed > 0) {
227
+ console.log(chalk.green(` Removed ${removed} rule(s) for ${sym}\n`));
228
+ }
229
+ else {
230
+ console.log(chalk.gray(` No rules found for ${sym}\n`));
231
+ }
232
+ });
233
+ // ── alerts start ──
234
+ alerts
235
+ .command("start")
236
+ .description("Start funding rate alert daemon")
237
+ .option("--interval <sec>", "Check interval in seconds")
238
+ .option("--cooldown <min>", "Cooldown per symbol after alert fires (minutes)")
239
+ .option("--background", "Run in background (tmux)")
240
+ .action(async (opts) => {
241
+ const settings = loadSettings();
242
+ if (!hasAnyService(settings.alerts)) {
243
+ console.log(chalk.red(" No messaging service configured. Run: perp alerts setup\n"));
244
+ return;
245
+ }
246
+ const rules = settings.alerts.rules.filter(r => r.enabled);
247
+ if (rules.length === 0) {
248
+ console.log(chalk.red(" No active alert rules. Add one: perp alerts add ETH 30\n"));
249
+ return;
250
+ }
251
+ const intervalSec = parseInt(opts.interval || String(settings.alerts.intervalSec)) || 300;
252
+ const cooldownMin = parseInt(opts.cooldown || String(settings.alerts.cooldownMin)) || 60;
253
+ // ── Background mode: launch in tmux ──
254
+ if (opts.background) {
255
+ const { execSync } = await import("child_process");
256
+ try {
257
+ execSync("which tmux", { stdio: "ignore" });
258
+ }
259
+ catch {
260
+ console.log(chalk.red(" tmux required for --background. Install: brew install tmux\n"));
261
+ return;
262
+ }
263
+ const session = `perp-alerts-${Date.now().toString(36)}`;
264
+ const nodeCmd = process.argv[0];
265
+ const cliPath = process.argv[1];
266
+ const args = [
267
+ "alerts", "start",
268
+ "--interval", String(intervalSec),
269
+ "--cooldown", String(cooldownMin),
270
+ ].join(" ");
271
+ // Pass through env vars for keys
272
+ const envKeys = ["LIGHTER_PRIVATE_KEY", "LIGHTER_API_KEY", "LIGHTER_ACCOUNT_INDEX", "HL_PRIVATE_KEY", "PACIFICA_PRIVATE_KEY", "PRIVATE_KEY"];
273
+ const envStr = envKeys.filter(k => process.env[k]).map(k => `${k}='${process.env[k]}'`).join(" ");
274
+ const cmd = `${envStr} ${nodeCmd} ${cliPath} ${args}`;
275
+ execSync(`tmux new-session -d -s ${session} '${cmd.replace(/'/g, "'\\''")}'`);
276
+ if (isJson())
277
+ return printJson(jsonOk({ status: "started", session, rules: rules.length, interval: intervalSec }));
278
+ console.log(chalk.green(`\n Alert daemon started in background.`));
279
+ console.log(` Session: ${chalk.white.bold(session)}`);
280
+ console.log(` Rules: ${rules.length} active, ${intervalSec}s interval`);
281
+ console.log(` Attach: ${chalk.gray(`tmux attach -t ${session}`)}`);
282
+ console.log(` Stop: ${chalk.gray(`tmux kill-session -t ${session}`)}\n`);
283
+ return;
284
+ }
285
+ // ── Foreground mode ──
286
+ const cooldowns = new Map();
287
+ const services = serviceStatus(settings.alerts);
288
+ console.log(chalk.cyan.bold("\n Funding Rate Alert Daemon\n"));
289
+ console.log(chalk.gray(` Rules: ${rules.length} active`));
290
+ console.log(chalk.gray(` Interval: ${intervalSec}s`));
291
+ console.log(chalk.gray(` Cooldown: ${cooldownMin}min`));
292
+ console.log(chalk.gray(` Send via: ${services.join(", ")}`));
293
+ console.log(chalk.gray(` Press Ctrl+C to stop\n`));
294
+ try {
295
+ await sendAlert(settings.alerts, `Alert daemon started — ${rules.length} rule(s), ${intervalSec}s interval`);
296
+ }
297
+ catch { /* non-critical */ }
298
+ let cycle = 0;
299
+ const check = async () => {
300
+ cycle++;
301
+ const ts = new Date().toLocaleTimeString();
302
+ try {
303
+ const snapshot = await fetchAllFundingRates();
304
+ let alertCount = 0;
305
+ for (const comparison of snapshot.symbols) {
306
+ for (const rate of comparison.rates) {
307
+ const annualPct = Math.abs(rate.annualizedPct);
308
+ for (const rule of rules) {
309
+ if (rule.symbol !== "*" && rule.symbol !== comparison.symbol)
310
+ continue;
311
+ if (rule.exchange !== "*" && rule.exchange !== rate.exchange)
312
+ continue;
313
+ const triggered = rule.direction === "above"
314
+ ? annualPct >= rule.threshold
315
+ : annualPct <= rule.threshold;
316
+ if (!triggered)
317
+ continue;
318
+ const cooldownKey = `${comparison.symbol}:${rate.exchange}:${rule.id}`;
319
+ const lastAlert = cooldowns.get(cooldownKey) ?? 0;
320
+ if (Date.now() - lastAlert < cooldownMin * 60_000)
321
+ continue;
322
+ cooldowns.set(cooldownKey, Date.now());
323
+ alertCount++;
324
+ const exAbbr = rate.exchange === "pacifica" ? "PAC" : rate.exchange === "hyperliquid" ? "HL" : "LT";
325
+ const sign = rate.annualizedPct > 0 ? "+" : "";
326
+ const msg = [
327
+ `${comparison.symbol} funding alert`,
328
+ `${exAbbr}: ${sign}${rate.annualizedPct.toFixed(1)}% annual (raw: ${(rate.fundingRate * 100).toFixed(4)}%)`,
329
+ `Threshold: ${rule.direction} ${rule.threshold}%`,
330
+ `Price: $${comparison.bestMarkPrice.toLocaleString()}`,
331
+ ].join("\n");
332
+ try {
333
+ await sendAlert(settings.alerts, msg);
334
+ }
335
+ catch (err) {
336
+ console.log(chalk.red(` [${ts}] Send failed: ${err instanceof Error ? err.message : err}`));
337
+ }
338
+ }
339
+ }
340
+ }
341
+ console.log(chalk.gray(` [${ts}] Cycle ${cycle} — ${snapshot.symbols.length} symbols, ${alertCount} alert(s)`));
342
+ }
343
+ catch (err) {
344
+ console.log(chalk.red(` [${ts}] Error: ${err instanceof Error ? err.message : err}`));
345
+ }
346
+ };
347
+ await check();
348
+ const timer = setInterval(check, intervalSec * 1000);
349
+ process.on("SIGINT", () => {
350
+ clearInterval(timer);
351
+ console.log(chalk.gray("\n Alert daemon stopped.\n"));
352
+ process.exit(0);
353
+ });
354
+ await new Promise(() => { });
355
+ });
356
+ // ── alerts stop ──
357
+ alerts
358
+ .command("stop")
359
+ .description("Stop background alert daemon")
360
+ .action(async () => {
361
+ const { execSync } = await import("child_process");
362
+ try {
363
+ const sessions = execSync("tmux list-sessions -F '#{session_name}'", { encoding: "utf-8" })
364
+ .trim().split("\n").filter(s => s.startsWith("perp-alerts-"));
365
+ if (sessions.length === 0) {
366
+ if (isJson())
367
+ return printJson(jsonOk({ status: "none" }));
368
+ console.log(chalk.gray(" No alert daemon running.\n"));
369
+ return;
370
+ }
371
+ for (const session of sessions) {
372
+ try {
373
+ execSync(`tmux kill-session -t ${session}`);
374
+ }
375
+ catch { /* already dead */ }
376
+ }
377
+ if (isJson())
378
+ return printJson(jsonOk({ status: "stopped", sessions }));
379
+ console.log(chalk.green(` Stopped ${sessions.length} alert daemon(s): ${sessions.join(", ")}\n`));
380
+ }
381
+ catch {
382
+ if (isJson())
383
+ return printJson(jsonOk({ status: "no_tmux" }));
384
+ console.log(chalk.gray(" No tmux sessions found.\n"));
385
+ }
386
+ });
387
+ }
388
+ // ── Setup flows ──
389
+ async function setupTelegram(rl, settings) {
390
+ console.log(chalk.cyan.bold("\n Telegram Setup\n"));
391
+ console.log(chalk.white(" Step 1: Create a bot"));
392
+ console.log(chalk.gray(" 1. Open Telegram, search @BotFather"));
393
+ console.log(chalk.gray(" 2. Send /newbot, follow prompts"));
394
+ console.log(chalk.gray(" 3. Copy the bot token\n"));
395
+ const botToken = await ask(rl, " Bot Token: ");
396
+ if (!botToken) {
397
+ console.log(chalk.red(" No token provided."));
398
+ return;
399
+ }
400
+ console.log(chalk.gray(" Validating..."));
401
+ const { valid, username } = await validateTelegramBot(botToken);
402
+ if (!valid) {
403
+ console.log(chalk.red(" Invalid bot token.\n"));
404
+ return;
405
+ }
406
+ console.log(chalk.green(` Bot: ${username}\n`));
407
+ console.log(chalk.white(" Step 2: Get your Chat ID"));
408
+ console.log(chalk.gray(` 1. Open Telegram, search ${username}`));
409
+ console.log(chalk.gray(" 2. Send /start to the bot"));
410
+ console.log(chalk.gray(" 3. Come back here\n"));
411
+ const method = await askChoice(rl, " How to get Chat ID?", ["auto-detect", "manual"]);
412
+ let chatId = "";
413
+ if (method === "auto-detect") {
414
+ console.log(chalk.gray(`\n Make sure you sent /start to ${username}, then press Enter...`));
415
+ await ask(rl, " ");
416
+ console.log(chalk.gray(" Checking..."));
417
+ const recent = await getRecentChatId(botToken);
418
+ if (recent) {
419
+ chatId = recent.chatId;
420
+ console.log(chalk.green(` Found: ${recent.firstName} (${chatId})\n`));
421
+ }
422
+ else {
423
+ console.log(chalk.yellow(" No messages found. Enter Chat ID manually.\n"));
424
+ }
425
+ }
426
+ if (!chatId) {
427
+ console.log(chalk.gray(" You can find your Chat ID at @userinfobot or @RawDataBot\n"));
428
+ chatId = await ask(rl, " Chat ID: ");
429
+ }
430
+ if (!chatId) {
431
+ console.log(chalk.red(" No Chat ID provided.\n"));
432
+ return;
433
+ }
434
+ // Test message
435
+ console.log(chalk.gray(" Sending test message..."));
436
+ const config = { botToken, chatId };
437
+ try {
438
+ await sendTelegramMessage(config, "perp-cli alert setup complete. Funding rate alerts will be sent here.");
439
+ console.log(chalk.green(" Test message sent! Check Telegram."));
440
+ }
441
+ catch (err) {
442
+ console.log(chalk.red(` Failed: ${err instanceof Error ? err.message : err}`));
443
+ console.log(chalk.gray(" Make sure you sent /start to the bot first.\n"));
444
+ return;
445
+ }
446
+ settings.alerts.telegram = { botToken, chatId };
447
+ }
448
+ async function setupDiscord(rl, settings) {
449
+ console.log(chalk.cyan.bold("\n Discord Setup\n"));
450
+ console.log(chalk.white(" Step 1: Create a bot"));
451
+ console.log(chalk.gray(" 1. Go to https://discord.com/developers/applications"));
452
+ console.log(chalk.gray(" 2. New Application → Bot → Reset Token → Copy"));
453
+ console.log(chalk.gray(" 3. Enable MESSAGE CONTENT intent"));
454
+ console.log(chalk.gray(" 4. OAuth2 → bot scope → Send Messages → Invite\n"));
455
+ const botToken = await ask(rl, " Bot Token: ");
456
+ if (!botToken) {
457
+ console.log(chalk.red(" No token provided."));
458
+ return;
459
+ }
460
+ console.log(chalk.gray(" Validating..."));
461
+ const { valid, username } = await validateDiscordBot(botToken);
462
+ if (!valid) {
463
+ console.log(chalk.red(" Invalid bot token.\n"));
464
+ return;
465
+ }
466
+ console.log(chalk.green(` Bot: ${username}\n`));
467
+ console.log(chalk.white(" Step 2: Your User ID"));
468
+ console.log(chalk.gray(" Settings → Advanced → Developer Mode → Right-click → Copy ID\n"));
469
+ const userId = await ask(rl, " User ID: ");
470
+ if (!userId || !/^\d{17,20}$/.test(userId)) {
471
+ console.log(chalk.red(" Invalid User ID.\n"));
472
+ return;
473
+ }
474
+ console.log(chalk.gray(" Sending test DM..."));
475
+ const config = { botToken, userId };
476
+ try {
477
+ const channelId = await openDMChannel(config);
478
+ config.channelId = channelId;
479
+ await sendDiscordDM(config, "perp-cli alert setup complete. Funding rate alerts will be sent here.");
480
+ console.log(chalk.green(" Test DM sent! Check Discord."));
481
+ }
482
+ catch (err) {
483
+ console.log(chalk.red(` Failed: ${err instanceof Error ? err.message : err}`));
484
+ return;
485
+ }
486
+ settings.alerts.discord = { botToken, userId, channelId: config.channelId };
487
+ }