perp-cli 0.12.2 → 0.12.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commands/agent.js +122 -21
- package/dist/commands/init.d.ts +19 -0
- package/dist/commands/init.js +162 -36
- package/dist/commands/wallet.js +48 -11
- package/dist/config.js +5 -1
- package/package.json +3 -2
- package/skills/perp-cli/SKILL.md +2 -2
package/dist/commands/agent.js
CHANGED
|
@@ -266,8 +266,15 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
266
266
|
.option("--ip-whitelist <list>", "Comma-separated IP whitelist (pass empty string to include empty)")
|
|
267
267
|
.option("--api-key-index <n>", "Lighter slot index (4-254). Default: next free slot.")
|
|
268
268
|
.option("--json", "Machine-readable output")
|
|
269
|
-
.action(async (exchange, opts) => {
|
|
269
|
+
.action(async (exchange, opts, command) => {
|
|
270
270
|
const useJson = opts.json ?? isJson();
|
|
271
|
+
// optsWithGlobals() merges parent + subcommand options so --passphrase
|
|
272
|
+
// works whether placed on `perp --passphrase X wallet agent approve` or
|
|
273
|
+
// on the subcommand. Commander v13 silently shadows the subcommand flag
|
|
274
|
+
// when the parent defines an identically-named flag — the visible bug
|
|
275
|
+
// was approve throwing PASSPHRASE_REQUIRED even with --passphrase set.
|
|
276
|
+
const mergedOpts = command.optsWithGlobals();
|
|
277
|
+
const passphraseFlag = mergedOpts.passphrase ?? opts.passphrase;
|
|
271
278
|
// Normalize aliases
|
|
272
279
|
const exchangeNorm = exchange === "lt" ? "lighter"
|
|
273
280
|
: exchange === "hl" ? "hyperliquid"
|
|
@@ -314,7 +321,7 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
314
321
|
}
|
|
315
322
|
chosenSlot = n;
|
|
316
323
|
}
|
|
317
|
-
const passphrase = await resolvePassphrase({ flag:
|
|
324
|
+
const passphrase = await resolvePassphrase({ flag: passphraseFlag });
|
|
318
325
|
if (passphrase === null) {
|
|
319
326
|
reportErrorAndExit(new PerpError("PASSPHRASE_REQUIRED", "No passphrase provided and stdin is non-TTY", {
|
|
320
327
|
remediation: "Provide passphrase via --passphrase flag, OWS_PASSPHRASE env var, or stdin pipe",
|
|
@@ -377,7 +384,7 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
377
384
|
remediation: "Run: perp setup or use --master <name>",
|
|
378
385
|
}));
|
|
379
386
|
}
|
|
380
|
-
const passphrase = await resolvePassphrase({ flag:
|
|
387
|
+
const passphrase = await resolvePassphrase({ flag: passphraseFlag });
|
|
381
388
|
if (passphrase === null) {
|
|
382
389
|
reportErrorAndExit(new PerpError("PASSPHRASE_REQUIRED", "No passphrase provided and stdin is non-TTY", {
|
|
383
390
|
remediation: "Provide passphrase via --passphrase flag, OWS_PASSPHRASE env var, or stdin pipe",
|
|
@@ -438,7 +445,7 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
438
445
|
remediation: "Run: perp setup or use --master <name>",
|
|
439
446
|
}));
|
|
440
447
|
}
|
|
441
|
-
const passphrase = await resolvePassphrase({ flag:
|
|
448
|
+
const passphrase = await resolvePassphrase({ flag: passphraseFlag });
|
|
442
449
|
if (passphrase === null) {
|
|
443
450
|
reportErrorAndExit(new PerpError("PASSPHRASE_REQUIRED", "No passphrase provided and stdin is non-TTY", {
|
|
444
451
|
remediation: "Provide passphrase via --passphrase flag, OWS_PASSPHRASE env var, or stdin pipe",
|
|
@@ -526,7 +533,7 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
526
533
|
canSpot = opts.canSpot ?? false;
|
|
527
534
|
canWithdraw = opts.canWithdraw ?? false;
|
|
528
535
|
// 3-path passphrase resolver (non-TTY path)
|
|
529
|
-
passphrase = await resolvePassphrase({ flag:
|
|
536
|
+
passphrase = await resolvePassphrase({ flag: passphraseFlag });
|
|
530
537
|
if (passphrase === null) {
|
|
531
538
|
// TTY but non-wizard (some flags were supplied) — throw PASSPHRASE_REQUIRED
|
|
532
539
|
reportErrorAndExit(new PerpError("PASSPHRASE_REQUIRED", "No passphrase provided and stdin is non-TTY or wizard skipped", {
|
|
@@ -666,8 +673,11 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
666
673
|
.option("--force", "Skip Aster POST, only clear local state")
|
|
667
674
|
.option("--passphrase <pp>", "Master OWS passphrase")
|
|
668
675
|
.option("--json", "Machine-readable output")
|
|
669
|
-
.action(async (exchange, agentName, opts) => {
|
|
676
|
+
.action(async (exchange, agentName, opts, command) => {
|
|
670
677
|
const useJson = opts.json ?? isJson();
|
|
678
|
+
// optsWithGlobals(): see wallet agent approve for full rationale.
|
|
679
|
+
const mergedOpts = command.optsWithGlobals();
|
|
680
|
+
const passphraseFlag = mergedOpts.passphrase ?? opts.passphrase;
|
|
671
681
|
try {
|
|
672
682
|
// Step 1: Check local meta (idempotent — absent = already revoked)
|
|
673
683
|
const meta = getAgent(exchange, agentName);
|
|
@@ -683,7 +693,7 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
683
693
|
}
|
|
684
694
|
if (!opts.force) {
|
|
685
695
|
// Step 2: Resolve passphrase
|
|
686
|
-
const passphrase = await resolvePassphrase({ flag:
|
|
696
|
+
const passphrase = await resolvePassphrase({ flag: passphraseFlag });
|
|
687
697
|
if (passphrase === null) {
|
|
688
698
|
throw new PerpError("PASSPHRASE_REQUIRED", "No passphrase provided and stdin is non-TTY", {
|
|
689
699
|
remediation: "Provide passphrase via --passphrase flag, OWS_PASSPHRASE env var, or stdin pipe",
|
|
@@ -791,8 +801,11 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
791
801
|
.option("--can-withdraw", "Allow withdrawals (default: inherit or off)")
|
|
792
802
|
.option("--no-can-withdraw", "Disallow withdrawals")
|
|
793
803
|
.option("--json", "Machine-readable output")
|
|
794
|
-
.action(async (exchange, oldAgentNameArg, opts) => {
|
|
804
|
+
.action(async (exchange, oldAgentNameArg, opts, command) => {
|
|
795
805
|
const useJson = opts.json ?? isJson();
|
|
806
|
+
// optsWithGlobals(): see wallet agent approve for full rationale.
|
|
807
|
+
const mergedOpts = command.optsWithGlobals();
|
|
808
|
+
const passphraseFlag = mergedOpts.passphrase ?? opts.passphrase;
|
|
796
809
|
try {
|
|
797
810
|
// Determine which agent to rotate
|
|
798
811
|
const agentNameToRotate = oldAgentNameArg ?? opts.agentName ?? getAgent(exchange)?.agentName;
|
|
@@ -802,7 +815,7 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
802
815
|
});
|
|
803
816
|
}
|
|
804
817
|
// Resolve passphrase early (needed for both revoke and approve phases)
|
|
805
|
-
const passphrase = await resolvePassphrase({ flag:
|
|
818
|
+
const passphrase = await resolvePassphrase({ flag: passphraseFlag });
|
|
806
819
|
if (passphrase === null) {
|
|
807
820
|
throw new PerpError("PASSPHRASE_REQUIRED", "No passphrase provided and stdin is non-TTY", {
|
|
808
821
|
remediation: "Provide passphrase via --passphrase flag, OWS_PASSPHRASE env var, or stdin pipe",
|
|
@@ -1006,11 +1019,14 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
1006
1019
|
.option("--account-index <n>", "Account index (Lighter only)")
|
|
1007
1020
|
.option("--passphrase <pp>", "Master OWS passphrase (Aster/PAC). Falls back to OWS_PASSPHRASE env.")
|
|
1008
1021
|
.option("--json", "Machine-readable output")
|
|
1009
|
-
.action(async (exchange, agentName, opts) => {
|
|
1022
|
+
.action(async (exchange, agentName, opts, command) => {
|
|
1010
1023
|
const useJson = opts.json ?? isJson();
|
|
1011
1024
|
const ts = new Date().toISOString();
|
|
1025
|
+
// optsWithGlobals(): see wallet agent approve for full rationale.
|
|
1026
|
+
const mergedOpts = command.optsWithGlobals();
|
|
1027
|
+
const passphraseFlag = mergedOpts.passphrase ?? opts.passphrase;
|
|
1012
1028
|
// Resolve passphrase via 3-path: flag > env > (no stdin prompt in verify)
|
|
1013
|
-
const passphrase =
|
|
1029
|
+
const passphrase = passphraseFlag ?? process.env["OWS_PASSPHRASE"] ?? "";
|
|
1014
1030
|
const verifyOpts = {
|
|
1015
1031
|
agentName,
|
|
1016
1032
|
master: opts.master,
|
|
@@ -1157,6 +1173,56 @@ export function registerWalletAgentCommands(walletCmd, isJson) {
|
|
|
1157
1173
|
}
|
|
1158
1174
|
});
|
|
1159
1175
|
}
|
|
1176
|
+
// ── Pre-venue rollback helpers ───────────────────────────────────────────────
|
|
1177
|
+
//
|
|
1178
|
+
// When `wallet agent approve <dex>` fails BEFORE the venue accepted the agent
|
|
1179
|
+
// (HTTP error, body-code rejection, parse error, network drop), the locally
|
|
1180
|
+
// generated `agent-<dex>-<master>` OWS wallet is orphaned in `~/.ows/wallets/`.
|
|
1181
|
+
// Retry collides with "wallet name already exists". These helpers auto-delete
|
|
1182
|
+
// the orphan and produce an actionable remediation string.
|
|
1183
|
+
//
|
|
1184
|
+
// Post-venue (persist-only) failures must NOT use these — those leave a
|
|
1185
|
+
// status:"partial" record so the user can still revoke venue-side state.
|
|
1186
|
+
/**
|
|
1187
|
+
* Best-effort delete of an orphan local OWS wallet. Returns true on success,
|
|
1188
|
+
* false on failure (also logs the failure to stderr per SSOT Rule #2: rollback
|
|
1189
|
+
* failures must not silently mask the original error). The caller threads the
|
|
1190
|
+
* boolean into the user-visible remediation.
|
|
1191
|
+
*/
|
|
1192
|
+
function tryDeleteOrphanWallet(ows, walletName) {
|
|
1193
|
+
if (!walletName)
|
|
1194
|
+
return false;
|
|
1195
|
+
try {
|
|
1196
|
+
ows.deleteWallet(walletName);
|
|
1197
|
+
return true;
|
|
1198
|
+
}
|
|
1199
|
+
catch (cleanupErr) {
|
|
1200
|
+
// Don't swallow — surface the rollback failure on stderr so the user can
|
|
1201
|
+
// see exactly what's stuck. Original error still propagates from caller.
|
|
1202
|
+
process.stderr.write(`[warn] failed to clean up orphan wallet "${walletName}": ${cleanupErr instanceof Error ? cleanupErr.message : String(cleanupErr)}\n`);
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
1206
|
+
/**
|
|
1207
|
+
* Build an actionable remediation message for pre-venue approve failures.
|
|
1208
|
+
* On clean rollback: tells the user to deposit + retry the canonical
|
|
1209
|
+
* `wallet agent approve <dex> --master <master>` command (no manual steps).
|
|
1210
|
+
* On rollback failure: tells the user to manually delete the orphan wallet
|
|
1211
|
+
* before retrying. The agent address is appended for diagnostic purposes
|
|
1212
|
+
* (some users may need it for the venue UI).
|
|
1213
|
+
*/
|
|
1214
|
+
function buildPreVenueRemediation(opts) {
|
|
1215
|
+
const { cleanupOk, dex, masterName, agentAddress, hint } = opts;
|
|
1216
|
+
const retry = `perp wallet agent approve ${dex} --master ${masterName}`;
|
|
1217
|
+
const head = cleanupOk
|
|
1218
|
+
? `Local agent wallet cleaned up. Deposit funds and retry: ${retry}`
|
|
1219
|
+
: `Manual cleanup required — local agent wallet may still exist. Run: perp wallet remove agent-${dex === "hyperliquid" ? "hl" : dex === "pacifica" ? "pac" : "aster"}-${masterName}, then retry: ${retry}`;
|
|
1220
|
+
const parts = [head];
|
|
1221
|
+
if (hint)
|
|
1222
|
+
parts.push(hint);
|
|
1223
|
+
parts.push(`Agent address (diagnostic): ${agentAddress}`);
|
|
1224
|
+
return parts.join(" | ");
|
|
1225
|
+
}
|
|
1160
1226
|
/**
|
|
1161
1227
|
* Core approve flow: Steps 2-12 (OWS wallet + policy + api-key + EIP-712 + Aster POST + persist).
|
|
1162
1228
|
* Caller MUST hold the exchange lock before calling this function.
|
|
@@ -1233,13 +1299,15 @@ async function runApproveFlow(opts) {
|
|
|
1233
1299
|
});
|
|
1234
1300
|
if (!builderRes.ok) {
|
|
1235
1301
|
const errText = await builderRes.text().catch(() => "");
|
|
1236
|
-
// Aster builder POST failed —
|
|
1302
|
+
// Aster builder POST failed — pre-venue (no Aster agent registered yet).
|
|
1303
|
+
// Rollback both: api key + orphan local OWS wallet.
|
|
1237
1304
|
if (owsApiKeyId) {
|
|
1238
1305
|
try {
|
|
1239
1306
|
ows.revokeApiKey(owsApiKeyId);
|
|
1240
1307
|
}
|
|
1241
1308
|
catch { /* best effort */ }
|
|
1242
1309
|
}
|
|
1310
|
+
tryDeleteOrphanWallet(ows, agentWalletName);
|
|
1243
1311
|
throw new PerpError("EXCHANGE_ERROR", `Aster approveBuilder failed (${builderRes.status}): ${errText.slice(0, 200)}`, {
|
|
1244
1312
|
remediation: "Check builder address and max-fee-rate parameters",
|
|
1245
1313
|
});
|
|
@@ -1253,6 +1321,7 @@ async function runApproveFlow(opts) {
|
|
|
1253
1321
|
}
|
|
1254
1322
|
catch { /* best effort */ }
|
|
1255
1323
|
}
|
|
1324
|
+
tryDeleteOrphanWallet(ows, agentWalletName);
|
|
1256
1325
|
const errMsg = builderResp.msg ?? builderResp.message ?? String(builderResp.code);
|
|
1257
1326
|
throw new PerpError("EXCHANGE_ERROR", `Aster approveBuilder failed: ${errMsg}`, {
|
|
1258
1327
|
remediation: "Check builder address and max-fee-rate parameters",
|
|
@@ -1307,30 +1376,37 @@ async function runApproveFlow(opts) {
|
|
|
1307
1376
|
asterPostSucceeded = true;
|
|
1308
1377
|
}
|
|
1309
1378
|
else {
|
|
1310
|
-
// Aster POST body indicated failure —
|
|
1379
|
+
// Aster POST body indicated failure — pre-venue (Aster did not register).
|
|
1380
|
+
// Rollback both api key + orphan local OWS wallet.
|
|
1311
1381
|
if (owsApiKeyId) {
|
|
1312
1382
|
try {
|
|
1313
1383
|
ows.revokeApiKey(owsApiKeyId);
|
|
1314
1384
|
}
|
|
1315
1385
|
catch { /* best effort */ }
|
|
1316
1386
|
}
|
|
1387
|
+
const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
|
|
1317
1388
|
const errMsg = resp.msg ?? resp.message ?? String(resp.code);
|
|
1318
1389
|
throw new PerpError("APPROVE_PARTIAL", `Aster approveAgent failed: ${errMsg}`, {
|
|
1319
|
-
remediation:
|
|
1390
|
+
remediation: buildPreVenueRemediation({
|
|
1391
|
+
cleanupOk, dex: "aster", masterName, agentAddress: agentEvmAddress,
|
|
1392
|
+
}),
|
|
1320
1393
|
});
|
|
1321
1394
|
}
|
|
1322
1395
|
}
|
|
1323
1396
|
else {
|
|
1324
|
-
// HTTP error —
|
|
1397
|
+
// HTTP error — pre-venue (Aster did not register). Rollback both.
|
|
1325
1398
|
if (owsApiKeyId) {
|
|
1326
1399
|
try {
|
|
1327
1400
|
ows.revokeApiKey(owsApiKeyId);
|
|
1328
1401
|
}
|
|
1329
1402
|
catch { /* best effort */ }
|
|
1330
1403
|
}
|
|
1404
|
+
const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
|
|
1331
1405
|
const errText = await agentRes.text().catch(() => "");
|
|
1332
1406
|
throw new PerpError("APPROVE_PARTIAL", `Aster approveAgent failed (${agentRes.status}): ${errText.slice(0, 200)}`, {
|
|
1333
|
-
remediation:
|
|
1407
|
+
remediation: buildPreVenueRemediation({
|
|
1408
|
+
cleanupOk, dex: "aster", masterName, agentAddress: agentEvmAddress,
|
|
1409
|
+
}),
|
|
1334
1410
|
});
|
|
1335
1411
|
}
|
|
1336
1412
|
// Step 12: Persist agent meta
|
|
@@ -1551,13 +1627,26 @@ async function runHlApproveFlow(opts) {
|
|
|
1551
1627
|
resp = JSON.parse(text);
|
|
1552
1628
|
}
|
|
1553
1629
|
catch {
|
|
1630
|
+
// Pre-venue parse failure — Hyperliquid did NOT register the agent.
|
|
1631
|
+
// Roll back the orphan local OWS wallet so retry doesn't collide with
|
|
1632
|
+
// "wallet name already exists". Errors during cleanup must NOT mask the
|
|
1633
|
+
// original network/parse error (SSOT Rule #2).
|
|
1634
|
+
const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
|
|
1554
1635
|
throw new PerpError("EXCHANGE_ERROR", `HL exchange API parse error (${agentRes.status}): ${text.slice(0, 200)}`, {
|
|
1555
|
-
remediation:
|
|
1636
|
+
remediation: buildPreVenueRemediation({
|
|
1637
|
+
cleanupOk, dex: "hyperliquid", masterName, agentAddress: agentEvmAddress,
|
|
1638
|
+
hint: "Check Hyperliquid API status",
|
|
1639
|
+
}),
|
|
1556
1640
|
});
|
|
1557
1641
|
}
|
|
1558
1642
|
if (resp?.status === "err") {
|
|
1643
|
+
// Pre-venue rejection — Hyperliquid did NOT register the agent. Roll
|
|
1644
|
+
// back the orphan local OWS wallet so retry can proceed cleanly.
|
|
1645
|
+
const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
|
|
1559
1646
|
throw new PerpError("APPROVE_PARTIAL", `HL approveAgent failed: ${typeof resp.response === "string" ? resp.response : JSON.stringify(resp)}`, {
|
|
1560
|
-
remediation:
|
|
1647
|
+
remediation: buildPreVenueRemediation({
|
|
1648
|
+
cleanupOk, dex: "hyperliquid", masterName, agentAddress: agentEvmAddress,
|
|
1649
|
+
}),
|
|
1561
1650
|
});
|
|
1562
1651
|
}
|
|
1563
1652
|
// Step 6: Persist
|
|
@@ -1682,24 +1771,36 @@ async function runPacApproveFlow(opts) {
|
|
|
1682
1771
|
// Pacifica REST envelope: success === true OR explicit data field
|
|
1683
1772
|
if (resp.success === false || resp.error) {
|
|
1684
1773
|
const errMsg = typeof resp.error === "string" ? resp.error : JSON.stringify(resp);
|
|
1774
|
+
// Pre-venue rejection — Pacifica did NOT register the agent.
|
|
1775
|
+
const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
|
|
1685
1776
|
throw new PerpError("APPROVE_PARTIAL", `Pacifica bind_agent_wallet failed: ${errMsg}`, {
|
|
1686
|
-
remediation:
|
|
1777
|
+
remediation: buildPreVenueRemediation({
|
|
1778
|
+
cleanupOk, dex: "pacifica", masterName, agentAddress: agentSolanaAddress,
|
|
1779
|
+
}),
|
|
1687
1780
|
});
|
|
1688
1781
|
}
|
|
1689
1782
|
pacPostSucceeded = true;
|
|
1690
1783
|
}
|
|
1691
1784
|
else {
|
|
1692
1785
|
const errText = await httpRes.text().catch(() => "");
|
|
1786
|
+
// Pre-venue rejection — Pacifica did NOT register the agent.
|
|
1787
|
+
const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
|
|
1693
1788
|
throw new PerpError("APPROVE_PARTIAL", `Pacifica bind_agent_wallet failed (${httpRes.status}): ${errText.slice(0, 200)}`, {
|
|
1694
|
-
remediation:
|
|
1789
|
+
remediation: buildPreVenueRemediation({
|
|
1790
|
+
cleanupOk, dex: "pacifica", masterName, agentAddress: agentSolanaAddress,
|
|
1791
|
+
}),
|
|
1695
1792
|
});
|
|
1696
1793
|
}
|
|
1697
1794
|
}
|
|
1698
1795
|
catch (httpErr) {
|
|
1699
1796
|
if (httpErr instanceof PerpError)
|
|
1700
1797
|
throw httpErr;
|
|
1798
|
+
// Network-level failure — Pacifica did NOT register the agent.
|
|
1799
|
+
const cleanupOk = tryDeleteOrphanWallet(ows, agentWalletName);
|
|
1701
1800
|
throw new PerpError("APPROVE_PARTIAL", `Pacifica bind_agent_wallet network error: ${httpErr instanceof Error ? httpErr.message : String(httpErr)}`, {
|
|
1702
|
-
remediation:
|
|
1801
|
+
remediation: buildPreVenueRemediation({
|
|
1802
|
+
cleanupOk, dex: "pacifica", masterName, agentAddress: agentSolanaAddress,
|
|
1803
|
+
}),
|
|
1703
1804
|
});
|
|
1704
1805
|
}
|
|
1705
1806
|
// Step 5: Persist
|
package/dist/commands/init.d.ts
CHANGED
|
@@ -14,3 +14,22 @@ export declare function validateKey(chain: "solana" | "evm" | "apikey", key: str
|
|
|
14
14
|
address: string;
|
|
15
15
|
}>;
|
|
16
16
|
export declare function registerInitCommand(program: Command): void;
|
|
17
|
+
/**
|
|
18
|
+
* Drive `perp setup` end-to-end without TTY. Required:
|
|
19
|
+
* - --passphrase or OWS_PASSPHRASE env (3-path resolver)
|
|
20
|
+
* Optional:
|
|
21
|
+
* - --wallet-name (default: "main")
|
|
22
|
+
* - --default-exchange (must be one of pacifica/hyperliquid/lighter/aster)
|
|
23
|
+
*
|
|
24
|
+
* Side effects:
|
|
25
|
+
* 1. Creates an OWS wallet via createWallet(name, passphrase)
|
|
26
|
+
* 2. Sets settings.owsActiveWallet (if unset) + settings.defaultExchange (if provided)
|
|
27
|
+
*
|
|
28
|
+
* Errors are returned as INVALID_PARAMS / PASSPHRASE_REQUIRED — never via
|
|
29
|
+
* readline. JSON envelope is emitted on stdout when --json is present.
|
|
30
|
+
*/
|
|
31
|
+
export declare function runNonInteractiveSetup(opts: {
|
|
32
|
+
walletName?: string;
|
|
33
|
+
passphrase?: string;
|
|
34
|
+
defaultExchange?: string;
|
|
35
|
+
}): Promise<void>;
|
package/dist/commands/init.js
CHANGED
|
@@ -3,6 +3,10 @@ import { createInterface } from "readline";
|
|
|
3
3
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
4
4
|
import { resolve } from "path";
|
|
5
5
|
import { loadSettings, saveSettings } from "../settings.js";
|
|
6
|
+
import { PerpError } from "../errors.js";
|
|
7
|
+
import { jsonOk, jsonError, printJson } from "../utils.js";
|
|
8
|
+
import { resolvePassphrase } from "../agent-wallet/passphrase.js";
|
|
9
|
+
import { loadOws } from "../signer/ows-loader.js";
|
|
6
10
|
const PERP_DIR = resolve(process.env.HOME || "~", ".perp");
|
|
7
11
|
const ENV_FILE = resolve(PERP_DIR, ".env");
|
|
8
12
|
// ── .env helpers ──────────────────────────────────────────────
|
|
@@ -66,11 +70,15 @@ function askChoice(rl, question, choices) {
|
|
|
66
70
|
});
|
|
67
71
|
}
|
|
68
72
|
// ── Exchange → env var mapping ────────────────────────────────
|
|
73
|
+
// Aster removed in v0.12.4: legacy HMAC API-key signing path was retired in
|
|
74
|
+
// Plan v3.0. New users must onboard via `perp wallet agent approve aster`
|
|
75
|
+
// (off-chain agent wallet, same flow as HL/PAC/LT). The setup wizard
|
|
76
|
+
// surfaces a redirect when Aster is selected; `wallet set aster <key>`
|
|
77
|
+
// throws INVALID_PARAMS pointing at the agent flow.
|
|
69
78
|
export const EXCHANGE_ENV_MAP = {
|
|
70
79
|
pacifica: { envKey: "PACIFICA_PRIVATE_KEY", chain: "solana", label: "Pacifica (Solana)" },
|
|
71
80
|
hyperliquid: { envKey: "HL_PRIVATE_KEY", chain: "evm", label: "Hyperliquid (EVM)" },
|
|
72
81
|
lighter: { envKey: "LIGHTER_PRIVATE_KEY", chain: "evm", label: "Lighter (EVM)" },
|
|
73
|
-
aster: { envKey: "ASTER_API_KEY", envKeySecret: "ASTER_API_SECRET", chain: "apikey", label: "Aster (BNB Chain)" },
|
|
74
82
|
};
|
|
75
83
|
// ── Validate keys ─────────────────────────────────────────────
|
|
76
84
|
export async function validateKey(chain, key) {
|
|
@@ -109,12 +117,34 @@ export async function validateKey(chain, key) {
|
|
|
109
117
|
}
|
|
110
118
|
// ── Commands ──────────────────────────────────────────────────
|
|
111
119
|
export function registerInitCommand(program) {
|
|
112
|
-
// ── perp setup (interactive) ──
|
|
120
|
+
// ── perp setup (interactive + non-interactive) ──
|
|
113
121
|
program
|
|
114
122
|
.command("setup")
|
|
115
123
|
.alias("init")
|
|
116
|
-
.description("
|
|
117
|
-
.
|
|
124
|
+
.description("Setup wizard — interactive by default, scriptable with --non-interactive")
|
|
125
|
+
.option("--non-interactive", "Run without prompts; requires --passphrase or OWS_PASSPHRASE env")
|
|
126
|
+
.option("--wallet-name <name>", "OWS wallet alias to create (default: 'main')")
|
|
127
|
+
.option("--passphrase <pp>", "OWS vault passphrase (fallback: OWS_PASSPHRASE env / stdin)")
|
|
128
|
+
.option("--default-exchange <name>", "Default exchange (pacifica/hyperliquid/lighter/aster)")
|
|
129
|
+
.action(async (opts, command) => {
|
|
130
|
+
// optsWithGlobals() merges parent + subcommand options so --passphrase
|
|
131
|
+
// works whether placed on `perp --passphrase X setup` (parent) or
|
|
132
|
+
// `perp setup --passphrase X` (subcommand). Commander v13 silently
|
|
133
|
+
// shadows the subcommand flag when the parent defines an identically
|
|
134
|
+
// named flag — the visible bug was non-interactive setup throwing
|
|
135
|
+
// PASSPHRASE_REQUIRED with --passphrase set on the subcommand.
|
|
136
|
+
const mergedOpts = command.optsWithGlobals();
|
|
137
|
+
const resolvedOpts = {
|
|
138
|
+
...opts,
|
|
139
|
+
passphrase: mergedOpts.passphrase ?? opts.passphrase,
|
|
140
|
+
};
|
|
141
|
+
// Non-interactive mode: skip all readline prompts, drive everything from
|
|
142
|
+
// flags + OWS_PASSPHRASE env. This is the agent-friendly onboarding path
|
|
143
|
+
// (CI / Docker / scripted init) — see plan v3.0 "agent-friendly five
|
|
144
|
+
// guarantees" P1 (no-prompt) and P3 (passphrase 3-path).
|
|
145
|
+
if (resolvedOpts.nonInteractive) {
|
|
146
|
+
return runNonInteractiveSetup(resolvedOpts);
|
|
147
|
+
}
|
|
118
148
|
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
119
149
|
try {
|
|
120
150
|
console.log(chalk.cyan.bold("\n perp-cli Setup Wizard\n"));
|
|
@@ -146,12 +176,28 @@ export function registerInitCommand(program) {
|
|
|
146
176
|
console.log(` 1) ${chalk.cyan("Pacifica")} ${chalk.gray("— Solana perps (USDC on Solana)")}`);
|
|
147
177
|
console.log(` 2) ${chalk.cyan("Hyperliquid")} ${chalk.gray("— EVM perps (USDC on Arbitrum)")}`);
|
|
148
178
|
console.log(` 3) ${chalk.cyan("Lighter")} ${chalk.gray("— EVM perps (USDC on Ethereum/Arb)")}`);
|
|
149
|
-
console.log(` 4) ${chalk.cyan("Aster")} ${chalk.gray("— BNB Chain perps (
|
|
179
|
+
console.log(` 4) ${chalk.cyan("Aster")} ${chalk.gray("— BNB Chain perps (use 'wallet agent approve aster')")}`);
|
|
150
180
|
console.log();
|
|
151
181
|
const exchangeChoice = await ask(rl, " Which exchange(s)? (1,2,3,4 or 'all'): ");
|
|
152
|
-
const
|
|
182
|
+
const selectedRaw = parseExchangeChoice(exchangeChoice);
|
|
183
|
+
// v0.12.4: legacy Aster HMAC API-key path is removed. The wizard can
|
|
184
|
+
// no longer collect ASTER_API_KEY/ASTER_API_SECRET — Aster onboarding
|
|
185
|
+
// is now exclusively via `perp wallet agent approve aster --master`.
|
|
186
|
+
// Print a redirect and drop Aster from the env-write set.
|
|
187
|
+
const asterRequested = selectedRaw.includes("aster");
|
|
188
|
+
const selected = selectedRaw.filter((e) => e !== "aster");
|
|
189
|
+
if (asterRequested) {
|
|
190
|
+
console.log(chalk.yellow.bold("\n Aster onboarding is via agent wallet (no API keys):"));
|
|
191
|
+
console.log(` ${chalk.green("perp wallet agent approve aster --master <name>")}`);
|
|
192
|
+
console.log(chalk.gray(" Run that after this wizard completes.\n"));
|
|
193
|
+
}
|
|
153
194
|
if (selected.length === 0) {
|
|
154
|
-
|
|
195
|
+
if (asterRequested) {
|
|
196
|
+
console.log(chalk.gray(" No additional exchanges to configure here. See the Aster instructions above.\n"));
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
console.log(chalk.red("\n No exchanges selected.\n"));
|
|
200
|
+
}
|
|
155
201
|
rl.close();
|
|
156
202
|
return;
|
|
157
203
|
}
|
|
@@ -159,7 +205,6 @@ export function registerInitCommand(program) {
|
|
|
159
205
|
// EVM exchanges can share the same key
|
|
160
206
|
const evmExchanges = selected.filter((e) => EXCHANGE_ENV_MAP[e].chain === "evm");
|
|
161
207
|
const solExchanges = selected.filter((e) => EXCHANGE_ENV_MAP[e].chain === "solana");
|
|
162
|
-
const apikeyExchanges = selected.filter((e) => EXCHANGE_ENV_MAP[e].chain === "apikey");
|
|
163
208
|
const newEnv = { ...env };
|
|
164
209
|
if (evmExchanges.length > 0) {
|
|
165
210
|
console.log(chalk.cyan.bold("\n EVM Private Key") + chalk.gray(` (for ${evmExchanges.join(", ")})`));
|
|
@@ -215,34 +260,9 @@ export function registerInitCommand(program) {
|
|
|
215
260
|
}
|
|
216
261
|
}
|
|
217
262
|
}
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
console.log(chalk.cyan.bold(`\n ${info.label} API Credentials`));
|
|
222
|
-
const apiKey = await ask(rl, ` ${info.label} API key: `);
|
|
223
|
-
if (!apiKey) {
|
|
224
|
-
console.log(chalk.gray(" Skipped."));
|
|
225
|
-
continue;
|
|
226
|
-
}
|
|
227
|
-
const { valid, address } = await validateKey("apikey", apiKey);
|
|
228
|
-
if (!valid) {
|
|
229
|
-
console.log(chalk.red(" Invalid API key (must be at least 16 chars), skipped."));
|
|
230
|
-
continue;
|
|
231
|
-
}
|
|
232
|
-
newEnv[info.envKey] = apiKey;
|
|
233
|
-
console.log(` ${chalk.green("OK")} ${chalk.gray(address)}`);
|
|
234
|
-
if (info.envKeySecret) {
|
|
235
|
-
const secret = await ask(rl, ` ${info.label} API secret: `);
|
|
236
|
-
if (secret) {
|
|
237
|
-
newEnv[info.envKeySecret] = secret;
|
|
238
|
-
console.log(` ${chalk.green("OK")} secret saved`);
|
|
239
|
-
}
|
|
240
|
-
else {
|
|
241
|
-
console.log(chalk.yellow(" No secret provided — some operations may fail."));
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
}
|
|
245
|
-
}
|
|
263
|
+
// v0.12.4: API-key (Aster legacy HMAC) onboarding removed from the
|
|
264
|
+
// wizard. Aster users follow the redirect above to
|
|
265
|
+
// `perp wallet agent approve aster`.
|
|
246
266
|
// Default exchange
|
|
247
267
|
const settings = loadSettings();
|
|
248
268
|
if (selected.length === 1) {
|
|
@@ -304,3 +324,109 @@ function parseExchangeChoice(input) {
|
|
|
304
324
|
}
|
|
305
325
|
return [...new Set(exchanges)];
|
|
306
326
|
}
|
|
327
|
+
// ── Non-interactive setup (agent / CI / Docker) ──────────────────────────────
|
|
328
|
+
const VALID_EXCHANGES = ["pacifica", "hyperliquid", "lighter", "aster"];
|
|
329
|
+
/**
|
|
330
|
+
* Test seam: emit JSON envelopes when the global --json flag is present.
|
|
331
|
+
* Mirrors the inline pattern used elsewhere (see src/index.ts:87) instead of
|
|
332
|
+
* wiring isJson() through registerInitCommand for one call site.
|
|
333
|
+
*/
|
|
334
|
+
function setupIsJson() {
|
|
335
|
+
return process.argv.includes("--json");
|
|
336
|
+
}
|
|
337
|
+
/**
|
|
338
|
+
* Drive `perp setup` end-to-end without TTY. Required:
|
|
339
|
+
* - --passphrase or OWS_PASSPHRASE env (3-path resolver)
|
|
340
|
+
* Optional:
|
|
341
|
+
* - --wallet-name (default: "main")
|
|
342
|
+
* - --default-exchange (must be one of pacifica/hyperliquid/lighter/aster)
|
|
343
|
+
*
|
|
344
|
+
* Side effects:
|
|
345
|
+
* 1. Creates an OWS wallet via createWallet(name, passphrase)
|
|
346
|
+
* 2. Sets settings.owsActiveWallet (if unset) + settings.defaultExchange (if provided)
|
|
347
|
+
*
|
|
348
|
+
* Errors are returned as INVALID_PARAMS / PASSPHRASE_REQUIRED — never via
|
|
349
|
+
* readline. JSON envelope is emitted on stdout when --json is present.
|
|
350
|
+
*/
|
|
351
|
+
export async function runNonInteractiveSetup(opts) {
|
|
352
|
+
const isJson = setupIsJson();
|
|
353
|
+
try {
|
|
354
|
+
// Validate --default-exchange shape early so users hit a precise error
|
|
355
|
+
// before passphrase resolution.
|
|
356
|
+
if (opts.defaultExchange !== undefined &&
|
|
357
|
+
!VALID_EXCHANGES.includes(opts.defaultExchange)) {
|
|
358
|
+
throw new PerpError("INVALID_PARAMS", `Invalid --default-exchange "${opts.defaultExchange}"`, {
|
|
359
|
+
remediation: `Use one of: ${VALID_EXCHANGES.join(", ")}`,
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
// Resolve passphrase. resolvePassphrase already throws PASSPHRASE_REQUIRED
|
|
363
|
+
// when stdin is non-TTY and nothing was provided. In TTY mode it returns
|
|
364
|
+
// null — non-interactive mode must NOT fall back to a prompt.
|
|
365
|
+
const resolved = await resolvePassphrase({ flag: opts.passphrase });
|
|
366
|
+
if (resolved === null) {
|
|
367
|
+
throw new PerpError("PASSPHRASE_REQUIRED", "Non-interactive setup requires a passphrase", {
|
|
368
|
+
remediation: "Provide --passphrase <pp> or set OWS_PASSPHRASE env var",
|
|
369
|
+
});
|
|
370
|
+
}
|
|
371
|
+
const pp = resolved;
|
|
372
|
+
const walletName = opts.walletName ?? "main";
|
|
373
|
+
// Create OWS wallet
|
|
374
|
+
const ows = loadOws();
|
|
375
|
+
const w = ows.createWallet(walletName, pp);
|
|
376
|
+
// Update settings
|
|
377
|
+
const settings = loadSettings();
|
|
378
|
+
if (!settings.owsActiveWallet) {
|
|
379
|
+
settings.owsActiveWallet = walletName;
|
|
380
|
+
}
|
|
381
|
+
if (opts.defaultExchange) {
|
|
382
|
+
settings.defaultExchange = opts.defaultExchange;
|
|
383
|
+
}
|
|
384
|
+
saveSettings(settings);
|
|
385
|
+
if (isJson) {
|
|
386
|
+
printJson(jsonOk({
|
|
387
|
+
walletName: w.name,
|
|
388
|
+
walletId: w.id,
|
|
389
|
+
accounts: w.accounts,
|
|
390
|
+
owsActiveWallet: settings.owsActiveWallet,
|
|
391
|
+
defaultExchange: settings.defaultExchange ?? null,
|
|
392
|
+
mode: "non-interactive",
|
|
393
|
+
}));
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
console.log(chalk.cyan.bold("\n perp-cli Setup (non-interactive) — Done\n"));
|
|
397
|
+
console.log(` Wallet: ${chalk.white.bold(w.name)}`);
|
|
398
|
+
console.log(` Active wallet: ${chalk.cyan(settings.owsActiveWallet)}`);
|
|
399
|
+
if (settings.defaultExchange) {
|
|
400
|
+
console.log(` Default exchange: ${chalk.cyan(settings.defaultExchange)}`);
|
|
401
|
+
}
|
|
402
|
+
console.log(chalk.gray(`\n Vault: ~/.ows/ (AES-256-GCM encrypted)\n`));
|
|
403
|
+
}
|
|
404
|
+
catch (err) {
|
|
405
|
+
if (isJson) {
|
|
406
|
+
if (err instanceof PerpError) {
|
|
407
|
+
printJson(jsonError(err.structured.code, err.structured.message, {
|
|
408
|
+
...(err.structured.remediation
|
|
409
|
+
? { details: { remediation: err.structured.remediation } }
|
|
410
|
+
: {}),
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
else {
|
|
414
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
415
|
+
printJson(jsonError("UNKNOWN", msg));
|
|
416
|
+
}
|
|
417
|
+
process.exitCode = 1;
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
if (err instanceof PerpError) {
|
|
421
|
+
console.error(chalk.red(`\n Error [${err.structured.code}]: ${err.structured.message}`));
|
|
422
|
+
if (err.structured.remediation) {
|
|
423
|
+
console.error(chalk.gray(` Remediation: ${err.structured.remediation}\n`));
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
else {
|
|
427
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
428
|
+
console.error(chalk.red(`\n Error: ${msg}\n`));
|
|
429
|
+
}
|
|
430
|
+
process.exitCode = 1;
|
|
431
|
+
}
|
|
432
|
+
}
|
package/dist/commands/wallet.js
CHANGED
|
@@ -324,16 +324,30 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
324
324
|
.description("Generate a new wallet (OWS encrypted vault, multi-chain)")
|
|
325
325
|
.option("--words <count>", "Mnemonic word count (12 or 24)", "12")
|
|
326
326
|
.option("--show-mnemonic", "Display the mnemonic phrase (CAUTION)")
|
|
327
|
+
.option("--passphrase <pp>", "Master OWS passphrase (fallback: OWS_PASSPHRASE env / stdin)")
|
|
327
328
|
.option("--legacy <chain>", "Legacy mode: generate solana or evm key (unencrypted)")
|
|
328
|
-
.action(async (name, opts) => {
|
|
329
|
+
.action(async (name, opts, command) => {
|
|
329
330
|
// Legacy mode for backward compat
|
|
330
331
|
if (opts.legacy) {
|
|
331
332
|
return _legacyGenerate(opts.legacy, name, isJson);
|
|
332
333
|
}
|
|
333
334
|
const walletName = name || "default";
|
|
334
335
|
try {
|
|
336
|
+
// Resolve vault passphrase via the standard 3-path resolver (--passphrase
|
|
337
|
+
// > OWS_PASSPHRASE env > stdin pipe). When stdin is a TTY and nothing was
|
|
338
|
+
// provided, the resolver returns null; fall back to "" to preserve the
|
|
339
|
+
// historical "no passphrase" default for interactive use.
|
|
340
|
+
//
|
|
341
|
+
// optsWithGlobals() merges parent + subcommand options, so --passphrase
|
|
342
|
+
// works whether placed on `perp --passphrase X subcmd` (parent) or
|
|
343
|
+
// `perp subcmd --passphrase X` (subcommand). Without this, Commander v13
|
|
344
|
+
// shadows the subcommand flag with the parent's same-named flag and
|
|
345
|
+
// opts.passphrase is always undefined.
|
|
346
|
+
const merged = command.optsWithGlobals();
|
|
347
|
+
const { resolvePassphrase } = await import("../agent-wallet/passphrase.js");
|
|
348
|
+
const pp = (await resolvePassphrase({ flag: merged.passphrase ?? opts.passphrase })) ?? "";
|
|
335
349
|
const ows = loadOws();
|
|
336
|
-
const w = ows.createWallet(walletName,
|
|
350
|
+
const w = ows.createWallet(walletName, pp, parseInt(opts.words));
|
|
337
351
|
// Set as active wallet
|
|
338
352
|
const { loadSettings: ls, saveSettings: ss } = await import("../settings.js");
|
|
339
353
|
const settings = ls();
|
|
@@ -393,9 +407,9 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
393
407
|
.option("--evm <hex>", "EVM (secp256k1) private key — use with --solana to import both curves")
|
|
394
408
|
.option("--solana <key>", "Solana (ed25519) private key — accepts hex (64), base58 (Phantom export), or JSON byte array. Use with --evm to import both curves")
|
|
395
409
|
.option("--mnemonic", "Import as mnemonic phrase instead of private key")
|
|
396
|
-
.option("--passphrase <pp>", "OWS
|
|
410
|
+
.option("--passphrase <pp>", "Master OWS passphrase (fallback: OWS_PASSPHRASE env / stdin)")
|
|
397
411
|
.option("--legacy <chain>", "Legacy mode: import to wallets.json (solana or evm)")
|
|
398
|
-
.action(async (privateKey, opts) => {
|
|
412
|
+
.action(async (privateKey, opts, command) => {
|
|
399
413
|
// Legacy mode (positional only)
|
|
400
414
|
if (opts.legacy) {
|
|
401
415
|
if (!privateKey) {
|
|
@@ -475,9 +489,17 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
475
489
|
throw new Error(`Invalid Solana key — expected hex (64 chars), base58 (Phantom export), or JSON byte array. Inner: ${msg}`);
|
|
476
490
|
}
|
|
477
491
|
};
|
|
478
|
-
// Resolve vault passphrase
|
|
479
|
-
|
|
480
|
-
|
|
492
|
+
// Resolve vault passphrase via the standard 3-path resolver (--passphrase
|
|
493
|
+
// > OWS_PASSPHRASE env > stdin pipe). When stdin is a TTY and nothing was
|
|
494
|
+
// provided, the resolver returns null; fall back to "" to preserve the
|
|
495
|
+
// historical "no passphrase" default for interactive use.
|
|
496
|
+
//
|
|
497
|
+
// optsWithGlobals() merges parent + subcommand options — see the
|
|
498
|
+
// wallet generate action for context. Without it, the parent program's
|
|
499
|
+
// --passphrase shadows the subcommand's same-named flag.
|
|
500
|
+
const merged = command.optsWithGlobals();
|
|
501
|
+
const { resolvePassphrase } = await import("../agent-wallet/passphrase.js");
|
|
502
|
+
const pp = (await resolvePassphrase({ flag: merged.passphrase ?? opts.passphrase })) ?? "";
|
|
481
503
|
let w;
|
|
482
504
|
if (opts.mnemonic) {
|
|
483
505
|
w = ows.importWalletMnemonic(opts.name, privateKey, pp);
|
|
@@ -588,17 +610,32 @@ export function registerWalletCommands(program, isJson, getAdapter, getPacificaA
|
|
|
588
610
|
.description("Set private key for an exchange")
|
|
589
611
|
.option("--default", "Also set as default exchange")
|
|
590
612
|
.action(async (exchange, key, opts) => {
|
|
591
|
-
// Resolve alias (hl → hyperliquid, pac → pacifica, lt → lighter)
|
|
592
|
-
const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter" };
|
|
613
|
+
// Resolve alias (hl → hyperliquid, pac → pacifica, lt → lighter, ast → aster)
|
|
614
|
+
const aliases = { hl: "hyperliquid", pac: "pacifica", lt: "lighter", ast: "aster" };
|
|
593
615
|
const resolved = aliases[exchange.toLowerCase()] || exchange.toLowerCase();
|
|
616
|
+
// v0.12.4: Aster legacy HMAC API-key path is removed entirely. Plan v3.0
|
|
617
|
+
// de-wired the runtime/signing surface but left the wallet set entry
|
|
618
|
+
// accepting keys that no longer drive any signing path. Fail loudly
|
|
619
|
+
// (SSOT Rule #2) and point at the agent flow.
|
|
620
|
+
if (resolved === "aster") {
|
|
621
|
+
const msg = "'wallet set aster' is removed in v0.13. Use 'wallet agent approve aster' instead.";
|
|
622
|
+
const remediation = "perp wallet agent approve aster --master <wallet>";
|
|
623
|
+
if (isJson()) {
|
|
624
|
+
const { jsonError } = await import("../utils.js");
|
|
625
|
+
return printJson(jsonError("INVALID_PARAMS", msg, { details: { remediation } }));
|
|
626
|
+
}
|
|
627
|
+
console.error(chalk.red(`\n ${msg}`));
|
|
628
|
+
console.error(chalk.gray(` Run: ${remediation}\n`));
|
|
629
|
+
process.exit(1);
|
|
630
|
+
}
|
|
594
631
|
const info = EXCHANGE_ENV_MAP[resolved];
|
|
595
632
|
if (!info) {
|
|
596
633
|
if (isJson()) {
|
|
597
634
|
const { jsonError } = await import("../utils.js");
|
|
598
|
-
return printJson(jsonError("INVALID_PARAMS", `Unknown exchange: ${exchange}. Use: pacifica, hyperliquid, lighter
|
|
635
|
+
return printJson(jsonError("INVALID_PARAMS", `Unknown exchange: ${exchange}. Use: pacifica, hyperliquid, lighter (or hl, pac, lt). For Aster: 'perp wallet agent approve aster'.`));
|
|
599
636
|
}
|
|
600
637
|
console.error(chalk.red(`\n Unknown exchange: ${exchange}`));
|
|
601
|
-
console.error(chalk.gray(` Use: pacifica, hyperliquid, lighter
|
|
638
|
+
console.error(chalk.gray(` Use: pacifica, hyperliquid, lighter (or hl, pac, lt). For Aster: 'perp wallet agent approve aster'.\n`));
|
|
602
639
|
process.exit(1);
|
|
603
640
|
}
|
|
604
641
|
const { valid, address } = await validateKey(info.chain, key);
|
package/dist/config.js
CHANGED
|
@@ -74,11 +74,15 @@ export async function loadPrivateKey(exchange, pkOverride, walletName) {
|
|
|
74
74
|
// settings module not available, skip
|
|
75
75
|
}
|
|
76
76
|
// 3. Exchange-specific env vars
|
|
77
|
+
// v0.12.4: Aster's ASTER_API_KEY entry is removed — the legacy HMAC
|
|
78
|
+
// signing path was retired in Plan v3.0. Aster onboarding is now via
|
|
79
|
+
// `perp wallet agent approve aster` (off-chain agent wallet); the runtime
|
|
80
|
+
// adapter pulls credentials from the agent store, not the env.
|
|
77
81
|
const envMap = {
|
|
78
82
|
pacifica: ["PACIFICA_PRIVATE_KEY", "pk"],
|
|
79
83
|
hyperliquid: ["HYPERLIQUID_PRIVATE_KEY", "HL_PRIVATE_KEY"],
|
|
80
84
|
lighter: ["LIGHTER_PRIVATE_KEY"],
|
|
81
|
-
aster: [
|
|
85
|
+
aster: [],
|
|
82
86
|
};
|
|
83
87
|
for (const envVar of envMap[exchange]) {
|
|
84
88
|
if (process.env[envVar])
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "perp-cli",
|
|
3
|
-
"version": "0.12.
|
|
3
|
+
"version": "0.12.5",
|
|
4
4
|
"description": "Multi-DEX Perpetual Futures CLI - Pacifica, Hyperliquid, Lighter, Aster",
|
|
5
5
|
"bin": {
|
|
6
6
|
"perp": "dist/index.js",
|
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
"test": "vitest run --exclude '**/integration/**'",
|
|
19
19
|
"test:integration": "vitest run -c vitest.integration.config.ts",
|
|
20
20
|
"test:all": "vitest run",
|
|
21
|
-
"
|
|
21
|
+
"sync-skill-version": "node scripts/sync-skill-version.mjs",
|
|
22
|
+
"prepublishOnly": "node scripts/sync-skill-version.mjs && pnpm run build"
|
|
22
23
|
},
|
|
23
24
|
"keywords": [
|
|
24
25
|
"pacifica",
|
package/skills/perp-cli/SKILL.md
CHANGED
|
@@ -5,7 +5,7 @@ allowed-tools: "Bash(perp:*), Bash(npx perp-cli:*), Bash(npx -y perp-cli:*)"
|
|
|
5
5
|
license: MIT
|
|
6
6
|
metadata:
|
|
7
7
|
author: hypurrquant
|
|
8
|
-
version: "0.
|
|
8
|
+
version: "0.12.5"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# perp-cli Agent Guide
|
|
@@ -41,7 +41,7 @@ Multi-DEX perpetual futures CLI + MCP server — Pacifica (Solana), Hyperliquid
|
|
|
41
41
|
## Install
|
|
42
42
|
|
|
43
43
|
```bash
|
|
44
|
-
perp --version 2>/dev/null # check if exists (must be >= 0.
|
|
44
|
+
perp --version 2>/dev/null # check if exists (must be >= 0.12.5)
|
|
45
45
|
npm install -g perp-cli@latest 2>/dev/null || npx -y perp-cli@latest --json --version
|
|
46
46
|
```
|
|
47
47
|
|