perp-cli 0.4.8 → 0.4.10
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 +44 -5
- package/dist/arb/reconcile.d.ts +28 -0
- package/dist/arb/reconcile.js +162 -0
- package/dist/commands/account.d.ts +1 -1
- package/dist/commands/account.js +366 -62
- package/dist/commands/agent.js +9 -5
- package/dist/commands/arb/index.js +243 -49
- package/dist/commands/arb-auto.js +67 -19
- package/dist/commands/arb.d.ts +2 -1
- package/dist/commands/arb.js +535 -131
- package/dist/commands/bot.js +207 -0
- package/dist/commands/funds.d.ts +4 -0
- package/dist/commands/funds.js +741 -0
- package/dist/commands/history.d.ts +2 -1
- package/dist/commands/history.js +508 -5
- package/dist/commands/manage.js +5 -3
- package/dist/commands/market.js +34 -1
- package/dist/commands/pnl.js +1 -1
- package/dist/commands/risk.d.ts +2 -0
- package/dist/commands/risk.js +56 -0
- package/dist/commands/settings.js +147 -0
- package/dist/commands/trade.d.ts +1 -1
- package/dist/commands/trade.js +241 -259
- package/dist/commands/wallet.d.ts +4 -0
- package/dist/commands/wallet.js +10 -0
- package/dist/config.d.ts +2 -2
- package/dist/config.js +11 -3
- package/dist/exchanges/hyperliquid-spot.d.ts +1 -0
- package/dist/exchanges/hyperliquid-spot.js +13 -21
- package/dist/exchanges/lighter-spot.js +13 -15
- package/dist/exchanges/lighter.js +82 -21
- package/dist/exchanges/pacifica.js +12 -0
- package/dist/execution-log.d.ts +1 -1
- package/dist/index.js +39 -123
- package/dist/mcp-server.js +43 -96
- package/dist/strategies/split-order.d.ts +49 -0
- package/dist/strategies/split-order.js +138 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +13 -26
- package/skills/perp-cli/references/agent-operations.md +19 -9
- package/skills/perp-cli/references/commands.md +38 -31
- package/skills/perp-cli/references/strategies.md +26 -16
- package/skills/perp-cli/scripts/funding-analysis.sh +2 -2
- package/skills/perp-cli/scripts/preflight.sh +1 -1
- package/skills/perp-cli/scripts/spot-perp-scan.sh +1 -1
package/dist/commands/risk.js
CHANGED
|
@@ -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
|
}
|
package/dist/commands/trade.d.ts
CHANGED
|
@@ -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;
|