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.
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 +366 -62
  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,42 +1,277 @@
1
1
  import { PacificaAdapter } from "../exchanges/pacifica.js";
2
- import { HyperliquidAdapter } from "../exchanges/hyperliquid.js";
3
- import { makeTable, formatUsd, formatPnl, printJson, jsonOk, jsonError, symbolMatch } from "../utils.js";
2
+ import { makeTable, formatUsd, formatPnl, printJson, jsonOk, jsonError, symbolMatch, withJsonErrors } from "../utils.js";
4
3
  import chalk from "chalk";
4
+ import { assessRisk } from "../risk.js";
5
+ const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
6
+ async function fetchExchangeSnapshot(name, getAdapter) {
7
+ try {
8
+ const adapter = await getAdapter(name);
9
+ const [balance, positions, orders] = await Promise.all([
10
+ adapter.getBalance(),
11
+ adapter.getPositions(),
12
+ adapter.getOpenOrders(),
13
+ ]);
14
+ return {
15
+ exchange: name,
16
+ connected: true,
17
+ balance,
18
+ positions,
19
+ openOrders: orders.length,
20
+ };
21
+ }
22
+ catch (err) {
23
+ return {
24
+ exchange: name,
25
+ connected: false,
26
+ balance: null,
27
+ positions: [],
28
+ openOrders: 0,
29
+ error: err instanceof Error ? err.message : String(err),
30
+ };
31
+ }
32
+ }
33
+ function buildSummary(snapshots) {
34
+ let totalEquity = 0;
35
+ let totalAvailable = 0;
36
+ let totalMarginUsed = 0;
37
+ let totalUnrealizedPnl = 0;
38
+ let totalPositions = 0;
39
+ let totalOpenOrders = 0;
40
+ const allPositions = [];
41
+ for (const snap of snapshots) {
42
+ if (snap.balance) {
43
+ totalEquity += Number(snap.balance.equity);
44
+ totalAvailable += Number(snap.balance.available);
45
+ totalMarginUsed += Number(snap.balance.marginUsed);
46
+ totalUnrealizedPnl += Number(snap.balance.unrealizedPnl);
47
+ }
48
+ totalPositions += snap.positions.length;
49
+ totalOpenOrders += snap.openOrders;
50
+ for (const pos of snap.positions) {
51
+ allPositions.push({ ...pos, exchange: snap.exchange });
52
+ }
53
+ }
54
+ // Risk metrics
55
+ const marginUtilization = totalEquity > 0 ? (totalMarginUsed / totalEquity) * 100 : 0;
56
+ let largestPosition = null;
57
+ for (const pos of allPositions) {
58
+ const notional = Math.abs(Number(pos.size) * Number(pos.markPrice));
59
+ if (!largestPosition || notional > largestPosition.notional) {
60
+ largestPosition = { symbol: pos.symbol, exchange: pos.exchange, notional };
61
+ }
62
+ }
63
+ const exchangeConcentration = snapshots
64
+ .filter(s => s.balance && Number(s.balance.equity) > 0)
65
+ .map(s => ({
66
+ exchange: s.exchange,
67
+ pct: totalEquity > 0 ? (Number(s.balance.equity) / totalEquity) * 100 : 0,
68
+ }))
69
+ .sort((a, b) => b.pct - a.pct);
70
+ // Full risk assessment (reuses same data, no extra API calls)
71
+ const riskBalances = snapshots
72
+ .filter(s => s.balance)
73
+ .map(s => ({ exchange: s.exchange, balance: s.balance }));
74
+ const riskPositions = allPositions.map(p => ({ exchange: p.exchange, position: p }));
75
+ const assessment = assessRisk(riskBalances, riskPositions);
76
+ return {
77
+ totalEquity,
78
+ totalAvailable,
79
+ totalMarginUsed,
80
+ totalUnrealizedPnl,
81
+ totalPositions,
82
+ totalOpenOrders,
83
+ exchanges: snapshots,
84
+ positions: allPositions,
85
+ riskMetrics: {
86
+ marginUtilization,
87
+ largestPosition,
88
+ exchangeConcentration,
89
+ },
90
+ risk: {
91
+ level: assessment.level,
92
+ canTrade: assessment.canTrade,
93
+ violations: assessment.violations,
94
+ },
95
+ };
96
+ }
5
97
  function pac(adapter) {
6
98
  if (!(adapter instanceof PacificaAdapter))
7
99
  throw new Error("This command requires --exchange pacifica");
8
100
  return adapter;
9
101
  }
10
- export function registerAccountCommands(program, getAdapter, isJson) {
102
+ export function registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange) {
11
103
  const account = program.command("account").description("Account commands");
12
- account
13
- .command("info")
14
- .description("Show account balance and info")
15
- .action(async () => {
16
- const adapter = await getAdapter();
17
- const bal = await adapter.getBalance();
18
- if (isJson())
19
- return printJson(jsonOk(bal));
20
- console.log(chalk.cyan.bold(`\n ${adapter.name.toUpperCase()} Account Info\n`));
21
- console.log(` Equity: $${formatUsd(bal.equity)}`);
22
- console.log(` Available: $${formatUsd(bal.available)}`);
23
- console.log(` Margin Used: $${formatUsd(bal.marginUsed)}`);
24
- console.log(` Unreal. PnL: ${formatPnl(bal.unrealizedPnl)}`);
25
- console.log();
26
- });
104
+ async function fetchExchangeBalance(exName, adapter) {
105
+ const name = exName.toLowerCase();
106
+ const [bal, fundingPayments] = await Promise.all([
107
+ adapter.getBalance(),
108
+ adapter.getFundingPayments(200).catch(() => []),
109
+ ]);
110
+ let spotBalances = [];
111
+ try {
112
+ if (name === "hyperliquid") {
113
+ const { HyperliquidSpotAdapter } = await import("../exchanges/hyperliquid-spot.js");
114
+ const hlSpot = new HyperliquidSpotAdapter(adapter);
115
+ await hlSpot.init();
116
+ const raw = await hlSpot.getSpotBalances();
117
+ const markets = await hlSpot.getSpotMarkets();
118
+ const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
119
+ const stripSuffix = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
120
+ spotBalances = raw
121
+ .filter(b => Number(b.total) > 0)
122
+ .map(b => {
123
+ const base = stripSuffix(b.token);
124
+ return {
125
+ ...b,
126
+ valueUsd: base === "USDC" ? Number(b.total) : (priceMap.get(base) ?? 0) * Number(b.total),
127
+ };
128
+ });
129
+ }
130
+ else if (name === "lighter") {
131
+ const { LighterAdapter } = await import("../exchanges/lighter.js");
132
+ const { LighterSpotAdapter } = await import("../exchanges/lighter-spot.js");
133
+ const ltSpot = new LighterSpotAdapter(adapter);
134
+ await ltSpot.init();
135
+ const raw = await ltSpot.getSpotBalances();
136
+ const markets = await ltSpot.getSpotMarkets();
137
+ const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
138
+ spotBalances = raw
139
+ .filter(b => Number(b.total) > 0)
140
+ .map(b => ({
141
+ ...b,
142
+ valueUsd: b.token === "USDC" || b.token === "USDC_SPOT"
143
+ ? Number(b.total)
144
+ : (priceMap.get(b.token.toUpperCase()) ?? 0) * Number(b.total),
145
+ }));
146
+ }
147
+ }
148
+ catch { /* spot not available */ }
149
+ const cutoff24h = Date.now() - 24 * 60 * 60 * 1000;
150
+ const recent = fundingPayments.filter(f => f.time >= cutoff24h);
151
+ const funding24h = recent.reduce((sum, f) => sum + Number(f.payment), 0);
152
+ const totalSpotUsd = spotBalances.reduce((s, b) => s + (b.valueUsd ?? 0), 0);
153
+ return {
154
+ exchange: name,
155
+ perp: bal,
156
+ spot: spotBalances,
157
+ funding24h,
158
+ fundingPayments24h: recent.length,
159
+ totalSpotValueUsd: totalSpotUsd,
160
+ totalAccountValueUsd: Number(bal.equity) + totalSpotUsd,
161
+ };
162
+ }
163
+ function printSingleBalance(exName, r) {
164
+ console.log(chalk.cyan.bold(`\n ${exName.toUpperCase()} Account Balance\n`));
165
+ console.log(chalk.white.bold(" Perp Account"));
166
+ console.log(` Equity: $${formatUsd(r.perp.equity)}`);
167
+ console.log(` Available: $${formatUsd(r.perp.available)}`);
168
+ console.log(` Margin Used: $${formatUsd(r.perp.marginUsed)}`);
169
+ console.log(` Unreal. PnL: ${formatPnl(r.perp.unrealizedPnl)}`);
170
+ if (r.spot.length > 0) {
171
+ console.log(chalk.white.bold("\n Spot Holdings"));
172
+ const rows = r.spot.map(b => [
173
+ chalk.white.bold(b.token),
174
+ b.total,
175
+ b.available,
176
+ b.held !== "0" ? b.held : chalk.gray("-"),
177
+ b.valueUsd ? `$${formatUsd(b.valueUsd)}` : chalk.gray("-"),
178
+ ]);
179
+ console.log(makeTable(["Token", "Total", "Available", "Held", "Value"], rows));
180
+ }
181
+ console.log(chalk.white.bold(" Funding (24h)"));
182
+ console.log(` Payments: ${r.fundingPayments24h}`);
183
+ console.log(` Total: ${formatPnl(r.funding24h)}`);
184
+ if (r.totalSpotValueUsd > 0) {
185
+ console.log(chalk.white.bold("\n Total"));
186
+ console.log(` Perp Equity: $${formatUsd(r.perp.equity)}`);
187
+ console.log(` Spot Value: $${formatUsd(r.totalSpotValueUsd)}`);
188
+ console.log(` Total Value: $${formatUsd(r.totalAccountValueUsd)}`);
189
+ }
190
+ }
27
191
  account
28
192
  .command("balance")
29
- .description("Alias for 'account info' — show account balance")
193
+ .alias("info")
194
+ .description("Account overview: perp balance, spot holdings, and 24h funding")
30
195
  .action(async () => {
196
+ // Detect if user explicitly set -e (single exchange mode)
197
+ const explicitExchange = program.getOptionValueSource?.("exchange") === "cli";
198
+ // Multi-exchange mode: show all exchanges
199
+ if (!explicitExchange && getAdapterForExchange) {
200
+ const results = [];
201
+ const errors = {};
202
+ await Promise.all(EXCHANGES.map(async (ex) => {
203
+ try {
204
+ const adapter = await getAdapterForExchange(ex);
205
+ results.push(await fetchExchangeBalance(ex, adapter));
206
+ }
207
+ catch (err) {
208
+ errors[ex] = err instanceof Error ? err.message : String(err);
209
+ }
210
+ }));
211
+ // Sort by equity desc
212
+ results.sort((a, b) => b.totalAccountValueUsd - a.totalAccountValueUsd);
213
+ if (isJson()) {
214
+ const grandTotal = results.reduce((s, r) => s + r.totalAccountValueUsd, 0);
215
+ return printJson(jsonOk({
216
+ exchanges: results,
217
+ errors: Object.keys(errors).length > 0 ? errors : undefined,
218
+ totalEquity: results.reduce((s, r) => s + Number(r.perp.equity), 0),
219
+ totalSpotValueUsd: results.reduce((s, r) => s + r.totalSpotValueUsd, 0),
220
+ totalAccountValueUsd: grandTotal,
221
+ totalFunding24h: results.reduce((s, r) => s + r.funding24h, 0),
222
+ }));
223
+ }
224
+ console.log(chalk.cyan.bold("\n All Exchanges — Account Balance\n"));
225
+ // Summary table
226
+ const balRows = results.map(r => [
227
+ chalk.white.bold(r.exchange),
228
+ `$${formatUsd(r.perp.equity)}`,
229
+ `$${formatUsd(r.perp.available)}`,
230
+ `$${formatUsd(r.perp.marginUsed)}`,
231
+ r.totalSpotValueUsd > 0 ? `$${formatUsd(r.totalSpotValueUsd)}` : chalk.gray("-"),
232
+ formatPnl(r.funding24h),
233
+ `$${formatUsd(r.totalAccountValueUsd)}`,
234
+ ]);
235
+ const grandEquity = results.reduce((s, r) => s + Number(r.perp.equity), 0);
236
+ const grandSpot = results.reduce((s, r) => s + r.totalSpotValueUsd, 0);
237
+ const grandFunding = results.reduce((s, r) => s + r.funding24h, 0);
238
+ const grandTotal = results.reduce((s, r) => s + r.totalAccountValueUsd, 0);
239
+ balRows.push([
240
+ chalk.cyan.bold("TOTAL"),
241
+ chalk.cyan.bold(`$${formatUsd(grandEquity)}`),
242
+ "",
243
+ "",
244
+ grandSpot > 0 ? chalk.cyan.bold(`$${formatUsd(grandSpot)}`) : "",
245
+ formatPnl(grandFunding),
246
+ chalk.cyan.bold(`$${formatUsd(grandTotal)}`),
247
+ ]);
248
+ console.log(makeTable(["Exchange", "Equity", "Available", "Margin", "Spot", "Funding 24h", "Total"], balRows));
249
+ // Spot details per exchange
250
+ for (const r of results) {
251
+ if (r.spot.length > 0) {
252
+ console.log(chalk.white.bold(` ${r.exchange.toUpperCase()} Spot Holdings`));
253
+ const rows = r.spot.map(b => [
254
+ chalk.white.bold(b.token),
255
+ b.total,
256
+ b.valueUsd ? `$${formatUsd(b.valueUsd)}` : chalk.gray("-"),
257
+ ]);
258
+ console.log(makeTable(["Token", "Total", "Value"], rows));
259
+ }
260
+ }
261
+ // Errors
262
+ for (const [ex, msg] of Object.entries(errors)) {
263
+ console.log(chalk.gray(` ${ex}: ${msg}`));
264
+ }
265
+ console.log();
266
+ return;
267
+ }
268
+ // Single exchange mode (explicit -e or no getAdapterForExchange)
31
269
  const adapter = await getAdapter();
32
- const bal = await adapter.getBalance();
270
+ const exName = adapter.name.toLowerCase();
271
+ const result = await fetchExchangeBalance(exName, adapter);
33
272
  if (isJson())
34
- return printJson(jsonOk(bal));
35
- console.log(chalk.cyan.bold(`\n ${adapter.name.toUpperCase()} Account Balance\n`));
36
- console.log(` Equity: $${formatUsd(bal.equity)}`);
37
- console.log(` Available: $${formatUsd(bal.available)}`);
38
- console.log(` Margin Used: $${formatUsd(bal.marginUsed)}`);
39
- console.log(` Unreal. PnL: ${formatPnl(bal.unrealizedPnl)}`);
273
+ return printJson(jsonOk(result));
274
+ printSingleBalance(exName, result);
40
275
  console.log();
41
276
  });
42
277
  account
@@ -172,42 +407,9 @@ export function registerAccountCommands(program, getAdapter, isJson) {
172
407
  ]);
173
408
  console.log(makeTable(["Time", "Symbol", "Payment"], rows));
174
409
  });
175
- account
176
- .command("portfolio")
177
- .description("Portfolio overview")
178
- .action(async () => {
179
- const adapter = await getAdapter();
180
- if (adapter instanceof PacificaAdapter) {
181
- const portfolio = await adapter.sdk.getPortfolio(adapter.publicKey);
182
- printJson(jsonOk(portfolio));
183
- }
184
- else if (adapter instanceof HyperliquidAdapter) {
185
- const state = await adapter.client.info.perpetuals.getClearinghouseState(adapter.address);
186
- printJson(jsonOk(state));
187
- }
188
- });
189
- account
190
- .command("balance-history")
191
- .description("Balance change history")
192
- .action(async () => {
193
- const adapter = await getAdapter();
194
- const p = pac(adapter);
195
- const raw = await p.sdk.getBalanceHistory(p.publicKey);
196
- if (isJson())
197
- return printJson(jsonOk(raw));
198
- const history = (raw.data ?? raw);
199
- if (!Array.isArray(history) || history.length === 0) {
200
- console.log(chalk.gray("\n No balance history.\n"));
201
- return;
202
- }
203
- const rows = history.slice(0, 30).map((h) => [
204
- new Date(Number(h.created_at ?? h.timestamp ?? 0)).toLocaleString(),
205
- String(h.type ?? h.event_type ?? ""),
206
- formatPnl(String(h.amount ?? h.change ?? "0")),
207
- `$${formatUsd(String(h.balance ?? "0"))}`,
208
- ]);
209
- console.log(makeTable(["Time", "Type", "Change", "Balance"], rows));
210
- });
410
+ // "account portfolio" removed — use top-level "portfolio" for cross-exchange view
411
+ // or "account balance" for single-exchange balance
412
+ // "account balance-history" removed — Pacifica-only, rarely used
211
413
  account
212
414
  .command("margin <symbol>")
213
415
  .description("Margin details for a specific symbol position")
@@ -422,4 +624,106 @@ export function registerAccountCommands(program, getAdapter, isJson) {
422
624
  ]);
423
625
  console.log(makeTable(["ID", "Symbol", "Side", "Size", "Filled", "Duration"], rows));
424
626
  });
627
+ // ── Portfolio (top-level command) ──
628
+ if (getAdapterForExchange) {
629
+ program
630
+ .command("portfolio")
631
+ .description("Cross-exchange portfolio overview (all exchanges at once)")
632
+ .option("--exchange <exchanges>", "Comma-separated exchanges to include (default: all)")
633
+ .action(async (opts) => {
634
+ await withJsonErrors(isJson(), async () => {
635
+ const exchanges = opts.exchange
636
+ ? opts.exchange.split(",").map(e => e.trim())
637
+ : [...EXCHANGES];
638
+ const snapshots = await Promise.all(exchanges.map(ex => fetchExchangeSnapshot(ex, getAdapterForExchange)));
639
+ const summary = buildSummary(snapshots);
640
+ if (isJson()) {
641
+ return printJson(jsonOk(summary));
642
+ }
643
+ // ── Header ──
644
+ console.log(chalk.cyan.bold("\n Cross-Exchange Portfolio\n"));
645
+ // ── Balance Summary ──
646
+ console.log(chalk.white.bold(" Balances"));
647
+ const balRows = snapshots.map(s => {
648
+ if (!s.connected) {
649
+ return [chalk.white(s.exchange), chalk.red("disconnected"), "-", "-", "-", chalk.gray(s.error ?? "")];
650
+ }
651
+ const b = s.balance;
652
+ return [
653
+ chalk.white.bold(s.exchange),
654
+ `$${formatUsd(b.equity)}`,
655
+ `$${formatUsd(b.available)}`,
656
+ `$${formatUsd(b.marginUsed)}`,
657
+ formatPnl(b.unrealizedPnl),
658
+ chalk.green("connected"),
659
+ ];
660
+ });
661
+ // Totals row
662
+ balRows.push([
663
+ chalk.cyan.bold("TOTAL"),
664
+ chalk.cyan.bold(`$${formatUsd(summary.totalEquity)}`),
665
+ chalk.cyan.bold(`$${formatUsd(summary.totalAvailable)}`),
666
+ chalk.cyan.bold(`$${formatUsd(summary.totalMarginUsed)}`),
667
+ formatPnl(summary.totalUnrealizedPnl),
668
+ "",
669
+ ]);
670
+ console.log(makeTable(["Exchange", "Equity", "Available", "Margin Used", "uPnL", "Status"], balRows));
671
+ // ── Positions ──
672
+ if (summary.positions.length > 0) {
673
+ console.log(chalk.white.bold("\n Open Positions"));
674
+ const posRows = summary.positions.map(p => {
675
+ const sideColor = p.side === "long" ? chalk.green : chalk.red;
676
+ const notional = Math.abs(Number(p.size) * Number(p.markPrice));
677
+ return [
678
+ chalk.white.bold(p.symbol),
679
+ chalk.gray(p.exchange),
680
+ sideColor(p.side.toUpperCase()),
681
+ p.size,
682
+ `$${formatUsd(p.entryPrice)}`,
683
+ `$${formatUsd(p.markPrice)}`,
684
+ formatPnl(p.unrealizedPnl),
685
+ `$${formatUsd(notional)}`,
686
+ `${p.leverage}x`,
687
+ ];
688
+ });
689
+ console.log(makeTable(["Symbol", "Exchange", "Side", "Size", "Entry", "Mark", "uPnL", "Notional", "Lev"], posRows));
690
+ }
691
+ else {
692
+ console.log(chalk.gray("\n No open positions.\n"));
693
+ }
694
+ // ── Risk Metrics ──
695
+ console.log(chalk.white.bold("\n Risk Metrics"));
696
+ const levelColor = {
697
+ low: chalk.green,
698
+ medium: chalk.yellow,
699
+ high: chalk.red,
700
+ critical: chalk.bgRed.white,
701
+ }[summary.risk.level];
702
+ console.log(` Risk Level: ${levelColor(summary.risk.level.toUpperCase())}`);
703
+ console.log(` Can Trade: ${summary.risk.canTrade ? chalk.green("YES") : chalk.red("NO")}`);
704
+ const mu = summary.riskMetrics.marginUtilization;
705
+ const muColor = mu < 30 ? chalk.green : mu < 60 ? chalk.yellow : chalk.red;
706
+ console.log(` Margin Utilization: ${muColor(`${mu.toFixed(1)}%`)}`);
707
+ if (summary.riskMetrics.largestPosition) {
708
+ const lp = summary.riskMetrics.largestPosition;
709
+ console.log(` Largest Position: ${lp.symbol} on ${lp.exchange} ($${formatUsd(lp.notional)})`);
710
+ }
711
+ if (summary.riskMetrics.exchangeConcentration.length > 0) {
712
+ console.log(` Exchange Allocation:`);
713
+ for (const ec of summary.riskMetrics.exchangeConcentration) {
714
+ const bar = "\u2588".repeat(Math.round(ec.pct / 5)) + "\u2591".repeat(20 - Math.round(ec.pct / 5));
715
+ console.log(` ${ec.exchange.padEnd(12)} ${bar} ${ec.pct.toFixed(1)}%`);
716
+ }
717
+ }
718
+ if (summary.risk.violations.length > 0) {
719
+ console.log(chalk.red.bold("\n Risk Violations"));
720
+ for (const v of summary.risk.violations) {
721
+ const sevColor = { low: chalk.green, medium: chalk.yellow, high: chalk.red, critical: chalk.bgRed.white }[v.severity];
722
+ console.log(` ${sevColor(v.severity.toUpperCase().padEnd(8))} ${v.message}`);
723
+ }
724
+ }
725
+ console.log();
726
+ });
727
+ });
728
+ }
425
729
  }
@@ -30,9 +30,11 @@ function extractSchema(cmd, parentPath = "perp") {
30
30
  const fullCommand = `${parentPath} ${cmd.name()}`.trim();
31
31
  const args = (cmd.registeredArguments ?? []).map((a) => {
32
32
  const desc = a.description || "";
33
+ // Args are always values (not flags), so infer from name/desc semantics, never "boolean"
34
+ const rawType = inferType(`<${a.name()}>`, desc);
33
35
  return {
34
36
  name: a.name(),
35
- type: inferType(a.name(), desc),
37
+ type: rawType === "boolean" ? "string" : rawType,
36
38
  required: a.required,
37
39
  description: desc,
38
40
  enum: inferEnum(desc),
@@ -63,7 +65,7 @@ function extractSchema(cmd, parentPath = "perp") {
63
65
  export function registerAgentCommands(program, getAdapter, isJson) {
64
66
  const agent = program
65
67
  .command("agent")
66
- .description("Agent-friendly commands (structured I/O for AI agents)");
68
+ .description("Agent-friendly commands");
67
69
  // ── agent schema ── dump full command tree as JSON
68
70
  agent
69
71
  .command("schema")
@@ -85,10 +87,12 @@ export function registerAgentCommands(program, getAdapter, isJson) {
85
87
  };
86
88
  printJson(jsonOk(envelope));
87
89
  });
88
- // ── Top-level schema alias ──
89
- program
90
+ // ── Top-level schema alias (hidden) ──
91
+ const schemaAlias = program
90
92
  .command("schema")
91
- .description("Output CLI schema as JSON (alias for agent schema)")
93
+ .description("Output CLI schema as JSON (alias for agent schema)");
94
+ schemaAlias._hidden = true;
95
+ schemaAlias
92
96
  .action(() => {
93
97
  const errorCodeDocs = {};
94
98
  for (const [key, val] of Object.entries(ERROR_CODES)) {