perp-cli 0.12.12 → 0.12.14
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/portfolio.js +8 -1
- package/dist/errors.d.ts +5 -1
- package/dist/errors.js +11 -1
- package/dist/exchanges/aster.d.ts +5 -1
- package/dist/exchanges/aster.js +6 -2
- package/dist/exchanges/hyperliquid.d.ts +8 -1
- package/dist/exchanges/hyperliquid.js +24 -1
- package/dist/mcp-server.js +88 -39
- package/dist/trade-validator.js +7 -5
- package/dist/utils.d.ts +8 -0
- package/dist/utils.js +11 -0
- package/package.json +1 -1
- package/skills/perp-cli/SKILL.md +2 -2
|
@@ -183,7 +183,14 @@ async function fetchExchangeEntry(exName, getAdapter) {
|
|
|
183
183
|
valueUsd: base === "USDC" ? Number(b.total) : (priceMap.get(base) ?? 0) * Number(b.total),
|
|
184
184
|
};
|
|
185
185
|
});
|
|
186
|
-
|
|
186
|
+
// Read the actual abstraction mode (populated during init()) instead
|
|
187
|
+
// of assuming all main HL accounts are unified. Standard/default mode
|
|
188
|
+
// accounts must include spot USDC in totalAccountValueUsd because
|
|
189
|
+
// perp equity does NOT contain it. HIP-3 dex accounts always run
|
|
190
|
+
// standard semantics. Codex v0.12.12 final QA #1.
|
|
191
|
+
const hlAdapter = adapter;
|
|
192
|
+
const dexScoped = isDexCapable(adapter) && !!adapter.dex;
|
|
193
|
+
isUnified = !dexScoped && hlAdapter.isUnifiedAccount === true;
|
|
187
194
|
}
|
|
188
195
|
else if (exName === "lighter") {
|
|
189
196
|
const { LighterAdapter } = await import("../exchanges/lighter.js");
|
package/dist/errors.d.ts
CHANGED
|
@@ -163,7 +163,11 @@ export interface StructuredError {
|
|
|
163
163
|
export declare function extractErrorMessage(err: unknown): string;
|
|
164
164
|
/**
|
|
165
165
|
* Classify an error from any exchange into a structured error code.
|
|
166
|
-
*
|
|
166
|
+
*
|
|
167
|
+
* If the input is already a PerpError, return its structured payload verbatim
|
|
168
|
+
* — the typed code and remediation are the source of truth and must not be
|
|
169
|
+
* re-derived from message text. Pattern-matching only kicks in for plain
|
|
170
|
+
* Error instances or unknown thrown values. Codex v0.12.12 final QA #2.
|
|
167
171
|
*/
|
|
168
172
|
export declare function classifyError(err: unknown, exchange?: string): StructuredError;
|
|
169
173
|
/** Custom error class that carries a structured error code */
|
package/dist/errors.js
CHANGED
|
@@ -59,9 +59,19 @@ export function extractErrorMessage(err) {
|
|
|
59
59
|
}
|
|
60
60
|
/**
|
|
61
61
|
* Classify an error from any exchange into a structured error code.
|
|
62
|
-
*
|
|
62
|
+
*
|
|
63
|
+
* If the input is already a PerpError, return its structured payload verbatim
|
|
64
|
+
* — the typed code and remediation are the source of truth and must not be
|
|
65
|
+
* re-derived from message text. Pattern-matching only kicks in for plain
|
|
66
|
+
* Error instances or unknown thrown values. Codex v0.12.12 final QA #2.
|
|
63
67
|
*/
|
|
64
68
|
export function classifyError(err, exchange) {
|
|
69
|
+
if (err instanceof PerpError) {
|
|
70
|
+
// Preserve the PerpError's typed shape; only fill in `exchange` if the
|
|
71
|
+
// caller provided one and the error didn't already attach it.
|
|
72
|
+
const ex = (err.structured.exchange ?? exchange);
|
|
73
|
+
return ex !== undefined ? { ...err.structured, exchange: ex } : { ...err.structured };
|
|
74
|
+
}
|
|
65
75
|
const message = extractErrorMessage(err);
|
|
66
76
|
const lower = message.toLowerCase();
|
|
67
77
|
// Margin-specific checks first (before generic "insufficient" catch)
|
|
@@ -156,7 +156,11 @@ export declare class AsterAdapter implements ExchangeAdapter {
|
|
|
156
156
|
* Same retry behavior as _signedGetEip712.
|
|
157
157
|
*/
|
|
158
158
|
private _signedDeleteEip712;
|
|
159
|
-
/** Shared GET/DELETE retry loop with fresh-nonce per attempt.
|
|
159
|
+
/** Shared GET/DELETE retry loop with fresh-nonce per attempt.
|
|
160
|
+
*
|
|
161
|
+
* Total 4 attempts: initial + 3 retries with backoffs 2s, 4s, 8s.
|
|
162
|
+
* Codex v0.12.12 final QA #3: previously MAX_ATTEMPTS = 3 truncated the
|
|
163
|
+
* documented curve so the 8s backoff was never reachable in practice. */
|
|
160
164
|
private _signedRequestWithRetry;
|
|
161
165
|
/**
|
|
162
166
|
* Unified response validator for ALL signed Aster requests.
|
package/dist/exchanges/aster.js
CHANGED
|
@@ -634,9 +634,13 @@ export class AsterAdapter {
|
|
|
634
634
|
async _signedDeleteEip712(path, params, resolved) {
|
|
635
635
|
return this._signedRequestWithRetry("DELETE", path, params, resolved);
|
|
636
636
|
}
|
|
637
|
-
/** Shared GET/DELETE retry loop with fresh-nonce per attempt.
|
|
637
|
+
/** Shared GET/DELETE retry loop with fresh-nonce per attempt.
|
|
638
|
+
*
|
|
639
|
+
* Total 4 attempts: initial + 3 retries with backoffs 2s, 4s, 8s.
|
|
640
|
+
* Codex v0.12.12 final QA #3: previously MAX_ATTEMPTS = 3 truncated the
|
|
641
|
+
* documented curve so the 8s backoff was never reachable in practice. */
|
|
638
642
|
async _signedRequestWithRetry(method, path, params, resolved) {
|
|
639
|
-
const MAX_ATTEMPTS =
|
|
643
|
+
const MAX_ATTEMPTS = 4;
|
|
640
644
|
let lastErr;
|
|
641
645
|
for (let attempt = 0; attempt < MAX_ATTEMPTS; attempt++) {
|
|
642
646
|
// Rebuild signed query string with fresh nonce on each attempt.
|
|
@@ -6,7 +6,14 @@ export declare class HyperliquidAdapter implements ExchangeAdapter {
|
|
|
6
6
|
readonly name = "hyperliquid";
|
|
7
7
|
readonly chain = "evm";
|
|
8
8
|
readonly aliases: readonly ["hl"];
|
|
9
|
-
|
|
9
|
+
/**
|
|
10
|
+
* Reflects whether perp + spot share the same USDC pool (true for unified /
|
|
11
|
+
* portfolio mode) or are separately margined (false for standard mode and
|
|
12
|
+
* HIP-3 dex accounts). Populated during init() from _getAbstractionMode();
|
|
13
|
+
* defaults to false until init resolves so callers never assume unified
|
|
14
|
+
* semantics for accounts that have never opted in. Codex v0.12.12 final QA #1.
|
|
15
|
+
*/
|
|
16
|
+
isUnifiedAccount: boolean;
|
|
10
17
|
private sdk;
|
|
11
18
|
private _address;
|
|
12
19
|
private _privateKey;
|
|
@@ -5,7 +5,14 @@ export class HyperliquidAdapter {
|
|
|
5
5
|
name = "hyperliquid";
|
|
6
6
|
chain = "evm";
|
|
7
7
|
aliases = ["hl"];
|
|
8
|
-
|
|
8
|
+
/**
|
|
9
|
+
* Reflects whether perp + spot share the same USDC pool (true for unified /
|
|
10
|
+
* portfolio mode) or are separately margined (false for standard mode and
|
|
11
|
+
* HIP-3 dex accounts). Populated during init() from _getAbstractionMode();
|
|
12
|
+
* defaults to false until init resolves so callers never assume unified
|
|
13
|
+
* semantics for accounts that have never opted in. Codex v0.12.12 final QA #1.
|
|
14
|
+
*/
|
|
15
|
+
isUnifiedAccount = false;
|
|
9
16
|
sdk;
|
|
10
17
|
_address;
|
|
11
18
|
_privateKey;
|
|
@@ -147,6 +154,22 @@ export class HyperliquidAdapter {
|
|
|
147
154
|
}
|
|
148
155
|
// Build asset index map
|
|
149
156
|
await this._loadAssetMap();
|
|
157
|
+
// Resolve abstraction mode → isUnifiedAccount flag for callers
|
|
158
|
+
// (portfolio accounting + spot-perp arb sizing). Standard/HIP-3 dex
|
|
159
|
+
// accounts are NOT unified (perp + spot are separately margined).
|
|
160
|
+
// Read-only paths (no address yet) can't query userAbstraction; leave the
|
|
161
|
+
// default false until a downstream caller invokes setAddress + the cache
|
|
162
|
+
// populates during getBalance(). Codex v0.12.12 final QA #1.
|
|
163
|
+
if (this._address) {
|
|
164
|
+
try {
|
|
165
|
+
const mode = await this._getAbstractionMode();
|
|
166
|
+
this.isUnifiedAccount = mode === "unified" || mode === "portfolio";
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
// If userAbstraction lookup fails, retain conservative default
|
|
170
|
+
// (false). getBalance() will surface the underlying error when called.
|
|
171
|
+
}
|
|
172
|
+
}
|
|
150
173
|
}
|
|
151
174
|
/** Load asset index map — supports native and HIP-3 dex. */
|
|
152
175
|
async _loadAssetMap() {
|
package/dist/mcp-server.js
CHANGED
|
@@ -15,6 +15,7 @@ import { tryLoadPrivateKey, parseSolanaKeypair } from "./config.js";
|
|
|
15
15
|
import { PacificaAdapter } from "./exchanges/pacifica.js";
|
|
16
16
|
import { HyperliquidAdapter } from "./exchanges/hyperliquid.js";
|
|
17
17
|
import { LighterAdapter } from "./exchanges/lighter.js";
|
|
18
|
+
import { AsterAdapter } from "./exchanges/aster.js";
|
|
18
19
|
import { fetchPacificaPrices, fetchHyperliquidMeta, fetchLighterOrderBookDetails, pingPacifica, pingHyperliquid, pingLighter, } from "./shared-api.js";
|
|
19
20
|
import { fetchAllFundingRates } from "./funding-rates.js";
|
|
20
21
|
import { loadArbState } from "./arb/state.js";
|
|
@@ -47,8 +48,14 @@ async function getOrCreateAdapter(exchange) {
|
|
|
47
48
|
adapter = lt;
|
|
48
49
|
break;
|
|
49
50
|
}
|
|
51
|
+
case "aster": {
|
|
52
|
+
const ast = new AsterAdapter(pk);
|
|
53
|
+
await ast.init();
|
|
54
|
+
adapter = ast;
|
|
55
|
+
break;
|
|
56
|
+
}
|
|
50
57
|
default:
|
|
51
|
-
throw new Error(`Unknown exchange: ${exchange}. Supported: pacifica, hyperliquid, lighter`);
|
|
58
|
+
throw new Error(`Unknown exchange: ${exchange}. Supported: pacifica, hyperliquid, lighter, aster`);
|
|
52
59
|
}
|
|
53
60
|
adapters.set(key, adapter);
|
|
54
61
|
return adapter;
|
|
@@ -80,7 +87,14 @@ async function fetchMarketsPublic(exchange) {
|
|
|
80
87
|
const data = await fetchLighterOrderBookDetails();
|
|
81
88
|
return data.map(d => ({ symbol: d.symbol, markPrice: String(d.lastTradePrice), indexPrice: String(d.lastTradePrice), fundingRate: "0", volume24h: "0", openInterest: "0", maxLeverage: 20 }));
|
|
82
89
|
}
|
|
83
|
-
|
|
90
|
+
else if (ex === "aster" || ex === "ast") {
|
|
91
|
+
// Aster has no shared-api helper; AsterAdapter.getMarkets() uses
|
|
92
|
+
// public-only endpoints (exchangeInfo + ticker/24hr + premiumIndex).
|
|
93
|
+
const ast = new AsterAdapter();
|
|
94
|
+
await ast.init();
|
|
95
|
+
return ast.getMarkets();
|
|
96
|
+
}
|
|
97
|
+
throw new Error(`Unknown exchange: ${exchange}. Supported: pacifica, hyperliquid, lighter, aster`);
|
|
84
98
|
}
|
|
85
99
|
/** Fetch orderbook using public API (no key needed) */
|
|
86
100
|
async function fetchOrderbookPublic(exchange, symbol) {
|
|
@@ -116,6 +130,12 @@ async function fetchOrderbookPublic(exchange, symbol) {
|
|
|
116
130
|
asks: (data.l?.[1] ?? []).map(e => [e.p, e.a]),
|
|
117
131
|
};
|
|
118
132
|
}
|
|
133
|
+
else if (ex === "aster" || ex === "ast") {
|
|
134
|
+
// AsterAdapter.getOrderbook() uses /fapi/v1/depth (public).
|
|
135
|
+
const ast = new AsterAdapter();
|
|
136
|
+
await ast.init();
|
|
137
|
+
return ast.getOrderbook(symbol);
|
|
138
|
+
}
|
|
119
139
|
throw new Error(`Unknown exchange: ${exchange}`);
|
|
120
140
|
}
|
|
121
141
|
// ── JSON envelope helpers ──
|
|
@@ -132,7 +152,7 @@ const server = new McpServer({ name: "perp-cli", version: _pkg.version }, { capa
|
|
|
132
152
|
// ============================================================
|
|
133
153
|
// Market Data tools (read-only, no private key needed)
|
|
134
154
|
// ============================================================
|
|
135
|
-
server.tool("get_markets", "Get all available perpetual futures markets on an exchange, including price, funding rate, volume, and max leverage. Works without API keys.", { exchange: z.string().describe("Exchange name: pacifica, hyperliquid, or
|
|
155
|
+
server.tool("get_markets", "Get all available perpetual futures markets on an exchange, including price, funding rate, volume, and max leverage. Works without API keys.", { exchange: z.string().describe("Exchange name: pacifica, hyperliquid, lighter, or aster") }, async ({ exchange }) => {
|
|
136
156
|
try {
|
|
137
157
|
// Try adapter first (richer data), fall back to public API (no key needed)
|
|
138
158
|
const adapter = await tryGetAdapter(exchange);
|
|
@@ -148,7 +168,7 @@ server.tool("get_markets", "Get all available perpetual futures markets on an ex
|
|
|
148
168
|
}
|
|
149
169
|
});
|
|
150
170
|
server.tool("get_orderbook", "Get the order book (bids and asks) for a symbol on an exchange. Works without API keys.", {
|
|
151
|
-
exchange: z.string().describe("Exchange name: pacifica, hyperliquid, or
|
|
171
|
+
exchange: z.string().describe("Exchange name: pacifica, hyperliquid, lighter, or aster"),
|
|
152
172
|
symbol: z.string().describe("Trading pair symbol, e.g. BTC, ETH, SOL"),
|
|
153
173
|
}, async ({ exchange, symbol }) => {
|
|
154
174
|
try {
|
|
@@ -165,7 +185,7 @@ server.tool("get_orderbook", "Get the order book (bids and asks) for a symbol on
|
|
|
165
185
|
return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e), { exchange, symbol }) }], isError: true };
|
|
166
186
|
}
|
|
167
187
|
});
|
|
168
|
-
server.tool("get_funding_rates", "Compare funding rates across all
|
|
188
|
+
server.tool("get_funding_rates", "Compare funding rates across all 4 exchanges (Pacifica, Hyperliquid, Lighter, Aster). Returns rates per symbol with spread analysis", {
|
|
169
189
|
symbols: z
|
|
170
190
|
.array(z.string())
|
|
171
191
|
.optional()
|
|
@@ -185,17 +205,24 @@ server.tool("get_funding_rates", "Compare funding rates across all 3 exchanges (
|
|
|
185
205
|
return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e)) }], isError: true };
|
|
186
206
|
}
|
|
187
207
|
});
|
|
188
|
-
server.tool("get_prices", "Get cross-exchange prices for symbols. Fetches mark prices from all
|
|
208
|
+
server.tool("get_prices", "Get cross-exchange prices for symbols. Fetches mark prices from all 4 exchanges for comparison", {
|
|
189
209
|
symbols: z
|
|
190
210
|
.array(z.string())
|
|
191
211
|
.optional()
|
|
192
212
|
.describe("Symbols to fetch prices for (e.g. ['BTC','ETH']). Omit for top assets"),
|
|
193
213
|
}, async ({ symbols }) => {
|
|
194
214
|
try {
|
|
195
|
-
|
|
215
|
+
// Aster has no shared-api helper; use AsterAdapter.getMarkets() (public).
|
|
216
|
+
const asterMarkets = (async () => {
|
|
217
|
+
const ast = new AsterAdapter();
|
|
218
|
+
await ast.init();
|
|
219
|
+
return ast.getMarkets();
|
|
220
|
+
})();
|
|
221
|
+
const [pacifica, hl, lighter, aster] = await Promise.all([
|
|
196
222
|
fetchPacificaPrices(),
|
|
197
223
|
fetchHyperliquidMeta(),
|
|
198
224
|
fetchLighterOrderBookDetails(),
|
|
225
|
+
asterMarkets,
|
|
199
226
|
]);
|
|
200
227
|
const filter = symbols ? new Set(symbols.map(s => s.toUpperCase())) : null;
|
|
201
228
|
// Build symbol → exchange prices map
|
|
@@ -224,6 +251,14 @@ server.tool("get_prices", "Get cross-exchange prices for symbols. Fetches mark p
|
|
|
224
251
|
priceMap.set(sym, {});
|
|
225
252
|
priceMap.get(sym).lighter = m.lastTradePrice;
|
|
226
253
|
}
|
|
254
|
+
for (const a of aster) {
|
|
255
|
+
const sym = a.symbol.toUpperCase();
|
|
256
|
+
if (filter && !filter.has(sym))
|
|
257
|
+
continue;
|
|
258
|
+
if (!priceMap.has(sym))
|
|
259
|
+
priceMap.set(sym, {});
|
|
260
|
+
priceMap.get(sym).aster = Number(a.markPrice);
|
|
261
|
+
}
|
|
227
262
|
const data = Array.from(priceMap.entries()).map(([symbol, prices]) => ({ symbol, prices }));
|
|
228
263
|
return { content: [{ type: "text", text: ok(data, { count: data.length }) }] };
|
|
229
264
|
}
|
|
@@ -234,7 +269,7 @@ server.tool("get_prices", "Get cross-exchange prices for symbols. Fetches mark p
|
|
|
234
269
|
// ============================================================
|
|
235
270
|
// Account tools (need private key)
|
|
236
271
|
// ============================================================
|
|
237
|
-
server.tool("get_balance", "Get account balance (equity, available margin, margin used, unrealized PnL) on an exchange", { exchange: z.string().describe("Exchange name: pacifica, hyperliquid, or
|
|
272
|
+
server.tool("get_balance", "Get account balance (equity, available margin, margin used, unrealized PnL) on an exchange", { exchange: z.string().describe("Exchange name: pacifica, hyperliquid, lighter, or aster") }, async ({ exchange }) => {
|
|
238
273
|
try {
|
|
239
274
|
const adapter = await getOrCreateAdapter(exchange);
|
|
240
275
|
const balance = await adapter.getBalance();
|
|
@@ -244,7 +279,7 @@ server.tool("get_balance", "Get account balance (equity, available margin, margi
|
|
|
244
279
|
return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e), { exchange }) }], isError: true };
|
|
245
280
|
}
|
|
246
281
|
});
|
|
247
|
-
server.tool("get_positions", "Get all open positions on an exchange, including size, entry price, PnL, leverage", { exchange: z.string().describe("Exchange name: pacifica, hyperliquid, or
|
|
282
|
+
server.tool("get_positions", "Get all open positions on an exchange, including size, entry price, PnL, leverage", { exchange: z.string().describe("Exchange name: pacifica, hyperliquid, lighter, or aster") }, async ({ exchange }) => {
|
|
248
283
|
try {
|
|
249
284
|
const adapter = await getOrCreateAdapter(exchange);
|
|
250
285
|
const positions = await adapter.getPositions();
|
|
@@ -254,7 +289,7 @@ server.tool("get_positions", "Get all open positions on an exchange, including s
|
|
|
254
289
|
return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e), { exchange }) }], isError: true };
|
|
255
290
|
}
|
|
256
291
|
});
|
|
257
|
-
server.tool("get_open_orders", "Get all open/pending orders on an exchange", { exchange: z.string().describe("Exchange name: pacifica, hyperliquid, or
|
|
292
|
+
server.tool("get_open_orders", "Get all open/pending orders on an exchange", { exchange: z.string().describe("Exchange name: pacifica, hyperliquid, lighter, or aster") }, async ({ exchange }) => {
|
|
258
293
|
try {
|
|
259
294
|
const adapter = await getOrCreateAdapter(exchange);
|
|
260
295
|
const orders = await adapter.getOpenOrders();
|
|
@@ -264,8 +299,8 @@ server.tool("get_open_orders", "Get all open/pending orders on an exchange", { e
|
|
|
264
299
|
return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e), { exchange }) }], isError: true };
|
|
265
300
|
}
|
|
266
301
|
});
|
|
267
|
-
server.tool("portfolio", "Cross-exchange portfolio summary: balances, positions, and risk metrics across all exchanges", {}, async () => {
|
|
268
|
-
const EXCHANGES = ["pacifica", "hyperliquid", "lighter"];
|
|
302
|
+
server.tool("portfolio", "Cross-exchange portfolio summary: balances, positions, and risk metrics across all 4 exchanges", {}, async () => {
|
|
303
|
+
const EXCHANGES = ["pacifica", "hyperliquid", "lighter", "aster"];
|
|
269
304
|
const snapshots = await Promise.all(EXCHANGES.map(async (name) => {
|
|
270
305
|
try {
|
|
271
306
|
const adapter = await getOrCreateAdapter(name);
|
|
@@ -340,7 +375,7 @@ server.tool("portfolio", "Cross-exchange portfolio summary: balances, positions,
|
|
|
340
375
|
// ============================================================
|
|
341
376
|
server.tool("suggest_command", "Given a natural language trading goal, suggest the exact perp CLI commands to run. Does NOT execute anything — only returns commands for the user to review and run manually", {
|
|
342
377
|
goal: z.string().describe("Natural language goal, e.g. 'buy 0.1 BTC on pacifica', 'close all positions', 'check funding arb opportunities'"),
|
|
343
|
-
exchange: z.string().optional().describe("Preferred exchange (default: pacifica). Options: pacifica, hyperliquid, lighter"),
|
|
378
|
+
exchange: z.string().optional().describe("Preferred exchange (default: pacifica). Options: pacifica, hyperliquid, lighter, aster"),
|
|
344
379
|
}, async ({ goal, exchange }) => {
|
|
345
380
|
try {
|
|
346
381
|
const ex = exchange ?? "pacifica";
|
|
@@ -349,18 +384,18 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
349
384
|
if (g.includes("buy") || g.includes("long")) {
|
|
350
385
|
const symbol = extractSymbol(g) || "BTC";
|
|
351
386
|
const size = extractNumber(g) || "<size>";
|
|
352
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json
|
|
387
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json portfolio`, description: "Check available balance and margin" }, { step: 3, command: `perp -e ${ex} --json trade check ${symbol} buy ${size}`, description: "Pre-flight validation (dry run)" }, { step: 4, command: `perp -e ${ex} --json trade market ${symbol} buy ${size}`, description: `Buy ${size} ${symbol} at market`, dangerous: true }, { step: 5, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
|
|
353
388
|
}
|
|
354
389
|
else if (g.includes("sell") || g.includes("short")) {
|
|
355
390
|
const symbol = extractSymbol(g) || "BTC";
|
|
356
391
|
const size = extractNumber(g) || "<size>";
|
|
357
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json
|
|
392
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook and liquidity` }, { step: 2, command: `perp -e ${ex} --json portfolio`, description: "Check available balance and margin" }, { step: 3, command: `perp -e ${ex} --json trade check ${symbol} sell ${size}`, description: "Pre-flight validation (dry run)" }, { step: 4, command: `perp -e ${ex} --json trade market ${symbol} sell ${size}`, description: `Sell ${size} ${symbol} at market`, dangerous: true }, { step: 5, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
|
|
358
393
|
}
|
|
359
394
|
else if (g.includes("limit")) {
|
|
360
395
|
const symbol = extractSymbol(g) || "BTC";
|
|
361
396
|
const size = extractNumber(g) || "<size>";
|
|
362
397
|
const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
|
|
363
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook for price levels` }, { step: 2, command: `perp -e ${ex} --json
|
|
398
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook for price levels` }, { step: 2, command: `perp -e ${ex} --json portfolio`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade limit ${symbol} ${side} <price> ${size}`, description: `Place limit ${side} order`, dangerous: true }, { step: 4, command: `perp -e ${ex} --json account orders`, description: "Verify order placed" });
|
|
364
399
|
}
|
|
365
400
|
else if (g.includes("close") || g.includes("exit") || g.includes("flatten")) {
|
|
366
401
|
const symbol = extractSymbol(g);
|
|
@@ -382,7 +417,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
382
417
|
else if (g.includes("scale-in") || g.includes("scale in") || g.includes("dca in")) {
|
|
383
418
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
384
419
|
const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
|
|
385
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: "Check current prices" }, { step: 2, command: `perp -e ${ex} --json
|
|
420
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: "Check current prices" }, { step: 2, command: `perp -e ${ex} --json portfolio`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade scale-in ${symbol} ${side} --levels '<price1>:<size>,<price2>:<size>'`, description: "Place scaled entry orders at multiple levels", dangerous: true });
|
|
386
421
|
}
|
|
387
422
|
else if (g.includes("tpsl") || g.includes("tp/sl") || g.includes("tp sl") || (g.includes("take profit") && g.includes("stop loss"))) {
|
|
388
423
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
@@ -401,13 +436,13 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
401
436
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
402
437
|
const amount = extractNumber(g) || "<usd>";
|
|
403
438
|
const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
|
|
404
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook depth` }, { step: 2, command: `perp -e ${ex} --json
|
|
439
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json market book ${symbol}`, description: `Check ${symbol} orderbook depth` }, { step: 2, command: `perp -e ${ex} --json portfolio`, description: "Check available balance" }, { step: 3, command: `perp -e ${ex} --json trade split ${symbol} ${side} ${amount}`, description: `Split ${side} $${amount} ${symbol} into depth-based slices`, dangerous: true }, { step: 4, command: `perp -e ${ex} --json account positions`, description: "Verify position opened" });
|
|
405
440
|
}
|
|
406
441
|
else if (g.includes("twap")) {
|
|
407
442
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
408
443
|
const size = extractNumber(g) || "<size>";
|
|
409
444
|
const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
|
|
410
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json
|
|
445
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json portfolio`, description: "Check available balance" }, { step: 2, command: `perp -e ${ex} --json trade twap ${symbol} ${side} ${size} <duration>`, description: `TWAP ${side} ${size} ${symbol} over duration`, dangerous: true });
|
|
411
446
|
}
|
|
412
447
|
else if (g.includes("reduce")) {
|
|
413
448
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
@@ -441,7 +476,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
441
476
|
else if (g.includes("dca")) {
|
|
442
477
|
const amount = extractNumber(g) || "<amount>";
|
|
443
478
|
const side = g.includes("sell") || g.includes("short") ? "sell" : "buy";
|
|
444
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json
|
|
479
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json portfolio`, description: "Check available balance" }, { step: 2, command: `perp -e ${ex} --json strategy quick-dca ${symbol} ${side} ${amount} <interval>`, description: "Start DCA bot", dangerous: true }, { step: 3, command: "perp --json background list", description: "Verify bot is running" });
|
|
445
480
|
}
|
|
446
481
|
else {
|
|
447
482
|
steps.push({ step: 1, command: "perp --json strategy preset-list", description: "List available bot presets" }, { step: 2, command: `perp -e ${ex} --json strategy quick-arb`, description: "Start arb bot", dangerous: true }, { step: 3, command: "perp --json background list", description: "Verify bot is running" });
|
|
@@ -495,15 +530,15 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
495
530
|
steps.push({ step: 1, command: "perp strategy plan example", description: "Show example execution plan format" }, { step: 2, command: "perp --json strategy plan validate <file>", description: "Validate plan file" }, { step: 3, command: "perp --json strategy plan execute <file> --dry-run", description: "Dry-run the plan", dangerous: true });
|
|
496
531
|
}
|
|
497
532
|
else if (g.includes("status") || g.includes("check") || g.includes("overview") || g.includes("portfolio")) {
|
|
498
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json
|
|
533
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json portfolio`, description: "Single-exchange portfolio overview" }, { step: 2, command: `perp -e ${ex} --json account positions`, description: "Detailed positions" }, { step: 3, command: `perp -e ${ex} --json account orders`, description: "Open orders" }, { step: 4, command: "perp --json portfolio --arb", description: "Cross-exchange portfolio + top arb opportunities" });
|
|
499
534
|
}
|
|
500
535
|
else if (g.includes("deposit")) {
|
|
501
536
|
const amount = extractNumber(g) || "<amount>";
|
|
502
|
-
steps.push({ step: 1, command: "perp --json wallet balance", description: "Check wallet balance" }, { step: 2, command: `perp --json funds deposit ${ex} ${amount}`, description: `Deposit $${amount} to ${ex}`, dangerous: true }, { step: 3, command: `perp -e ${ex} --json
|
|
537
|
+
steps.push({ step: 1, command: "perp --json wallet balance", description: "Check wallet balance" }, { step: 2, command: `perp --json funds deposit ${ex} ${amount}`, description: `Deposit $${amount} to ${ex}`, dangerous: true }, { step: 3, command: `perp -e ${ex} --json portfolio`, description: "Verify deposit arrived" });
|
|
503
538
|
}
|
|
504
539
|
else if (g.includes("withdraw")) {
|
|
505
540
|
const amount = extractNumber(g) || "<amount>";
|
|
506
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json
|
|
541
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json portfolio`, description: "Check available balance" }, { step: 2, command: `perp --json funds withdraw ${ex} ${amount}`, description: `Withdraw $${amount} from ${ex}`, dangerous: true }, { step: 3, command: "perp --json wallet balance", description: "Verify withdrawal received" });
|
|
507
542
|
}
|
|
508
543
|
else if (g.includes("leverage")) {
|
|
509
544
|
const symbol = extractSymbol(g) || "<symbol>";
|
|
@@ -532,7 +567,7 @@ server.tool("suggest_command", "Given a natural language trading goal, suggest t
|
|
|
532
567
|
steps.push({ step: 1, command: "perp settings show", description: "Show current settings" }, { step: 2, command: "perp settings referrals on", description: "Enable referral codes (no extra fees, just volume tracking)" });
|
|
533
568
|
}
|
|
534
569
|
else {
|
|
535
|
-
steps.push({ step: 1, command: `perp -e ${ex} --json
|
|
570
|
+
steps.push({ step: 1, command: `perp -e ${ex} --json portfolio`, description: "Check account overview" }, { step: 2, command: `perp --json portfolio --health --exchanges ${ex}`, description: "Check exchange connectivity" });
|
|
536
571
|
}
|
|
537
572
|
return {
|
|
538
573
|
content: [{
|
|
@@ -981,18 +1016,31 @@ server.tool("arb_scan", "Scan for funding rate arbitrage opportunities across ex
|
|
|
981
1016
|
return { content: [{ type: "text", text: err(e instanceof Error ? e.message : String(e)) }], isError: true };
|
|
982
1017
|
}
|
|
983
1018
|
});
|
|
984
|
-
server.tool("health_check", "Ping all exchanges and return connectivity status and latency", {}, async () => {
|
|
1019
|
+
server.tool("health_check", "Ping all 4 exchanges and return connectivity status and latency", {}, async () => {
|
|
985
1020
|
try {
|
|
986
|
-
|
|
1021
|
+
// Aster has no shared-api ping helper; ping /fapi/v1/time directly.
|
|
1022
|
+
const pingAster = async () => {
|
|
1023
|
+
const start = Date.now();
|
|
1024
|
+
try {
|
|
1025
|
+
const res = await fetch("https://fapi.asterdex.com/fapi/v1/time");
|
|
1026
|
+
return { ok: res.ok, latencyMs: Date.now() - start, status: res.status };
|
|
1027
|
+
}
|
|
1028
|
+
catch {
|
|
1029
|
+
return { ok: false, latencyMs: Date.now() - start, status: 0 };
|
|
1030
|
+
}
|
|
1031
|
+
};
|
|
1032
|
+
const [pacifica, hyperliquid, lighter, aster] = await Promise.all([
|
|
987
1033
|
pingPacifica(),
|
|
988
1034
|
pingHyperliquid(),
|
|
989
1035
|
pingLighter(),
|
|
1036
|
+
pingAster(),
|
|
990
1037
|
]);
|
|
991
1038
|
const result = {
|
|
992
1039
|
pacifica: { ...pacifica, statusText: pacifica.ok ? "healthy" : "unreachable" },
|
|
993
1040
|
hyperliquid: { ...hyperliquid, statusText: hyperliquid.ok ? "healthy" : "unreachable" },
|
|
994
1041
|
lighter: { ...lighter, statusText: lighter.ok ? "healthy" : "unreachable" },
|
|
995
|
-
|
|
1042
|
+
aster: { ...aster, statusText: aster.ok ? "healthy" : "unreachable" },
|
|
1043
|
+
allHealthy: pacifica.ok && hyperliquid.ok && lighter.ok && aster.ok,
|
|
996
1044
|
};
|
|
997
1045
|
return { content: [{ type: "text", text: ok(result) }] };
|
|
998
1046
|
}
|
|
@@ -1007,10 +1055,10 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
1007
1055
|
const schema = {
|
|
1008
1056
|
schemaVersion: "2.0",
|
|
1009
1057
|
name: "perp",
|
|
1010
|
-
description: "Multi-DEX Perpetual Futures CLI (Pacifica, Hyperliquid, Lighter)",
|
|
1011
|
-
exchanges: ["pacifica", "hyperliquid", "lighter"],
|
|
1058
|
+
description: "Multi-DEX Perpetual Futures CLI (Pacifica, Hyperliquid, Lighter, Aster)",
|
|
1059
|
+
exchanges: ["pacifica", "hyperliquid", "lighter", "aster"],
|
|
1012
1060
|
globalFlags: [
|
|
1013
|
-
{ flag: "-e, --exchange <name>", description: "Exchange to use (pacifica, hyperliquid, lighter)", default: "pacifica" },
|
|
1061
|
+
{ flag: "-e, --exchange <name>", description: "Exchange to use (pacifica, hyperliquid, lighter, aster)", default: "pacifica" },
|
|
1014
1062
|
{ flag: "--json", description: "Output as JSON for structured parsing" },
|
|
1015
1063
|
{ flag: "-n, --network <net>", description: "Network: mainnet or testnet", default: "mainnet" },
|
|
1016
1064
|
{ flag: "--dry-run", description: "Simulate without executing (for trade commands)" },
|
|
@@ -1032,7 +1080,7 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
1032
1080
|
account: {
|
|
1033
1081
|
description: "Account data (read-only)",
|
|
1034
1082
|
subcommands: {
|
|
1035
|
-
info: { usage: "perp
|
|
1083
|
+
info: { usage: "perp portfolio -e <exchange>", description: "Single-exchange balance, equity, margin, PnL" },
|
|
1036
1084
|
positions: { usage: "perp account positions", description: "Open positions" },
|
|
1037
1085
|
orders: { usage: "perp account orders", description: "Open/pending orders" },
|
|
1038
1086
|
history: { usage: "perp account history", description: "Order history" },
|
|
@@ -1192,7 +1240,7 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
1192
1240
|
},
|
|
1193
1241
|
portfolio: { usage: "perp portfolio", description: "Cross-exchange portfolio summary" },
|
|
1194
1242
|
status: { usage: "perp portfolio", description: "Full account overview" },
|
|
1195
|
-
health: { usage: "perp
|
|
1243
|
+
health: { usage: "perp portfolio --health", description: "Exchange connectivity check" },
|
|
1196
1244
|
settings: {
|
|
1197
1245
|
description: "CLI settings",
|
|
1198
1246
|
subcommands: {
|
|
@@ -1216,7 +1264,7 @@ server.resource("cli_schema", "perp://schema", { mimeType: "application/json", d
|
|
|
1216
1264
|
// Trade execution tools (dry-run + confirm safety model)
|
|
1217
1265
|
// ============================================================
|
|
1218
1266
|
server.tool("trade_preview", "Preview a trade WITHOUT executing. Returns estimated fill price, fees, margin impact, and risk checks. ALWAYS call this before trade_execute and show the result to the user for confirmation.", {
|
|
1219
|
-
exchange: z.string().describe("Exchange: pacifica, hyperliquid, or
|
|
1267
|
+
exchange: z.string().describe("Exchange: pacifica, hyperliquid, lighter, or aster"),
|
|
1220
1268
|
symbol: z.string().describe("Trading symbol (e.g., BTC, ETH, SOL)"),
|
|
1221
1269
|
side: z.enum(["buy", "sell"]).describe("Order side"),
|
|
1222
1270
|
size: z.string().describe("Order size (base currency units, e.g., '0.1' for 0.1 BTC)"),
|
|
@@ -1293,7 +1341,7 @@ server.tool("trade_preview", "Preview a trade WITHOUT executing. Returns estimat
|
|
|
1293
1341
|
}
|
|
1294
1342
|
});
|
|
1295
1343
|
server.tool("trade_execute", "Execute a trade. IMPORTANT: Always call trade_preview first and get explicit user confirmation before calling this tool. Supports market and limit orders.", {
|
|
1296
|
-
exchange: z.string().describe("Exchange: pacifica, hyperliquid, or
|
|
1344
|
+
exchange: z.string().describe("Exchange: pacifica, hyperliquid, lighter, or aster"),
|
|
1297
1345
|
symbol: z.string().describe("Trading symbol (e.g., BTC, ETH, SOL)"),
|
|
1298
1346
|
side: z.enum(["buy", "sell"]).describe("Order side"),
|
|
1299
1347
|
size: z.string().describe("Order size (base currency units)"),
|
|
@@ -1332,7 +1380,7 @@ server.tool("trade_execute", "Execute a trade. IMPORTANT: Always call trade_prev
|
|
|
1332
1380
|
}
|
|
1333
1381
|
});
|
|
1334
1382
|
server.tool("trade_close", "Close an existing position. IMPORTANT: Call trade_preview first with the opposite side to show impact, then get user confirmation.", {
|
|
1335
|
-
exchange: z.string().describe("Exchange: pacifica, hyperliquid, or
|
|
1383
|
+
exchange: z.string().describe("Exchange: pacifica, hyperliquid, lighter, or aster"),
|
|
1336
1384
|
symbol: z.string().describe("Symbol to close (e.g., BTC, ETH)"),
|
|
1337
1385
|
}, async ({ exchange, symbol }) => {
|
|
1338
1386
|
try {
|
|
@@ -1370,7 +1418,7 @@ server.tool("get_funding_analysis", "Analyze funding income across exchanges —
|
|
|
1370
1418
|
symbol: z.string().optional().describe("Filter by symbol"),
|
|
1371
1419
|
}, async ({ period, exchange: exFilter, symbol: symFilter }) => {
|
|
1372
1420
|
try {
|
|
1373
|
-
const exchanges = exFilter ? [exFilter] : ["pacifica", "hyperliquid", "lighter"];
|
|
1421
|
+
const exchanges = exFilter ? [exFilter] : ["pacifica", "hyperliquid", "lighter", "aster"];
|
|
1374
1422
|
const periodMatch = period.match(/^(\d+)(d|w|m)$/);
|
|
1375
1423
|
const sinceMs = periodMatch
|
|
1376
1424
|
? Date.now() - parseInt(periodMatch[1]) * ({ d: 86400000, w: 604800000, m: 2592000000 }[periodMatch[2]] ?? 86400000)
|
|
@@ -1440,7 +1488,7 @@ server.tool("get_pnl_analysis", "P&L analysis combining trading fees and funding
|
|
|
1440
1488
|
exchange: z.string().optional().describe("Filter by exchange"),
|
|
1441
1489
|
}, async ({ period, exchange: exFilter }) => {
|
|
1442
1490
|
try {
|
|
1443
|
-
const exchanges = exFilter ? [exFilter] : ["pacifica", "hyperliquid", "lighter"];
|
|
1491
|
+
const exchanges = exFilter ? [exFilter] : ["pacifica", "hyperliquid", "lighter", "aster"];
|
|
1444
1492
|
const periodMatch = period.match(/^(\d+)(d|w|m)$/);
|
|
1445
1493
|
const sinceMs = periodMatch
|
|
1446
1494
|
? Date.now() - parseInt(periodMatch[1]) * ({ d: 86400000, w: 604800000, m: 2592000000 }[periodMatch[2]] ?? 86400000)
|
|
@@ -1554,7 +1602,7 @@ server.tool("get_arb_compare", "Compare active arbitrage positions side by side
|
|
|
1554
1602
|
// ============================================================
|
|
1555
1603
|
server.resource("market_prices", "market://prices", { mimeType: "application/json", description: "Live cross-exchange prices for all perpetual futures markets" }, async () => {
|
|
1556
1604
|
const prices = {};
|
|
1557
|
-
await Promise.allSettled(["pacifica", "hyperliquid", "lighter"].map(async (ex) => {
|
|
1605
|
+
await Promise.allSettled(["pacifica", "hyperliquid", "lighter", "aster"].map(async (ex) => {
|
|
1558
1606
|
try {
|
|
1559
1607
|
const adapter = await getOrCreateAdapter(ex);
|
|
1560
1608
|
const markets = await adapter.getMarkets();
|
|
@@ -1621,7 +1669,8 @@ server.prompt("trading-guide", "Step-by-step guide for trading with perp-cli via
|
|
|
1621
1669
|
## Supported Exchanges
|
|
1622
1670
|
- **pacifica** (Solana) — SOL-based perpetual futures
|
|
1623
1671
|
- **hyperliquid** (HyperEVM) — Low-fee perpetuals with HIP-3 DEXes
|
|
1624
|
-
- **lighter** (Ethereum) — Ethereum-based perpetual futures
|
|
1672
|
+
- **lighter** (Ethereum) — Ethereum-based perpetual futures
|
|
1673
|
+
- **aster** (BNB Chain) — BNB Chain perpetual futures`,
|
|
1625
1674
|
},
|
|
1626
1675
|
}],
|
|
1627
1676
|
}));
|
package/dist/trade-validator.js
CHANGED
|
@@ -10,21 +10,20 @@ export async function validateTrade(adapter, params) {
|
|
|
10
10
|
const checks = [];
|
|
11
11
|
const warnings = [];
|
|
12
12
|
const sym = params.symbol.toUpperCase();
|
|
13
|
-
// Fetch market
|
|
13
|
+
// Fetch market list, balance, and positions in parallel.
|
|
14
|
+
// Defer the orderbook call until symbol membership is confirmed so invalid
|
|
15
|
+
// symbols return a structured symbol_valid failure instead of a venue error.
|
|
14
16
|
// SSOT rule #2: errors must propagate, never silently substitute defaults.
|
|
15
17
|
// getMarkets retains a tracked-error wrapper so the failed-load case surfaces
|
|
16
18
|
// a structured symbol_valid check (not a generic exception).
|
|
17
19
|
let marketsError = null;
|
|
18
|
-
const [markets, balance, positions
|
|
20
|
+
const [markets, balance, positions] = await Promise.all([
|
|
19
21
|
adapter.getMarkets().catch((e) => {
|
|
20
22
|
marketsError = e instanceof Error ? e.message : String(e);
|
|
21
23
|
return [];
|
|
22
24
|
}),
|
|
23
25
|
adapter.getBalance(),
|
|
24
26
|
adapter.getPositions(),
|
|
25
|
-
params.type !== "limit"
|
|
26
|
-
? adapter.getOrderbook(sym)
|
|
27
|
-
: Promise.resolve({ bids: [], asks: [] }),
|
|
28
27
|
]);
|
|
29
28
|
if (markets.length === 0 && marketsError) {
|
|
30
29
|
checks.push({ check: "symbol_valid", passed: false, message: `Failed to load markets from ${adapter.name}: ${marketsError}` });
|
|
@@ -42,6 +41,9 @@ export async function validateTrade(adapter, params) {
|
|
|
42
41
|
checks.push({ check: "symbol_valid", passed: false, message: `${sym} not found on ${adapter.name}` });
|
|
43
42
|
return { valid: false, checks, warnings, timestamp: new Date().toISOString() };
|
|
44
43
|
}
|
|
44
|
+
const orderbook = params.type !== "limit"
|
|
45
|
+
? await adapter.getOrderbook(sym)
|
|
46
|
+
: { bids: [], asks: [] };
|
|
45
47
|
const markPrice = Number(market.markPrice);
|
|
46
48
|
if (params.type === "limit" && (params.price === undefined || !Number.isFinite(params.price))) {
|
|
47
49
|
throw new Error(`Limit order requires explicit price (received ${params.price}); refusing silent mark-price substitution.`);
|
package/dist/utils.d.ts
CHANGED
|
@@ -17,6 +17,8 @@ export interface ApiResponse<T = unknown> {
|
|
|
17
17
|
status?: number;
|
|
18
18
|
retryable?: boolean;
|
|
19
19
|
retryAfterMs?: number;
|
|
20
|
+
/** Actionable hint for automated callers (Codex v0.12.12 final QA #2). */
|
|
21
|
+
remediation?: string;
|
|
20
22
|
details?: Record<string, unknown>;
|
|
21
23
|
};
|
|
22
24
|
meta?: {
|
|
@@ -33,9 +35,15 @@ export declare function jsonError(code: string, message: string, meta?: Partial<
|
|
|
33
35
|
retryable?: boolean;
|
|
34
36
|
retryAfterMs?: number;
|
|
35
37
|
details?: Record<string, unknown>;
|
|
38
|
+
remediation?: string;
|
|
36
39
|
}): ApiResponse<never>;
|
|
37
40
|
/** Execute a command action with structured error handling.
|
|
38
41
|
* In JSON mode, errors are returned as JSON instead of crashing.
|
|
42
|
+
*
|
|
43
|
+
* PerpError-typed throws are preserved end-to-end: their `code`,
|
|
44
|
+
* `remediation`, and other StructuredError fields surface in the JSON
|
|
45
|
+
* envelope's `error` block so machine consumers (agents / MCP / automation)
|
|
46
|
+
* receive actionable hints. Codex v0.12.12 final QA #2.
|
|
39
47
|
*/
|
|
40
48
|
export declare function withJsonErrors<T>(isJson: boolean, fn: () => Promise<T>, exchange?: string): Promise<T | undefined>;
|
|
41
49
|
/**
|
package/dist/utils.js
CHANGED
|
@@ -194,6 +194,7 @@ export function jsonError(code, message, meta) {
|
|
|
194
194
|
...(meta?.status !== undefined ? { status: meta.status } : {}),
|
|
195
195
|
...(meta?.retryable !== undefined ? { retryable: meta.retryable } : {}),
|
|
196
196
|
...(meta?.retryAfterMs !== undefined ? { retryAfterMs: meta.retryAfterMs } : {}),
|
|
197
|
+
...(meta?.remediation !== undefined ? { remediation: meta.remediation } : {}),
|
|
197
198
|
...(meta?.details ? { details: meta.details } : {}),
|
|
198
199
|
},
|
|
199
200
|
meta: { timestamp: new Date().toISOString(), ...meta },
|
|
@@ -201,6 +202,11 @@ export function jsonError(code, message, meta) {
|
|
|
201
202
|
}
|
|
202
203
|
/** Execute a command action with structured error handling.
|
|
203
204
|
* In JSON mode, errors are returned as JSON instead of crashing.
|
|
205
|
+
*
|
|
206
|
+
* PerpError-typed throws are preserved end-to-end: their `code`,
|
|
207
|
+
* `remediation`, and other StructuredError fields surface in the JSON
|
|
208
|
+
* envelope's `error` block so machine consumers (agents / MCP / automation)
|
|
209
|
+
* receive actionable hints. Codex v0.12.12 final QA #2.
|
|
204
210
|
*/
|
|
205
211
|
export async function withJsonErrors(isJson, fn, exchange) {
|
|
206
212
|
try {
|
|
@@ -213,10 +219,15 @@ export async function withJsonErrors(isJson, fn, exchange) {
|
|
|
213
219
|
status: classified.status,
|
|
214
220
|
retryable: classified.retryable,
|
|
215
221
|
retryAfterMs: classified.retryAfterMs,
|
|
222
|
+
remediation: classified.remediation,
|
|
223
|
+
details: classified.details,
|
|
216
224
|
})));
|
|
217
225
|
}
|
|
218
226
|
else {
|
|
219
227
|
console.error(chalk.red(`Error: ${classified.message}`));
|
|
228
|
+
if (classified.remediation) {
|
|
229
|
+
console.error(chalk.yellow(`Remediation: ${classified.remediation}`));
|
|
230
|
+
}
|
|
220
231
|
}
|
|
221
232
|
return undefined;
|
|
222
233
|
}
|
package/package.json
CHANGED
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.12.
|
|
8
|
+
version: "0.12.14"
|
|
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.12.
|
|
44
|
+
perp --version 2>/dev/null # check if exists (must be >= 0.12.14)
|
|
45
45
|
npm install -g perp-cli@latest 2>/dev/null || npx -y perp-cli@latest --json --version
|
|
46
46
|
```
|
|
47
47
|
|