perp-cli 0.4.8 → 0.4.9

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 (45) hide show
  1. package/README.md +44 -5
  2. package/dist/arb/reconcile.d.ts +28 -0
  3. package/dist/arb/reconcile.js +162 -0
  4. package/dist/commands/account.d.ts +1 -1
  5. package/dist/commands/account.js +300 -63
  6. package/dist/commands/agent.js +9 -5
  7. package/dist/commands/arb/index.js +243 -49
  8. package/dist/commands/arb-auto.js +67 -19
  9. package/dist/commands/arb.d.ts +2 -1
  10. package/dist/commands/arb.js +535 -131
  11. package/dist/commands/bot.js +207 -0
  12. package/dist/commands/funds.d.ts +4 -0
  13. package/dist/commands/funds.js +741 -0
  14. package/dist/commands/history.d.ts +2 -1
  15. package/dist/commands/history.js +508 -5
  16. package/dist/commands/manage.js +5 -3
  17. package/dist/commands/market.js +34 -1
  18. package/dist/commands/pnl.js +1 -1
  19. package/dist/commands/risk.d.ts +2 -0
  20. package/dist/commands/risk.js +56 -0
  21. package/dist/commands/settings.js +147 -0
  22. package/dist/commands/trade.d.ts +1 -1
  23. package/dist/commands/trade.js +241 -259
  24. package/dist/commands/wallet.d.ts +4 -0
  25. package/dist/commands/wallet.js +10 -0
  26. package/dist/config.d.ts +2 -2
  27. package/dist/config.js +11 -3
  28. package/dist/exchanges/hyperliquid-spot.d.ts +1 -0
  29. package/dist/exchanges/hyperliquid-spot.js +13 -21
  30. package/dist/exchanges/lighter-spot.js +13 -15
  31. package/dist/exchanges/lighter.js +82 -21
  32. package/dist/exchanges/pacifica.js +12 -0
  33. package/dist/execution-log.d.ts +1 -1
  34. package/dist/index.js +39 -123
  35. package/dist/mcp-server.js +43 -96
  36. package/dist/strategies/split-order.d.ts +49 -0
  37. package/dist/strategies/split-order.js +138 -0
  38. package/package.json +1 -1
  39. package/skills/perp-cli/SKILL.md +13 -26
  40. package/skills/perp-cli/references/agent-operations.md +19 -9
  41. package/skills/perp-cli/references/commands.md +38 -31
  42. package/skills/perp-cli/references/strategies.md +26 -16
  43. package/skills/perp-cli/scripts/funding-analysis.sh +2 -2
  44. package/skills/perp-cli/scripts/preflight.sh +1 -1
  45. package/skills/perp-cli/scripts/spot-perp-scan.sh +1 -1
@@ -1,6 +1,52 @@
1
1
  import chalk from "chalk";
2
2
  import { printJson, jsonOk, makeTable, formatUsd, withJsonErrors } from "../utils.js";
3
3
  import { loadRiskLimits, saveRiskLimits, assessRisk, getLiquidationDistances } from "../risk.js";
4
+ import { pingPacifica, pingHyperliquid, pingLighter } from "../shared-api.js";
5
+ /** Run health check and return results + render. Exported for use by status --health. */
6
+ export async function runHealthCheck(isJson) {
7
+ const [pacPing, hlPing, ltPing] = await Promise.all([
8
+ pingPacifica(),
9
+ pingHyperliquid(),
10
+ pingLighter(),
11
+ ]);
12
+ const toPingResult = (name, p) => ({
13
+ exchange: name,
14
+ status: p.ok ? "ok" : "down",
15
+ latency_ms: p.latencyMs,
16
+ error: p.ok ? undefined : `HTTP ${p.status}`,
17
+ });
18
+ const checks = [
19
+ toPingResult("pacifica", pacPing),
20
+ toPingResult("hyperliquid", hlPing),
21
+ toPingResult("lighter", ltPing),
22
+ ];
23
+ const allOk = checks.every(c => c.status === "ok");
24
+ if (isJson()) {
25
+ return printJson(jsonOk({ healthy: allOk, exchanges: checks }));
26
+ }
27
+ console.log(chalk.cyan.bold("\n Exchange Health Check\n"));
28
+ const rows = checks.map(c => {
29
+ const statusIcon = c.status === "ok"
30
+ ? chalk.green("OK")
31
+ : c.status === "degraded"
32
+ ? chalk.yellow("DEGRADED")
33
+ : chalk.red("DOWN");
34
+ const latency = c.latency_ms < 500
35
+ ? chalk.green(`${c.latency_ms}ms`)
36
+ : c.latency_ms < 2000
37
+ ? chalk.yellow(`${c.latency_ms}ms`)
38
+ : chalk.red(`${c.latency_ms}ms`);
39
+ return [
40
+ chalk.white.bold(c.exchange),
41
+ statusIcon,
42
+ latency,
43
+ c.error ? chalk.red(c.error) : chalk.gray("-"),
44
+ ];
45
+ });
46
+ console.log(makeTable(["Exchange", "Status", "Latency", "Error"], rows));
47
+ const overall = allOk ? chalk.green("ALL HEALTHY") : chalk.red("ISSUES DETECTED");
48
+ console.log(`\n Overall: ${overall}\n`);
49
+ }
4
50
  const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
5
51
  export function registerRiskCommands(program, getAdapterForExchange, isJson) {
6
52
  const risk = program.command("risk").description("Risk management and guardrails");
@@ -278,4 +324,14 @@ export function registerRiskCommands(program, getAdapterForExchange, isJson) {
278
324
  }
279
325
  });
280
326
  });
327
+ // ── deprecated: health (merged into risk) ──
328
+ const healthCmd = program
329
+ .command("health")
330
+ .description("Use 'perp status --health'")
331
+ .action(async () => {
332
+ if (!isJson())
333
+ console.log(chalk.yellow(" Use 'perp status --health' instead.\n"));
334
+ await runHealthCheck(isJson);
335
+ });
336
+ healthCmd._hidden = true;
281
337
  }
@@ -1,6 +1,26 @@
1
1
  import chalk from "chalk";
2
2
  import { printJson, jsonOk, withJsonErrors } from "../utils.js";
3
3
  import { loadSettings, saveSettings } from "../settings.js";
4
+ import { ENV_FILE, loadEnvFile, setEnvVar, EXCHANGE_ENV_MAP, validateKey } from "./init.js";
5
+ // Resolve exchange alias → env var key
6
+ function resolveEnvKey(nameOrKey) {
7
+ // Direct env var name
8
+ const upper = nameOrKey.toUpperCase();
9
+ for (const info of Object.values(EXCHANGE_ENV_MAP)) {
10
+ if (info.envKey === upper)
11
+ return { envKey: info.envKey, chain: info.chain };
12
+ }
13
+ // Exchange name alias
14
+ const info = EXCHANGE_ENV_MAP[nameOrKey.toLowerCase()];
15
+ if (info)
16
+ return { envKey: info.envKey, chain: info.chain };
17
+ // Common aliases
18
+ const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter" };
19
+ const aliased = aliases[nameOrKey.toLowerCase()];
20
+ if (aliased)
21
+ return resolveEnvKey(aliased);
22
+ return null;
23
+ }
4
24
  export function registerSettingsCommands(program, isJson, getAdapterForExchange) {
5
25
  const settings = program.command("settings").description("CLI settings (referrals, defaults, fees)");
6
26
  // ── settings show ──
@@ -230,4 +250,131 @@ export function registerSettingsCommands(program, isJson, getAdapterForExchange)
230
250
  console.log(chalk.green(`\n ${key} = ${value}\n`));
231
251
  });
232
252
  });
253
+ // ── settings env ──
254
+ const env = settings.command("env").description("Manage ~/.perp/.env configuration");
255
+ // ── settings env show ──
256
+ env
257
+ .command("show")
258
+ .description("Show current configuration")
259
+ .action(async () => {
260
+ const stored = loadEnvFile();
261
+ const entries = [];
262
+ for (const [exchange, info] of Object.entries(EXCHANGE_ENV_MAP)) {
263
+ const fromFile = stored[info.envKey];
264
+ const fromEnv = process.env[info.envKey];
265
+ if (fromFile) {
266
+ entries.push({ name: exchange, chain: info.chain, key: fromFile, source: "~/.perp/.env" });
267
+ }
268
+ else if (fromEnv) {
269
+ entries.push({ name: exchange, chain: info.chain, key: fromEnv, source: "environment" });
270
+ }
271
+ }
272
+ // Derive addresses
273
+ const results = [];
274
+ for (const entry of entries) {
275
+ const { valid, address } = await validateKey(entry.chain, entry.key);
276
+ results.push({ name: entry.name, address: valid ? address : "(invalid key)", source: entry.source });
277
+ }
278
+ if (isJson()) {
279
+ const data = results.map((r) => ({ exchange: r.name, address: r.address, source: r.source }));
280
+ return printJson(jsonOk({ envFile: ENV_FILE, exchanges: data }));
281
+ }
282
+ console.log(chalk.cyan.bold("\n perp-cli Configuration\n"));
283
+ console.log(` File: ${chalk.gray(ENV_FILE)}\n`);
284
+ if (results.length === 0) {
285
+ console.log(chalk.gray(" No keys configured. Run 'perp init' or 'perp settings env set <exchange> <key>'\n"));
286
+ return;
287
+ }
288
+ for (const { name, address, source } of results) {
289
+ console.log(` ${chalk.cyan(name.padEnd(14))} ${chalk.green(address)} ${chalk.gray(source)}`);
290
+ }
291
+ console.log();
292
+ });
293
+ // ── settings env set <exchange|key> <value> ──
294
+ env
295
+ .command("set <name> <value>")
296
+ .description("Set a key (exchange name or env var name)")
297
+ .action(async (name, value) => {
298
+ const resolved = resolveEnvKey(name);
299
+ if (resolved) {
300
+ // Validate the key
301
+ const { valid, address } = await validateKey(resolved.chain, value);
302
+ if (!valid) {
303
+ if (isJson()) {
304
+ const { jsonError } = await import("../utils.js");
305
+ return printJson(jsonError("INVALID_PARAMS", `Invalid ${resolved.chain} private key`));
306
+ }
307
+ console.error(chalk.red(`\n Invalid ${resolved.chain} private key.\n`));
308
+ process.exit(1);
309
+ }
310
+ const normalized = resolved.chain === "evm"
311
+ ? (value.startsWith("0x") ? value : `0x${value}`)
312
+ : value;
313
+ setEnvVar(resolved.envKey, normalized);
314
+ if (isJson())
315
+ return printJson(jsonOk({ key: resolved.envKey, address, file: ENV_FILE }));
316
+ console.log(chalk.green(`\n ${resolved.envKey} set.`));
317
+ console.log(` Address: ${chalk.gray(address)}`);
318
+ console.log(` File: ${chalk.gray("~/.perp/.env")}\n`);
319
+ }
320
+ else {
321
+ // Raw env var (e.g. LIGHTER_API_KEY, custom vars)
322
+ setEnvVar(name, value);
323
+ if (isJson())
324
+ return printJson(jsonOk({ key: name, file: ENV_FILE }));
325
+ console.log(chalk.green(`\n ${name} set.`));
326
+ console.log(` File: ${chalk.gray("~/.perp/.env")}\n`);
327
+ }
328
+ });
329
+ // ── settings env remove <name> ──
330
+ env
331
+ .command("remove <name>")
332
+ .description("Remove a key from ~/.perp/.env")
333
+ .action(async (name) => {
334
+ const resolved = resolveEnvKey(name);
335
+ const envKey = resolved?.envKey || name;
336
+ const env = loadEnvFile();
337
+ if (!(envKey in env)) {
338
+ if (isJson()) {
339
+ const { jsonError } = await import("../utils.js");
340
+ return printJson(jsonError("NOT_FOUND", `${envKey} not found in ~/.perp/.env`));
341
+ }
342
+ console.log(chalk.gray(`\n ${envKey} not found in ~/.perp/.env\n`));
343
+ return;
344
+ }
345
+ delete env[envKey];
346
+ // Rewrite file
347
+ const { writeFileSync } = await import("fs");
348
+ const lines = ["# perp-cli configuration", "# Generated by 'perp init' — edit freely", ""];
349
+ for (const [k, v] of Object.entries(env))
350
+ lines.push(`${k}=${v}`);
351
+ lines.push("");
352
+ writeFileSync(ENV_FILE, lines.join("\n"), { mode: 0o600 });
353
+ if (isJson())
354
+ return printJson(jsonOk({ removed: envKey }));
355
+ console.log(chalk.yellow(`\n ${envKey} removed from ~/.perp/.env\n`));
356
+ });
357
+ // ── settings env path ──
358
+ env
359
+ .command("path")
360
+ .description("Print env file path")
361
+ .action(() => {
362
+ if (isJson())
363
+ return printJson(jsonOk({ path: ENV_FILE }));
364
+ console.log(ENV_FILE);
365
+ });
366
+ // ── Deprecated top-level `env` command ──
367
+ const deprecatedEnv = program
368
+ .command("env")
369
+ .description("[deprecated] Use 'perp settings env'")
370
+ .allowUnknownOption()
371
+ .action(() => {
372
+ console.log(chalk.yellow("\n 'perp env' is deprecated. Use 'perp settings env' instead.\n"));
373
+ console.log(chalk.gray(" Examples:"));
374
+ console.log(chalk.gray(" perp settings env show"));
375
+ console.log(chalk.gray(" perp settings env set <exchange> <key>"));
376
+ console.log(chalk.gray(" perp settings env remove <name>"));
377
+ console.log(chalk.gray(" perp settings env path\n"));
378
+ });
379
+ deprecatedEnv._hidden = true;
233
380
  }
@@ -1,3 +1,3 @@
1
1
  import { Command } from "commander";
2
2
  import type { ExchangeAdapter } from "../exchanges/interface.js";
3
- export declare function registerTradeCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean, isDryRun?: () => boolean): void;
3
+ export declare function registerTradeCommands(program: Command, getAdapter: () => Promise<ExchangeAdapter>, isJson: () => boolean, isDryRun?: () => boolean, getAdapterForExchange?: (exchange: string) => Promise<ExchangeAdapter>): void;