perp-cli 0.9.8 → 0.12.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.
Files changed (254) hide show
  1. package/README.md +30 -25
  2. package/dist/_stderr-filter.d.ts +0 -0
  3. package/dist/_stderr-filter.js +30 -0
  4. package/dist/agent-wallet/expiry.d.ts +15 -0
  5. package/dist/agent-wallet/expiry.js +26 -0
  6. package/dist/agent-wallet/lighter-keystore.d.ts +34 -0
  7. package/dist/agent-wallet/lighter-keystore.js +232 -0
  8. package/dist/agent-wallet/passphrase.d.ts +13 -0
  9. package/dist/agent-wallet/passphrase.js +54 -0
  10. package/dist/agent-wallet/signing-strategy.d.ts +67 -0
  11. package/dist/agent-wallet/signing-strategy.js +51 -0
  12. package/dist/agent-wallet/store.d.ts +37 -0
  13. package/dist/agent-wallet/store.js +172 -0
  14. package/dist/api/public/_http.d.ts +15 -0
  15. package/dist/api/public/_http.js +24 -0
  16. package/dist/api/public/hyperliquid.js +36 -24
  17. package/dist/api/public/lighter.d.ts +7 -1
  18. package/dist/api/public/lighter.js +85 -37
  19. package/dist/api/public/pacifica.js +47 -18
  20. package/dist/bot/engine.js +23 -4
  21. package/dist/bot/presets.js +3 -3
  22. package/dist/bot/strategies/funding-arb-strategy.d.ts +0 -23
  23. package/dist/bot/strategies/funding-arb-strategy.js +41 -176
  24. package/dist/bot/strategies/funding-auto.js +2 -2
  25. package/dist/bridge-engine.js +102 -49
  26. package/dist/cli-spec.js +1 -2
  27. package/dist/commands/account.js +2 -500
  28. package/dist/commands/agent.d.ts +8 -2
  29. package/dist/commands/agent.js +1903 -366
  30. package/dist/commands/alerts.js +3 -1
  31. package/dist/commands/arb/index.js +37 -7
  32. package/dist/commands/arb-auto.js +44 -11
  33. package/dist/commands/arb.js +83 -34
  34. package/dist/commands/bot.d.ts +1 -1
  35. package/dist/commands/bot.js +20 -16
  36. package/dist/commands/bridge.d.ts +1 -1
  37. package/dist/commands/bridge.js +4 -4
  38. package/dist/commands/funds.d.ts +1 -1
  39. package/dist/commands/funds.js +18 -68
  40. package/dist/commands/jobs.d.ts +1 -1
  41. package/dist/commands/jobs.js +2 -2
  42. package/dist/commands/manage.d.ts +1 -1
  43. package/dist/commands/manage.js +12 -84
  44. package/dist/commands/plan.d.ts +1 -1
  45. package/dist/commands/plan.js +2 -2
  46. package/dist/commands/portfolio.d.ts +19 -3
  47. package/dist/commands/portfolio.js +747 -163
  48. package/dist/commands/rebalance.d.ts +1 -1
  49. package/dist/commands/rebalance.js +18 -12
  50. package/dist/commands/risk.js +2 -6
  51. package/dist/commands/settings.js +30 -53
  52. package/dist/commands/trade.js +16 -15
  53. package/dist/commands/wallet.d.ts +2 -1
  54. package/dist/commands/wallet.js +1122 -176
  55. package/dist/config.d.ts +9 -0
  56. package/dist/config.js +55 -9
  57. package/dist/errors.d.ts +64 -0
  58. package/dist/errors.js +82 -2
  59. package/dist/event-stream.js +12 -1
  60. package/dist/exchanges/aster-typed-data.d.ts +156 -0
  61. package/dist/exchanges/aster-typed-data.js +189 -0
  62. package/dist/exchanges/aster.d.ts +60 -11
  63. package/dist/exchanges/aster.js +278 -136
  64. package/dist/exchanges/hyperliquid.d.ts +14 -0
  65. package/dist/exchanges/hyperliquid.js +88 -34
  66. package/dist/exchanges/lighter-spot.js +19 -5
  67. package/dist/exchanges/lighter.d.ts +66 -0
  68. package/dist/exchanges/lighter.js +213 -31
  69. package/dist/exchanges/pacifica-typed-data.d.ts +75 -0
  70. package/dist/exchanges/pacifica-typed-data.js +74 -0
  71. package/dist/exchanges/pacifica.d.ts +29 -1
  72. package/dist/exchanges/pacifica.js +94 -6
  73. package/dist/funding/rates.js +59 -21
  74. package/dist/guardrail/contracts.d.ts +14 -0
  75. package/dist/guardrail/contracts.js +72 -0
  76. package/dist/guardrail/perp-guardrail.d.ts +21 -0
  77. package/dist/guardrail/perp-guardrail.js +145 -0
  78. package/dist/index.d.ts +1 -1
  79. package/dist/index.js +307 -362
  80. package/dist/jobs.js +3 -1
  81. package/dist/mcp-server.js +92 -88
  82. package/dist/position-history.js +47 -4
  83. package/dist/settings.d.ts +69 -3
  84. package/dist/settings.js +3 -2
  85. package/dist/signer/index.d.ts +2 -0
  86. package/dist/signer/index.js +2 -0
  87. package/dist/signer/ows-evm.d.ts +32 -0
  88. package/dist/signer/ows-evm.js +86 -0
  89. package/dist/signer/ows-loader.d.ts +2 -0
  90. package/dist/signer/ows-loader.js +6 -0
  91. package/dist/signer/ows-solana.d.ts +25 -0
  92. package/dist/signer/ows-solana.js +56 -0
  93. package/dist/smart-order.d.ts +8 -2
  94. package/dist/smart-order.js +46 -11
  95. package/dist/strategies/funding-arb.js +29 -8
  96. package/dist/trade-validator.js +30 -5
  97. package/dist/utils.d.ts +10 -0
  98. package/dist/utils.js +19 -0
  99. package/package.json +4 -2
  100. package/skills/perp-cli/SKILL.md +23 -14
  101. package/skills/perp-cli/references/agent-operations.md +6 -4
  102. package/skills/perp-cli/references/commands.md +55 -34
  103. package/skills/perp-cli/scripts/preflight.sh +4 -4
  104. package/skills/perp-cli/scripts/spot-perp-scan.sh +2 -2
  105. package/dist/__tests__/alert-logic.test.d.ts +0 -1
  106. package/dist/__tests__/alert-logic.test.js +0 -107
  107. package/dist/__tests__/arb-auto-3dex.test.d.ts +0 -1
  108. package/dist/__tests__/arb-auto-3dex.test.js +0 -397
  109. package/dist/__tests__/arb-history-stats.test.d.ts +0 -1
  110. package/dist/__tests__/arb-history-stats.test.js +0 -176
  111. package/dist/__tests__/arb-logic.test.d.ts +0 -1
  112. package/dist/__tests__/arb-logic.test.js +0 -84
  113. package/dist/__tests__/arb-manage.test.d.ts +0 -1
  114. package/dist/__tests__/arb-manage.test.js +0 -253
  115. package/dist/__tests__/arb-new-features.test.d.ts +0 -1
  116. package/dist/__tests__/arb-new-features.test.js +0 -457
  117. package/dist/__tests__/arb-sizing.test.d.ts +0 -1
  118. package/dist/__tests__/arb-sizing.test.js +0 -48
  119. package/dist/__tests__/arb-state.test.d.ts +0 -1
  120. package/dist/__tests__/arb-state.test.js +0 -284
  121. package/dist/__tests__/arb-userflow.test.d.ts +0 -1
  122. package/dist/__tests__/arb-userflow.test.js +0 -945
  123. package/dist/__tests__/arb-utils.test.d.ts +0 -1
  124. package/dist/__tests__/arb-utils.test.js +0 -264
  125. package/dist/__tests__/bot-conditions.test.d.ts +0 -1
  126. package/dist/__tests__/bot-conditions.test.js +0 -341
  127. package/dist/__tests__/client-id-tracker.test.d.ts +0 -1
  128. package/dist/__tests__/client-id-tracker.test.js +0 -137
  129. package/dist/__tests__/commands/new-atomic-commands.test.d.ts +0 -1
  130. package/dist/__tests__/commands/new-atomic-commands.test.js +0 -502
  131. package/dist/__tests__/commands/order-intent.test.d.ts +0 -1
  132. package/dist/__tests__/commands/order-intent.test.js +0 -600
  133. package/dist/__tests__/commands/trade-commands.test.d.ts +0 -1
  134. package/dist/__tests__/commands/trade-commands.test.js +0 -821
  135. package/dist/__tests__/config.test.d.ts +0 -1
  136. package/dist/__tests__/config.test.js +0 -86
  137. package/dist/__tests__/cross-chain-margin.test.d.ts +0 -1
  138. package/dist/__tests__/cross-chain-margin.test.js +0 -287
  139. package/dist/__tests__/dex-asset-map.test.d.ts +0 -1
  140. package/dist/__tests__/dex-asset-map.test.js +0 -191
  141. package/dist/__tests__/errors.test.d.ts +0 -1
  142. package/dist/__tests__/errors.test.js +0 -110
  143. package/dist/__tests__/event-stream.test.d.ts +0 -1
  144. package/dist/__tests__/event-stream.test.js +0 -276
  145. package/dist/__tests__/exchanges/interface.test.d.ts +0 -1
  146. package/dist/__tests__/exchanges/interface.test.js +0 -132
  147. package/dist/__tests__/exchanges/mock-adapter.d.ts +0 -69
  148. package/dist/__tests__/exchanges/mock-adapter.js +0 -137
  149. package/dist/__tests__/execution-log.test.d.ts +0 -1
  150. package/dist/__tests__/execution-log.test.js +0 -106
  151. package/dist/__tests__/funding-calc.test.d.ts +0 -1
  152. package/dist/__tests__/funding-calc.test.js +0 -71
  153. package/dist/__tests__/funding-history.test.d.ts +0 -1
  154. package/dist/__tests__/funding-history.test.js +0 -343
  155. package/dist/__tests__/funding-rates.test.d.ts +0 -1
  156. package/dist/__tests__/funding-rates.test.js +0 -342
  157. package/dist/__tests__/funding.test.d.ts +0 -1
  158. package/dist/__tests__/funding.test.js +0 -173
  159. package/dist/__tests__/gap-logic.test.d.ts +0 -1
  160. package/dist/__tests__/gap-logic.test.js +0 -43
  161. package/dist/__tests__/hip3-dex.test.d.ts +0 -1
  162. package/dist/__tests__/hip3-dex.test.js +0 -234
  163. package/dist/__tests__/integration/agent-features.integration.test.d.ts +0 -1
  164. package/dist/__tests__/integration/agent-features.integration.test.js +0 -553
  165. package/dist/__tests__/integration/atomic-commands.integration.test.d.ts +0 -13
  166. package/dist/__tests__/integration/atomic-commands.integration.test.js +0 -246
  167. package/dist/__tests__/integration/bridge-simulation.integration.test.d.ts +0 -1
  168. package/dist/__tests__/integration/bridge-simulation.integration.test.js +0 -453
  169. package/dist/__tests__/integration/bridge-strict.integration.test.d.ts +0 -1
  170. package/dist/__tests__/integration/bridge-strict.integration.test.js +0 -812
  171. package/dist/__tests__/integration/bridge.integration.test.d.ts +0 -1
  172. package/dist/__tests__/integration/bridge.integration.test.js +0 -309
  173. package/dist/__tests__/integration/cli-e2e.integration.test.d.ts +0 -1
  174. package/dist/__tests__/integration/cli-e2e.integration.test.js +0 -202
  175. package/dist/__tests__/integration/dex-arb.integration.test.d.ts +0 -1
  176. package/dist/__tests__/integration/dex-arb.integration.test.js +0 -116
  177. package/dist/__tests__/integration/envelope-consistency.integration.test.d.ts +0 -13
  178. package/dist/__tests__/integration/envelope-consistency.integration.test.js +0 -205
  179. package/dist/__tests__/integration/hip3-dex.integration.test.d.ts +0 -1
  180. package/dist/__tests__/integration/hip3-dex.integration.test.js +0 -147
  181. package/dist/__tests__/integration/hyperliquid.integration.test.d.ts +0 -1
  182. package/dist/__tests__/integration/hyperliquid.integration.test.js +0 -79
  183. package/dist/__tests__/integration/lighter.integration.test.d.ts +0 -1
  184. package/dist/__tests__/integration/lighter.integration.test.js +0 -53
  185. package/dist/__tests__/integration/new-commands-e2e.integration.test.d.ts +0 -9
  186. package/dist/__tests__/integration/new-commands-e2e.integration.test.js +0 -236
  187. package/dist/__tests__/integration/order-verification.integration.test.d.ts +0 -1
  188. package/dist/__tests__/integration/order-verification.integration.test.js +0 -321
  189. package/dist/__tests__/integration/pacifica.integration.test.d.ts +0 -1
  190. package/dist/__tests__/integration/pacifica.integration.test.js +0 -75
  191. package/dist/__tests__/integration/response-shapes.integration.test.d.ts +0 -1
  192. package/dist/__tests__/integration/response-shapes.integration.test.js +0 -278
  193. package/dist/__tests__/liquidity.test.d.ts +0 -1
  194. package/dist/__tests__/liquidity.test.js +0 -225
  195. package/dist/__tests__/plan-executor.test.d.ts +0 -1
  196. package/dist/__tests__/plan-executor.test.js +0 -314
  197. package/dist/__tests__/position-history.test.d.ts +0 -1
  198. package/dist/__tests__/position-history.test.js +0 -367
  199. package/dist/__tests__/retry.test.d.ts +0 -1
  200. package/dist/__tests__/retry.test.js +0 -310
  201. package/dist/__tests__/risk-assessment.test.d.ts +0 -1
  202. package/dist/__tests__/risk-assessment.test.js +0 -288
  203. package/dist/__tests__/security-adversarial.test.d.ts +0 -1
  204. package/dist/__tests__/security-adversarial.test.js +0 -574
  205. package/dist/__tests__/strategies.test.d.ts +0 -1
  206. package/dist/__tests__/strategies.test.js +0 -539
  207. package/dist/__tests__/trade-execution.test.d.ts +0 -1
  208. package/dist/__tests__/trade-execution.test.js +0 -129
  209. package/dist/__tests__/trade-validator.test.d.ts +0 -1
  210. package/dist/__tests__/trade-validator.test.js +0 -655
  211. package/dist/__tests__/utils.test.d.ts +0 -1
  212. package/dist/__tests__/utils.test.js +0 -76
  213. package/dist/arb/reconcile.d.ts +0 -28
  214. package/dist/arb/reconcile.js +0 -162
  215. package/dist/commands/alert.d.ts +0 -2
  216. package/dist/commands/alert.js +0 -421
  217. package/dist/commands/analytics.d.ts +0 -3
  218. package/dist/commands/analytics.js +0 -311
  219. package/dist/commands/arb-manage.d.ts +0 -5
  220. package/dist/commands/arb-manage.js +0 -5
  221. package/dist/commands/dashboard.d.ts +0 -3
  222. package/dist/commands/dashboard.js +0 -176
  223. package/dist/commands/deposit.d.ts +0 -4
  224. package/dist/commands/deposit.js +0 -573
  225. package/dist/commands/dex.d.ts +0 -3
  226. package/dist/commands/dex.js +0 -120
  227. package/dist/commands/env.d.ts +0 -2
  228. package/dist/commands/env.js +0 -136
  229. package/dist/commands/funding.d.ts +0 -3
  230. package/dist/commands/funding.js +0 -554
  231. package/dist/commands/gap.d.ts +0 -2
  232. package/dist/commands/gap.js +0 -305
  233. package/dist/commands/health.d.ts +0 -2
  234. package/dist/commands/health.js +0 -67
  235. package/dist/commands/multileg.d.ts +0 -16
  236. package/dist/commands/multileg.js +0 -174
  237. package/dist/commands/pnl.d.ts +0 -12
  238. package/dist/commands/pnl.js +0 -243
  239. package/dist/commands/run.d.ts +0 -3
  240. package/dist/commands/run.js +0 -202
  241. package/dist/commands/stream.d.ts +0 -5
  242. package/dist/commands/stream.js +0 -200
  243. package/dist/commands/withdraw.d.ts +0 -3
  244. package/dist/commands/withdraw.js +0 -187
  245. package/dist/exchanges/edgex.d.ts +0 -62
  246. package/dist/exchanges/edgex.js +0 -552
  247. package/dist/exchanges/nado.d.ts +0 -52
  248. package/dist/exchanges/nado.js +0 -223
  249. package/dist/funding/index.d.ts +0 -3
  250. package/dist/funding/index.js +0 -3
  251. package/dist/retry.d.ts +0 -74
  252. package/dist/retry.js +0 -129
  253. package/dist/ws/market-feed.d.ts +0 -74
  254. package/dist/ws/market-feed.js +0 -227
package/dist/index.js CHANGED
@@ -1,43 +1,50 @@
1
1
  #!/usr/bin/env node
2
+ // MUST be first import — installs a stderr filter for --json mode so SDK
3
+ // startup banners don't leak into machine output. ESM hoists all static
4
+ // imports to the top, but executes them in source order, so this side-effect
5
+ // import runs before any DEX SDK is loaded.
6
+ import "./_stderr-filter.js";
2
7
  // Save original process.exit before any SDK (Go WASM) can patch it
3
8
  const _origExit = process.exit.bind(process);
4
9
  import { config } from "dotenv";
5
10
  import { resolve } from "path";
6
11
  import { createRequire } from "node:module";
7
- // Load ~/.perp/.env first (global), then CWD .env (overrides)
12
+ // SSOT Rule #3: load only ~/.perp/.env. The CWD .env auto-load was removed
13
+ // because it caused master-address pollution when run from a project tree
14
+ // containing leftover dev keys (`wallet show` returned env-derived addresses
15
+ // instead of the OWS active wallet's). If you need ad-hoc env overrides,
16
+ // export them in your shell — keystore is the canonical source.
8
17
  config({ path: resolve(process.env.HOME || "~", ".perp", ".env") });
9
- config();
10
18
  import { Command } from "commander";
11
19
  import chalk from "chalk";
12
20
  import { Keypair } from "@solana/web3.js";
13
- import { tryLoadPrivateKey, parseSolanaKeypair } from "./config.js";
21
+ import { tryLoadPrivateKey, parseSolanaKeypair, isOwsKey, getOwsWalletName } from "./config.js";
14
22
  import { PacificaAdapter } from "./exchanges/pacifica.js";
15
23
  import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
16
24
  import { resolveExchangeName } from "./exchanges/registry.js";
17
25
  import { registerMarketCommands } from "./commands/market.js";
18
26
  import { registerAccountCommands } from "./commands/account.js";
19
27
  import { registerTradeCommands } from "./commands/trade.js";
20
- import { registerManageCommands } from "./commands/manage.js";
28
+ // manage commands now register under `wallet manage ...` via registerWalletCommands.
21
29
  // stream commands removed — WS feeds still used by dashboard/event-stream internally
22
30
  import { registerArbCommands } from "./commands/arb.js";
23
31
  import { registerWalletCommands } from "./commands/wallet.js";
24
- import { registerBridgeCommands } from "./commands/bridge.js";
25
- // deposit + withdraw merged into funds
32
+ // bridge + rebalance now nested under `funds` — registered inside registerFundsCommands.
26
33
  import { registerFundsCommands } from "./commands/funds.js";
27
34
  // alert commands removed
28
35
  import { registerArbAutoCommands } from "./commands/arb-auto.js";
29
36
  import { registerArbManageCommands } from "./commands/arb/index.js";
30
- import { registerAgentCommands } from "./commands/agent.js";
31
- import { registerRebalanceCommands } from "./commands/rebalance.js";
32
- import { registerBotCommands } from "./commands/bot.js";
37
+ // Agent commands now register under `wallet agent ...` via registerWalletCommands.
38
+ import { registerStrategyCommands } from "./commands/bot.js";
33
39
  import { registerRiskCommands } from "./commands/risk.js";
34
40
  import { registerHistoryCommands } from "./commands/history.js";
35
41
  import { registerSettingsCommands } from "./commands/settings.js";
36
42
  // dex commands merged into market (hip3) — use --dex flag for markets/balance
37
- import { registerPlanCommands } from "./commands/plan.js";
43
+ // `plan` is no longer top-level — registered as `strategy plan` inside
44
+ // registerStrategyCommands.
38
45
  // funding merged into arb.ts
39
46
  import { registerBacktestCommands } from "./commands/backtest.js";
40
- import { registerDashboardCommands } from "./commands/dashboard.js";
47
+ import { registerPortfolioCommand } from "./commands/portfolio.js";
41
48
  import { registerInitCommand } from "./commands/init.js";
42
49
  import { registerAlertCommands } from "./commands/alerts.js";
43
50
  import { loadSettings, saveSettings } from "./settings.js";
@@ -60,6 +67,10 @@ program
60
67
  .option("--ndjson", "Output newline-delimited JSON (one object per line for streaming)")
61
68
  .option("--dry-run", "Simulate trades without executing (log as simulated)")
62
69
  .option("-w, --wallet <name>", "Use a specific wallet by name (from 'perp wallet list')")
70
+ .option("--ows <name>", "Use an OWS (Open Wallet Standard) wallet by name")
71
+ .option("--ows-key <token>", "OWS API key token (ows_key_...) for policy-gated agent access")
72
+ .option("--no-agent", "Bypass agent wallet routing for this run; falls back to OWS master or PK direct")
73
+ .option("--passphrase <pp>", "Master OWS passphrase (also accepts OWS_PASSPHRASE env or stdin)")
63
74
  .option("--dex <name>", "HIP-3 deployed perp dex name (Hyperliquid only)")
64
75
  .configureOutput({
65
76
  writeErr: (str) => {
@@ -90,8 +101,13 @@ function getExchange() {
90
101
  }
91
102
  async function getAdapter() {
92
103
  const opts = program.opts();
93
- const walletName = opts.wallet;
94
- // Skip cache when --wallet is specified (different wallet = different account)
104
+ const owsName = opts.ows;
105
+ const owsKeyToken = opts.owsKey || process.env.OWS_API_KEY;
106
+ const walletName = owsName ? `ows:${owsName}` : opts.wallet;
107
+ // Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
108
+ // Normalize so downstream wiring can use a single boolean consistently.
109
+ const noAgent = opts.agent === false;
110
+ // Skip cache when --wallet/--ows is specified (different wallet = different account)
95
111
  if (!walletName && _adapter)
96
112
  return _adapter;
97
113
  const exchange = resolveExchangeAlias(opts.exchange);
@@ -99,6 +115,10 @@ async function getAdapter() {
99
115
  const isTestnet = network === "testnet";
100
116
  // Try to load key — null means no key configured (read-only mode)
101
117
  const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
118
+ // OWS wallet detected — use OWS signers instead of raw keys
119
+ if (pk && isOwsKey(pk)) {
120
+ return _initWithOws(exchange, getOwsWalletName(pk), isTestnet, opts, owsKeyToken);
121
+ }
102
122
  switch (exchange) {
103
123
  case "pacifica": {
104
124
  const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
@@ -106,6 +126,27 @@ async function getAdapter() {
106
126
  const settings = loadSettings();
107
127
  const builderCode = process.env.PACIFICA_BUILDER_CODE || settings.referralCodes.pacifica || "PERPCLI";
108
128
  _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
129
+ // Tier 2: OWS master if --ows or settings.owsActiveWallet (Phase 2c)
130
+ const pacOwsName = owsName || _settings.owsActiveWallet;
131
+ if (pacOwsName && !pk) {
132
+ const { OwsSolanaSigner } = await import("./signer/ows-solana.js");
133
+ const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
134
+ const pp = await resolvePassphrase({ flag: opts.passphrase });
135
+ if (pp !== null)
136
+ _pacificaAdapter.setSigner(OwsSolanaSigner.create(pacOwsName, pp));
137
+ }
138
+ // Tier 1: agent if registered (Phase 2c)
139
+ const { getAgent: getPacAgent } = await import("./agent-wallet/store.js");
140
+ const pacAgentMeta = getPacAgent("pacifica");
141
+ if (pacAgentMeta) {
142
+ const { OwsSolanaSigner: PacOwsSolanaSigner } = await import("./signer/ows-solana.js");
143
+ // Pacifica agent wallet was created with empty passphrase (see runPacApproveFlow);
144
+ // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
145
+ const agentSigner = PacOwsSolanaSigner.create(pacAgentMeta.agentWalletName, "");
146
+ _pacificaAdapter.setAgentSigner(pacAgentMeta, agentSigner);
147
+ }
148
+ if (noAgent)
149
+ _pacificaAdapter.setNoAgent(true);
109
150
  _adapter = _pacificaAdapter;
110
151
  break;
111
152
  }
@@ -116,43 +157,79 @@ async function getAdapter() {
116
157
  await _hlAdapter.init();
117
158
  if (pk) {
118
159
  const hlSettings = loadSettings();
119
- if (hlSettings.referrals && !hlSettings.referralApplied.hyperliquid) {
160
+ if (!hlSettings.referralApplied.hyperliquid) {
120
161
  const hlRef = process.env.HL_REFERRAL_CODE || hlSettings.referralCodes.hyperliquid;
121
162
  if (hlRef) {
122
163
  _hlAdapter.autoSetReferrer(hlRef).then(() => {
123
164
  const s = loadSettings();
124
165
  s.referralApplied.hyperliquid = true;
125
166
  saveSettings(s);
126
- }).catch(() => {
127
- const s = loadSettings();
128
- s.referralApplied.hyperliquid = true;
129
- saveSettings(s);
167
+ }).catch((err) => {
168
+ // Per SSOT Rule #2: do NOT mark applied=true on failure.
169
+ // Leave referralApplied[ex]=false so next adapter init retries.
170
+ process.stderr.write(`[hyperliquid] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
130
171
  });
131
172
  }
132
173
  }
133
174
  }
175
+ // Tier 2: OWS master if --ows or settings.owsActiveWallet
176
+ const hlOwsName = owsName || _settings.owsActiveWallet;
177
+ if (hlOwsName && !pk) {
178
+ const { OwsEvmSigner } = await import("./signer/ows-evm.js");
179
+ const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
180
+ const pp = await resolvePassphrase({ flag: opts.passphrase });
181
+ if (pp !== null)
182
+ _hlAdapter.setSigner(OwsEvmSigner.create(hlOwsName, pp));
183
+ }
184
+ // Tier 1: agent if registered
185
+ const { getAgent: getHlAgent } = await import("./agent-wallet/store.js");
186
+ const hlAgentMeta = getHlAgent("hyperliquid");
187
+ if (hlAgentMeta) {
188
+ const { OwsEvmSigner: HlOwsEvmSigner } = await import("./signer/ows-evm.js");
189
+ // HL agent wallet was created with empty passphrase (see runHlApproveFlow);
190
+ // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
191
+ const agentSigner = HlOwsEvmSigner.create(hlAgentMeta.agentWalletName, "");
192
+ _hlAdapter.setAgentSigner(hlAgentMeta, agentSigner);
193
+ }
194
+ if (noAgent)
195
+ _hlAdapter.setNoAgent(true);
134
196
  _adapter = _hlAdapter;
135
197
  break;
136
198
  }
137
199
  case "lighter": {
138
200
  const { LighterAdapter } = await import("./exchanges/lighter.js");
201
+ // Phase 2d (SSOT Rule #3): when an agent is registered AND not bypassed,
202
+ // load the agent's L2 secp256k1 key from the encrypted keystore at
203
+ // ~/.perp/lighter-agents/<accountIndex>-<slot>.json BEFORE init() so the
204
+ // WASM client is created with the agent identity. Missing keystore →
205
+ // loadLighterKey throws LIGHTER_KEYSTORE_NOT_FOUND with remediation
206
+ // (no fallback to env per SSOT Rule #2).
207
+ const { getAgent: getLtAgent } = await import("./agent-wallet/store.js");
208
+ const ltAgentMeta = getLtAgent("lighter");
139
209
  _lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
210
+ if (ltAgentMeta && !noAgent) {
211
+ const { loadLighterKey } = await import("./agent-wallet/lighter-keystore.js");
212
+ const ltL2Key = loadLighterKey(ltAgentMeta.accountIndex, ltAgentMeta.apiKeyIndex, "");
213
+ _lighterAdapter.setAgentSigner(ltAgentMeta, ltL2Key);
214
+ }
215
+ if (noAgent)
216
+ _lighterAdapter.setNoAgent(true);
140
217
  await _lighterAdapter.init();
141
- if (pk) {
142
- const ltSettings = loadSettings();
143
- if (ltSettings.referrals && !ltSettings.referralApplied.lighter) {
144
- const ltRef = process.env.LIGHTER_REFERRAL_CODE || ltSettings.referralCodes.lighter;
145
- if (ltRef) {
146
- _lighterAdapter.useReferralCode(ltRef).then(() => {
147
- const s = loadSettings();
148
- s.referralApplied.lighter = true;
149
- saveSettings(s);
150
- }).catch(() => {
151
- const s = loadSettings();
152
- s.referralApplied.lighter = true;
153
- saveSettings(s);
154
- });
155
- }
218
+ // LT referral apply is L2-signed (POST /referral/use uses the WASM slot
219
+ // signer's auth token, not the master EVM). Trigger on any active
220
+ // signer tier — agent / OWS master / PK direct — gated by isReadOnly.
221
+ const ltSettings = loadSettings();
222
+ if (!ltSettings.referralApplied.lighter && !_lighterAdapter.isReadOnly) {
223
+ const ltRef = process.env.LIGHTER_REFERRAL_CODE || ltSettings.referralCodes.lighter;
224
+ if (ltRef) {
225
+ _lighterAdapter.useReferralCode(ltRef).then(() => {
226
+ const s = loadSettings();
227
+ s.referralApplied.lighter = true;
228
+ saveSettings(s);
229
+ }).catch((err) => {
230
+ // Per SSOT Rule #2: do NOT mark applied=true on failure.
231
+ process.stderr.write(`[lighter] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
232
+ });
156
233
  }
157
234
  }
158
235
  _adapter = _lighterAdapter;
@@ -160,8 +237,27 @@ async function getAdapter() {
160
237
  }
161
238
  case "aster": {
162
239
  const { AsterAdapter } = await import("./exchanges/aster.js");
163
- const ast = new AsterAdapter(undefined, undefined, isTestnet);
240
+ const ast = new AsterAdapter(pk ?? undefined, isTestnet); // Tier 3 from PK
164
241
  await ast.init();
242
+ // Tier 2: OWS master if --ows or settings.owsActiveWallet
243
+ const asterOwsName = owsName || _settings.owsActiveWallet;
244
+ if (asterOwsName) {
245
+ const { OwsEvmSigner } = await import("./signer/ows-evm.js");
246
+ const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
247
+ const pp = await resolvePassphrase({ flag: opts.passphrase });
248
+ if (pp !== null)
249
+ ast.setMasterSigner(OwsEvmSigner.create(asterOwsName, pp));
250
+ }
251
+ // Tier 1: agent if registered
252
+ const { getAgent: getAsterAgent } = await import("./agent-wallet/store.js");
253
+ const agentMeta = getAsterAgent("aster");
254
+ if (agentMeta) {
255
+ const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
256
+ const strat = agentSigningStrategyFor(agentMeta, owsKeyToken ?? "");
257
+ ast.setAgent(agentMeta, strat);
258
+ }
259
+ if (noAgent)
260
+ ast.setNoAgent(true);
165
261
  _adapter = ast;
166
262
  break;
167
263
  }
@@ -170,6 +266,72 @@ async function getAdapter() {
170
266
  }
171
267
  return _adapter;
172
268
  }
269
+ /** Initialize adapter with OWS wallet signer injection. */
270
+ async function _initWithOws(exchange, owsWalletName, isTestnet, opts, owsKeyToken) {
271
+ const { OwsEvmSigner } = await import("./signer/ows-evm.js");
272
+ const { OwsSolanaSigner } = await import("./signer/ows-solana.js");
273
+ // If an OWS API key token is provided, use it as passphrase → routes through policy engine
274
+ const passphrase = owsKeyToken || "";
275
+ // Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
276
+ const noAgent = opts.agent === false;
277
+ switch (exchange) {
278
+ case "pacifica": {
279
+ const pacNetwork = (isTestnet ? "testnet" : "mainnet");
280
+ const settings = loadSettings();
281
+ const builderCode = process.env.PACIFICA_BUILDER_CODE || settings.referralCodes.pacifica || "PERPCLI";
282
+ const dummyKeypair = Keypair.generate();
283
+ _pacificaAdapter = new PacificaAdapter(dummyKeypair, pacNetwork, builderCode, true);
284
+ _pacificaAdapter.setSigner(OwsSolanaSigner.create(owsWalletName, passphrase));
285
+ // Tier 1: agent if registered (Phase 2c)
286
+ const { getAgent: getPacAgentOws } = await import("./agent-wallet/store.js");
287
+ const pacAgentMetaOws = getPacAgentOws("pacifica");
288
+ if (pacAgentMetaOws) {
289
+ const agentSignerOws = OwsSolanaSigner.create(pacAgentMetaOws.agentWalletName, "");
290
+ _pacificaAdapter.setAgentSigner(pacAgentMetaOws, agentSignerOws);
291
+ }
292
+ if (noAgent)
293
+ _pacificaAdapter.setNoAgent(true);
294
+ _adapter = _pacificaAdapter;
295
+ return _adapter;
296
+ }
297
+ case "hyperliquid": {
298
+ _hlAdapter = new HyperliquidAdapter(undefined, isTestnet);
299
+ if (opts.dex)
300
+ _hlAdapter.setDex(opts.dex);
301
+ _hlAdapter.setSigner(OwsEvmSigner.create(owsWalletName, passphrase));
302
+ await _hlAdapter.init();
303
+ _adapter = _hlAdapter;
304
+ return _adapter;
305
+ }
306
+ case "lighter": {
307
+ const { LighterAdapter } = await import("./exchanges/lighter.js");
308
+ _lighterAdapter = new LighterAdapter("", isTestnet);
309
+ _lighterAdapter.setSigner(OwsEvmSigner.create(owsWalletName, passphrase));
310
+ await _lighterAdapter.init();
311
+ _adapter = _lighterAdapter;
312
+ return _adapter;
313
+ }
314
+ case "aster": {
315
+ const { AsterAdapter } = await import("./exchanges/aster.js");
316
+ const asterOws = new AsterAdapter(undefined, isTestnet);
317
+ await asterOws.init();
318
+ asterOws.setMasterSigner(OwsEvmSigner.create(owsWalletName, passphrase));
319
+ // Optionally also set agent if registered
320
+ const { getAgent: getAsterAgentOws } = await import("./agent-wallet/store.js");
321
+ const asterMeta = getAsterAgentOws("aster");
322
+ if (asterMeta) {
323
+ const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
324
+ asterOws.setAgent(asterMeta, agentSigningStrategyFor(asterMeta, owsKeyToken ?? ""));
325
+ }
326
+ if (noAgent)
327
+ asterOws.setNoAgent(true);
328
+ _adapter = asterOws;
329
+ return _adapter;
330
+ }
331
+ default:
332
+ throw new Error(`Unknown exchange: ${exchange}`);
333
+ }
334
+ }
173
335
  // Sync wrapper for commands that need adapter (lazy init)
174
336
  function getAdapterSync() {
175
337
  if (!_adapter)
@@ -197,12 +359,11 @@ function getHLAdapter() {
197
359
  registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange);
198
360
  registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
199
361
  registerTradeCommands(program, getAdapter, isJson, isDryRun, getAdapterForExchange);
200
- registerManageCommands(program, getAdapter, isJson, getPacificaAdapter);
362
+ // manage tree wired inside registerWalletCommands below.
201
363
  // stream commands removed
202
364
  registerArbCommands(program, isJson, getAdapterForExchange);
203
- registerWalletCommands(program, isJson);
204
- registerBridgeCommands(program, isJson);
205
- registerFundsCommands(program, getAdapter, isJson, () => program.opts().network);
365
+ registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter);
366
+ registerFundsCommands(program, getAdapter, isJson, () => program.opts().network, getAdapterForExchange);
206
367
  // alert commands removed
207
368
  // Helper to get adapter for a specific exchange (used by arb-auto)
208
369
  async function getAdapterForExchange(rawExchange) {
@@ -210,8 +371,19 @@ async function getAdapterForExchange(rawExchange) {
210
371
  const opts = program.opts();
211
372
  const network = opts.network;
212
373
  const isTestnet = network === "testnet";
213
- const walletName = opts.wallet;
374
+ const owsName = opts.ows;
375
+ const owsKeyToken = opts.owsKey || process.env.OWS_API_KEY;
376
+ const walletName = owsName ? `ows:${owsName}` : opts.wallet;
377
+ // Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
378
+ const noAgent = opts.agent === false;
214
379
  const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
380
+ // Route through OWS init when pk is an OWS reference (e.g. "ows:main").
381
+ // Mirrors getAdapter() at the top of this file. Without this, adapters
382
+ // would try to parse "ows:main" as a raw private key and ethers would
383
+ // throw "invalid BytesLike value" / parseSolanaKeypair would fail.
384
+ if (pk && isOwsKey(pk)) {
385
+ return _initWithOws(exchange, getOwsWalletName(pk), isTestnet, opts, owsKeyToken);
386
+ }
215
387
  switch (exchange) {
216
388
  case "pacifica": {
217
389
  if (_pacificaAdapter)
@@ -221,6 +393,18 @@ async function getAdapterForExchange(rawExchange) {
221
393
  const s1 = loadSettings();
222
394
  const builderCode = process.env.PACIFICA_BUILDER_CODE || s1.referralCodes.pacifica || "PERPCLI";
223
395
  _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
396
+ // Tier 1: agent if registered (Phase 2c)
397
+ const { getAgent: getPacAgentEx } = await import("./agent-wallet/store.js");
398
+ const pacAgentMetaEx = getPacAgentEx("pacifica");
399
+ if (pacAgentMetaEx) {
400
+ const { OwsSolanaSigner: PacOwsSolanaSignerEx } = await import("./signer/ows-solana.js");
401
+ // Pacifica agent wallet was created with empty passphrase (see runPacApproveFlow);
402
+ // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
403
+ const agentSignerEx = PacOwsSolanaSignerEx.create(pacAgentMetaEx.agentWalletName, "");
404
+ _pacificaAdapter.setAgentSigner(pacAgentMetaEx, agentSignerEx);
405
+ }
406
+ if (noAgent)
407
+ _pacificaAdapter.setNoAgent(true);
224
408
  if (!_adapter)
225
409
  _adapter = _pacificaAdapter;
226
410
  return _pacificaAdapter;
@@ -234,21 +418,35 @@ async function getAdapterForExchange(rawExchange) {
234
418
  await _hlAdapter.init();
235
419
  if (pk) {
236
420
  const s2 = loadSettings();
237
- if (s2.referrals && !s2.referralApplied.hyperliquid) {
421
+ if (!s2.referralApplied.hyperliquid) {
238
422
  const hlRef = process.env.HL_REFERRAL_CODE || s2.referralCodes.hyperliquid;
239
423
  if (hlRef) {
240
424
  _hlAdapter.autoSetReferrer(hlRef).then(() => {
241
425
  const s = loadSettings();
242
426
  s.referralApplied.hyperliquid = true;
243
427
  saveSettings(s);
244
- }).catch(() => {
245
- const s = loadSettings();
246
- s.referralApplied.hyperliquid = true;
247
- saveSettings(s);
428
+ }).catch((err) => {
429
+ // Per SSOT Rule #2: do NOT mark applied=true on failure.
430
+ // Leave referralApplied[ex]=false so next adapter init retries.
431
+ process.stderr.write(`[hyperliquid] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
248
432
  });
249
433
  }
250
434
  }
251
435
  }
436
+ // Tier 1: agent if registered
437
+ {
438
+ const { getAgent: getHlAgent2 } = await import("./agent-wallet/store.js");
439
+ const hlAgentMeta2 = getHlAgent2("hyperliquid");
440
+ if (hlAgentMeta2) {
441
+ const { OwsEvmSigner: HlOwsEvmSigner2 } = await import("./signer/ows-evm.js");
442
+ // HL agent wallet was created with empty passphrase (see runHlApproveFlow);
443
+ // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
444
+ const agentSigner2 = HlOwsEvmSigner2.create(hlAgentMeta2.agentWalletName, "");
445
+ _hlAdapter.setAgentSigner(hlAgentMeta2, agentSigner2);
446
+ }
447
+ if (noAgent)
448
+ _hlAdapter.setNoAgent(true);
449
+ }
252
450
  if (!_adapter)
253
451
  _adapter = _hlAdapter;
254
452
  return _hlAdapter;
@@ -257,23 +455,32 @@ async function getAdapterForExchange(rawExchange) {
257
455
  if (_lighterAdapter)
258
456
  return _lighterAdapter;
259
457
  const { LighterAdapter } = await import("./exchanges/lighter.js");
458
+ const { getAgent: getLtAgentEx } = await import("./agent-wallet/store.js");
459
+ const ltAgentMetaEx = getLtAgentEx("lighter");
260
460
  _lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
461
+ if (ltAgentMetaEx && !noAgent) {
462
+ // SSOT Rule #3: load agent L2 key from encrypted keystore — no env fallback.
463
+ const { loadLighterKey } = await import("./agent-wallet/lighter-keystore.js");
464
+ const ltL2KeyEx = loadLighterKey(ltAgentMetaEx.accountIndex, ltAgentMetaEx.apiKeyIndex, "");
465
+ _lighterAdapter.setAgentSigner(ltAgentMetaEx, ltL2KeyEx);
466
+ }
467
+ if (noAgent)
468
+ _lighterAdapter.setNoAgent(true);
261
469
  await _lighterAdapter.init();
262
- if (pk) {
263
- const s3 = loadSettings();
264
- if (s3.referrals && !s3.referralApplied.lighter) {
265
- const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
266
- if (ltRef) {
267
- _lighterAdapter.useReferralCode(ltRef).then(() => {
268
- const s = loadSettings();
269
- s.referralApplied.lighter = true;
270
- saveSettings(s);
271
- }).catch(() => {
272
- const s = loadSettings();
273
- s.referralApplied.lighter = true;
274
- saveSettings(s);
275
- });
276
- }
470
+ // LT referral apply is L2-signed; trigger on any active tier (agent
471
+ // included), gated by isReadOnly. See first LT case for rationale.
472
+ const s3 = loadSettings();
473
+ if (!s3.referralApplied.lighter && !_lighterAdapter.isReadOnly) {
474
+ const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
475
+ if (ltRef) {
476
+ _lighterAdapter.useReferralCode(ltRef).then(() => {
477
+ const s = loadSettings();
478
+ s.referralApplied.lighter = true;
479
+ saveSettings(s);
480
+ }).catch((err) => {
481
+ // Per SSOT Rule #2: do NOT mark applied=true on failure.
482
+ process.stderr.write(`[lighter] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
483
+ });
277
484
  }
278
485
  }
279
486
  if (!_adapter)
@@ -282,9 +489,30 @@ async function getAdapterForExchange(rawExchange) {
282
489
  }
283
490
  case "aster": {
284
491
  const { AsterAdapter } = await import("./exchanges/aster.js");
285
- const ast = new AsterAdapter(undefined, undefined, isTestnet);
286
- await ast.init();
287
- return ast;
492
+ const astEx = new AsterAdapter(pk ?? undefined, isTestnet);
493
+ await astEx.init();
494
+ // Tier 2: OWS master if --ows or settings.owsActiveWallet
495
+ const asterExOwsName = opts.ows || _settings.owsActiveWallet;
496
+ if (asterExOwsName) {
497
+ const { OwsEvmSigner } = await import("./signer/ows-evm.js");
498
+ const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
499
+ const pp = await resolvePassphrase({ flag: opts.passphrase });
500
+ if (pp !== null)
501
+ astEx.setMasterSigner(OwsEvmSigner.create(asterExOwsName, pp));
502
+ }
503
+ // Tier 1: agent if registered
504
+ const owsKeyEx = opts.owsKey || process.env.OWS_API_KEY;
505
+ const { getAgent: getAsterAgentEx } = await import("./agent-wallet/store.js");
506
+ const agentMetaEx = getAsterAgentEx("aster");
507
+ if (agentMetaEx) {
508
+ const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
509
+ astEx.setAgent(agentMetaEx, agentSigningStrategyFor(agentMetaEx, owsKeyEx ?? ""));
510
+ }
511
+ if (noAgent)
512
+ astEx.setNoAgent(true);
513
+ if (!_adapter)
514
+ _adapter = astEx;
515
+ return astEx;
288
516
  }
289
517
  default:
290
518
  throw new Error(`Unknown exchange: ${exchange}`);
@@ -307,312 +535,27 @@ async function getHLAdapterForDex(dex) {
307
535
  }
308
536
  registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
309
537
  registerArbManageCommands(program, getAdapterForExchange, isJson);
310
- registerAgentCommands(program, getAdapter, isJson);
311
- // withdraw merged into funds
312
- registerRebalanceCommands(program, getAdapterForExchange, isJson);
313
- // Jobs & strategies
314
- import { registerJobsCommands } from "./commands/jobs.js";
315
- registerJobsCommands(program, isJson);
316
- registerBotCommands(program, getAdapter, getAdapterForExchange, isJson);
538
+ // `agent` is no longer top-level — registered as `wallet agent` inside
539
+ // registerWalletCommands above.
540
+ // withdraw merged into funds.
541
+ // `rebalance` is no longer top-level — registered as `funds rebalance` inside
542
+ // registerFundsCommands above.
543
+ // Background processes & strategies
544
+ import { registerBackgroundCommands } from "./commands/jobs.js";
545
+ registerBackgroundCommands(program, isJson);
546
+ registerStrategyCommands(program, getAdapter, getAdapterForExchange, isJson);
317
547
  // Agent-friendly commands
318
548
  registerRiskCommands(program, getAdapterForExchange, isJson);
319
549
  registerHistoryCommands(program, isJson, getAdapterForExchange);
320
550
  registerSettingsCommands(program, isJson, getAdapterForExchange);
321
551
  // dex commands removed — use 'market hip3' + --dex flag
322
- registerPlanCommands(program, getAdapter, isJson);
552
+ // `plan` is no longer top-level — registered as `strategy plan` inside
553
+ // registerStrategyCommands above.
323
554
  // funding merged into arb — registerFundingCommands removed
324
555
  registerBacktestCommands(program, isJson);
325
- registerDashboardCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
556
+ registerPortfolioCommand(program, getAdapterForExchange, isJson, _pkg.version, getHLAdapterForDex);
326
557
  registerInitCommand(program);
327
558
  registerAlertCommands(program, isJson);
328
- // Agent discovery: perp api-spec — deprecated, use 'perp agent schema'
329
- const apiSpecCmd = program
330
- .command("api-spec")
331
- .description("Use 'perp agent schema'")
332
- .action(async () => {
333
- const { jsonOk, printJson } = await import("./utils.js");
334
- const { getCliSpec } = await import("./cli-spec.js");
335
- printJson(jsonOk(getCliSpec(program)));
336
- });
337
- apiSpecCmd._hidden = true;
338
- // Unified dashboard: balances + positions + top arb opportunities
339
- program.command("status")
340
- .description("Unified dashboard: balances, positions, and top arb opportunities")
341
- .option("--health", "Check connectivity only")
342
- .action(async (opts) => {
343
- if (opts.health) {
344
- const { runHealthCheck } = await import("./commands/risk.js");
345
- return runHealthCheck(isJson);
346
- }
347
- const json = isJson();
348
- const { formatUsd, formatPnl, makeTable, printJson, jsonOk, withJsonErrors } = await import("./utils.js");
349
- const { fetchAllFundingRates, TOP_SYMBOLS } = await import("./funding-rates.js");
350
- const { saveFundingSnapshot, getHistoricalRates } = await import("./funding-history.js");
351
- await withJsonErrors(json, async () => {
352
- const EX_LIST = ["pacifica", "hyperliquid", "lighter", "aster"];
353
- const exAbbr = (e) => e === "pacifica" ? "PAC" : e === "hyperliquid" ? "HL" : e === "lighter" ? "LT" : "AST";
354
- const snapshotPromises = EX_LIST.map(async (ex) => {
355
- try {
356
- const adapter = await getAdapterForExchange(ex);
357
- const [balance, positions, orders] = await Promise.all([
358
- adapter.getBalance(), adapter.getPositions(), adapter.getOpenOrders(),
359
- ]);
360
- // Fetch spot balances for HL and LT (Pacifica is perp-only)
361
- let spotHoldings = [];
362
- let isUnified = false;
363
- try {
364
- if (ex === "hyperliquid") {
365
- const { HyperliquidSpotAdapter } = await import("./exchanges/hyperliquid-spot.js");
366
- const hlSpot = new HyperliquidSpotAdapter(adapter);
367
- await hlSpot.init();
368
- const [raw, markets] = await Promise.all([hlSpot.getSpotBalances(), hlSpot.getSpotMarkets()]);
369
- const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
370
- const strip = (t) => t.replace(/-SPOT$/i, "").toUpperCase();
371
- spotHoldings = raw.filter(b => Number(b.total) > 0).map(b => {
372
- const base = strip(b.token);
373
- return { ...b, valueUsd: base === "USDC" ? Number(b.total) : (priceMap.get(base) ?? 0) * Number(b.total) };
374
- });
375
- isUnified = !adapter.dex;
376
- }
377
- else if (ex === "lighter") {
378
- const { LighterAdapter } = await import("./exchanges/lighter.js");
379
- const { LighterSpotAdapter } = await import("./exchanges/lighter-spot.js");
380
- const ltSpot = new LighterSpotAdapter(adapter);
381
- await ltSpot.init();
382
- const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
383
- const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
384
- spotHoldings = raw.filter(b => Number(b.total) > 0).map(b => ({
385
- ...b,
386
- valueUsd: b.token === "USDC" || b.token === "USDC_SPOT" ? Number(b.total) : (priceMap.get(b.token.toUpperCase()) ?? 0) * Number(b.total),
387
- }));
388
- }
389
- }
390
- catch { /* spot not available */ }
391
- return { exchange: ex, connected: true, balance, positions, openOrders: orders.length, spotHoldings, isUnified, error: undefined };
392
- }
393
- catch (err) {
394
- return { exchange: ex, connected: false, balance: null, positions: [], openOrders: 0, spotHoldings: [], isUnified: false, error: err instanceof Error ? err.message : String(err) };
395
- }
396
- });
397
- const arbPromise = fetchAllFundingRates({ symbols: TOP_SYMBOLS, minSpread: 0 }).catch(() => null);
398
- const [snapshots, arbSnapshot] = await Promise.all([Promise.all(snapshotPromises), arbPromise]);
399
- // Save funding snapshot for sparkline history
400
- if (arbSnapshot) {
401
- try {
402
- const allRates = arbSnapshot.symbols.flatMap(s => s.rates);
403
- if (allRates.length > 0)
404
- saveFundingSnapshot(allRates);
405
- }
406
- catch { /* */ }
407
- }
408
- // Build totals (include non-USDC spot value for unified accounts)
409
- let totalEquity = 0;
410
- let totalSpotValue = 0;
411
- const allPositions = [];
412
- const allSpotHoldings = [];
413
- for (const s of snapshots) {
414
- if (s.balance)
415
- totalEquity += Number(s.balance.equity);
416
- for (const p of s.positions)
417
- allPositions.push({ ...p, exchange: s.exchange });
418
- // For unified accounts (HL), USDC is already in perp equity — only add non-USDC spot
419
- const displaySpot = s.isUnified
420
- ? s.spotHoldings.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
421
- : s.spotHoldings;
422
- const spotVal = displaySpot.reduce((sum, b) => sum + b.valueUsd, 0);
423
- totalSpotValue += spotVal;
424
- totalEquity += spotVal;
425
- for (const b of displaySpot)
426
- allSpotHoldings.push({ ...b, exchange: s.exchange });
427
- }
428
- // Top arb
429
- const topArb = arbSnapshot
430
- ? arbSnapshot.symbols.filter(s => s.maxSpreadAnnual >= 5).sort((a, b) => b.maxSpreadAnnual - a.maxSpreadAnnual).slice(0, 5)
431
- : [];
432
- // Risk
433
- const marginUsed = snapshots.reduce((s, x) => s + (x.balance ? Number(x.balance.marginUsed) : 0), 0);
434
- const marginPct = totalEquity > 0 ? (marginUsed / totalEquity) * 100 : 0;
435
- const riskLevel = marginPct < 30 ? "LOW" : marginPct < 60 ? "MEDIUM" : "HIGH";
436
- if (json) {
437
- const now = new Date();
438
- const h24 = new Date(now.getTime() - 24 * 3600_000);
439
- return printJson(jsonOk({
440
- version: _pkg.version,
441
- totalEquity,
442
- riskLevel,
443
- exchanges: snapshots.map(s => ({
444
- name: s.exchange, connected: s.connected,
445
- equity: s.balance ? Number(s.balance.equity) : 0,
446
- available: s.balance ? Number(s.balance.available) : 0,
447
- spotHoldings: s.spotHoldings.filter(b => Number(b.total) > 0).map(b => ({
448
- token: b.token, total: b.total, available: b.available, valueUsd: b.valueUsd,
449
- })),
450
- })),
451
- totalSpotValue,
452
- positions: allPositions,
453
- topArbOpportunities: await (async () => {
454
- const { getHistoricalAverages: getAvgs } = await import("./funding-history.js");
455
- const syms = topArb.map(a => a.symbol);
456
- const avgs = syms.length > 0 ? getAvgs(syms, ["hyperliquid", "pacifica", "lighter"]) : new Map();
457
- return topArb.map(a => {
458
- const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
459
- const avg = avgs.get(`${a.symbol}:${bestEx}`);
460
- return {
461
- symbol: a.symbol, spreadAnnual: a.maxSpreadAnnual,
462
- direction: `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`,
463
- avg24h: avg?.avg24h != null ? Math.abs(avg.avg24h) * 8760 * 100 : null,
464
- avg7d: avg?.avg7d != null ? Math.abs(avg.avg7d) * 8760 * 100 : null,
465
- rateHistory: getHistoricalRates(a.symbol, bestEx, h24, now).map(h => ({ ts: h.ts, hourlyRate: h.hourlyRate })),
466
- };
467
- });
468
- })(),
469
- }));
470
- }
471
- // ── Terminal UI ──
472
- console.log(chalk.cyan.bold(`\n perp-cli v${_pkg.version}`) + chalk.gray(` — ${snapshots.filter(s => s.connected).length} exchanges connected\n`));
473
- // Balance bars + Top Arb side by side
474
- const now = new Date();
475
- const h24 = new Date(now.getTime() - 24 * 3600_000);
476
- // Left column: Balances (perp + spot per exchange)
477
- // Compute per-exchange totals: perp equity + non-USDC spot (unified) or all spot
478
- const exTotals = snapshots.map(s => {
479
- if (!s.connected || !s.balance)
480
- return { total: 0, margin: 0, equity: 0 };
481
- const equity = Number(s.balance.equity);
482
- const margin = Number(s.balance.marginUsed);
483
- const displaySpot = s.isUnified
484
- ? s.spotHoldings.filter(b => b.token.replace(/-SPOT$/i, "").toUpperCase() !== "USDC")
485
- : s.spotHoldings;
486
- const spotVal = displaySpot.reduce((sum, b) => sum + b.valueUsd, 0);
487
- return { total: equity + spotVal, margin, equity };
488
- });
489
- const balLines = [];
490
- balLines.push(chalk.white.bold(" Balances"));
491
- for (let i = 0; i < snapshots.length; i++) {
492
- const s = snapshots[i];
493
- if (!s.connected) {
494
- balLines.push(` ${chalk.gray(exAbbr(s.exchange).padEnd(4))} ${chalk.red("disconnected")}`);
495
- continue;
496
- }
497
- const { total, margin, equity } = exTotals[i];
498
- const usagePct = equity > 0 ? (margin / equity) * 100 : 0;
499
- const barFull = Math.min(20, Math.round(usagePct / 5));
500
- const usageColor = usagePct < 30 ? chalk.green : usagePct < 60 ? chalk.yellow : chalk.red;
501
- const bar = usageColor("\u2588".repeat(barFull)) + chalk.gray("\u2591".repeat(20 - barFull));
502
- balLines.push(` ${chalk.white.bold(exAbbr(s.exchange).padEnd(4))} $${formatUsd(total).padEnd(9)} ${bar} ${usagePct.toFixed(0)}% used`);
503
- }
504
- // Spot holdings (non-USDC tokens with value)
505
- const displaySpotHoldings = allSpotHoldings.filter(b => {
506
- const tk = b.token.replace(/[-_]SPOT$/i, "").toUpperCase();
507
- return tk !== "USDC";
508
- });
509
- if (displaySpotHoldings.length > 0) {
510
- balLines.push("");
511
- balLines.push(chalk.white.bold(" Spot Holdings"));
512
- for (const b of displaySpotHoldings) {
513
- const token = b.token.replace(/-SPOT$/i, "");
514
- const ex = exAbbr(b.exchange);
515
- balLines.push(` ${chalk.gray(ex.padEnd(4))} ${chalk.white.bold(token.padEnd(6))} ${b.total.padEnd(12)} ${chalk.gray(`$${formatUsd(b.valueUsd)}`)}`);
516
- }
517
- }
518
- const riskColor = riskLevel === "LOW" ? chalk.green : riskLevel === "MEDIUM" ? chalk.yellow : chalk.red;
519
- balLines.push(` ${"─".repeat(44)}`);
520
- balLines.push(` ${chalk.cyan.bold("Total")} ${chalk.cyan.bold(`$${formatUsd(totalEquity)}`.padEnd(9))} Risk: ${riskColor(riskLevel)}`);
521
- // Right column: Top Arb Opportunities (with 24h/7d averages from local history — 0 API calls)
522
- const { getHistoricalAverages } = await import("./funding-history.js");
523
- const arbSymbols = topArb.map(a => a.symbol);
524
- const arbExchanges = ["hyperliquid", "pacifica", "lighter"];
525
- const histAvgs = arbSymbols.length > 0 ? getHistoricalAverages(arbSymbols, arbExchanges) : new Map();
526
- const arbLines = [];
527
- arbLines.push(chalk.white.bold(" Top Arb Opportunities"));
528
- if (topArb.length > 0) {
529
- // SYMBOL SPREAD 24h TREND 7d DIR
530
- arbLines.push(chalk.gray(" " + "".padEnd(8) + "now".padEnd(9) + "24h".padEnd(7) + "".padEnd(9) + "7d".padEnd(7)));
531
- for (const a of topArb) {
532
- const spreadColor = a.maxSpreadAnnual >= 100 ? chalk.green.bold : a.maxSpreadAnnual >= 30 ? chalk.green : chalk.yellow;
533
- const dir = `${exAbbr(a.shortExchange)}>${exAbbr(a.longExchange)}`;
534
- const bestEx = a.rates.find(r => r.exchange === "hyperliquid")?.exchange ?? a.rates[0]?.exchange ?? "hyperliquid";
535
- // 24h trend arrow
536
- const history = getHistoricalRates(a.symbol, bestEx, h24, now);
537
- let trend = "";
538
- if (history.length >= 2) {
539
- const oldAnn = Math.abs(history[0].hourlyRate) * 8760 * 100;
540
- const newAnn = Math.abs(history[history.length - 1].hourlyRate) * 8760 * 100;
541
- const delta = newAnn - oldAnn;
542
- const abs = Math.abs(delta);
543
- trend = abs < 1 ? chalk.gray(`\u2500${abs.toFixed(0)}%`)
544
- : delta > 0 ? chalk.red(`\u25B2+${abs.toFixed(0)}%`)
545
- : chalk.green(`\u25BC-${abs.toFixed(0)}%`);
546
- }
547
- // 24h and 7d averages (annualized from hourly)
548
- const avg = histAvgs.get(`${a.symbol}:${bestEx}`);
549
- const avg24h = avg?.avg24h != null ? `${(Math.abs(avg.avg24h) * 8760 * 100).toFixed(0)}%` : "-";
550
- const avg7d = avg?.avg7d != null ? `${(Math.abs(avg.avg7d) * 8760 * 100).toFixed(0)}%` : "-";
551
- arbLines.push(` ${chalk.white.bold(a.symbol.padEnd(8))} ${spreadColor(`${a.maxSpreadAnnual.toFixed(1)}%`.padEnd(9))}${chalk.gray(avg24h.padEnd(7))}${trend.padEnd(9)}${chalk.gray(avg7d.padEnd(7))}${chalk.gray(dir)}`);
552
- }
553
- }
554
- else {
555
- arbLines.push(chalk.gray(" No opportunities above 5%"));
556
- }
557
- // Print layout — side-by-side if terminal is wide enough, else stacked
558
- const stripAnsi = (s) => s.replace(/\u001b\[[0-9;]*m/g, "");
559
- const termW = process.stdout.columns || 80;
560
- const SIDE_BY_SIDE_MIN = 100; // 2 indent + 1 + 46 + 1 + 48 + 1
561
- if (termW >= SIDE_BY_SIDE_MIN) {
562
- const LEFT_W = 46;
563
- const RIGHT_W = 48;
564
- const maxLines = Math.max(balLines.length, arbLines.length);
565
- console.log(` ${"┌"}${"─".repeat(LEFT_W)}${"┬"}${"─".repeat(RIGHT_W)}${"┐"}`);
566
- for (let i = 0; i < maxLines; i++) {
567
- const left = balLines[i] ?? "";
568
- const right = arbLines[i] ?? "";
569
- const leftPad = LEFT_W - stripAnsi(left).length;
570
- const rightPad = RIGHT_W - stripAnsi(right).length;
571
- console.log(` \u2502${left}${" ".repeat(Math.max(0, leftPad))}\u2502${right}${" ".repeat(Math.max(0, rightPad))}\u2502`);
572
- }
573
- console.log(` ${"└"}${"─".repeat(LEFT_W)}${"┴"}${"─".repeat(RIGHT_W)}${"┘"}`);
574
- }
575
- else {
576
- // Stacked layout for narrow terminals
577
- const boxW = Math.min(termW - 4, 74); // 2 indent + 2 borders
578
- const printBox = (lines) => {
579
- console.log(` ${"┌"}${"─".repeat(boxW)}${"┐"}`);
580
- for (const line of lines) {
581
- const pad = boxW - stripAnsi(line).length;
582
- console.log(` \u2502${line}${" ".repeat(Math.max(0, pad))}\u2502`);
583
- }
584
- console.log(` ${"└"}${"─".repeat(boxW)}${"┘"}`);
585
- };
586
- printBox(balLines);
587
- console.log();
588
- printBox(arbLines);
589
- }
590
- // Positions
591
- if (allPositions.length > 0) {
592
- console.log(chalk.white.bold("\n Positions"));
593
- const posRows = allPositions.map(p => {
594
- const sideColor = p.side === "long" ? chalk.green : chalk.red;
595
- const notional = Math.abs(Number(p.size) * Number(p.markPrice));
596
- const levStr = p.leverage > 0 ? `${p.leverage}x` : "-";
597
- const warn = p.leverage >= 5 ? chalk.red(" \u26A0") : "";
598
- return [
599
- chalk.white.bold(p.symbol.replace("-PERP", "")),
600
- chalk.gray(exAbbr(p.exchange)),
601
- sideColor(p.side.toUpperCase()),
602
- p.size,
603
- `$${formatUsd(p.entryPrice)}\u2192$${formatUsd(p.markPrice)}`,
604
- formatPnl(p.unrealizedPnl),
605
- `$${formatUsd(notional)}`,
606
- levStr + warn,
607
- ];
608
- });
609
- console.log(makeTable(["Symbol", "Ex", "Side", "Size", "Entry\u2192Mark", "uPnL", "Notional", "Lev"], posRows));
610
- }
611
- else {
612
- console.log(chalk.gray("\n No open positions.\n"));
613
- }
614
- });
615
- });
616
559
  // Switch shared API URLs if --network testnet is used
617
560
  program.hook("preAction", () => {
618
561
  const network = program.opts().network;
@@ -632,6 +575,7 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
632
575
  const hasEnvKey = !!(process.env.PRIVATE_KEY || process.env.PACIFICA_PRIVATE_KEY ||
633
576
  process.env.HL_PRIVATE_KEY || process.env.HYPERLIQUID_PRIVATE_KEY ||
634
577
  process.env.LIGHTER_PRIVATE_KEY);
578
+ const hasOwsWallet = !!settings.owsActiveWallet;
635
579
  // ── ASCII banner ──
636
580
  const banner = [
637
581
  "",
@@ -645,9 +589,10 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
645
589
  chalk.gray(` 4 exchanges · 19 strategies · v${_pkg.version}`),
646
590
  ];
647
591
  console.log(banner.join("\n"));
648
- if (!status.hasWallets && !hasEnvKey && !settings.defaultExchange) {
592
+ if (!status.hasWallets && !hasEnvKey && !hasOwsWallet && !settings.defaultExchange) {
649
593
  // Fresh install — onboarding
650
594
  console.log(chalk.yellow.bold("\n ⚡ Get started:\n"));
595
+ console.log(` ${chalk.cyan("perp wallet generate")} create OWS encrypted wallet`);
651
596
  console.log(` ${chalk.cyan("perp setup")} setup wizard`);
652
597
  console.log(chalk.gray(`\n Explore without a wallet:`));
653
598
  console.log(` ${chalk.green("perp market mid BTC")} quick price check`);
@@ -727,8 +672,8 @@ if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawAr
727
672
  console.log(` ${chalk.green("perp portfolio")} balances + positions + risk`);
728
673
  console.log(` ${chalk.green("perp arb scan")} funding arbitrage opportunities`);
729
674
  console.log(` ${chalk.green("perp arb status")} open arb positions + PnL`);
730
- console.log(` ${chalk.green("perp status")} full dashboard`);
731
- console.log(` ${chalk.green("perp dashboard")} live web monitoring`);
675
+ console.log(` ${chalk.green("perp portfolio --arb")} full dashboard (balances + arb top 5)`);
676
+ console.log(` ${chalk.green("perp portfolio --serve")} live web monitoring`);
732
677
  console.log(` ${chalk.green("perp --help")} all commands\n`);
733
678
  }
734
679
  }