minara 0.2.3 → 0.2.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -21,7 +21,8 @@
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
22
  - **Wallet & Balance** — Unified balance view, spot holdings with PnL, perps account overview, deposits and withdrawals
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,14 +81,14 @@ 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 |
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 |
89
90
  | `minara deposit` | Deposit to spot (view addresses) or perps (direct / from spot) |
90
- | `minara withdraw` | Withdraw tokens to an external wallet |
91
+ | `minara withdraw` | Withdraw tokens to an external wallet |
91
92
 
92
93
  ```bash
93
94
  minara balance # Quick total: Spot + Perps available balance
@@ -121,25 +122,35 @@ minara swap --dry-run # Simulate without executing
121
122
 
122
123
  ### Perpetual Futures
123
124
 
124
- | Command | Description |
125
- | --------------------------- | ----------------------------------------- |
125
+ | Command | Description |
126
+ | --------------------------- | ----------------------------------------------------- |
127
+ | `minara perps positions` | View all open positions with PnL |
128
+ | `minara perps order` | Place an order (interactive builder) |
129
+ | `minara perps cancel` | Cancel open orders |
130
+ | `minara perps leverage` | Update leverage for a symbol |
131
+ | `minara perps trades` | View trade history (Hyperliquid fills) |
126
132
  | `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 |
133
+ | `minara perps withdraw` | Withdraw USDC from perps account |
134
+ | `minara perps fund-records` | View fund deposit/withdrawal records |
135
+ | `minara perps autopilot` | Manage AI autopilot trading strategy (on/off/config) |
136
+ | `minara perps ask` | AI long/short analysis with quick order |
134
137
 
135
138
  ```bash
139
+ minara perps positions # List positions with equity, margin, PnL
140
+ minara perps order # Interactive: symbol selector → side → size → confirm
141
+ minara perps leverage # Interactive: shows max leverage per asset
142
+ minara perps trades # Recent fills from Hyperliquid (default 7 days)
143
+ minara perps trades -d 30 # Last 30 days of trade history
136
144
  minara perps deposit -a 100 # Deposit 100 USDC to perps
137
145
  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
146
+ minara perps autopilot # Toggle AI autopilot, create/update strategy
147
+ minara perps ask # AI analysis optional quick order
141
148
  ```
142
149
 
150
+ > **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`.
151
+ >
152
+ > **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.
153
+
143
154
  ### Limit Orders
144
155
 
145
156
  | Command | Description |
@@ -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
+ }
@@ -1,26 +1,68 @@
1
1
  import { Command } from 'commander';
2
2
  import { input, select } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
- import { searchTokens, getTrendingTokens, searchStocks, getFearGreedIndex, getBitcoinMetrics } from '../api/tokens.js';
4
+ import { searchTokens, getTrendingTokens, getTrendingStocks, searchStocks, getFearGreedIndex, getBitcoinMetrics } from '../api/tokens.js';
5
5
  import { spinner, assertApiOk, wrapAction } from '../utils.js';
6
- import { printKV, printTable, printFearGreed, printCryptoMetrics, TOKEN_COLUMNS } from '../formatters.js';
6
+ import { printKV, printTable, printFearGreed, printCryptoMetrics, TOKEN_COLUMNS, STOCK_COLUMNS } from '../formatters.js';
7
+ function flattenStock(item) {
8
+ const td = (item.tradeData ?? {});
9
+ return {
10
+ symbol: item.symbol ?? td.symbol,
11
+ name: item.name ?? td.name,
12
+ price: td.price,
13
+ priceChange24H: td.price_change_24h_percent != null
14
+ ? Number(td.price_change_24h_percent) * 100
15
+ : undefined,
16
+ volume24H: td.volume_24h_usd,
17
+ marketCap: td.market,
18
+ };
19
+ }
7
20
  // ─── trending ────────────────────────────────────────────────────────────
8
21
  const trendingCmd = new Command('trending')
9
- .description('View trending tokens')
10
- .action(wrapAction(async () => {
11
- const spin = spinner('Fetching trending tokens…');
12
- const res = await getTrendingTokens();
13
- spin.stop();
14
- assertApiOk(res, 'Failed to fetch trending tokens');
15
- console.log('');
16
- console.log(chalk.bold('Trending Tokens:'));
17
- if (Array.isArray(res.data) && res.data.length > 0) {
18
- printTable(res.data, TOKEN_COLUMNS);
22
+ .description('View trending tokens or stocks')
23
+ .argument('[category]', 'tokens or stocks (default: interactive)')
24
+ .action(wrapAction(async (categoryArg) => {
25
+ let category = categoryArg?.toLowerCase();
26
+ if (!category || (category !== 'tokens' && category !== 'stocks')) {
27
+ category = await select({
28
+ message: 'Trending:',
29
+ choices: [
30
+ { name: 'Tokens (crypto)', value: 'tokens' },
31
+ { name: 'Stocks (tokenized)', value: 'stocks' },
32
+ ],
33
+ });
19
34
  }
20
- else if (res.data && typeof res.data === 'object') {
21
- printKV(res.data);
35
+ if (category === 'stocks') {
36
+ const spin = spinner('Fetching trending stocks…');
37
+ const res = await getTrendingStocks();
38
+ spin.stop();
39
+ assertApiOk(res, 'Failed to fetch trending stocks');
40
+ console.log('');
41
+ console.log(chalk.bold('Trending Stocks:'));
42
+ if (Array.isArray(res.data) && res.data.length > 0) {
43
+ const rows = res.data.map((s) => flattenStock(s));
44
+ printTable(rows, STOCK_COLUMNS);
45
+ }
46
+ else {
47
+ console.log(chalk.dim(' No trending stocks found.'));
48
+ }
49
+ console.log('');
50
+ }
51
+ else {
52
+ const spin = spinner('Fetching trending tokens…');
53
+ const res = await getTrendingTokens();
54
+ spin.stop();
55
+ assertApiOk(res, 'Failed to fetch trending tokens');
56
+ console.log('');
57
+ console.log(chalk.bold('Trending Tokens:'));
58
+ if (Array.isArray(res.data) && res.data.length > 0) {
59
+ printTable(res.data, TOKEN_COLUMNS);
60
+ }
61
+ else if (res.data && typeof res.data === 'object') {
62
+ printKV(res.data);
63
+ }
64
+ console.log('');
22
65
  }
23
- console.log('');
24
66
  }));
25
67
  // ─── search ──────────────────────────────────────────────────────────────
26
68
  const searchCmd = new Command('search')
@@ -91,7 +133,7 @@ export const discoverCommand = new Command('discover')
91
133
  const action = await select({
92
134
  message: 'Discover:',
93
135
  choices: [
94
- { name: 'Trending tokens', value: 'trending' },
136
+ { name: 'Trending tokens / stocks', value: 'trending' },
95
137
  { name: 'Search tokens / stocks', value: 'search' },
96
138
  { name: 'Fear & Greed Index', value: 'fear-greed' },
97
139
  { name: 'Bitcoin metrics', value: 'btc-metrics' },
@@ -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,10 +40,15 @@ 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[]) */
46
50
  export declare const TOKEN_COLUMNS: ColumnDef[];
51
+ export declare const STOCK_COLUMNS: ColumnDef[];
47
52
  /**
48
53
  * Pretty-print Fear & Greed Index — hides redundant `timestamp` and `price`.
49
54
  */
@@ -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)) },
@@ -309,6 +375,14 @@ export const TOKEN_COLUMNS = [
309
375
  { key: 'volume24H', label: 'Volume 24h', format: compactUsd },
310
376
  { key: 'marketCap', label: 'Market Cap', format: compactUsd },
311
377
  ];
378
+ export const STOCK_COLUMNS = [
379
+ { key: 'symbol', label: 'Symbol', format: (v) => chalk.bold(String(v ?? '—')) },
380
+ { key: 'name', label: 'Name' },
381
+ { key: 'price', label: 'Price', format: (v) => formatValue(v, 'price') },
382
+ { key: 'priceChange24H', label: '24h %', format: (v) => formatValue(v, 'change') },
383
+ { key: 'volume24H', label: 'Volume 24h', format: compactUsd },
384
+ { key: 'marketCap', label: 'Market Cap', format: compactUsd },
385
+ ];
312
386
  // ═══════════════════════════════════════════════════════════════════════════
313
387
  // Specialised display helpers for discover commands
314
388
  // ═══════════════════════════════════════════════════════════════════════════
package/dist/index.js CHANGED
@@ -2,24 +2,28 @@
2
2
  import { createRequire } from 'node:module';
3
3
  import { Command } from 'commander';
4
4
  import chalk from 'chalk';
5
+ import { setRawJson } from './formatters.js';
6
+ // Auth & Account
5
7
  import { loginCommand } from './commands/login.js';
6
- const require = createRequire(import.meta.url);
7
- const { version } = require('../package.json');
8
8
  import { logoutCommand } from './commands/logout.js';
9
9
  import { accountCommand } from './commands/account.js';
10
- import { assetsCommand } from './commands/assets.js';
10
+ // Wallet & Funds
11
11
  import { balanceCommand } from './commands/balance.js';
12
+ import { assetsCommand } from './commands/assets.js';
12
13
  import { depositCommand } from './commands/deposit.js';
13
14
  import { withdrawCommand } from './commands/withdraw.js';
15
+ // Trading
14
16
  import { swapCommand } from './commands/swap.js';
15
17
  import { transferCommand } from './commands/transfer.js';
16
18
  import { perpsCommand } from './commands/perps.js';
17
19
  import { limitOrderCommand } from './commands/limit-order.js';
20
+ // AI, Market, Premium, Config
18
21
  import { chatCommand } from './commands/chat.js';
19
22
  import { discoverCommand } from './commands/discover.js';
20
- import { configCommand } from './commands/config.js';
21
23
  import { premiumCommand } from './commands/premium.js';
22
- import { setRawJson } from './formatters.js';
24
+ import { configCommand } from './commands/config.js';
25
+ const require = createRequire(import.meta.url);
26
+ const { version } = require('../package.json');
23
27
  const program = new Command();
24
28
  program
25
29
  .name('minara')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minara",
3
- "version": "0.2.3",
3
+ "version": "0.2.5",
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",