perp-cli 0.12.15 → 0.14.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 (361) hide show
  1. package/README.md +278 -31
  2. package/dist/_stderr-filter.js +0 -1
  3. package/dist/agent-wallet/lighter-keystore.d.ts +11 -0
  4. package/dist/agent-wallet/lighter-keystore.js +92 -10
  5. package/dist/agent-wallet/passphrase.d.ts +1 -1
  6. package/dist/agent-wallet/passphrase.js +16 -3
  7. package/dist/agent-wallet/store.d.ts +37 -4
  8. package/dist/agent-wallet/store.js +132 -0
  9. package/dist/agent-wallet/venue-keystore.d.ts +14 -0
  10. package/dist/agent-wallet/venue-keystore.js +121 -0
  11. package/dist/api/public/hyperliquid.js +4 -4
  12. package/dist/api/public/lighter.js +5 -5
  13. package/dist/api/public/pacifica.d.ts +0 -1
  14. package/dist/api/public/pacifica.js +4 -13
  15. package/dist/api/public/urls.d.ts +9 -1
  16. package/dist/api/public/urls.js +35 -13
  17. package/dist/arb/file-lock.d.ts +11 -0
  18. package/dist/arb/file-lock.js +74 -0
  19. package/dist/arb/index.d.ts +2 -2
  20. package/dist/arb/index.js +2 -2
  21. package/dist/arb/pending-entry.d.ts +32 -0
  22. package/dist/arb/pending-entry.js +77 -0
  23. package/dist/arb/sizing.d.ts +6 -13
  24. package/dist/arb/sizing.js +12 -51
  25. package/dist/arb/state.d.ts +45 -12
  26. package/dist/arb/state.js +214 -42
  27. package/dist/arb/utils.d.ts +20 -6
  28. package/dist/arb/utils.js +43 -39
  29. package/dist/bot/conditions.d.ts +4 -4
  30. package/dist/bot/conditions.js +11 -6
  31. package/dist/bot/config.d.ts +17 -1
  32. package/dist/bot/config.js +48 -12
  33. package/dist/bot/engine.d.ts +61 -2
  34. package/dist/bot/engine.js +610 -144
  35. package/dist/bot/fill-size.d.ts +7 -0
  36. package/dist/bot/fill-size.js +18 -0
  37. package/dist/bot/index.d.ts +1 -1
  38. package/dist/bot/index.js +1 -1
  39. package/dist/bot/perp-order.d.ts +99 -0
  40. package/dist/bot/perp-order.js +299 -0
  41. package/dist/bot/presets.d.ts +7 -0
  42. package/dist/bot/presets.js +17 -0
  43. package/dist/bot/quarantine.d.ts +75 -0
  44. package/dist/bot/quarantine.js +190 -0
  45. package/dist/bot/quote-rules.d.ts +23 -0
  46. package/dist/bot/quote-rules.js +69 -0
  47. package/dist/bot/strategies/avellaneda-mm.d.ts +32 -2
  48. package/dist/bot/strategies/avellaneda-mm.js +72 -70
  49. package/dist/bot/strategies/basis-arb.js +2 -0
  50. package/dist/bot/strategies/dca-strategy.d.ts +2 -0
  51. package/dist/bot/strategies/dca-strategy.js +5 -0
  52. package/dist/bot/strategies/funding-arb-strategy.d.ts +43 -3
  53. package/dist/bot/strategies/funding-arb-strategy.js +571 -380
  54. package/dist/bot/strategies/funding-arb-utils.d.ts +48 -23
  55. package/dist/bot/strategies/funding-arb-utils.js +159 -195
  56. package/dist/bot/strategies/funding-arb-v2.d.ts +32 -0
  57. package/dist/bot/strategies/funding-arb-v2.js +258 -47
  58. package/dist/bot/strategies/funding-auto.js +215 -194
  59. package/dist/bot/strategies/spot-perp-arb-strategy.d.ts +14 -0
  60. package/dist/bot/strategies/spot-perp-arb-strategy.js +217 -50
  61. package/dist/bot/strategies/spot-perp-executor.d.ts +73 -19
  62. package/dist/bot/strategies/spot-perp-executor.js +319 -120
  63. package/dist/bot/strategies/spot-perp-state.d.ts +43 -0
  64. package/dist/bot/strategies/spot-perp-state.js +70 -0
  65. package/dist/bot/strategy-types.d.ts +6 -0
  66. package/dist/bot/tui/BotDashboard.d.ts +7 -2
  67. package/dist/bot/tui/BotDashboard.js +6 -3
  68. package/dist/bridge-engine.d.ts +120 -19
  69. package/dist/bridge-engine.js +710 -422
  70. package/dist/bridge-signers.d.ts +54 -0
  71. package/dist/bridge-signers.js +161 -0
  72. package/dist/cache.d.ts +4 -1
  73. package/dist/cache.js +11 -0
  74. package/dist/cli-flags.d.ts +50 -0
  75. package/dist/cli-flags.js +104 -0
  76. package/dist/cli-program.d.ts +35 -0
  77. package/dist/cli-program.js +286 -0
  78. package/dist/cli-spec.js +1 -1
  79. package/dist/client-id-tracker.js +11 -2
  80. package/dist/commands/account.d.ts +7 -1
  81. package/dist/commands/account.js +74 -84
  82. package/dist/commands/agent-import.d.ts +114 -0
  83. package/dist/commands/agent-import.js +794 -0
  84. package/dist/commands/agent.d.ts +232 -2
  85. package/dist/commands/agent.js +1858 -955
  86. package/dist/commands/alerts.d.ts +1 -0
  87. package/dist/commands/alerts.js +31 -28
  88. package/dist/commands/arb/index.d.ts +60 -1
  89. package/dist/commands/arb/index.js +381 -384
  90. package/dist/commands/arb-auto.d.ts +132 -4
  91. package/dist/commands/arb-auto.js +1474 -967
  92. package/dist/commands/arb.d.ts +3 -1
  93. package/dist/commands/arb.js +62 -30
  94. package/dist/commands/bot.js +127 -47
  95. package/dist/commands/bridge.d.ts +2 -1
  96. package/dist/commands/bridge.js +339 -192
  97. package/dist/commands/builder.d.ts +55 -0
  98. package/dist/commands/builder.js +387 -0
  99. package/dist/commands/funds-onchain.d.ts +32 -0
  100. package/dist/commands/funds-onchain.js +286 -0
  101. package/dist/commands/funds.d.ts +25 -0
  102. package/dist/commands/funds.js +939 -245
  103. package/dist/commands/history.js +72 -45
  104. package/dist/commands/init.d.ts +107 -15
  105. package/dist/commands/init.js +1116 -249
  106. package/dist/commands/jobs.d.ts +2 -1
  107. package/dist/commands/jobs.js +77 -15
  108. package/dist/commands/manage.js +136 -91
  109. package/dist/commands/market.d.ts +4 -1
  110. package/dist/commands/market.js +35 -32
  111. package/dist/commands/outcome.d.ts +10 -0
  112. package/dist/commands/outcome.js +396 -0
  113. package/dist/commands/plan.js +52 -12
  114. package/dist/commands/portfolio.d.ts +1 -0
  115. package/dist/commands/portfolio.js +60 -72
  116. package/dist/commands/rebalance.d.ts +2 -1
  117. package/dist/commands/rebalance.js +33 -43
  118. package/dist/commands/risk.js +46 -19
  119. package/dist/commands/settings.d.ts +1 -0
  120. package/dist/commands/settings.js +246 -51
  121. package/dist/commands/trade.d.ts +50 -0
  122. package/dist/commands/trade.js +1943 -695
  123. package/dist/commands/wallet.d.ts +30 -1
  124. package/dist/commands/wallet.js +316 -337
  125. package/dist/config.d.ts +60 -9
  126. package/dist/config.js +252 -82
  127. package/dist/constants.d.ts +29 -4
  128. package/dist/constants.js +62 -20
  129. package/dist/dashboard/server.d.ts +2 -0
  130. package/dist/dashboard/server.js +9 -6
  131. package/dist/dashboard/ui.js +6 -4
  132. package/dist/dashboard/ws-feeds.js +20 -8
  133. package/dist/dex-asset-map.js +5 -3
  134. package/dist/errors.d.ts +122 -0
  135. package/dist/errors.js +194 -0
  136. package/dist/event-stream.js +75 -19
  137. package/dist/evm-master-tx.d.ts +59 -0
  138. package/dist/evm-master-tx.js +154 -0
  139. package/dist/exchanges/adapter-network.d.ts +10 -0
  140. package/dist/exchanges/adapter-network.js +10 -0
  141. package/dist/exchanges/arcus/adapter.d.ts +196 -0
  142. package/dist/exchanges/arcus/adapter.js +1092 -0
  143. package/dist/exchanges/arcus/agent.d.ts +55 -0
  144. package/dist/exchanges/arcus/agent.js +188 -0
  145. package/dist/exchanges/arcus/client.d.ts +63 -0
  146. package/dist/exchanges/arcus/client.js +285 -0
  147. package/dist/exchanges/arcus/public.d.ts +65 -0
  148. package/dist/exchanges/arcus/public.js +138 -0
  149. package/dist/exchanges/arcus/signing.d.ts +176 -0
  150. package/dist/exchanges/arcus/signing.js +288 -0
  151. package/dist/exchanges/arcus/spot-chain.d.ts +60 -0
  152. package/dist/exchanges/arcus/spot-chain.js +241 -0
  153. package/dist/exchanges/arcus/spot-router.d.ts +177 -0
  154. package/dist/exchanges/arcus/spot-router.js +183 -0
  155. package/dist/exchanges/arcus/spot.d.ts +109 -0
  156. package/dist/exchanges/arcus/spot.js +645 -0
  157. package/dist/exchanges/aster-funds.d.ts +127 -0
  158. package/dist/exchanges/aster-funds.js +252 -0
  159. package/dist/exchanges/aster-nonce.d.ts +2 -0
  160. package/dist/exchanges/aster-nonce.js +107 -0
  161. package/dist/exchanges/aster-spot.d.ts +117 -0
  162. package/dist/exchanges/aster-spot.js +686 -0
  163. package/dist/exchanges/aster-typed-data.d.ts +133 -7
  164. package/dist/exchanges/aster-typed-data.js +121 -7
  165. package/dist/exchanges/aster.d.ts +219 -6
  166. package/dist/exchanges/aster.js +1023 -153
  167. package/dist/exchanges/builder-fee.d.ts +254 -0
  168. package/dist/exchanges/builder-fee.js +421 -0
  169. package/dist/exchanges/canonical-symbols.d.ts +32 -0
  170. package/dist/exchanges/canonical-symbols.js +96 -0
  171. package/dist/exchanges/capabilities.d.ts +7 -1
  172. package/dist/exchanges/capabilities.js +7 -0
  173. package/dist/exchanges/hl-signing.d.ts +165 -0
  174. package/dist/exchanges/hl-signing.js +158 -0
  175. package/dist/exchanges/hyperliquid-outcome.d.ts +211 -0
  176. package/dist/exchanges/hyperliquid-outcome.js +646 -0
  177. package/dist/exchanges/hyperliquid-spot.d.ts +44 -5
  178. package/dist/exchanges/hyperliquid-spot.js +308 -117
  179. package/dist/exchanges/hyperliquid.d.ts +462 -57
  180. package/dist/exchanges/hyperliquid.js +1575 -449
  181. package/dist/exchanges/index.d.ts +6 -0
  182. package/dist/exchanges/index.js +4 -0
  183. package/dist/exchanges/interface.d.ts +63 -10
  184. package/dist/exchanges/interface.js +18 -3
  185. package/dist/exchanges/ioc-outcome.d.ts +44 -0
  186. package/dist/exchanges/ioc-outcome.js +57 -0
  187. package/dist/exchanges/legacy-trading-v2.d.ts +39 -0
  188. package/dist/exchanges/legacy-trading-v2.js +940 -0
  189. package/dist/exchanges/lighter-public.d.ts +43 -0
  190. package/dist/exchanges/lighter-public.js +276 -0
  191. package/dist/exchanges/lighter-rh-deposit.d.ts +62 -0
  192. package/dist/exchanges/lighter-rh-deposit.js +185 -0
  193. package/dist/exchanges/lighter-signer.d.ts +123 -0
  194. package/dist/exchanges/lighter-signer.js +215 -0
  195. package/dist/exchanges/lighter-spot.d.ts +91 -13
  196. package/dist/exchanges/lighter-spot.js +595 -233
  197. package/dist/exchanges/lighter.d.ts +611 -39
  198. package/dist/exchanges/lighter.js +1890 -649
  199. package/dist/exchanges/ondo/adapter.d.ts +166 -0
  200. package/dist/exchanges/ondo/adapter.js +835 -0
  201. package/dist/exchanges/ondo/agent.d.ts +31 -0
  202. package/dist/exchanges/ondo/agent.js +101 -0
  203. package/dist/exchanges/ondo/client.d.ts +60 -0
  204. package/dist/exchanges/ondo/client.js +297 -0
  205. package/dist/exchanges/ondo/funds.d.ts +122 -0
  206. package/dist/exchanges/ondo/funds.js +290 -0
  207. package/dist/exchanges/ondo/public.d.ts +95 -0
  208. package/dist/exchanges/ondo/public.js +214 -0
  209. package/dist/exchanges/ondo/spot.d.ts +127 -0
  210. package/dist/exchanges/ondo/spot.js +392 -0
  211. package/dist/exchanges/outcome-interface.d.ts +140 -0
  212. package/dist/exchanges/outcome-interface.js +16 -0
  213. package/dist/exchanges/pacifica-spot.d.ts +83 -0
  214. package/dist/exchanges/pacifica-spot.js +324 -0
  215. package/dist/exchanges/pacifica-typed-data.d.ts +30 -13
  216. package/dist/exchanges/pacifica-typed-data.js +57 -9
  217. package/dist/exchanges/pacifica.d.ts +288 -4
  218. package/dist/exchanges/pacifica.js +1013 -76
  219. package/dist/exchanges/precision.d.ts +47 -0
  220. package/dist/exchanges/precision.js +209 -0
  221. package/dist/exchanges/qfex/adapter.d.ts +341 -0
  222. package/dist/exchanges/qfex/adapter.js +1383 -0
  223. package/dist/exchanges/qfex/agent.d.ts +58 -0
  224. package/dist/exchanges/qfex/agent.js +188 -0
  225. package/dist/exchanges/qfex/client.d.ts +54 -0
  226. package/dist/exchanges/qfex/client.js +138 -0
  227. package/dist/exchanges/qfex/errors.d.ts +70 -0
  228. package/dist/exchanges/qfex/errors.js +236 -0
  229. package/dist/exchanges/qfex/public.d.ts +122 -0
  230. package/dist/exchanges/qfex/public.js +268 -0
  231. package/dist/exchanges/qfex/signing.d.ts +160 -0
  232. package/dist/exchanges/qfex/signing.js +283 -0
  233. package/dist/exchanges/qfex/ws-transport.d.ts +294 -0
  234. package/dist/exchanges/qfex/ws-transport.js +798 -0
  235. package/dist/exchanges/rate-limiter.d.ts +12 -0
  236. package/dist/exchanges/rate-limiter.js +64 -0
  237. package/dist/exchanges/registry.d.ts +14 -2
  238. package/dist/exchanges/registry.js +20 -32
  239. package/dist/exchanges/slippage.d.ts +37 -0
  240. package/dist/exchanges/slippage.js +70 -0
  241. package/dist/exchanges/spot-factory.d.ts +166 -0
  242. package/dist/exchanges/spot-factory.js +431 -0
  243. package/dist/exchanges/spot-interface.d.ts +98 -4
  244. package/dist/exchanges/spot-interface.js +27 -1
  245. package/dist/exchanges/spot-result.d.ts +79 -0
  246. package/dist/exchanges/spot-result.js +273 -0
  247. package/dist/exchanges/trading-v2.d.ts +117 -0
  248. package/dist/exchanges/trading-v2.js +301 -0
  249. package/dist/exchanges/venue-types.d.ts +231 -0
  250. package/dist/exchanges/venue-types.js +87 -0
  251. package/dist/exchanges/venue.d.ts +211 -0
  252. package/dist/exchanges/venue.js +132 -0
  253. package/dist/exchanges/venues/arcus.d.ts +8 -0
  254. package/dist/exchanges/venues/arcus.js +109 -0
  255. package/dist/exchanges/venues/aster.d.ts +49 -0
  256. package/dist/exchanges/venues/aster.js +176 -0
  257. package/dist/exchanges/venues/create-helpers.d.ts +18 -0
  258. package/dist/exchanges/venues/create-helpers.js +199 -0
  259. package/dist/exchanges/venues/hyperliquid.d.ts +23 -0
  260. package/dist/exchanges/venues/hyperliquid.js +126 -0
  261. package/dist/exchanges/venues/index.d.ts +16 -0
  262. package/dist/exchanges/venues/index.js +26 -0
  263. package/dist/exchanges/venues/lighter-family.d.ts +92 -0
  264. package/dist/exchanges/venues/lighter-family.js +166 -0
  265. package/dist/exchanges/venues/lighter-rh.d.ts +7 -0
  266. package/dist/exchanges/venues/lighter-rh.js +58 -0
  267. package/dist/exchanges/venues/lighter.d.ts +6 -0
  268. package/dist/exchanges/venues/lighter.js +55 -0
  269. package/dist/exchanges/venues/ondo.d.ts +34 -0
  270. package/dist/exchanges/venues/ondo.js +140 -0
  271. package/dist/exchanges/venues/pacifica.d.ts +8 -0
  272. package/dist/exchanges/venues/pacifica.js +95 -0
  273. package/dist/exchanges/venues/qfex.d.ts +28 -0
  274. package/dist/exchanges/venues/qfex.js +132 -0
  275. package/dist/execution-log.js +10 -2
  276. package/dist/funding/legacy-public.d.ts +31 -0
  277. package/dist/funding/legacy-public.js +369 -0
  278. package/dist/funding/normalize.d.ts +28 -16
  279. package/dist/funding/normalize.js +61 -29
  280. package/dist/funding/rates.d.ts +46 -11
  281. package/dist/funding/rates.js +228 -277
  282. package/dist/guardrail/perp-guardrail.js +0 -0
  283. package/dist/index.js +157 -682
  284. package/dist/jobs.d.ts +84 -6
  285. package/dist/jobs.js +239 -41
  286. package/dist/landing.d.ts +14 -0
  287. package/dist/landing.js +36 -0
  288. package/dist/mcp-server.d.ts +14 -4
  289. package/dist/mcp-server.js +820 -402
  290. package/dist/mcp-trade-guard.d.ts +16 -0
  291. package/dist/mcp-trade-guard.js +59 -0
  292. package/dist/pacifica/client.d.ts +41 -10
  293. package/dist/pacifica/client.js +167 -63
  294. package/dist/pacifica/constants.d.ts +0 -1
  295. package/dist/pacifica/constants.js +0 -5
  296. package/dist/pacifica/deposit.d.ts +30 -3
  297. package/dist/pacifica/deposit.js +119 -2
  298. package/dist/pacifica/faucet.d.ts +29 -0
  299. package/dist/pacifica/faucet.js +100 -0
  300. package/dist/pacifica/sign-tx.d.ts +15 -0
  301. package/dist/pacifica/sign-tx.js +41 -0
  302. package/dist/pacifica/types/account.d.ts +28 -4
  303. package/dist/pacifica/types/lake.d.ts +20 -5
  304. package/dist/pacifica/types/lake.js +4 -0
  305. package/dist/pacifica/types/market.d.ts +5 -0
  306. package/dist/pacifica/types/order.d.ts +4 -1
  307. package/dist/pacifica/types/position.d.ts +9 -1
  308. package/dist/plan-executor.d.ts +10 -5
  309. package/dist/plan-executor.js +165 -54
  310. package/dist/position-history.js +13 -0
  311. package/dist/rebalance.d.ts +26 -2
  312. package/dist/rebalance.js +86 -7
  313. package/dist/relay-swap.d.ts +232 -0
  314. package/dist/relay-swap.js +630 -0
  315. package/dist/risk.js +5 -1
  316. package/dist/secret-env.d.ts +15 -0
  317. package/dist/secret-env.js +36 -0
  318. package/dist/settings.d.ts +169 -6
  319. package/dist/settings.js +219 -38
  320. package/dist/signer/ows-evm.d.ts +13 -0
  321. package/dist/signer/ows-evm.js +22 -0
  322. package/dist/smart-order.d.ts +9 -0
  323. package/dist/smart-order.js +53 -8
  324. package/dist/strategies/dca.d.ts +6 -2
  325. package/dist/strategies/dca.js +66 -26
  326. package/dist/strategies/funding-arb.d.ts +11 -1
  327. package/dist/strategies/funding-arb.js +202 -89
  328. package/dist/strategies/grid.d.ts +31 -0
  329. package/dist/strategies/grid.js +89 -11
  330. package/dist/strategies/pending-stop.d.ts +8 -0
  331. package/dist/strategies/pending-stop.js +12 -0
  332. package/dist/strategies/split-order.d.ts +45 -17
  333. package/dist/strategies/split-order.js +157 -91
  334. package/dist/strategies/trailing-stop.d.ts +2 -1
  335. package/dist/strategies/trailing-stop.js +52 -6
  336. package/dist/strategies/twap.d.ts +6 -1
  337. package/dist/strategies/twap.js +39 -11
  338. package/dist/trade-validator.d.ts +22 -0
  339. package/dist/trade-validator.js +119 -15
  340. package/dist/utils/numeric.d.ts +46 -0
  341. package/dist/utils/numeric.js +79 -0
  342. package/dist/utils.d.ts +19 -0
  343. package/dist/utils.js +96 -19
  344. package/dist/withdraw-gate.d.ts +49 -0
  345. package/dist/withdraw-gate.js +131 -0
  346. package/package.json +15 -6
  347. package/skills/perp-cli/SKILL.md +338 -134
  348. package/skills/perp-cli/references/agent-operations.md +181 -92
  349. package/skills/perp-cli/references/commands.md +475 -132
  350. package/skills/perp-cli/references/strategies.md +44 -38
  351. package/skills/perp-cli/scripts/arb-monitor.sh +22 -17
  352. package/skills/perp-cli/scripts/funding-analysis.sh +20 -10
  353. package/skills/perp-cli/scripts/preflight.sh +21 -14
  354. package/skills/perp-cli/scripts/spot-perp-scan.sh +12 -7
  355. package/skills/perp-cli/scripts/validate-arb.sh +33 -16
  356. package/vendor/lighter-signer/SHA256SUMS +2 -0
  357. package/vendor/lighter-signer/VERSION +8 -0
  358. package/vendor/lighter-signer/lighter-signer.wasm +0 -0
  359. package/vendor/lighter-signer/wasm_exec.js +575 -0
  360. package/dist/bot/strategies/funding-arb-v2-strategy.d.ts +0 -42
  361. package/dist/bot/strategies/funding-arb-v2-strategy.js +0 -864
package/dist/index.js CHANGED
@@ -15,715 +15,190 @@ import { createRequire } from "node:module";
15
15
  // instead of the OWS active wallet's). If you need ad-hoc env overrides,
16
16
  // export them in your shell — keystore is the canonical source.
17
17
  config({ path: resolve(process.env.HOME || "~", ".perp", ".env") });
18
- import { Command } from "commander";
19
18
  import chalk from "chalk";
20
- import { Keypair } from "@solana/web3.js";
21
- import { tryLoadPrivateKey, parseSolanaKeypair, isOwsKey, getOwsWalletName } from "./config.js";
22
- import { PacificaAdapter } from "./exchanges/pacifica.js";
23
- import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
24
- import { resolveExchangeName } from "./exchanges/registry.js";
25
- import { registerMarketCommands } from "./commands/market.js";
26
- import { registerAccountCommands } from "./commands/account.js";
27
- import { registerTradeCommands } from "./commands/trade.js";
28
- // manage commands now register under `wallet manage ...` via registerWalletCommands.
29
- // stream commands removed — WS feeds still used by dashboard/event-stream internally
30
- import { registerArbCommands } from "./commands/arb.js";
31
- import { registerWalletCommands } from "./commands/wallet.js";
32
- // bridge + rebalance now nested under `funds` — registered inside registerFundsCommands.
33
- import { registerFundsCommands } from "./commands/funds.js";
34
- // alert commands removed
35
- import { registerArbAutoCommands } from "./commands/arb-auto.js";
36
- import { registerArbManageCommands } from "./commands/arb/index.js";
37
- // Agent commands now register under `wallet agent ...` via registerWalletCommands.
38
- import { registerStrategyCommands } from "./commands/bot.js";
39
- import { registerRiskCommands } from "./commands/risk.js";
40
- import { registerHistoryCommands } from "./commands/history.js";
41
- import { registerSettingsCommands } from "./commands/settings.js";
42
- // dex commands merged into market (hip3) — use --dex flag for markets/balance
43
- // `plan` is no longer top-level — registered as `strategy plan` inside
44
- // registerStrategyCommands.
45
- // funding merged into arb.ts
46
- import { registerBacktestCommands } from "./commands/backtest.js";
47
- import { registerPortfolioCommand } from "./commands/portfolio.js";
48
- import { registerInitCommand } from "./commands/init.js";
49
- import { registerAlertCommands } from "./commands/alerts.js";
50
- import { loadSettings, saveSettings } from "./settings.js";
51
- import { setSharedApiNetwork } from "./shared-api.js";
19
+ import { listVenues } from "./exchanges/venue.js";
20
+ import { PerpError } from "./errors.js";
21
+ import { loadSettings } from "./settings.js";
22
+ import { jsonError, jsonOk, printCliError, printJson } from "./utils.js";
23
+ import { LANDING_EXCHANGES, asterAgentMissing as getAsterAgentMissing, renderLandingExchangeLine } from "./landing.js";
24
+ import { buildProgram, landingData, numericNetworkFlagError } from "./cli-program.js";
52
25
  const _require = createRequire(import.meta.url);
53
26
  const _pkg = _require("../package.json");
54
- const program = new Command();
55
- // Resolve default exchange from settings (fallback: "pacifica")
56
- const _settings = loadSettings();
57
- const _defaultExchange = _settings.defaultExchange || "pacifica";
58
- program
59
- .name("perp")
60
- .description("Multi-DEX Perpetual Futures CLI (Pacifica, Hyperliquid, Lighter, Aster)")
61
- .version(_pkg.version)
62
- .option("-e, --exchange <exchange>", `Exchange: pacifica, hyperliquid, lighter, aster (default: ${_defaultExchange})`, _defaultExchange)
63
- .option("-n, --network <network>", "Network: mainnet or testnet", "mainnet")
64
- .option("-k, --private-key <key>", "Private key")
65
- .option("--json", "Output raw JSON (for piping)")
66
- .option("--fields <fields>", "Comma-separated fields to include in JSON output (e.g. totalEquity,positions)")
67
- .option("--ndjson", "Output newline-delimited JSON (one object per line for streaming)")
68
- .option("--dry-run", "Simulate trades without executing (log as simulated)")
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)")
74
- .option("--dex <name>", "HIP-3 deployed perp dex name (Hyperliquid only)")
75
- .configureOutput({
76
- writeErr: (str) => {
27
+ // Resolve default exchange from settings (fallback: "pacifica").
28
+ // A malformed settings.json is a hard stop (SSOT Rule #2): report it cleanly and exit.
29
+ function loadStartupSettings() {
30
+ try {
31
+ return loadSettings();
32
+ }
33
+ catch (err) {
34
+ const s = err instanceof PerpError ? err.structured : null;
35
+ const msg = err instanceof Error ? err.message : String(err);
77
36
  if (process.argv.includes("--json")) {
78
- const msg = str.replace(/^error:\s*/i, "").trim();
79
- // Use inline envelope (jsonError import not available in sync context)
80
- console.log(JSON.stringify({
81
- ok: false,
82
- error: { code: "CLI_ERROR", message: msg },
83
- meta: { timestamp: new Date().toISOString() },
84
- }));
37
+ console.log(JSON.stringify(jsonError(s?.code ?? "INVALID_PARAMS", msg, { status: s?.status ?? 400, retryable: false, remediation: s?.remediation })));
85
38
  }
86
39
  else {
87
- process.stderr.write(str);
88
- }
89
- },
90
- });
91
- let _adapter = null;
92
- let _pacificaAdapter = null;
93
- let _hlAdapter = null;
94
- let _lighterAdapter = null;
95
- /** Map short aliases to canonical exchange names. Full names pass through. */
96
- function resolveExchangeAlias(name) {
97
- return resolveExchangeName(name);
98
- }
99
- function getExchange() {
100
- return resolveExchangeAlias(program.opts().exchange);
101
- }
102
- async function getAdapter() {
103
- const opts = program.opts();
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)
111
- if (!walletName && _adapter)
112
- return _adapter;
113
- const exchange = resolveExchangeAlias(opts.exchange);
114
- const network = opts.network;
115
- const isTestnet = network === "testnet";
116
- // Try to load key — null means no key configured (read-only mode)
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
- }
122
- switch (exchange) {
123
- case "pacifica": {
124
- const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
125
- const pacNetwork = (isTestnet ? "testnet" : "mainnet");
126
- const settings = loadSettings();
127
- const builderCode = process.env.PACIFICA_BUILDER_CODE || settings.referralCodes.pacifica || "PERPCLI";
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);
150
- _adapter = _pacificaAdapter;
151
- break;
152
- }
153
- case "hyperliquid": {
154
- _hlAdapter = new HyperliquidAdapter(pk ?? undefined, isTestnet);
155
- if (opts.dex)
156
- _hlAdapter.setDex(opts.dex);
157
- await _hlAdapter.init();
158
- if (pk) {
159
- const hlSettings = loadSettings();
160
- if (!hlSettings.referralApplied.hyperliquid) {
161
- const hlRef = process.env.HL_REFERRAL_CODE || hlSettings.referralCodes.hyperliquid;
162
- if (hlRef) {
163
- _hlAdapter.autoSetReferrer(hlRef).then(() => {
164
- const s = loadSettings();
165
- s.referralApplied.hyperliquid = true;
166
- 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`);
171
- });
172
- }
173
- }
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);
196
- _adapter = _hlAdapter;
197
- break;
198
- }
199
- case "lighter": {
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");
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);
217
- await _lighterAdapter.init();
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
- });
233
- }
234
- }
235
- _adapter = _lighterAdapter;
236
- break;
237
- }
238
- case "aster": {
239
- const { AsterAdapter } = await import("./exchanges/aster.js");
240
- const ast = new AsterAdapter(pk ?? undefined, isTestnet); // Tier 3 from PK
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);
261
- _adapter = ast;
262
- break;
263
- }
264
- default:
265
- throw new Error(`Unknown exchange: ${exchange}`);
266
- }
267
- return _adapter;
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
- // Tier 1: agent if registered (mirrors getAdapter / getAdapterForExchange).
311
- // Without this, _initWithOws would skip Tier 1 and fall through to
312
- // auto-setup-at-slot-4 which is wrong for agent users (and breaks under
313
- // SDK 1.0.11+ where signChangePubKey signature changed).
314
- const { getAgent: getLtAgentOws } = await import("./agent-wallet/store.js");
315
- const ltAgentMetaOws = getLtAgentOws("lighter");
316
- if (ltAgentMetaOws && !noAgent) {
317
- const { loadLighterKey } = await import("./agent-wallet/lighter-keystore.js");
318
- const ltL2KeyOws = loadLighterKey(ltAgentMetaOws.accountIndex, ltAgentMetaOws.apiKeyIndex, "");
319
- _lighterAdapter.setAgentSigner(ltAgentMetaOws, ltL2KeyOws);
320
- }
321
- if (noAgent)
322
- _lighterAdapter.setNoAgent(true);
323
- await _lighterAdapter.init();
324
- _adapter = _lighterAdapter;
325
- return _adapter;
326
- }
327
- case "aster": {
328
- const { AsterAdapter } = await import("./exchanges/aster.js");
329
- const asterOws = new AsterAdapter(undefined, isTestnet);
330
- await asterOws.init();
331
- asterOws.setMasterSigner(OwsEvmSigner.create(owsWalletName, passphrase));
332
- // Optionally also set agent if registered
333
- const { getAgent: getAsterAgentOws } = await import("./agent-wallet/store.js");
334
- const asterMeta = getAsterAgentOws("aster");
335
- if (asterMeta) {
336
- const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
337
- asterOws.setAgent(asterMeta, agentSigningStrategyFor(asterMeta, owsKeyToken ?? ""));
338
- }
339
- if (noAgent)
340
- asterOws.setNoAgent(true);
341
- _adapter = asterOws;
342
- return _adapter;
343
- }
344
- default:
345
- throw new Error(`Unknown exchange: ${exchange}`);
346
- }
347
- }
348
- // Sync wrapper for commands that need adapter (lazy init)
349
- function getAdapterSync() {
350
- if (!_adapter)
351
- throw new Error("Adapter not initialized");
352
- return _adapter;
353
- }
354
- function isJson() {
355
- return !!program.opts().json || process.argv.includes("--ndjson");
356
- }
357
- function isDryRun() {
358
- return !!program.opts().dryRun;
359
- }
360
- // Helper to get PacificaAdapter specifically (for Pacifica-only commands)
361
- function getPacificaAdapter() {
362
- if (!_pacificaAdapter)
363
- throw new Error("This command requires --exchange pacifica");
364
- return _pacificaAdapter;
365
- }
366
- function getHLAdapter() {
367
- if (!_hlAdapter)
368
- throw new Error("This command requires --exchange hyperliquid");
369
- return _hlAdapter;
370
- }
371
- // Register command groups with async adapter getter
372
- registerMarketCommands(program, getAdapter, isJson, getAdapterForExchange);
373
- registerAccountCommands(program, getAdapter, isJson, getAdapterForExchange);
374
- registerTradeCommands(program, getAdapter, isJson, isDryRun, getAdapterForExchange);
375
- // manage tree wired inside registerWalletCommands below.
376
- // stream commands removed
377
- registerArbCommands(program, isJson, getAdapterForExchange);
378
- registerWalletCommands(program, isJson, getAdapter, getPacificaAdapter);
379
- registerFundsCommands(program, getAdapter, isJson, () => program.opts().network, getAdapterForExchange);
380
- // alert commands removed
381
- // Helper to get adapter for a specific exchange (used by arb-auto)
382
- async function getAdapterForExchange(rawExchange) {
383
- const exchange = resolveExchangeAlias(rawExchange);
384
- const opts = program.opts();
385
- const network = opts.network;
386
- const isTestnet = network === "testnet";
387
- const owsName = opts.ows;
388
- const owsKeyToken = opts.owsKey || process.env.OWS_API_KEY;
389
- const walletName = owsName ? `ows:${owsName}` : opts.wallet;
390
- // Commander's `--no-agent` flag sets opts.agent === false (NOT opts.noAgent).
391
- const noAgent = opts.agent === false;
392
- const pk = await tryLoadPrivateKey(exchange, opts.privateKey, walletName);
393
- // Route through OWS init when pk is an OWS reference (e.g. "ows:main").
394
- // Mirrors getAdapter() at the top of this file. Without this, adapters
395
- // would try to parse "ows:main" as a raw private key and ethers would
396
- // throw "invalid BytesLike value" / parseSolanaKeypair would fail.
397
- if (pk && isOwsKey(pk)) {
398
- return _initWithOws(exchange, getOwsWalletName(pk), isTestnet, opts, owsKeyToken);
399
- }
400
- switch (exchange) {
401
- case "pacifica": {
402
- if (_pacificaAdapter)
403
- return _pacificaAdapter;
404
- const keypair = pk ? parseSolanaKeypair(pk) : Keypair.generate();
405
- const pacNetwork = (isTestnet ? "testnet" : "mainnet");
406
- const s1 = loadSettings();
407
- const builderCode = process.env.PACIFICA_BUILDER_CODE || s1.referralCodes.pacifica || "PERPCLI";
408
- _pacificaAdapter = new PacificaAdapter(keypair, pacNetwork, builderCode, !!pk);
409
- // Tier 1: agent if registered (Phase 2c)
410
- const { getAgent: getPacAgentEx } = await import("./agent-wallet/store.js");
411
- const pacAgentMetaEx = getPacAgentEx("pacifica");
412
- if (pacAgentMetaEx) {
413
- const { OwsSolanaSigner: PacOwsSolanaSignerEx } = await import("./signer/ows-solana.js");
414
- // Pacifica agent wallet was created with empty passphrase (see runPacApproveFlow);
415
- // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
416
- const agentSignerEx = PacOwsSolanaSignerEx.create(pacAgentMetaEx.agentWalletName, "");
417
- _pacificaAdapter.setAgentSigner(pacAgentMetaEx, agentSignerEx);
418
- }
419
- if (noAgent)
420
- _pacificaAdapter.setNoAgent(true);
421
- if (!_adapter)
422
- _adapter = _pacificaAdapter;
423
- return _pacificaAdapter;
424
- }
425
- case "hyperliquid": {
426
- if (_hlAdapter)
427
- return _hlAdapter;
428
- _hlAdapter = new HyperliquidAdapter(pk ?? undefined, isTestnet);
429
- if (opts.dex)
430
- _hlAdapter.setDex(opts.dex);
431
- await _hlAdapter.init();
432
- if (pk) {
433
- const s2 = loadSettings();
434
- if (!s2.referralApplied.hyperliquid) {
435
- const hlRef = process.env.HL_REFERRAL_CODE || s2.referralCodes.hyperliquid;
436
- if (hlRef) {
437
- _hlAdapter.autoSetReferrer(hlRef).then(() => {
438
- const s = loadSettings();
439
- s.referralApplied.hyperliquid = true;
440
- saveSettings(s);
441
- }).catch((err) => {
442
- // Per SSOT Rule #2: do NOT mark applied=true on failure.
443
- // Leave referralApplied[ex]=false so next adapter init retries.
444
- process.stderr.write(`[hyperliquid] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
445
- });
446
- }
447
- }
448
- }
449
- // Tier 1: agent if registered
450
- {
451
- const { getAgent: getHlAgent2 } = await import("./agent-wallet/store.js");
452
- const hlAgentMeta2 = getHlAgent2("hyperliquid");
453
- if (hlAgentMeta2) {
454
- const { OwsEvmSigner: HlOwsEvmSigner2 } = await import("./signer/ows-evm.js");
455
- // HL agent wallet was created with empty passphrase (see runHlApproveFlow);
456
- // open with "" to match. Agent at-rest encryption handled by OWS storage layer.
457
- const agentSigner2 = HlOwsEvmSigner2.create(hlAgentMeta2.agentWalletName, "");
458
- _hlAdapter.setAgentSigner(hlAgentMeta2, agentSigner2);
459
- }
460
- if (noAgent)
461
- _hlAdapter.setNoAgent(true);
462
- }
463
- if (!_adapter)
464
- _adapter = _hlAdapter;
465
- return _hlAdapter;
40
+ console.error(chalk.red(`Error: ${msg}`));
41
+ if (s?.remediation)
42
+ console.error(chalk.yellow(` ${s.remediation}`));
466
43
  }
467
- case "lighter": {
468
- if (_lighterAdapter)
469
- return _lighterAdapter;
470
- const { LighterAdapter } = await import("./exchanges/lighter.js");
471
- const { getAgent: getLtAgentEx } = await import("./agent-wallet/store.js");
472
- const ltAgentMetaEx = getLtAgentEx("lighter");
473
- _lighterAdapter = new LighterAdapter(pk ?? "", isTestnet);
474
- if (ltAgentMetaEx && !noAgent) {
475
- // SSOT Rule #3: load agent L2 key from encrypted keystore — no env fallback.
476
- const { loadLighterKey } = await import("./agent-wallet/lighter-keystore.js");
477
- const ltL2KeyEx = loadLighterKey(ltAgentMetaEx.accountIndex, ltAgentMetaEx.apiKeyIndex, "");
478
- _lighterAdapter.setAgentSigner(ltAgentMetaEx, ltL2KeyEx);
479
- }
480
- if (noAgent)
481
- _lighterAdapter.setNoAgent(true);
482
- await _lighterAdapter.init();
483
- // LT referral apply is L2-signed; trigger on any active tier (agent
484
- // included), gated by isReadOnly. See first LT case for rationale.
485
- const s3 = loadSettings();
486
- if (!s3.referralApplied.lighter && !_lighterAdapter.isReadOnly) {
487
- const ltRef = process.env.LIGHTER_REFERRAL_CODE || s3.referralCodes.lighter;
488
- if (ltRef) {
489
- _lighterAdapter.useReferralCode(ltRef).then(() => {
490
- const s = loadSettings();
491
- s.referralApplied.lighter = true;
492
- saveSettings(s);
493
- }).catch((err) => {
494
- // Per SSOT Rule #2: do NOT mark applied=true on failure.
495
- process.stderr.write(`[lighter] referral apply failed: ${err instanceof Error ? err.message : String(err)}\n`);
496
- });
497
- }
498
- }
499
- if (!_adapter)
500
- _adapter = _lighterAdapter;
501
- return _lighterAdapter;
502
- }
503
- case "aster": {
504
- const { AsterAdapter } = await import("./exchanges/aster.js");
505
- const astEx = new AsterAdapter(pk ?? undefined, isTestnet);
506
- await astEx.init();
507
- // Tier 2: OWS master if --ows or settings.owsActiveWallet
508
- const asterExOwsName = opts.ows || _settings.owsActiveWallet;
509
- if (asterExOwsName) {
510
- const { OwsEvmSigner } = await import("./signer/ows-evm.js");
511
- const { resolvePassphrase } = await import("./agent-wallet/passphrase.js");
512
- const pp = await resolvePassphrase({ flag: opts.passphrase });
513
- if (pp !== null)
514
- astEx.setMasterSigner(OwsEvmSigner.create(asterExOwsName, pp));
515
- }
516
- // Tier 1: agent if registered
517
- const owsKeyEx = opts.owsKey || process.env.OWS_API_KEY;
518
- const { getAgent: getAsterAgentEx } = await import("./agent-wallet/store.js");
519
- const agentMetaEx = getAsterAgentEx("aster");
520
- if (agentMetaEx) {
521
- const { agentSigningStrategyFor } = await import("./agent-wallet/signing-strategy.js");
522
- astEx.setAgent(agentMetaEx, agentSigningStrategyFor(agentMetaEx, owsKeyEx ?? ""));
523
- }
524
- if (noAgent)
525
- astEx.setNoAgent(true);
526
- if (!_adapter)
527
- _adapter = astEx;
528
- return astEx;
529
- }
530
- default:
531
- throw new Error(`Unknown exchange: ${exchange}`);
44
+ process.exit(1);
532
45
  }
533
46
  }
534
- // Helper to get an HL adapter configured for a specific HIP-3 dex
535
- const _dexAdapters = new Map();
536
- async function getHLAdapterForDex(dex) {
537
- if (_dexAdapters.has(dex))
538
- return _dexAdapters.get(dex);
539
- const opts = program.opts();
540
- const walletName = opts.wallet;
541
- const pk = await tryLoadPrivateKey("hyperliquid", opts.privateKey, walletName);
542
- const adapter = new HyperliquidAdapter(pk ?? undefined, opts.network === "testnet");
543
- if (dex !== "hl")
544
- adapter.setDex(dex);
545
- await adapter.init();
546
- _dexAdapters.set(dex, adapter);
547
- return adapter;
548
- }
549
- registerArbAutoCommands(program, getAdapterForExchange, isJson, getHLAdapterForDex);
550
- registerArbManageCommands(program, getAdapterForExchange, isJson);
551
- // `agent` is no longer top-level — registered as `wallet agent` inside
552
- // registerWalletCommands above.
553
- // withdraw merged into funds.
554
- // `rebalance` is no longer top-level — registered as `funds rebalance` inside
555
- // registerFundsCommands above.
556
- // Background processes & strategies
557
- import { registerBackgroundCommands } from "./commands/jobs.js";
558
- registerBackgroundCommands(program, isJson);
559
- registerStrategyCommands(program, getAdapter, getAdapterForExchange, isJson);
560
- // Agent-friendly commands
561
- registerRiskCommands(program, getAdapterForExchange, isJson);
562
- registerHistoryCommands(program, isJson, getAdapterForExchange);
563
- registerSettingsCommands(program, isJson, getAdapterForExchange);
564
- // dex commands removed — use 'market hip3' + --dex flag
565
- // `plan` is no longer top-level — registered as `strategy plan` inside
566
- // registerStrategyCommands above.
567
- // funding merged into arb — registerFundingCommands removed
568
- registerBacktestCommands(program, isJson);
569
- registerPortfolioCommand(program, getAdapterForExchange, isJson, _pkg.version, getHLAdapterForDex);
570
- registerInitCommand(program);
571
- registerAlertCommands(program, isJson);
572
- // Switch shared API URLs if --network testnet is used
573
- program.hook("preAction", () => {
574
- const network = program.opts().network;
575
- if (network === "testnet")
576
- setSharedApiNetwork("testnet");
577
- });
47
+ const _settings = loadStartupSettings();
48
+ const _defaultExchange = _settings.defaultExchange || "pacifica";
49
+ const { program, getAdapterForExchange, isJson } = buildProgram({ defaultExchange: _defaultExchange, version: _pkg.version });
578
50
  // Smart landing page: `perp` with no subcommand
579
51
  const rawArgs = process.argv.slice(2);
580
- const hasSubcommand = rawArgs.some((a) => !a.startsWith("-") && !["pacifica", "hyperliquid", "lighter", "aster", "hl", "lt", "pac", "ast", "mainnet", "testnet"].includes(a));
52
+ const _venueWords = listVenues().flatMap(v => [v.id, ...v.aliases]);
53
+ const hasSubcommand = rawArgs.some((a) => !a.startsWith("-") && ![..._venueWords, "mainnet", "testnet"].includes(a));
54
+ // `history list -n 5` (old record-count flag) → INVALID_PARAMS with the --limit migration, before
55
+ // Commander turns it into a bare "invalid network" usage error.
56
+ const _numericNetwork = numericNetworkFlagError(rawArgs);
57
+ if (_numericNetwork !== null) {
58
+ printCliError(_numericNetwork, rawArgs.includes("--json"));
59
+ _origExit(1);
60
+ }
581
61
  if (rawArgs.length === 0 || (!hasSubcommand && !rawArgs.includes("-h") && !rawArgs.includes("--help") && !rawArgs.includes("-V") && !rawArgs.includes("--version"))) {
582
- // No subcommand — show smart landing instead of help dump
583
- (async () => {
584
- try {
585
- const { getWalletSetupStatus } = await import("./commands/wallet.js");
586
- const status = getWalletSetupStatus();
587
- const settings = loadSettings();
588
- const hasEnvKey = !!(process.env.PRIVATE_KEY || process.env.PACIFICA_PRIVATE_KEY ||
589
- process.env.HL_PRIVATE_KEY || process.env.HYPERLIQUID_PRIVATE_KEY ||
590
- process.env.LIGHTER_PRIVATE_KEY || process.env.ASTER_PRIVATE_KEY);
591
- const hasOwsWallet = !!settings.owsActiveWallet;
592
- // ── ASCII banner ──
593
- const banner = [
594
- "",
595
- chalk.cyan(" ██████╗ ███████╗██████╗ ██████╗ ██████╗██╗ ██╗"),
596
- chalk.cyan(" ██╔══██╗██╔════╝██╔══██╗██╔══██╗ ██╔════╝██║ ██║"),
597
- chalk.cyan(" ██████╔╝█████╗ ██████╔╝██████╔╝ ██║ ██║ ██║"),
598
- chalk.cyan(" ██╔═══╝ ██╔══╝ ██╔══██╗██╔═══╝ ██║ ██║ ██║"),
599
- chalk.cyan(" ██║ ███████╗██║ ██║██║ ╚██████╗███████╗██║"),
600
- chalk.cyan(" ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝╚══════╝╚═╝"),
601
- "",
602
- chalk.gray(` 4 exchanges · 19 strategies · v${_pkg.version}`),
603
- ];
604
- console.log(banner.join("\n"));
605
- if (!status.hasWallets && !hasEnvKey && !hasOwsWallet && !settings.defaultExchange) {
606
- // Fresh install — onboarding
607
- console.log(chalk.yellow.bold("\n ⚡ Get started:\n"));
608
- console.log(` ${chalk.cyan("perp wallet generate")} create OWS encrypted wallet`);
609
- console.log(` ${chalk.cyan("perp setup")} setup wizard`);
610
- console.log(chalk.gray(`\n Explore without a wallet:`));
611
- console.log(` ${chalk.green("perp market mid BTC")} quick price check`);
612
- console.log(` ${chalk.green("perp arb scan")} funding rate arbitrage`);
613
- console.log(` ${chalk.green("perp market list")} available markets`);
614
- console.log(` ${chalk.green("perp --help")} all commands\n`);
615
- }
616
- else {
617
- // Configured — show exchange status + balance
618
- const EX_NAMES = ["pacifica", "hyperliquid", "lighter", "aster"];
619
- const exLabel = (e) => e === "pacifica" ? "Pacifica" : e === "hyperliquid" ? "Hyperliquid" : e === "lighter" ? "Lighter" : "Aster";
620
- // Ping + balance in parallel (with 5s timeout to keep landing fast)
621
- const withTimeout = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms))]);
622
- const statusResults = await Promise.allSettled(EX_NAMES.map(async (ex) => {
623
- try {
624
- const adapter = await withTimeout(getAdapterForExchange(ex), 5000);
625
- const [balance, positions] = await withTimeout(Promise.all([adapter.getBalance(), adapter.getPositions()]), 5000);
626
- const posCount = positions.filter(p => Number(p.size) > 0).length;
627
- let spotValue = 0;
628
- // Include non-USDC spot value for unified accounts (HL, LT)
62
+ // No subcommand — show smart landing instead of help dump.
63
+ // Parse root options first so an invalid -n (e.g. "Testnet") is a usage error here too.
64
+ program.parseOptions(rawArgs);
65
+ if (isJson()) {
66
+ // JSON mode: one landing / usage document on stdout — no banner, no venue reads.
67
+ printJson(jsonOk(landingData(program, _pkg.version)));
68
+ setTimeout(() => _origExit(0), 100);
69
+ }
70
+ else
71
+ (async () => {
72
+ try {
73
+ const { getWalletSetupStatus } = await import("./commands/wallet.js");
74
+ const status = getWalletSetupStatus();
75
+ const settings = loadSettings();
76
+ const hasEnvKey = !!(process.env.PRIVATE_KEY || process.env.PACIFICA_PRIVATE_KEY ||
77
+ process.env.HL_PRIVATE_KEY || process.env.HYPERLIQUID_PRIVATE_KEY ||
78
+ process.env.LIGHTER_PRIVATE_KEY || process.env.ASTER_PRIVATE_KEY);
79
+ const hasOwsWallet = !!settings.owsActiveWallet;
80
+ // ── ASCII banner ──
81
+ const banner = [
82
+ "",
83
+ chalk.cyan(" ██████╗ ███████╗██████╗ ██████╗ ██████╗██╗ ██╗"),
84
+ chalk.cyan(" ██╔══██╗██╔════╝██╔══██╗██╔══██╗ ██╔════╝██║ ██║"),
85
+ chalk.cyan(" ██████╔╝█████╗ ██████╔╝██████╔╝ ██║ ██║ ██║"),
86
+ chalk.cyan(" ██╔═══╝ ██╔══╝ ██╔══██╗██╔═══╝ ██║ ██║ ██║"),
87
+ chalk.cyan(" ██║ ███████╗██║ ██║██║ ╚██████╗███████╗██║"),
88
+ chalk.cyan(" ╚═╝ ╚══════╝╚═╝ ╚═╝╚═╝ ╚═════╝╚══════╝╚═╝"),
89
+ "",
90
+ chalk.gray(` ${listVenues().length} exchanges · 19 strategies · v${_pkg.version}`),
91
+ ];
92
+ console.log(banner.join("\n"));
93
+ if (!status.hasWallets && !hasEnvKey && !hasOwsWallet && !settings.defaultExchange) {
94
+ // Fresh install — onboarding
95
+ console.log(chalk.yellow.bold("\n ⚡ Get started:\n"));
96
+ console.log(` ${chalk.cyan("perp wallet generate")} create OWS encrypted wallet`);
97
+ console.log(` ${chalk.cyan("perp setup")} setup wizard`);
98
+ console.log(chalk.gray(`\n Explore without a wallet:`));
99
+ console.log(` ${chalk.green("perp market mid BTC")} quick price check`);
100
+ console.log(` ${chalk.green("perp arb scan")} funding rate arbitrage`);
101
+ console.log(` ${chalk.green("perp market list")} available markets`);
102
+ console.log(` ${chalk.green("perp --help")} all commands\n`);
103
+ }
104
+ else {
105
+ // Configured — show exchange status + balance
106
+ // Ping + balance in parallel (with 5s timeout to keep landing fast)
107
+ const withTimeout = (p, ms) => Promise.race([p, new Promise((_, rej) => setTimeout(() => rej(new Error("timeout")), ms))]);
108
+ // Aster venue ships only signed account endpoints — no public
109
+ // address-based balance query exists (HL/PAC/LT all do). So when
110
+ // a user has an env-PK but no Aster agent, every getBalance()
111
+ // call throws NOT_SUPPORTED at venue. Detect that case here so
112
+ // landing can surface a clear "agent required" hint instead of
113
+ // a generic dash.
114
+ const asterAgentMissing = getAsterAgentMissing();
115
+ const { getVenue } = await import("./exchanges/venue.js");
116
+ const { hasCredentials, NO_CLI_CREDENTIAL_OPTS } = await import("./config.js");
117
+ // Market-data-only venues are listed as such; account venues are shown only when configured.
118
+ const landingVenues = [];
119
+ for (const ex of LANDING_EXCHANGES) {
120
+ const def = getVenue(ex);
121
+ if (def.create === null || await hasCredentials(def.id, NO_CLI_CREDENTIAL_OPTS))
122
+ landingVenues.push(ex);
123
+ }
124
+ const statusResults = await Promise.allSettled(landingVenues.map(async (ex) => {
125
+ if (getVenue(ex).create === null) {
126
+ return { exchange: ex, marketDataOnly: true, ok: false, equity: 0, positions: 0 };
127
+ }
629
128
  try {
630
- if (ex === "hyperliquid") {
631
- const { HyperliquidSpotAdapter } = await import("./exchanges/hyperliquid-spot.js");
632
- const hlSpot = new HyperliquidSpotAdapter(adapter);
633
- await hlSpot.init();
634
- const [raw, markets] = await Promise.all([hlSpot.getSpotBalances(), hlSpot.getSpotMarkets()]);
635
- const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
636
- for (const b of raw) {
637
- const base = b.token.replace(/-SPOT$/i, "").toUpperCase();
638
- if (base === "USDC" || Number(b.total) <= 0)
639
- continue;
640
- spotValue += (priceMap.get(base) ?? 0) * Number(b.total);
129
+ const adapter = await withTimeout(getAdapterForExchange(ex), 5000);
130
+ const [balance, positions] = await withTimeout(Promise.all([adapter.getBalance(), adapter.getPositions()]), 5000);
131
+ const posCount = positions.filter(p => Number(p.size) > 0).length;
132
+ // Spot value on top of perp equity for venues with def.spot (§12). Shared balances
133
+ // (already in perp equity) are skipped; unpriced tokens are excluded + warned.
134
+ let spotValue = 0;
135
+ const spotDef = getVenue(ex);
136
+ const warn = (msg) => process.stderr.write(`[warn] ${msg}\n`);
137
+ try {
138
+ const { getSpotAdapter, fetchValuedSpotBalances, SPOT_VALUATION_TIMEOUT_MS } = await import("./exchanges/spot-factory.js");
139
+ const spot = await withTimeout(getSpotAdapter(spotDef.id, adapter), SPOT_VALUATION_TIMEOUT_MS);
140
+ if (spot !== null) {
141
+ for (const b of await fetchValuedSpotBalances(spotDef, spot, warn)) {
142
+ if (!b.shared)
143
+ spotValue += b.valueUsd;
144
+ }
641
145
  }
642
146
  }
643
- else if (ex === "lighter") {
644
- const { LighterAdapter } = await import("./exchanges/lighter.js");
645
- const { LighterSpotAdapter } = await import("./exchanges/lighter-spot.js");
646
- const ltSpot = new LighterSpotAdapter(adapter);
647
- await ltSpot.init();
648
- const [raw, markets] = await Promise.all([ltSpot.getSpotBalances(), ltSpot.getSpotMarkets()]);
649
- const priceMap = new Map(markets.map(m => [m.baseToken.toUpperCase(), Number(m.markPrice)]));
650
- for (const b of raw) {
651
- if (b.token === "USDC" || b.token === "USDC_SPOT" || Number(b.total) <= 0)
652
- continue;
653
- spotValue += (priceMap.get(b.token.toUpperCase()) ?? 0) * Number(b.total);
654
- }
147
+ catch (e) {
148
+ warn(`${spotDef.displayName} spot balances unavailable: ${e instanceof Error ? e.message : String(e)}`);
655
149
  }
150
+ return { exchange: ex, ok: true, equity: Number(balance.equity) + spotValue, positions: posCount };
151
+ }
152
+ catch (err) {
153
+ const { PerpError } = await import("./errors.js");
154
+ const errorCode = err instanceof PerpError ? err.structured.code : undefined;
155
+ return { exchange: ex, ok: false, equity: 0, positions: 0, errorCode };
156
+ }
157
+ }));
158
+ console.log(chalk.white.bold("\n Exchanges:"));
159
+ let totalEquity = 0;
160
+ let totalPositions = 0;
161
+ for (const r of statusResults) {
162
+ if (r.status !== "fulfilled")
163
+ continue;
164
+ const s = r.value;
165
+ console.log(renderLandingExchangeLine(s, asterAgentMissing));
166
+ if (s.ok) {
167
+ totalEquity += s.equity;
168
+ totalPositions += s.positions;
656
169
  }
657
- catch { /* spot not available */ }
658
- return { exchange: ex, ok: true, equity: Number(balance.equity) + spotValue, positions: posCount };
659
- }
660
- catch {
661
- return { exchange: ex, ok: false, equity: 0, positions: 0 };
662
170
  }
663
- }));
664
- console.log(chalk.white.bold("\n Exchanges:"));
665
- let totalEquity = 0;
666
- let totalPositions = 0;
667
- for (const r of statusResults) {
668
- if (r.status !== "fulfilled")
669
- continue;
670
- const s = r.value;
671
- const icon = s.ok ? chalk.green("●") : chalk.red("○");
672
- const eq = s.ok ? chalk.white(`$${Number(s.equity).toLocaleString("en", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`) : chalk.gray("—");
673
- const pos = s.ok && s.positions > 0 ? chalk.yellow(` ${s.positions} pos`) : "";
674
- console.log(` ${icon} ${chalk.cyan(exLabel(s.exchange).padEnd(14))} ${eq}${pos}`);
675
- if (s.ok) {
676
- totalEquity += s.equity;
677
- totalPositions += s.positions;
171
+ if (totalEquity > 0) {
172
+ console.log(chalk.gray(" ─".repeat(20)));
173
+ console.log(` ${chalk.white.bold("Total".padEnd(16))} ${chalk.white.bold(`$${totalEquity.toLocaleString("en", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`)}${totalPositions > 0 ? chalk.yellow(` ${totalPositions} pos`) : ""}`);
678
174
  }
175
+ console.log(chalk.white.bold("\n Quick:"));
176
+ console.log(` ${chalk.green("perp portfolio")} balances + positions + risk`);
177
+ console.log(` ${chalk.green("perp arb scan")} funding arbitrage opportunities`);
178
+ console.log(` ${chalk.green("perp arb status")} open arb positions + PnL`);
179
+ console.log(` ${chalk.green("perp portfolio --arb")} full dashboard (balances + arb top 5)`);
180
+ console.log(` ${chalk.green("perp portfolio --serve")} live web monitoring`);
181
+ console.log(` ${chalk.green("perp --help")} all commands\n`);
679
182
  }
680
- if (totalEquity > 0) {
681
- console.log(chalk.gray(" ─".repeat(20)));
682
- console.log(` ${chalk.white.bold("Total".padEnd(16))} ${chalk.white.bold(`$${totalEquity.toLocaleString("en", { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`)}${totalPositions > 0 ? chalk.yellow(` ${totalPositions} pos`) : ""}`);
683
- }
684
- console.log(chalk.white.bold("\n Quick:"));
685
- console.log(` ${chalk.green("perp portfolio")} balances + positions + risk`);
686
- console.log(` ${chalk.green("perp arb scan")} funding arbitrage opportunities`);
687
- console.log(` ${chalk.green("perp arb status")} open arb positions + PnL`);
688
- console.log(` ${chalk.green("perp portfolio --arb")} full dashboard (balances + arb top 5)`);
689
- console.log(` ${chalk.green("perp portfolio --serve")} live web monitoring`);
690
- console.log(` ${chalk.green("perp --help")} all commands\n`);
691
183
  }
692
- }
693
- catch {
694
- program.help();
695
- }
696
- setTimeout(() => _origExit(0), 500);
697
- })();
184
+ catch {
185
+ program.help();
186
+ }
187
+ setTimeout(() => _origExit(0), 500);
188
+ })();
698
189
  }
699
190
  else {
700
191
  program.parseAsync().then(() => {
701
192
  // Allow a short delay for any pending output, then exit cleanly.
702
193
  // Without this, HL SDK's WebSocket keeps the process alive indefinitely.
703
- setTimeout(() => _origExit(0), 500);
194
+ // Keep a non-zero exitCode set by a command that printed its own failure (withJsonErrors).
195
+ setTimeout(() => _origExit(Number(process.exitCode ?? 0)), 500);
704
196
  }).catch(async (err) => {
705
- const msg = err instanceof Error ? err.message : String(err);
706
- if (isJson()) {
707
- const { jsonError } = await import("./utils.js");
708
- const { PerpError } = await import("./errors.js");
709
- // Preserve typed PerpError code + remediation through the top-level
710
- // catch (Rule #2: stay loud, keep semantic envelope).
711
- if (err instanceof PerpError) {
712
- const s = err.structured;
713
- console.log(JSON.stringify(jsonError(s.code, s.message, {
714
- status: s.status,
715
- retryable: s.retryable,
716
- retryAfterMs: s.retryAfterMs,
717
- remediation: s.remediation,
718
- })));
719
- }
720
- else {
721
- console.log(JSON.stringify(jsonError("FATAL", msg)));
722
- }
723
- }
724
- else {
725
- console.error(chalk.red(msg));
726
- }
197
+ // Central printer (utils.printCliError): typed PerpError code / remediation / details
198
+ // are preserved (Rule #2: stay loud, keep semantic envelope); generic Errors go
199
+ // through classifyError; CAPABILITY_UNSUPPORTED prints the uniform §13.6 line.
200
+ const { printCliError } = await import("./utils.js");
201
+ printCliError(err, isJson());
727
202
  _origExit(1);
728
203
  });
729
204
  }