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