minara 0.2.7 → 0.2.9

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 {
@@ -41,7 +41,10 @@ export const configCommand = new Command('config')
41
41
  default: config.baseUrl,
42
42
  validate: (v) => {
43
43
  try {
44
- new URL(v);
44
+ const u = new URL(v);
45
+ if (u.protocol !== 'https:' && u.hostname !== 'localhost' && u.hostname !== '127.0.0.1') {
46
+ return 'Base URL must use HTTPS (HTTP allowed only for localhost)';
47
+ }
45
48
  return true;
46
49
  }
47
50
  catch {
@@ -53,7 +53,7 @@ async function showSpotDeposit(token) {
53
53
  console.log('');
54
54
  }
55
55
  // ─── moonpay (credit card on-ramp) ───────────────────────────────────────
56
- const MOONPAY_PK = 'pk_live_yIf64w79W6ufwip4j51PWbymdwGtI';
56
+ const MOONPAY_PK = process.env.MOONPAY_PK ?? 'pk_live_yIf64w79W6ufwip4j51PWbymdwGtI';
57
57
  const MOONPAY_CURRENCIES = [
58
58
  { name: 'USDC (Base)', code: 'usdc_base', network: 'base' },
59
59
  { name: 'USDC (Ethereum)', code: 'usdc', network: 'ethereum' },
@@ -3,7 +3,7 @@ import { input, select, confirm, number as numberPrompt } from '@inquirer/prompt
3
3
  import chalk from 'chalk';
4
4
  import * as perpsApi from '../api/perps.js';
5
5
  import { requireAuth } from '../config.js';
6
- import { success, info, warn, spinner, assertApiOk, formatOrderSide, wrapAction, requireTransactionConfirmation } from '../utils.js';
6
+ import { success, info, warn, spinner, assertApiOk, formatOrderSide, wrapAction, requireTransactionConfirmation, validateAddress } from '../utils.js';
7
7
  import { requireTouchId } from '../touchid.js';
8
8
  import { printTxResult, printTable, printKV, POSITION_COLUMNS, FILL_COLUMNS } from '../formatters.js';
9
9
  // ─── deposit ─────────────────────────────────────────────────────────────
@@ -48,7 +48,7 @@ const withdrawCmd = new Command('withdraw')
48
48
  : await numberPrompt({ message: 'USDC amount to withdraw:', min: 0.01, required: true });
49
49
  const toAddress = opts.to ?? await input({
50
50
  message: 'Destination address:',
51
- validate: (v) => (v.length > 5 ? true : 'Enter a valid address'),
51
+ validate: (v) => validateAddress(v, 'arbitrum'),
52
52
  });
53
53
  console.log(`\n Withdraw : ${chalk.bold(amount)} USDC → ${chalk.yellow(toAddress)}\n`);
54
54
  warn('Withdrawals may take time to process.');
@@ -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' },
@@ -2,7 +2,7 @@ import { Command } from 'commander';
2
2
  import { input } from '@inquirer/prompts';
3
3
  import { transfer } from '../api/crosschain.js';
4
4
  import { requireAuth } from '../config.js';
5
- import { success, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken } from '../utils.js';
5
+ import { success, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, validateAddress } from '../utils.js';
6
6
  import { requireTouchId } from '../touchid.js';
7
7
  import { printTxResult } from '../formatters.js';
8
8
  export const transferCommand = new Command('transfer')
@@ -33,7 +33,7 @@ export const transferCommand = new Command('transfer')
33
33
  // ── 4. Recipient ─────────────────────────────────────────────────────
34
34
  const recipient = opts.to ?? await input({
35
35
  message: 'Recipient address:',
36
- validate: (v) => (v.length > 5 ? true : 'Enter a valid address'),
36
+ validate: (v) => validateAddress(v, chain),
37
37
  });
38
38
  // ── 5. Confirm & Touch ID ──────────────────────────────────────────
39
39
  if (!opts.yes) {
@@ -3,7 +3,7 @@ import { input } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
4
  import { transfer, getAssets } from '../api/crosschain.js';
5
5
  import { requireAuth } from '../config.js';
6
- import { success, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken } from '../utils.js';
6
+ import { success, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, validateAddress } from '../utils.js';
7
7
  import { requireTouchId } from '../touchid.js';
8
8
  import { printTxResult } from '../formatters.js';
9
9
  export const withdrawCommand = new Command('withdraw')
@@ -57,7 +57,7 @@ export const withdrawCommand = new Command('withdraw')
57
57
  // ── 5. Destination ───────────────────────────────────────────────────
58
58
  const recipient = opts.to ?? await input({
59
59
  message: 'Destination address (your external wallet):',
60
- validate: (v) => (v.length > 5 ? true : 'Enter a valid address'),
60
+ validate: (v) => validateAddress(v, chain),
61
61
  });
62
62
  // ── 6. Confirm & Touch ID ──────────────────────────────────────────
63
63
  if (!opts.yes) {
package/dist/config.js CHANGED
@@ -14,8 +14,7 @@ function ensureDir() {
14
14
  // ─── Credentials ─────────────────────────────────────────────────────────────
15
15
  export function saveCredentials(creds) {
16
16
  ensureDir();
17
- writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), 'utf-8');
18
- chmodSync(CREDENTIALS_FILE, 0o600);
17
+ writeFileSync(CREDENTIALS_FILE, JSON.stringify(creds, null, 2), { encoding: 'utf-8', mode: 0o600 });
19
18
  }
20
19
  export function loadCredentials() {
21
20
  if (!existsSync(CREDENTIALS_FILE))
@@ -65,7 +64,7 @@ export function saveConfig(config) {
65
64
  ensureDir();
66
65
  const current = loadConfig();
67
66
  const merged = { ...current, ...config };
68
- writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), 'utf-8');
67
+ writeFileSync(CONFIG_FILE, JSON.stringify(merged, null, 2), { encoding: 'utf-8', mode: 0o600 });
69
68
  }
70
69
  export function getMinaraDir() {
71
70
  ensureDir();
@@ -11,6 +11,15 @@
11
11
  */
12
12
  import { createServer } from 'node:http';
13
13
  import { URL } from 'node:url';
14
+ // ── Helpers ───────────────────────────────────────────────────────────────
15
+ function escapeHtml(str) {
16
+ return str
17
+ .replace(/&/g, '&amp;')
18
+ .replace(/</g, '&lt;')
19
+ .replace(/>/g, '&gt;')
20
+ .replace(/"/g, '&quot;')
21
+ .replace(/'/g, '&#39;');
22
+ }
14
23
  // ── HTML responses ────────────────────────────────────────────────────────
15
24
  const SUCCESS_HTML = `<!DOCTYPE html>
16
25
  <html>
@@ -48,7 +57,7 @@ const ERROR_HTML = (msg) => `<!DOCTYPE html>
48
57
  <body>
49
58
  <div class="card">
50
59
  <h1>✖ Login Failed</h1>
51
- <p>${msg}</p>
60
+ <p>${escapeHtml(msg)}</p>
52
61
  <p>Please return to the terminal and try again.</p>
53
62
  </div>
54
63
  </body>
package/dist/utils.d.ts CHANGED
@@ -75,5 +75,10 @@ export declare function requireTransactionConfirmation(description: string, toke
75
75
  amount?: string;
76
76
  destination?: string;
77
77
  }): Promise<void>;
78
+ /**
79
+ * Validate a blockchain address based on the target chain.
80
+ * Returns `true` on success or an error message string on failure.
81
+ */
82
+ export declare function validateAddress(address: string, chain?: string): true | string;
78
83
  /** Open a URL in the user's default browser (cross-platform). */
79
84
  export declare function openBrowser(url: string): void;
package/dist/utils.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import chalk from 'chalk';
2
2
  import ora from 'ora';
3
- import { exec } from 'node:child_process';
3
+ import { execFile } from 'node:child_process';
4
4
  import { platform } from 'node:os';
5
5
  import { select, confirm } from '@inquirer/prompts';
6
6
  import { SUPPORTED_CHAINS } from './types.js';
@@ -340,16 +340,54 @@ export async function requireTransactionConfirmation(description, token, details
340
340
  process.exit(0);
341
341
  }
342
342
  }
343
+ // ─── Address validation ──────────────────────────────────────────────────────
344
+ const SOLANA_ADDR_RE = /^[1-9A-HJ-NP-Za-km-z]{32,44}$/;
345
+ const EVM_ADDR_RE = /^0x[0-9a-fA-F]{40}$/;
346
+ /**
347
+ * Validate a blockchain address based on the target chain.
348
+ * Returns `true` on success or an error message string on failure.
349
+ */
350
+ export function validateAddress(address, chain) {
351
+ const v = address.trim();
352
+ if (!v)
353
+ return 'Address is required';
354
+ const c = chain?.toLowerCase();
355
+ if (c === 'solana' || c === 'sol' || c === '101') {
356
+ if (!SOLANA_ADDR_RE.test(v))
357
+ return 'Invalid Solana address (expected base58, 32–44 chars)';
358
+ return true;
359
+ }
360
+ if (c && c !== 'solana') {
361
+ if (!EVM_ADDR_RE.test(v))
362
+ return 'Invalid EVM address (expected 0x + 40 hex chars)';
363
+ return true;
364
+ }
365
+ // Unknown chain — accept either format
366
+ if (!SOLANA_ADDR_RE.test(v) && !EVM_ADDR_RE.test(v)) {
367
+ return 'Invalid address format';
368
+ }
369
+ return true;
370
+ }
343
371
  // ─── Browser ──────────────────────────────────────────────────────────────────
344
372
  /** Open a URL in the user's default browser (cross-platform). */
345
373
  export function openBrowser(url) {
346
374
  const plat = platform();
347
- const cmd = plat === 'darwin' ? 'open' :
348
- plat === 'win32' ? 'start ""' :
349
- /* linux / others */ 'xdg-open';
350
- exec(`${cmd} "${url}"`, (err) => {
375
+ let cmd;
376
+ let args;
377
+ if (plat === 'darwin') {
378
+ cmd = 'open';
379
+ args = [url];
380
+ }
381
+ else if (plat === 'win32') {
382
+ cmd = 'cmd';
383
+ args = ['/c', 'start', '', url];
384
+ }
385
+ else {
386
+ cmd = 'xdg-open';
387
+ args = [url];
388
+ }
389
+ execFile(cmd, args, (err) => {
351
390
  if (err) {
352
- // Don't crash — the user can manually open the URL
353
391
  console.log(chalk.dim(`Could not open browser automatically. Please open this URL manually:`));
354
392
  console.log(chalk.cyan(url));
355
393
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minara",
3
- "version": "0.2.7",
3
+ "version": "0.2.9",
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",