minara 0.2.7 → 0.2.8

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/README.md CHANGED
@@ -128,7 +128,8 @@ minara swap --dry-run # Simulate without executing
128
128
  | --------------------------- | ----------------------------------------------------- |
129
129
  | `minara perps positions` | View all open positions with PnL |
130
130
  | `minara perps order` | Place an order (interactive builder) |
131
- | `minara perps cancel` | Cancel open orders |
131
+ | `minara perps close` | Close an open position at market price |
132
+ | `minara perps cancel` | Cancel open orders (selectable list) |
132
133
  | `minara perps leverage` | Update leverage for a symbol |
133
134
  | `minara perps trades` | View trade history (Hyperliquid fills) |
134
135
  | `minara perps deposit` | Deposit USDC to perps (or use `minara deposit perps`) |
@@ -140,6 +141,8 @@ minara swap --dry-run # Simulate without executing
140
141
  ```bash
141
142
  minara perps positions # List positions with equity, margin, PnL
142
143
  minara perps order # Interactive: symbol selector → side → size → confirm
144
+ minara perps close # Close a position: pick from list → market close
145
+ minara perps cancel # Cancel an order: pick from open orders list
143
146
  minara perps leverage # Interactive: shows max leverage per asset
144
147
  minara perps trades # Recent fills from Hyperliquid (default 7 days)
145
148
  minara perps trades -d 30 # Last 30 days of trade history
@@ -149,6 +152,10 @@ minara perps autopilot # Toggle AI autopilot, create/update strategy
149
152
  minara perps ask # AI analysis → optional quick order
150
153
  ```
151
154
 
155
+ > **Close position:** Select an open position from the list, and it will be closed at market price with a reduce-only order in the opposite direction — no manual price or size entry needed.
156
+ >
157
+ > **Cancel order:** Open orders are fetched from Hyperliquid and shown as a selectable list with coin, side, size, and price — no need to look up order IDs.
158
+ >
152
159
  > **Autopilot:** When autopilot is ON, manual order placement (`minara perps order`) is blocked to prevent conflicts with AI-managed trades. Turn off autopilot first via `minara perps autopilot`.
153
160
  >
154
161
  > **Ask AI → Quick Order:** After the AI analysis, you can instantly place a market order based on the recommended direction, entry price, and position size — no need to re-enter parameters.
@@ -303,7 +310,7 @@ Minara CLI supports macOS Touch ID to protect all fund-related operations. When
303
310
  minara config # Select "Touch ID" to enable / disable
304
311
  ```
305
312
 
306
- **Protected operations:** `withdraw`, `transfer`, `swap`, `deposit` (Spot→Perps transfer), `perps deposit`, `perps withdraw`, `perps order`, `limit-order create`
313
+ **Protected operations:** `withdraw`, `transfer`, `swap`, `deposit` (Spot→Perps transfer), `perps deposit`, `perps withdraw`, `perps order`, `perps close`, `limit-order create`
307
314
 
308
315
  > **Note:** Touch ID requires macOS with Touch ID hardware. The `--yes` flag skips the initial confirmation prompt but does **not** bypass transaction confirmation or Touch ID.
309
316
 
@@ -63,6 +63,16 @@ export interface HlAssetInfo extends HlAssetMeta {
63
63
  }
64
64
  /** Fetch perpetuals universe metadata + live prices from Hyperliquid (cached per session). */
65
65
  export declare function getAssetMeta(): Promise<HlAssetInfo[]>;
66
+ export interface HlOpenOrder {
67
+ coin: string;
68
+ limitPx: string;
69
+ oid: number;
70
+ side: string;
71
+ sz: string;
72
+ timestamp: number;
73
+ }
74
+ /** Fetch user's open orders from Hyperliquid. */
75
+ export declare function getOpenOrders(address: string): Promise<HlOpenOrder[]>;
66
76
  export interface HlFill {
67
77
  coin: string;
68
78
  px: string;
package/dist/api/perps.js CHANGED
@@ -111,6 +111,21 @@ export async function getAssetMeta() {
111
111
  return [];
112
112
  }
113
113
  }
114
+ /** Fetch user's open orders from Hyperliquid. */
115
+ export async function getOpenOrders(address) {
116
+ try {
117
+ const res = await fetch('https://api.hyperliquid.xyz/info', {
118
+ method: 'POST',
119
+ headers: { 'Content-Type': 'application/json' },
120
+ body: JSON.stringify({ type: 'openOrders', user: address }),
121
+ });
122
+ const data = await res.json();
123
+ return Array.isArray(data) ? data : [];
124
+ }
125
+ catch {
126
+ return [];
127
+ }
128
+ }
114
129
  /** Fetch user trade fills directly from Hyperliquid (last 7 days by default). */
115
130
  export async function getUserFills(address, days = 7) {
116
131
  try {
@@ -181,7 +181,7 @@ const orderCmd = new Command('order')
181
181
  marketPx = assetMeta?.markPx;
182
182
  if (marketPx && marketPx > 0) {
183
183
  const slippagePx = isBuy ? marketPx * 1.01 : marketPx * 0.99;
184
- limitPx = slippagePx.toPrecision(6);
184
+ limitPx = slippagePx.toPrecision(5);
185
185
  info(`Market order at ~$${marketPx}`);
186
186
  }
187
187
  else {
@@ -240,44 +240,127 @@ const cancelCmd = new Command('cancel')
240
240
  .option('-y, --yes', 'Skip confirmation')
241
241
  .action(wrapAction(async (opts) => {
242
242
  const creds = requireAuth();
243
- const metaSpin = spinner('Fetching assets…');
244
- const assets = await perpsApi.getAssetMeta();
245
- metaSpin.stop();
246
- let asset;
247
- if (assets.length > 0) {
248
- asset = await select({
249
- message: 'Asset to cancel:',
250
- choices: assets.map((a) => {
251
- const pxStr = a.markPx > 0 ? `$${a.markPx.toLocaleString()}` : '';
252
- return {
253
- name: `${a.name.padEnd(6)} ${chalk.dim(pxStr.padStart(12))} ${chalk.dim(`max ${a.maxLeverage}x`)}`,
254
- value: a.name,
255
- };
256
- }),
257
- });
243
+ const spin = spinner('Fetching open orders…');
244
+ const address = await perpsApi.getPerpsAddress(creds.accessToken);
245
+ if (!address) {
246
+ spin.stop();
247
+ warn('Could not find your perps wallet address. Make sure your perps account is initialized.');
248
+ return;
258
249
  }
259
- else {
260
- asset = await input({ message: 'Asset symbol to cancel (e.g. BTC):' });
261
- }
262
- const oid = await input({
263
- message: 'Order ID (oid):',
264
- validate: (v) => {
265
- const n = parseInt(v, 10);
266
- return isNaN(n) ? 'Please enter a valid numeric order ID' : true;
267
- },
250
+ const openOrders = await perpsApi.getOpenOrders(address);
251
+ spin.stop();
252
+ if (openOrders.length === 0) {
253
+ info('No open orders to cancel.');
254
+ return;
255
+ }
256
+ const selected = await select({
257
+ message: 'Select order to cancel:',
258
+ choices: openOrders.map((o) => {
259
+ const side = o.side === 'B' ? chalk.green('BUY') : chalk.red('SELL');
260
+ const px = `$${Number(o.limitPx).toLocaleString()}`;
261
+ return {
262
+ name: `${chalk.bold(o.coin.padEnd(6))} ${side} ${o.sz} @ ${chalk.yellow(px)} ${chalk.dim(`oid:${o.oid}`)}`,
263
+ value: o,
264
+ };
265
+ }),
268
266
  });
269
267
  if (!opts.yes) {
270
- const ok = await confirm({ message: `Cancel order ${oid} for ${asset}?`, default: false });
268
+ const sideLabel = selected.side === 'B' ? 'BUY' : 'SELL';
269
+ const ok = await confirm({
270
+ message: `Cancel ${sideLabel} ${selected.coin} ${selected.sz} @ $${Number(selected.limitPx).toLocaleString()}?`,
271
+ default: false,
272
+ });
271
273
  if (!ok)
272
274
  return;
273
275
  }
274
- const spin = spinner('Cancelling…');
275
- const res = await perpsApi.cancelOrders(creds.accessToken, { cancels: [{ a: asset, o: parseInt(oid, 10) }] });
276
- spin.stop();
276
+ const cancelSpin = spinner('Cancelling…');
277
+ const res = await perpsApi.cancelOrders(creds.accessToken, {
278
+ cancels: [{ a: selected.coin, o: selected.oid }],
279
+ });
280
+ cancelSpin.stop();
277
281
  assertApiOk(res, 'Order cancellation failed');
278
282
  success('Order cancelled');
279
283
  printTxResult(res.data);
280
284
  }));
285
+ // ─── close position ─────────────────────────────────────────────────────
286
+ const closeCmd = new Command('close')
287
+ .description('Close an open perps position at market price')
288
+ .option('-y, --yes', 'Skip confirmation')
289
+ .action(wrapAction(async (opts) => {
290
+ const creds = requireAuth();
291
+ const spin = spinner('Fetching positions…');
292
+ const res = await perpsApi.getAccountSummary(creds.accessToken);
293
+ const assets = await perpsApi.getAssetMeta();
294
+ spin.stop();
295
+ if (!res.success || !res.data) {
296
+ warn('Could not fetch positions.');
297
+ return;
298
+ }
299
+ const d = res.data;
300
+ const positions = Array.isArray(d.positions) ? d.positions : [];
301
+ if (positions.length === 0) {
302
+ info('No open positions to close.');
303
+ return;
304
+ }
305
+ const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
306
+ const pnlFmt = (n) => {
307
+ const color = n >= 0 ? chalk.green : chalk.red;
308
+ return color(`${n >= 0 ? '+' : ''}${fmt(n)}`);
309
+ };
310
+ const selected = await select({
311
+ message: 'Select position to close:',
312
+ choices: positions.map((p) => {
313
+ const symbol = String(p.symbol ?? '');
314
+ const side = String(p.side ?? '').toLowerCase();
315
+ const sideLabel = side === 'long' || side === 'buy' ? chalk.green('LONG') : chalk.red('SHORT');
316
+ const sz = String(p.size ?? '');
317
+ const entry = fmt(Number(p.entryPrice ?? 0));
318
+ const pnl = pnlFmt(Number(p.unrealizedPnl ?? 0));
319
+ return {
320
+ name: `${chalk.bold(symbol.padEnd(6))} ${sideLabel} ${sz} @ ${chalk.yellow(entry)} PnL: ${pnl}`,
321
+ value: p,
322
+ };
323
+ }),
324
+ });
325
+ const symbol = String(selected.symbol ?? '');
326
+ const side = String(selected.side ?? '').toLowerCase();
327
+ const sz = String(selected.size ?? '');
328
+ const isLong = side === 'long' || side === 'buy';
329
+ const isBuy = !isLong;
330
+ const assetMeta = assets.find((a) => a.name.toUpperCase() === symbol.toUpperCase());
331
+ const marketPx = assetMeta?.markPx;
332
+ if (!marketPx || marketPx <= 0) {
333
+ warn(`Could not fetch current price for ${symbol}. Cannot place market close order.`);
334
+ return;
335
+ }
336
+ const slippagePx = isBuy ? marketPx * 1.01 : marketPx * 0.99;
337
+ const limitPx = slippagePx.toPrecision(5);
338
+ const order = {
339
+ a: symbol,
340
+ b: isBuy,
341
+ p: limitPx,
342
+ s: sz,
343
+ r: true,
344
+ t: { trigger: { triggerPx: String(marketPx), tpsl: 'tp', isMarket: true } },
345
+ };
346
+ const sideLabel = isLong ? 'LONG' : 'SHORT';
347
+ console.log('');
348
+ console.log(chalk.bold('Close Position:'));
349
+ console.log(` Asset : ${chalk.bold(symbol)}`);
350
+ console.log(` Position : ${formatOrderSide(isLong ? 'buy' : 'sell')} ${sz}`);
351
+ console.log(` Close : ${formatOrderSide(isBuy ? 'buy' : 'sell')} (market ~$${marketPx.toLocaleString()})`);
352
+ console.log('');
353
+ if (!opts.yes) {
354
+ await requireTransactionConfirmation(`Close ${sideLabel} ${symbol} · size ${sz} @ Market (~$${marketPx.toLocaleString()})`);
355
+ }
356
+ await requireTouchId();
357
+ const orderSpin = spinner('Closing position…');
358
+ const orderRes = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping: 'na' });
359
+ orderSpin.stop();
360
+ assertApiOk(orderRes, 'Close position failed');
361
+ success(`Position closed — ${sideLabel} ${symbol} ${sz}`);
362
+ printTxResult(orderRes.data);
363
+ }));
281
364
  // ─── leverage ────────────────────────────────────────────────────────────
282
365
  const leverageCmd = new Command('leverage')
283
366
  .description('Update leverage for a symbol')
@@ -615,7 +698,7 @@ const askCmd = new Command('ask')
615
698
  const order = {
616
699
  a: symbol,
617
700
  b: isBuy,
618
- p: slippagePx.toPrecision(6),
701
+ p: slippagePx.toPrecision(5),
619
702
  s: String(size),
620
703
  r: false,
621
704
  t: { trigger: { triggerPx: String(entryPrice), tpsl: 'tp', isMarket: true } },
@@ -707,6 +790,7 @@ export const perpsCommand = new Command('perps')
707
790
  .addCommand(positionsCmd)
708
791
  .addCommand(orderCmd)
709
792
  .addCommand(cancelCmd)
793
+ .addCommand(closeCmd)
710
794
  .addCommand(leverageCmd)
711
795
  .addCommand(tradesCmd)
712
796
  .addCommand(depositCmd)
@@ -724,6 +808,7 @@ export const perpsCommand = new Command('perps')
724
808
  choices: [
725
809
  { name: 'View positions', value: 'positions' },
726
810
  { name: 'Place order', value: 'order' },
811
+ { name: 'Close position', value: 'close' },
727
812
  { name: 'Cancel order', value: 'cancel' },
728
813
  { name: 'Update leverage', value: 'leverage' },
729
814
  { name: 'View trade history', value: 'trades' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minara",
3
- "version": "0.2.7",
3
+ "version": "0.2.8",
4
4
  "description": "CLI client for Minara.ai — login, trade, deposit/withdraw, chat and more from your terminal.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",