minara 0.2.4 → 0.2.6

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
@@ -19,9 +19,10 @@
19
19
  ## Features
20
20
 
21
21
  - **AI Chat** — Crypto-native AI for on-chain analysis, market research, and DeFi due diligence. Interactive REPL & single-shot queries with `fast` / `quality` / `thinking` modes
22
- - **Wallet & Balance** — Unified balance view, spot holdings with PnL, perps account overview, deposits and withdrawals
22
+ - **Wallet & Balance** — Unified balance view, spot holdings with PnL, perps account overview, deposits, withdrawals, and credit card on-ramp via MoonPay
23
23
  - **Chain-Abstracted Trading** — Cross-chain swaps with automatic chain detection, perpetual futures, and limit orders. Accepts `$TICKER`, token name, or contract address
24
- - **Market Discovery** — Trending tokens, Fear & Greed Index, on-chain metrics, and token / stock search
24
+ - **AI Autopilot & Analysis** — Fully managed AI trading strategies for perps, plus on-demand long/short analysis with one-click quick order
25
+ - **Market Discovery** — Trending tokens & stocks, Fear & Greed Index, on-chain metrics, and search
25
26
 
26
27
  ## Installation
27
28
 
@@ -68,7 +69,7 @@ minara discover trending
68
69
 
69
70
  | Command | Description |
70
71
  | ---------------- | ------------------------------------------- |
71
- | `minara login` | Login via device code or email |
72
+ | `minara login` | Login via device code or email |
72
73
  | `minara logout` | Logout and clear local credentials |
73
74
  | `minara account` | View your account info and wallet addresses |
74
75
 
@@ -80,23 +81,25 @@ minara login -e user@mail.com # Email verification code
80
81
 
81
82
  ### Wallet & Funds
82
83
 
83
- | Command | Description |
84
- | --------------------- | ----------------------------------------------- |
85
- | `minara balance` | Combined USDC/USDT balance across spot and perps |
86
- | `minara assets` | Full overview: spot holdings + perps account |
87
- | `minara assets spot` | Spot wallet: portfolio value, cost, PnL, holdings |
88
- | `minara assets perps` | Perps account: equity, margin, positions |
89
- | `minara deposit` | Deposit to spot (view addresses) or perps (direct / from spot) |
90
- | `minara withdraw` | Withdraw tokens to an external wallet |
84
+ | Command | Description |
85
+ | --------------------- | -------------------------------------------------------------- |
86
+ | `minara balance` | Combined USDC/USDT balance across spot and perps |
87
+ | `minara assets` | Full overview: spot holdings + perps account |
88
+ | `minara assets spot` | Spot wallet: portfolio value, cost, PnL, holdings |
89
+ | `minara assets perps` | Perps account: equity, margin, positions |
90
+ | `minara deposit` | Deposit to spot, perps, or buy crypto with credit card |
91
+ | `minara deposit buy` | Buy crypto with credit card via MoonPay |
92
+ | `minara withdraw` | Withdraw tokens to an external wallet |
91
93
 
92
94
  ```bash
93
95
  minara balance # Quick total: Spot + Perps available balance
94
96
  minara assets # Full overview (spot + perps)
95
97
  minara assets spot # Spot wallet with PnL breakdown
96
98
  minara assets perps # Perps equity, margin, positions
97
- minara deposit # Interactive: Spot (addresses) or Perps (address / transfer)
99
+ minara deposit # Interactive: Spot / Perps / Buy with credit card
98
100
  minara deposit spot # Show spot wallet deposit addresses (EVM + Solana)
99
101
  minara deposit perps # Perps: show Arbitrum deposit address, or transfer from Spot → Perps
102
+ minara deposit buy # Buy crypto with credit card via MoonPay (opens browser)
100
103
  minara withdraw -c solana -t '$SOL' -a 10 --to <address>
101
104
  minara withdraw # Interactive mode (accepts ticker or address)
102
105
  ```
@@ -121,25 +124,35 @@ minara swap --dry-run # Simulate without executing
121
124
 
122
125
  ### Perpetual Futures
123
126
 
124
- | Command | Description |
125
- | --------------------------- | ----------------------------------------- |
127
+ | Command | Description |
128
+ | --------------------------- | ----------------------------------------------------- |
129
+ | `minara perps positions` | View all open positions with PnL |
130
+ | `minara perps order` | Place an order (interactive builder) |
131
+ | `minara perps cancel` | Cancel open orders |
132
+ | `minara perps leverage` | Update leverage for a symbol |
133
+ | `minara perps trades` | View trade history (Hyperliquid fills) |
126
134
  | `minara perps deposit` | Deposit USDC to perps (or use `minara deposit perps`) |
127
- | `minara perps withdraw` | Withdraw USDC from perps account |
128
- | `minara perps positions` | View all open positions |
129
- | `minara perps order` | Place an order (interactive builder) |
130
- | `minara perps cancel` | Cancel open orders |
131
- | `minara perps leverage` | Update leverage for a symbol |
132
- | `minara perps trades` | View completed trade history |
133
- | `minara perps fund-records` | View fund deposit/withdrawal records |
135
+ | `minara perps withdraw` | Withdraw USDC from perps account |
136
+ | `minara perps fund-records` | View fund deposit/withdrawal records |
137
+ | `minara perps autopilot` | Manage AI autopilot trading strategy (on/off/config) |
138
+ | `minara perps ask` | AI long/short analysis with quick order |
134
139
 
135
140
  ```bash
141
+ minara perps positions # List positions with equity, margin, PnL
142
+ minara perps order # Interactive: symbol selector → side → size → confirm
143
+ minara perps leverage # Interactive: shows max leverage per asset
144
+ minara perps trades # Recent fills from Hyperliquid (default 7 days)
145
+ minara perps trades -d 30 # Last 30 days of trade history
136
146
  minara perps deposit -a 100 # Deposit 100 USDC to perps
137
147
  minara perps withdraw -a 50 # Withdraw 50 USDC from perps
138
- minara perps positions # List current positions
139
- minara perps order # Interactive: choose symbol, side, size, price
140
- minara perps leverage # Interactive: set leverage for a trading pair
148
+ minara perps autopilot # Toggle AI autopilot, create/update strategy
149
+ minara perps ask # AI analysis optional quick order
141
150
  ```
142
151
 
152
+ > **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
+ >
154
+ > **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.
155
+
143
156
  ### Limit Orders
144
157
 
145
158
  | Command | Description |
@@ -15,3 +15,7 @@ export declare function checkoutPackage(token: string, packageId: string, succes
15
15
  export declare function cryptoCheckoutPackage(token: string, packageId: string): Promise<import("../types.js").ApiResponse<CryptoCheckout>>;
16
16
  /** Cancel current subscription */
17
17
  export declare function cancelSubscription(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
18
+ /** Sign a MoonPay widget URL (backend appends HMAC signature). */
19
+ export declare function getMoonPaySignature(token: string, url: string): Promise<import("../types.js").ApiResponse<{
20
+ signature: string;
21
+ }>>;
@@ -1,4 +1,4 @@
1
- import { get, del } from './client.js';
1
+ import { get, post, del } from './client.js';
2
2
  /** Get all subscription plans and credit packages */
3
3
  export function getPlans() {
4
4
  return get('/payment/plans');
@@ -37,3 +37,7 @@ export function cryptoCheckoutPackage(token, packageId) {
37
37
  export function cancelSubscription(token) {
38
38
  return del('/payment/subscription', { token });
39
39
  }
40
+ /** Sign a MoonPay widget URL (backend appends HMAC signature). */
41
+ export function getMoonPaySignature(token, url) {
42
+ return post('/payment/moonpay/signature', { token, body: { url } });
43
+ }
@@ -27,3 +27,61 @@ export declare function getAccountSummary(token: string): Promise<import("../typ
27
27
  export declare function getDecisions(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>[]>>;
28
28
  /** Claim rewards */
29
29
  export declare function claimRewards(token: string): Promise<import("../types.js").ApiResponse<TransactionResult>>;
30
+ export declare function getStrategies(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>[]>>;
31
+ export declare function getSupportedSymbols(token: string): Promise<import("../types.js").ApiResponse<string[]>>;
32
+ export declare function createStrategy(token: string, dto: {
33
+ symbols: string[];
34
+ strategyConfig?: Record<string, unknown>;
35
+ language?: string;
36
+ }): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
37
+ export declare function enableStrategy(token: string, strategyId: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
38
+ export declare function disableStrategy(token: string, strategyId: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
39
+ export declare function updateStrategy(token: string, dto: {
40
+ strategyId: string;
41
+ symbols: string[];
42
+ strategyConfig?: Record<string, unknown>;
43
+ language?: string;
44
+ }): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
45
+ export declare function getPerformanceMetrics(token: string): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
46
+ export declare function priceAnalysis(token: string, dto: {
47
+ symbol: string;
48
+ startTime?: number;
49
+ endTime?: number;
50
+ interval?: string;
51
+ positionUSD?: number;
52
+ leverage?: number;
53
+ }): Promise<import("../types.js").ApiResponse<Record<string, unknown>>>;
54
+ /** Get perps wallet address from user profile */
55
+ export declare function getPerpsAddress(token: string): Promise<string | null>;
56
+ export interface HlAssetMeta {
57
+ name: string;
58
+ maxLeverage: number;
59
+ szDecimals: number;
60
+ }
61
+ export interface HlAssetInfo extends HlAssetMeta {
62
+ markPx: number;
63
+ }
64
+ /** Fetch perpetuals universe metadata + live prices from Hyperliquid (cached per session). */
65
+ export declare function getAssetMeta(): Promise<HlAssetInfo[]>;
66
+ export interface HlFill {
67
+ coin: string;
68
+ px: string;
69
+ sz: string;
70
+ side: string;
71
+ time: number;
72
+ dir: string;
73
+ closedPnl: string;
74
+ fee: string;
75
+ oid: number;
76
+ tid: number;
77
+ }
78
+ /** Fetch user trade fills directly from Hyperliquid (last 7 days by default). */
79
+ export declare function getUserFills(address: string, days?: number): Promise<HlFill[]>;
80
+ export interface HlLeverageInfo {
81
+ coin: string;
82
+ leverageType: string;
83
+ leverageValue: number;
84
+ maxLeverage: number;
85
+ }
86
+ /** Fetch user's per-asset leverage from Hyperliquid clearinghouseState. */
87
+ export declare function getUserLeverage(address: string): Promise<HlLeverageInfo[]>;
package/dist/api/perps.js CHANGED
@@ -55,3 +55,100 @@ export function getDecisions(token) {
55
55
  export function claimRewards(token) {
56
56
  return post('/v1/tx/perps/claim-rewards', { token });
57
57
  }
58
+ // ── Autopilot (Fully Managed Strategy) ───────────────────────────────────
59
+ export function getStrategies(token) {
60
+ return get('/v1/fully-managed/strategies', { token });
61
+ }
62
+ export function getSupportedSymbols(token) {
63
+ return get('/v1/fully-managed/supported-symbols', { token });
64
+ }
65
+ export function createStrategy(token, dto) {
66
+ return post('/v1/fully-managed/create-strategy', { token, body: dto });
67
+ }
68
+ export function enableStrategy(token, strategyId) {
69
+ return post('/v1/fully-managed/enable-strategy', { token, body: { strategyId } });
70
+ }
71
+ export function disableStrategy(token, strategyId) {
72
+ return post('/v1/fully-managed/disable-strategy', { token, body: { strategyId } });
73
+ }
74
+ export function updateStrategy(token, dto) {
75
+ return post('/v1/fully-managed/update-strategy', { token, body: dto });
76
+ }
77
+ export function getPerformanceMetrics(token) {
78
+ return get('/v1/fully-managed/performance/metrics/v2', { token });
79
+ }
80
+ // ── Price Analysis (Ask Long/Short) ──────────────────────────────────────
81
+ export function priceAnalysis(token, dto) {
82
+ return post('/tokens/price-analysis', { token, body: dto });
83
+ }
84
+ /** Get perps wallet address from user profile */
85
+ export async function getPerpsAddress(token) {
86
+ const res = await get('/auth/me', { token });
87
+ if (!res.success || !res.data)
88
+ return null;
89
+ return res.data.wallets?.['perpetual-evm'] ?? null;
90
+ }
91
+ let _assetInfoCache = null;
92
+ /** Fetch perpetuals universe metadata + live prices from Hyperliquid (cached per session). */
93
+ export async function getAssetMeta() {
94
+ if (_assetInfoCache)
95
+ return _assetInfoCache;
96
+ try {
97
+ const res = await fetch('https://api.hyperliquid.xyz/info', {
98
+ method: 'POST',
99
+ headers: { 'Content-Type': 'application/json' },
100
+ body: JSON.stringify({ type: 'metaAndAssetCtxs' }),
101
+ });
102
+ const json = (await res.json());
103
+ const [meta, ctxs] = json;
104
+ _assetInfoCache = (meta.universe ?? []).map((m, i) => ({
105
+ ...m,
106
+ markPx: Number(ctxs?.[i]?.markPx ?? 0),
107
+ }));
108
+ return _assetInfoCache;
109
+ }
110
+ catch {
111
+ return [];
112
+ }
113
+ }
114
+ /** Fetch user trade fills directly from Hyperliquid (last 7 days by default). */
115
+ export async function getUserFills(address, days = 7) {
116
+ try {
117
+ const startTime = Date.now() - days * 24 * 60 * 60 * 1000;
118
+ const res = await fetch('https://api.hyperliquid.xyz/info', {
119
+ method: 'POST',
120
+ headers: { 'Content-Type': 'application/json' },
121
+ body: JSON.stringify({
122
+ type: 'userFillsByTime',
123
+ user: address,
124
+ startTime,
125
+ aggregateByTime: true,
126
+ }),
127
+ });
128
+ const data = await res.json();
129
+ return Array.isArray(data) ? data : [];
130
+ }
131
+ catch {
132
+ return [];
133
+ }
134
+ }
135
+ /** Fetch user's per-asset leverage from Hyperliquid clearinghouseState. */
136
+ export async function getUserLeverage(address) {
137
+ try {
138
+ const res = await fetch('https://api.hyperliquid.xyz/info', {
139
+ method: 'POST',
140
+ headers: { 'Content-Type': 'application/json' },
141
+ body: JSON.stringify({ type: 'clearinghouseState', user: address }),
142
+ });
143
+ const data = (await res.json());
144
+ return (data.assetPositions ?? []).map((ap) => ({
145
+ coin: ap.position.coin,
146
+ leverageType: ap.position.leverage.type,
147
+ leverageValue: ap.position.leverage.value,
148
+ maxLeverage: ap.position.maxLeverage,
149
+ }));
150
+ }
151
+ catch {
152
+ return [];
153
+ }
154
+ }
@@ -5,7 +5,7 @@ import { getAccount } from '../api/crosschain.js';
5
5
  import { getCurrentUser } from '../api/auth.js';
6
6
  import * as perpsApi from '../api/perps.js';
7
7
  import { requireAuth } from '../config.js';
8
- import { info, success, spinner, assertApiOk, wrapAction, requireTransactionConfirmation } from '../utils.js';
8
+ import { info, success, warn, spinner, assertApiOk, wrapAction, requireTransactionConfirmation, openBrowser } from '../utils.js';
9
9
  import { requireTouchId } from '../touchid.js';
10
10
  import { printTxResult } from '../formatters.js';
11
11
  const EVM_CHAINS = 'Ethereum, Base, Arbitrum, Optimism, Polygon, Avalanche, BSC, Berachain, Blast';
@@ -52,6 +52,55 @@ async function showSpotDeposit(token) {
52
52
  console.log(chalk.red(' • Sending tokens on the wrong network may result in permanent loss.'));
53
53
  console.log('');
54
54
  }
55
+ // ─── moonpay (credit card on-ramp) ───────────────────────────────────────
56
+ const MOONPAY_PK = 'pk_live_yIf64w79W6ufwip4j51PWbymdwGtI';
57
+ const MOONPAY_CURRENCIES = [
58
+ { name: 'USDC (Base)', code: 'usdc_base', network: 'base' },
59
+ { name: 'USDC (Ethereum)', code: 'usdc', network: 'ethereum' },
60
+ { name: 'USDC (Arbitrum)', code: 'usdc_arbitrum', network: 'arbitrum' },
61
+ { name: 'USDC (Polygon)', code: 'usdc_polygon', network: 'polygon' },
62
+ { name: 'ETH', code: 'eth', network: 'ethereum' },
63
+ { name: 'ETH (Base)', code: 'eth_base', network: 'base' },
64
+ { name: 'SOL', code: 'sol', network: 'solana' },
65
+ ];
66
+ async function moonPayOnRamp(token) {
67
+ const addrSpin = spinner('Fetching wallet address…');
68
+ const accountRes = await getAccount(token);
69
+ addrSpin.stop();
70
+ const account = accountRes.data;
71
+ const evmAddr = account?.evmAddress;
72
+ const solAddr = account?.solanaAddress;
73
+ if (!evmAddr && !solAddr) {
74
+ warn('No wallet address found. Your account may not be fully initialized.');
75
+ return;
76
+ }
77
+ const currency = await select({
78
+ message: 'Currency to buy:',
79
+ choices: MOONPAY_CURRENCIES
80
+ .filter((c) => c.network === 'solana' ? !!solAddr : !!evmAddr)
81
+ .map((c) => ({ name: c.name, value: c })),
82
+ });
83
+ const walletAddress = currency.network === 'solana' ? solAddr : evmAddr;
84
+ // Build MoonPay buy URL (no walletAddress in URL — requires server-side signing
85
+ // which depends on backend MoonPay secret key config).
86
+ // User pastes their address in MoonPay's form instead.
87
+ const params = new URLSearchParams();
88
+ params.set('apiKey', MOONPAY_PK);
89
+ params.set('currencyCode', currency.code);
90
+ params.set('defaultCurrencyCode', currency.code);
91
+ const buyUrl = `https://buy.moonpay.com?${params.toString()}`;
92
+ console.log('');
93
+ console.log(chalk.bold('Buy Crypto with Credit Card (MoonPay)'));
94
+ console.log('');
95
+ console.log(` Currency : ${chalk.cyan(currency.name)}`);
96
+ console.log(` Wallet : ${chalk.yellow(walletAddress)}`);
97
+ console.log(chalk.dim(' ↑ Copy this address and paste it in MoonPay when prompted.'));
98
+ console.log('');
99
+ info('Opening MoonPay in your browser…');
100
+ openBrowser(buyUrl);
101
+ console.log(chalk.dim(' Complete the purchase in your browser. Funds will arrive in your Minara wallet.'));
102
+ console.log('');
103
+ }
55
104
  // ─── perps ───────────────────────────────────────────────────────────────
56
105
  const perpsCmd = new Command('perps')
57
106
  .description('Deposit USDC to perps account')
@@ -128,23 +177,34 @@ async function transferSpotToPerps(token, opts) {
128
177
  printTxResult(res.data);
129
178
  }
130
179
  // ─── parent ──────────────────────────────────────────────────────────────
180
+ const buyCmd = new Command('buy')
181
+ .description('Buy crypto with credit card via MoonPay')
182
+ .action(wrapAction(async () => {
183
+ const creds = requireAuth();
184
+ await moonPayOnRamp(creds.accessToken);
185
+ }));
131
186
  export const depositCommand = new Command('deposit')
132
- .description('Deposit to spot wallet or perps account')
187
+ .description('Deposit to spot wallet or perps account, or buy with credit card')
133
188
  .addCommand(spotCmd)
134
189
  .addCommand(perpsCmd)
190
+ .addCommand(buyCmd)
135
191
  .action(wrapAction(async () => {
136
192
  const action = await select({
137
193
  message: 'Deposit to:',
138
194
  choices: [
139
195
  { name: 'Spot wallet — view deposit addresses', value: 'spot' },
140
196
  { name: 'Perps wallet — view deposit address or transfer from Spot', value: 'perps' },
197
+ { name: `Buy crypto with credit card ${chalk.dim('(MoonPay)')}`, value: 'buy' },
141
198
  ],
142
199
  });
143
200
  const creds = requireAuth();
144
201
  if (action === 'spot') {
145
202
  await showSpotDeposit(creds.accessToken);
146
203
  }
147
- else {
204
+ else if (action === 'perps') {
148
205
  await perpsDepositFlow(creds.accessToken);
149
206
  }
207
+ else {
208
+ await moonPayOnRamp(creds.accessToken);
209
+ }
150
210
  }));
@@ -5,7 +5,7 @@ import * as perpsApi from '../api/perps.js';
5
5
  import { requireAuth } from '../config.js';
6
6
  import { success, info, warn, spinner, assertApiOk, formatOrderSide, wrapAction, requireTransactionConfirmation } from '../utils.js';
7
7
  import { requireTouchId } from '../touchid.js';
8
- import { printTxResult, printTable, POSITION_COLUMNS } from '../formatters.js';
8
+ import { printTxResult, printTable, printKV, POSITION_COLUMNS, FILL_COLUMNS } from '../formatters.js';
9
9
  // ─── deposit ─────────────────────────────────────────────────────────────
10
10
  const depositCmd = new Command('deposit')
11
11
  .description('Deposit USDC into Hyperliquid perps (min 5 USDC)')
@@ -73,19 +73,34 @@ const positionsCmd = new Command('positions')
73
73
  .action(wrapAction(async () => {
74
74
  const creds = requireAuth();
75
75
  const spin = spinner('Fetching positions…');
76
- const res = await perpsApi.getPositions(creds.accessToken);
76
+ const res = await perpsApi.getAccountSummary(creds.accessToken);
77
77
  spin.stop();
78
- assertApiOk(res, 'Failed to fetch positions');
79
- const positions = res.data;
80
- if (!positions || (Array.isArray(positions) && positions.length === 0)) {
81
- console.log(chalk.dim('No open positions.'));
78
+ if (!res.success || !res.data) {
79
+ console.log(chalk.dim('Could not fetch positions.'));
80
+ if (res.error?.message)
81
+ console.log(chalk.dim(` ${res.error.message}`));
82
+ return;
83
+ }
84
+ const d = res.data;
85
+ const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
86
+ const pnlFmt = (n) => {
87
+ const color = n >= 0 ? chalk.green : chalk.red;
88
+ return color(`${n >= 0 ? '+' : ''}${fmt(n)}`);
89
+ };
90
+ console.log('');
91
+ console.log(` Equity : ${fmt(Number(d.equityValue ?? 0))}`);
92
+ console.log(` Unrealized PnL: ${pnlFmt(Number(d.totalUnrealizedPnl ?? 0))}`);
93
+ console.log(` Margin Used : ${fmt(Number(d.totalMarginUsed ?? 0))}`);
94
+ const positions = Array.isArray(d.positions) ? d.positions : [];
95
+ console.log('');
96
+ console.log(chalk.bold(`Open Positions (${positions.length}):`));
97
+ if (positions.length === 0) {
98
+ console.log(chalk.dim(' No open positions.'));
82
99
  }
83
100
  else {
84
- console.log('');
85
- console.log(chalk.bold('Open Positions:'));
86
101
  printTable(positions, POSITION_COLUMNS);
87
- console.log('');
88
102
  }
103
+ console.log('');
89
104
  }));
90
105
  // ─── order ───────────────────────────────────────────────────────────────
91
106
  const orderCmd = new Command('order')
@@ -93,19 +108,29 @@ const orderCmd = new Command('order')
93
108
  .option('-y, --yes', 'Skip confirmation')
94
109
  .action(wrapAction(async (opts) => {
95
110
  const creds = requireAuth();
111
+ // Check autopilot — block manual orders while AI is trading
112
+ const apSpin = spinner('Checking autopilot…');
113
+ const apState = await getAutopilotState(creds.accessToken);
114
+ apSpin.stop();
115
+ if (apState.active) {
116
+ console.log('');
117
+ warn('Autopilot is currently ON. Manual order placement is disabled while AI is trading.');
118
+ info(`Trading symbols: ${apState.symbols?.join(', ') ?? 'unknown'}`);
119
+ info('Turn off autopilot first: minara perps autopilot');
120
+ console.log('');
121
+ return;
122
+ }
96
123
  info('Building a Hyperliquid perps order…');
97
- // Fetch prices for reference (non-blocking failure is OK)
98
- const pricesSpin = spinner('Fetching token prices…');
99
- const pricesRes = await perpsApi.getTokenPrices(creds.accessToken);
100
- pricesSpin.stop();
101
- if (pricesRes.success && pricesRes.data) {
102
- console.log(chalk.dim('Current prices:'));
103
- const prices = pricesRes.data;
104
- for (const p of prices.slice(0, 10)) {
105
- console.log(chalk.dim(` ${p.symbol ?? '?'}: $${p.price ?? '?'}`));
106
- }
107
- if (prices.length > 10)
108
- console.log(chalk.dim(` … and ${prices.length - 10} more`));
124
+ const dataSpin = spinner('Fetching market data…');
125
+ const address = await perpsApi.getPerpsAddress(creds.accessToken);
126
+ const [assets, leverages] = await Promise.all([
127
+ perpsApi.getAssetMeta(),
128
+ address ? perpsApi.getUserLeverage(address) : Promise.resolve([]),
129
+ ]);
130
+ dataSpin.stop();
131
+ const leverageMap = new Map();
132
+ for (const l of leverages) {
133
+ leverageMap.set(l.coin.toUpperCase(), { value: l.leverageValue, type: l.leverageType });
109
134
  }
110
135
  const isBuy = await select({
111
136
  message: 'Side:',
@@ -114,15 +139,57 @@ const orderCmd = new Command('order')
114
139
  { name: 'Short (sell)', value: false },
115
140
  ],
116
141
  });
117
- const asset = await input({ message: 'Asset symbol (e.g. BTC, ETH):' });
142
+ let asset;
143
+ if (assets.length > 0) {
144
+ asset = await select({
145
+ message: 'Asset:',
146
+ choices: assets.map((a) => {
147
+ const pxStr = a.markPx > 0 ? `$${a.markPx.toLocaleString()}` : '';
148
+ const lev = leverageMap.get(a.name.toUpperCase());
149
+ const levStr = lev ? `${lev.value}x ${lev.type}` : '';
150
+ return {
151
+ name: `${a.name.padEnd(6)} ${chalk.dim(pxStr.padStart(12))} ${chalk.dim(`max ${a.maxLeverage}x`)}${levStr ? ` ${chalk.cyan(levStr)}` : ''}`,
152
+ value: a.name,
153
+ };
154
+ }),
155
+ });
156
+ }
157
+ else {
158
+ asset = await input({ message: 'Asset symbol (e.g. BTC, ETH):' });
159
+ }
160
+ const currentLev = leverageMap.get(asset.toUpperCase());
161
+ if (currentLev) {
162
+ info(`Current leverage: ${currentLev.value}x (${currentLev.type})`);
163
+ }
164
+ else {
165
+ info(`No leverage set for ${asset} — use 'minara perps leverage' to configure`);
166
+ }
118
167
  const orderType = await select({
119
168
  message: 'Order type:',
120
169
  choices: [
170
+ { name: 'Market', value: 'market' },
121
171
  { name: 'Limit', value: 'limit' },
122
- { name: 'Market (trigger)', value: 'market' },
123
172
  ],
124
173
  });
125
- const limitPx = await input({ message: `Price (${orderType === 'limit' ? 'limit' : 'trigger'}):` });
174
+ const assetMeta = assets.find((a) => a.name.toUpperCase() === asset.toUpperCase());
175
+ let limitPx;
176
+ let marketPx;
177
+ if (orderType === 'limit') {
178
+ limitPx = await input({ message: 'Limit price:' });
179
+ }
180
+ else {
181
+ marketPx = assetMeta?.markPx;
182
+ if (marketPx && marketPx > 0) {
183
+ const slippagePx = isBuy ? marketPx * 1.01 : marketPx * 0.99;
184
+ limitPx = slippagePx.toPrecision(6);
185
+ info(`Market order at ~$${marketPx}`);
186
+ }
187
+ else {
188
+ warn(`Could not fetch current price for ${asset}. Enter the approximate market price.`);
189
+ limitPx = await input({ message: 'Price:' });
190
+ marketPx = Number(limitPx);
191
+ }
192
+ }
126
193
  const sz = await input({ message: 'Size (in contracts):' });
127
194
  const reduceOnly = await confirm({ message: 'Reduce only?', default: false });
128
195
  const grouping = await select({
@@ -141,26 +208,24 @@ const orderCmd = new Command('order')
141
208
  r: reduceOnly,
142
209
  t: orderType === 'limit'
143
210
  ? { limit: { tif: 'Gtc' } }
144
- : { trigger: { triggerPx: limitPx, tpsl: 'tp', isMarket: true } },
211
+ : { trigger: { triggerPx: String(marketPx ?? limitPx), tpsl: 'tp', isMarket: true } },
145
212
  };
213
+ const priceLabel = orderType === 'market' ? `Market (~$${marketPx ?? limitPx})` : `$${limitPx}`;
214
+ const levLabel = currentLev ? `${currentLev.value}x (${currentLev.type})` : '—';
146
215
  console.log('');
147
216
  console.log(chalk.bold('Order Preview:'));
148
217
  console.log(` Asset : ${chalk.bold(order.a)}`);
149
218
  console.log(` Side : ${formatOrderSide(order.b ? 'buy' : 'sell')}`);
150
- console.log(` Price : ${chalk.yellow(order.p)}`);
219
+ console.log(` Leverage : ${chalk.cyan(levLabel)}`);
220
+ console.log(` Type : ${orderType === 'market' ? 'Market' : 'Limit (GTC)'}`);
221
+ console.log(` Price : ${chalk.yellow(priceLabel)}`);
151
222
  console.log(` Size : ${chalk.bold(order.s)}`);
152
223
  console.log(` Reduce Only : ${order.r ? chalk.yellow('Yes') : 'No'}`);
153
- console.log(` Type : ${'limit' in order.t ? 'Limit (GTC)' : 'Trigger'}`);
154
224
  console.log(` Grouping : ${grouping}`);
155
225
  console.log('');
156
226
  if (!opts.yes) {
157
- const ok = await confirm({ message: 'Submit order?', default: false });
158
- if (!ok) {
159
- console.log(chalk.dim('Cancelled.'));
160
- return;
161
- }
227
+ await requireTransactionConfirmation(`Perps ${order.b ? 'LONG' : 'SHORT'} ${order.a} · size ${order.s} @ ${priceLabel}`);
162
228
  }
163
- await requireTransactionConfirmation(`Perps ${order.b ? 'LONG' : 'SHORT'} ${order.a} · size ${order.s} @ ${order.p}`);
164
229
  await requireTouchId();
165
230
  const spin = spinner('Placing order…');
166
231
  const res = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping });
@@ -175,7 +240,25 @@ const cancelCmd = new Command('cancel')
175
240
  .option('-y, --yes', 'Skip confirmation')
176
241
  .action(wrapAction(async (opts) => {
177
242
  const creds = requireAuth();
178
- const asset = await input({ message: 'Asset symbol to cancel (e.g. BTC):' });
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
+ });
258
+ }
259
+ else {
260
+ asset = await input({ message: 'Asset symbol to cancel (e.g. BTC):' });
261
+ }
179
262
  const oid = await input({
180
263
  message: 'Order ID (oid):',
181
264
  validate: (v) => {
@@ -200,8 +283,33 @@ const leverageCmd = new Command('leverage')
200
283
  .description('Update leverage for a symbol')
201
284
  .action(wrapAction(async () => {
202
285
  const creds = requireAuth();
203
- const symbol = await input({ message: 'Symbol (e.g. BTC):' });
204
- const leverage = await numberPrompt({ message: 'Leverage:', min: 1, max: 100, required: true });
286
+ const metaSpin = spinner('Fetching available assets…');
287
+ const assets = await perpsApi.getAssetMeta();
288
+ metaSpin.stop();
289
+ let symbol;
290
+ if (assets.length > 0) {
291
+ symbol = await select({
292
+ message: 'Asset:',
293
+ choices: assets.map((a) => {
294
+ const pxStr = a.markPx > 0 ? `$${a.markPx.toLocaleString()}` : '';
295
+ return {
296
+ name: `${a.name.padEnd(6)} ${chalk.dim(pxStr.padStart(12))} ${chalk.dim(`max ${a.maxLeverage}x`)}`,
297
+ value: a.name,
298
+ };
299
+ }),
300
+ });
301
+ }
302
+ else {
303
+ symbol = await input({ message: 'Symbol (e.g. BTC):' });
304
+ }
305
+ const meta = assets.find((a) => a.name.toUpperCase() === symbol.toUpperCase());
306
+ const maxLev = meta?.maxLeverage ?? 50;
307
+ const leverage = await numberPrompt({
308
+ message: `Leverage (1–${maxLev}x):`,
309
+ min: 1,
310
+ max: maxLev,
311
+ required: true,
312
+ });
205
313
  const isCross = await select({
206
314
  message: 'Margin mode:',
207
315
  choices: [
@@ -217,20 +325,43 @@ const leverageCmd = new Command('leverage')
217
325
  }));
218
326
  // ─── trades ──────────────────────────────────────────────────────────────
219
327
  const tradesCmd = new Command('trades')
220
- .description('View completed perps trades')
221
- .action(wrapAction(async () => {
328
+ .description('View your perps trade fills')
329
+ .option('-n, --count <n>', 'Number of recent fills to show', '20')
330
+ .option('-d, --days <n>', 'Look back N days', '7')
331
+ .action(wrapAction(async (opts) => {
222
332
  const creds = requireAuth();
223
- const spin = spinner('Fetching trades…');
224
- const res = await perpsApi.getCompletedTrades(creds.accessToken);
333
+ const spin = spinner('Fetching trade history…');
334
+ const address = await perpsApi.getPerpsAddress(creds.accessToken);
335
+ if (!address) {
336
+ spin.stop();
337
+ warn('Could not find your perps wallet address. Make sure your perps account is initialized.');
338
+ return;
339
+ }
340
+ const days = Math.max(1, parseInt(opts.days, 10) || 7);
341
+ const fills = await perpsApi.getUserFills(address, days);
225
342
  spin.stop();
226
- assertApiOk(res, 'Failed to fetch trades');
343
+ const limit = Math.max(1, parseInt(opts.count, 10) || 20);
344
+ const recent = fills.slice(0, limit);
345
+ const totalPnl = fills.reduce((s, f) => s + Number(f.closedPnl ?? 0), 0);
346
+ const totalFees = fills.reduce((s, f) => s + Number(f.fee ?? 0), 0);
347
+ const closingFills = fills.filter((f) => Number(f.closedPnl ?? 0) !== 0);
348
+ const wins = closingFills.filter((f) => Number(f.closedPnl) > 0).length;
349
+ const pnlColor = totalPnl >= 0 ? chalk.green : chalk.red;
350
+ const fmt = (n) => `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`;
227
351
  console.log('');
228
- console.log(chalk.bold('Completed Trades:'));
229
- if (Array.isArray(res.data) && res.data.length > 0) {
230
- printTable(res.data);
352
+ console.log(chalk.bold(`Trade Fills (last ${days}d — ${fills.length} fills):`));
353
+ console.log(` Realized PnL : ${pnlColor(`${totalPnl >= 0 ? '+' : ''}${fmt(totalPnl)}`)}`);
354
+ console.log(` Total Fees : ${chalk.dim(fmt(totalFees))}`);
355
+ if (closingFills.length > 0) {
356
+ console.log(` Win Rate : ${wins}/${closingFills.length} (${((wins / closingFills.length) * 100).toFixed(1)}%)`);
357
+ }
358
+ console.log('');
359
+ if (recent.length > 0) {
360
+ console.log(chalk.dim(`Showing ${recent.length} most recent:`));
361
+ printTable(recent, FILL_COLUMNS);
231
362
  }
232
363
  else {
233
- console.log(chalk.dim(' No completed trades.'));
364
+ console.log(chalk.dim(' No trade fills in this period.'));
234
365
  }
235
366
  console.log('');
236
367
  }));
@@ -255,31 +386,352 @@ const fundRecordsCmd = new Command('fund-records')
255
386
  }
256
387
  console.log('');
257
388
  }));
389
+ async function getAutopilotState(token) {
390
+ const res = await perpsApi.getStrategies(token);
391
+ if (!res.success || !res.data)
392
+ return { active: false };
393
+ // Response may be an array or { data: [...] } or nested object
394
+ let strategies = [];
395
+ const raw = res.data;
396
+ if (Array.isArray(raw)) {
397
+ strategies = raw;
398
+ }
399
+ else if (raw && typeof raw === 'object') {
400
+ // Might be wrapped in { strategies: [...] } or { data: [...] }
401
+ const inner = raw.strategies
402
+ ?? raw.data
403
+ ?? raw;
404
+ if (Array.isArray(inner)) {
405
+ strategies = inner;
406
+ }
407
+ else {
408
+ for (const v of Object.values(raw)) {
409
+ if (Array.isArray(v)) {
410
+ strategies.push(...v);
411
+ break;
412
+ }
413
+ }
414
+ }
415
+ }
416
+ if (strategies.length === 0)
417
+ return { active: false };
418
+ const s = strategies[0];
419
+ // Check all possible status field names
420
+ const status = String(s.status ?? s.state ?? s.isActive ?? s.enabled ?? '').toLowerCase();
421
+ const isActive = status === 'active' || status === 'enabled' || status === 'running'
422
+ || status === 'true' || s.isActive === true || s.enabled === true;
423
+ return {
424
+ active: isActive,
425
+ strategyId: String(s._id ?? s.id ?? s.strategyId ?? ''),
426
+ symbols: Array.isArray(s.symbols) ? s.symbols : [],
427
+ };
428
+ }
429
+ // ─── autopilot ──────────────────────────────────────────────────────────
430
+ const autopilotCmd = new Command('autopilot')
431
+ .alias('ap')
432
+ .description('Manage AI autopilot trading strategy')
433
+ .action(wrapAction(async () => {
434
+ const creds = requireAuth();
435
+ const statusSpin = spinner('Checking autopilot status…');
436
+ const state = await getAutopilotState(creds.accessToken);
437
+ statusSpin.stop();
438
+ const statusLabel = state.active ? chalk.green.bold('ON') : chalk.dim('OFF');
439
+ console.log('');
440
+ console.log(chalk.bold('Autopilot Status:') + ` ${statusLabel}`);
441
+ if (state.symbols && state.symbols.length > 0) {
442
+ console.log(` Symbols : ${state.symbols.join(', ')}`);
443
+ }
444
+ console.log('');
445
+ const action = await select({
446
+ message: 'What would you like to do?',
447
+ choices: [
448
+ ...(state.active
449
+ ? [{ name: chalk.red('Turn OFF autopilot'), value: 'off' }]
450
+ : [{ name: chalk.green('Turn ON autopilot'), value: 'on' }]),
451
+ ...(!state.strategyId ? [{ name: 'Create autopilot strategy', value: 'create' }] : []),
452
+ ...(state.strategyId ? [{ name: 'Update symbols', value: 'update' }] : []),
453
+ { name: 'View performance', value: 'perf' },
454
+ { name: 'Back', value: 'back' },
455
+ ],
456
+ });
457
+ if (action === 'back')
458
+ return;
459
+ if (action === 'on' && state.strategyId) {
460
+ const spin = spinner('Enabling autopilot…');
461
+ const res = await perpsApi.enableStrategy(creds.accessToken, state.strategyId);
462
+ spin.stop();
463
+ assertApiOk(res, 'Failed to enable autopilot');
464
+ success('Autopilot is now ON');
465
+ return;
466
+ }
467
+ if (action === 'off' && state.strategyId) {
468
+ const ok = await confirm({ message: 'Turn off autopilot? AI will stop trading.', default: false });
469
+ if (!ok)
470
+ return;
471
+ const spin = spinner('Disabling autopilot…');
472
+ const res = await perpsApi.disableStrategy(creds.accessToken, state.strategyId);
473
+ spin.stop();
474
+ assertApiOk(res, 'Failed to disable autopilot');
475
+ success('Autopilot is now OFF');
476
+ return;
477
+ }
478
+ if (action === 'create' || (action === 'on' && !state.strategyId)) {
479
+ const symSpin = spinner('Fetching supported symbols…');
480
+ const symRes = await perpsApi.getSupportedSymbols(creds.accessToken);
481
+ symSpin.stop();
482
+ const supported = symRes.success && Array.isArray(symRes.data) ? symRes.data : ['BTC', 'ETH', 'SOL'];
483
+ info(`Supported symbols: ${supported.join(', ')}`);
484
+ const symbolsInput = await input({
485
+ message: 'Symbols to trade (comma-separated):',
486
+ default: supported.slice(0, 3).join(','),
487
+ });
488
+ const symbols = symbolsInput.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
489
+ const spin = spinner('Creating autopilot strategy…');
490
+ const res = await perpsApi.createStrategy(creds.accessToken, { symbols });
491
+ spin.stop();
492
+ assertApiOk(res, 'Failed to create autopilot strategy');
493
+ success(`Autopilot created for ${symbols.join(', ')} and enabled!`);
494
+ return;
495
+ }
496
+ if (action === 'update' && state.strategyId) {
497
+ const symSpin = spinner('Fetching supported symbols…');
498
+ const symRes = await perpsApi.getSupportedSymbols(creds.accessToken);
499
+ symSpin.stop();
500
+ const supported = symRes.success && Array.isArray(symRes.data) ? symRes.data : ['BTC', 'ETH', 'SOL'];
501
+ info(`Supported: ${supported.join(', ')} | Current: ${state.symbols?.join(', ') ?? 'none'}`);
502
+ const symbolsInput = await input({
503
+ message: 'New symbols (comma-separated):',
504
+ default: state.symbols?.join(',') ?? '',
505
+ });
506
+ const symbols = symbolsInput.split(',').map((s) => s.trim().toUpperCase()).filter(Boolean);
507
+ const spin = spinner('Updating strategy…');
508
+ const res = await perpsApi.updateStrategy(creds.accessToken, { strategyId: state.strategyId, symbols });
509
+ spin.stop();
510
+ assertApiOk(res, 'Failed to update strategy');
511
+ success(`Autopilot updated: ${symbols.join(', ')}`);
512
+ return;
513
+ }
514
+ if (action === 'perf') {
515
+ const spin = spinner('Fetching performance…');
516
+ const res = await perpsApi.getPerformanceMetrics(creds.accessToken);
517
+ spin.stop();
518
+ if (res.success && res.data) {
519
+ console.log('');
520
+ console.log(chalk.bold('Autopilot Performance:'));
521
+ printKV(res.data);
522
+ console.log('');
523
+ }
524
+ else {
525
+ console.log(chalk.dim(' No performance data available.'));
526
+ }
527
+ }
528
+ }));
529
+ // ─── ask (long/short analysis) ──────────────────────────────────────────
530
+ const askCmd = new Command('ask')
531
+ .description('Get AI trading analysis for an asset (long/short recommendation)')
532
+ .action(wrapAction(async () => {
533
+ const creds = requireAuth();
534
+ const dataSpin = spinner('Fetching assets…');
535
+ const assets = await perpsApi.getAssetMeta();
536
+ dataSpin.stop();
537
+ let symbol;
538
+ if (assets.length > 0) {
539
+ symbol = await select({
540
+ message: 'Asset to analyze:',
541
+ choices: assets.map((a) => {
542
+ const pxStr = a.markPx > 0 ? `$${a.markPx.toLocaleString()}` : '';
543
+ return { name: `${a.name.padEnd(6)} ${chalk.dim(pxStr.padStart(12))}`, value: a.name };
544
+ }),
545
+ });
546
+ }
547
+ else {
548
+ symbol = await input({ message: 'Symbol (e.g. BTC):' });
549
+ }
550
+ const style = await select({
551
+ message: 'Analysis style:',
552
+ choices: [
553
+ { name: 'Scalping (minutes–hours)', value: 'scalping' },
554
+ { name: 'Day Trading (hours–day)', value: 'day-trading' },
555
+ { name: 'Swing Trading (days–weeks)', value: 'swing-trading' },
556
+ ],
557
+ });
558
+ const styleConfig = {
559
+ 'scalping': { interval: '5m', hours: 4 },
560
+ 'day-trading': { interval: '1h', hours: 24 },
561
+ 'swing-trading': { interval: '4h', hours: 24 * 7 },
562
+ };
563
+ const { interval, hours } = styleConfig[style] ?? styleConfig['day-trading'];
564
+ const endTime = Date.now();
565
+ const startTime = endTime - hours * 60 * 60 * 1000;
566
+ const marginInput = await input({ message: 'Margin in USD:', default: '1000' });
567
+ const leverageInput = await input({ message: 'Leverage:', default: '10' });
568
+ const spin = spinner(`Analyzing ${symbol}…`);
569
+ const res = await perpsApi.priceAnalysis(creds.accessToken, {
570
+ symbol,
571
+ startTime,
572
+ endTime,
573
+ interval,
574
+ positionUSD: Number(marginInput),
575
+ leverage: Number(leverageInput),
576
+ });
577
+ spin.stop();
578
+ if (!res.success || !res.data) {
579
+ warn(res.error?.message ?? 'Analysis failed. Try again later.');
580
+ return;
581
+ }
582
+ const data = res.data;
583
+ console.log('');
584
+ console.log(chalk.bold(`AI Analysis — ${symbol} (${style}):`));
585
+ console.log('');
586
+ if (typeof data === 'string') {
587
+ console.log(data);
588
+ }
589
+ else {
590
+ printKV(data);
591
+ }
592
+ console.log('');
593
+ // ── Quick Order ──────────────────────────────────────────────────
594
+ // Extract recommendation from the AI response
595
+ const recommendation = extractRecommendation(data, symbol, Number(marginInput), Number(leverageInput));
596
+ if (!recommendation)
597
+ return;
598
+ const { side, entryPrice, size } = recommendation;
599
+ const sideLabel = side === 'buy' ? chalk.green.bold('LONG') : chalk.red.bold('SHORT');
600
+ console.log(chalk.bold('Quick Order:'));
601
+ console.log(` ${sideLabel} ${chalk.bold(symbol)} | Entry ~$${entryPrice.toLocaleString()} | Size ${size} | ${Number(leverageInput)}x`);
602
+ console.log('');
603
+ const doQuick = await confirm({ message: 'Place this order now?', default: false });
604
+ if (!doQuick)
605
+ return;
606
+ // Check autopilot before placing
607
+ const apState = await getAutopilotState(creds.accessToken);
608
+ if (apState.active) {
609
+ warn('Autopilot is ON — manual orders are disabled while AI is trading.');
610
+ info('Turn off autopilot first: minara perps autopilot');
611
+ return;
612
+ }
613
+ const isBuy = side === 'buy';
614
+ const slippagePx = isBuy ? entryPrice * 1.01 : entryPrice * 0.99;
615
+ const order = {
616
+ a: symbol,
617
+ b: isBuy,
618
+ p: slippagePx.toPrecision(6),
619
+ s: String(size),
620
+ r: false,
621
+ t: { trigger: { triggerPx: String(entryPrice), tpsl: 'tp', isMarket: true } },
622
+ };
623
+ await requireTransactionConfirmation(`Perps ${isBuy ? 'LONG' : 'SHORT'} ${symbol} · size ${size} @ ~$${entryPrice.toLocaleString()}`);
624
+ await requireTouchId();
625
+ const orderSpin = spinner('Placing order…');
626
+ const orderRes = await perpsApi.placeOrders(creds.accessToken, { orders: [order], grouping: 'na' });
627
+ orderSpin.stop();
628
+ assertApiOk(orderRes, 'Order placement failed');
629
+ success('Order submitted!');
630
+ printTxResult(orderRes.data);
631
+ }));
632
+ /** Try to extract a tradeable recommendation from the AI analysis response. */
633
+ function extractRecommendation(data, symbol, marginUSD, leverage) {
634
+ if (typeof data === 'string') {
635
+ return parseRecommendationText(data, symbol, marginUSD, leverage);
636
+ }
637
+ // Structured response — look for common field names
638
+ const flat = flattenObj(data);
639
+ const sideRaw = String(flat['recommendation'] ?? flat['direction'] ?? flat['side'] ?? flat['signal']
640
+ ?? flat['action'] ?? flat['position'] ?? '').toLowerCase();
641
+ let side = null;
642
+ if (/long|buy|bullish/i.test(sideRaw))
643
+ side = 'buy';
644
+ else if (/short|sell|bearish/i.test(sideRaw))
645
+ side = 'sell';
646
+ if (!side) {
647
+ // Try to infer from the full JSON text
648
+ const jsonStr = JSON.stringify(data).toLowerCase();
649
+ if (/\blong\b|bullish/.test(jsonStr))
650
+ side = 'buy';
651
+ else if (/\bshort\b|bearish/.test(jsonStr))
652
+ side = 'sell';
653
+ }
654
+ if (!side)
655
+ return null;
656
+ const entryPrice = Number(flat['entryPrice'] ?? flat['entry_price'] ?? flat['entry'] ?? flat['price']
657
+ ?? flat['currentPrice'] ?? flat['current_price'] ?? flat['markPrice'] ?? 0);
658
+ if (!entryPrice || entryPrice <= 0)
659
+ return null;
660
+ let size = Number(flat['size'] ?? flat['contracts'] ?? flat['qty'] ?? flat['quantity'] ?? 0);
661
+ if (!size || size <= 0) {
662
+ size = parseFloat(((marginUSD * leverage) / entryPrice).toPrecision(4));
663
+ }
664
+ if (!size || size <= 0)
665
+ return null;
666
+ return { side, entryPrice, size };
667
+ }
668
+ function parseRecommendationText(text, symbol, marginUSD, leverage) {
669
+ let side = null;
670
+ if (/\blong\b|bullish|buy/i.test(text))
671
+ side = 'buy';
672
+ else if (/\bshort\b|bearish|sell/i.test(text))
673
+ side = 'sell';
674
+ if (!side)
675
+ return null;
676
+ const priceMatch = text.match(/entry[:\s]*\$?([\d,.]+)/i)
677
+ ?? text.match(/price[:\s]*\$?([\d,.]+)/i)
678
+ ?? text.match(/\$\s*([\d,.]+)/);
679
+ const entryPrice = priceMatch ? Number(priceMatch[1].replace(/,/g, '')) : 0;
680
+ if (!entryPrice || entryPrice <= 0)
681
+ return null;
682
+ const size = parseFloat(((marginUSD * leverage) / entryPrice).toPrecision(4));
683
+ if (!size || size <= 0)
684
+ return null;
685
+ return { side, entryPrice, size };
686
+ }
687
+ /** Recursively flatten nested object keys for easier field lookup. */
688
+ function flattenObj(obj, prefix = '') {
689
+ const result = {};
690
+ for (const [k, v] of Object.entries(obj)) {
691
+ const key = prefix ? `${prefix}.${k}` : k;
692
+ if (v && typeof v === 'object' && !Array.isArray(v)) {
693
+ Object.assign(result, flattenObj(v, key));
694
+ }
695
+ else {
696
+ result[k] = v;
697
+ result[key] = v;
698
+ }
699
+ }
700
+ return result;
701
+ }
258
702
  // ═════════════════════════════════════════════════════════════════════════
259
703
  // Parent
260
704
  // ═════════════════════════════════════════════════════════════════════════
261
705
  export const perpsCommand = new Command('perps')
262
- .description('Hyperliquid perpetual futures — deposit, withdraw, order, positions')
263
- .addCommand(depositCmd)
264
- .addCommand(withdrawCmd)
706
+ .description('Hyperliquid perpetual futures — order, positions, autopilot, analysis')
265
707
  .addCommand(positionsCmd)
266
708
  .addCommand(orderCmd)
267
709
  .addCommand(cancelCmd)
268
710
  .addCommand(leverageCmd)
269
711
  .addCommand(tradesCmd)
712
+ .addCommand(depositCmd)
713
+ .addCommand(withdrawCmd)
270
714
  .addCommand(fundRecordsCmd)
715
+ .addCommand(autopilotCmd)
716
+ .addCommand(askCmd)
271
717
  .action(wrapAction(async () => {
718
+ const creds = requireAuth();
719
+ // Show autopilot status inline
720
+ const apState = await getAutopilotState(creds.accessToken);
721
+ const apLabel = apState.active ? chalk.green.bold(' [ON]') : chalk.dim(' [OFF]');
272
722
  const action = await select({
273
723
  message: 'Perps — what would you like to do?',
274
724
  choices: [
275
- { name: 'Deposit USDC', value: 'deposit' },
276
- { name: 'Withdraw USDC', value: 'withdraw' },
725
+ { name: 'View positions', value: 'positions' },
277
726
  { name: 'Place order', value: 'order' },
278
727
  { name: 'Cancel order', value: 'cancel' },
279
- { name: 'View positions', value: 'positions' },
280
728
  { name: 'Update leverage', value: 'leverage' },
281
- { name: 'View completed trades', value: 'trades' },
729
+ { name: 'View trade history', value: 'trades' },
730
+ { name: 'Deposit USDC', value: 'deposit' },
731
+ { name: 'Withdraw USDC', value: 'withdraw' },
282
732
  { name: 'Fund records', value: 'fund-records' },
733
+ { name: `Autopilot${apLabel}`, value: 'autopilot' },
734
+ { name: 'Ask AI (long/short analysis)', value: 'ask' },
283
735
  ],
284
736
  });
285
737
  const sub = perpsCommand.commands.find((c) => c.name() === action || c.aliases().includes(action));
@@ -40,6 +40,10 @@ export declare function printTxResult(data: unknown): void;
40
40
  export declare const SPOT_COLUMNS: ColumnDef[];
41
41
  /** Perps positions — API uses snake_case field names */
42
42
  export declare const POSITION_COLUMNS: ColumnDef[];
43
+ /** Completed perps trades */
44
+ export declare const TRADE_COLUMNS: ColumnDef[];
45
+ /** Hyperliquid user fills (from public API) */
46
+ export declare const FILL_COLUMNS: ColumnDef[];
43
47
  /** Limit orders (LimitOrderInfo[]) */
44
48
  export declare const LIMIT_ORDER_COLUMNS: ColumnDef[];
45
49
  /** Trending / search tokens (TokenInfo[]) */
@@ -272,6 +272,72 @@ export const POSITION_COLUMNS = [
272
272
  { key: 'leverage', label: 'Lev', format: (v) => v ? `${v}x` : chalk.dim('—') },
273
273
  { key: 'marginUsed', label: 'Margin', format: (v) => formatValue(v, 'price') },
274
274
  ];
275
+ /** Completed perps trades */
276
+ export const TRADE_COLUMNS = [
277
+ { key: 'symbol', label: 'Symbol', format: (v) => chalk.bold(String(v ?? '—').replace('USDT', '')) },
278
+ { key: 'side', label: 'Side', format: (v) => {
279
+ const s = String(v ?? '').toLowerCase();
280
+ return s === 'long' || s === 'buy' ? chalk.green.bold(String(v)) : chalk.red.bold(String(v));
281
+ } },
282
+ { key: 'quantity', label: 'Size', format: (v) => {
283
+ const n = Number(v);
284
+ return isNaN(n) ? String(v ?? '—') : n.toLocaleString('en-US', { maximumFractionDigits: 4 });
285
+ } },
286
+ { key: 'open_price', label: 'Open', format: (v) => formatValue(v, 'price') },
287
+ { key: 'close_price', label: 'Close', format: (v) => formatValue(v, 'price') },
288
+ { key: 'pnl', label: 'PnL', format: (v) => {
289
+ if (!v && v !== 0)
290
+ return chalk.dim('—');
291
+ const n = Number(v);
292
+ const color = n >= 0 ? chalk.green : chalk.red;
293
+ return color(`${n >= 0 ? '+' : ''}$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
294
+ } },
295
+ { key: 'duration', label: 'Duration' },
296
+ { key: 'close_time', label: 'Closed', format: (v) => {
297
+ if (!v)
298
+ return chalk.dim('—');
299
+ const d = new Date(String(v));
300
+ return isNaN(d.getTime()) ? String(v) : d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
301
+ } },
302
+ ];
303
+ /** Hyperliquid user fills (from public API) */
304
+ export const FILL_COLUMNS = [
305
+ { key: 'coin', label: 'Asset', format: (v) => chalk.bold(String(v ?? '—')) },
306
+ { key: 'dir', label: 'Direction', format: (v) => {
307
+ const s = String(v ?? '');
308
+ if (/open.*long|buy/i.test(s))
309
+ return chalk.green.bold(s);
310
+ if (/close.*short/i.test(s))
311
+ return chalk.green(s);
312
+ if (/open.*short|sell/i.test(s))
313
+ return chalk.red.bold(s);
314
+ if (/close.*long/i.test(s))
315
+ return chalk.red(s);
316
+ return s;
317
+ } },
318
+ { key: 'sz', label: 'Size', format: (v) => {
319
+ const n = Number(v);
320
+ return isNaN(n) ? String(v ?? '—') : n.toLocaleString('en-US', { maximumFractionDigits: 4 });
321
+ } },
322
+ { key: 'px', label: 'Price', format: (v) => formatValue(v, 'price') },
323
+ { key: 'closedPnl', label: 'Realized PnL', format: (v) => {
324
+ const n = Number(v ?? 0);
325
+ if (n === 0)
326
+ return chalk.dim('—');
327
+ const color = n >= 0 ? chalk.green : chalk.red;
328
+ return color(`${n >= 0 ? '+' : ''}$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
329
+ } },
330
+ { key: 'fee', label: 'Fee', format: (v) => {
331
+ const n = Number(v ?? 0);
332
+ return n !== 0 ? `$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 4 })}` : chalk.dim('—');
333
+ } },
334
+ { key: 'time', label: 'Time', format: (v) => {
335
+ if (!v)
336
+ return chalk.dim('—');
337
+ const d = new Date(Number(v));
338
+ return isNaN(d.getTime()) ? String(v) : d.toLocaleDateString('en-US', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' });
339
+ } },
340
+ ];
275
341
  /** Limit orders (LimitOrderInfo[]) */
276
342
  export const LIMIT_ORDER_COLUMNS = [
277
343
  { key: 'id', label: 'ID', format: (v) => chalk.dim(truncate(String(v ?? ''), 12)) },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minara",
3
- "version": "0.2.4",
3
+ "version": "0.2.6",
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",