minara 0.1.5 → 0.2.0

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.
@@ -3,11 +3,13 @@ import { input, confirm } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
4
  import { transfer } from '../api/crosschain.js';
5
5
  import { requireAuth } from '../config.js';
6
- import { success, warn, spinner, assertApiOk, selectChain, wrapAction } from '../utils.js';
6
+ import { success, warn, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, formatTokenLabel } from '../utils.js';
7
+ import { requireTouchId } from '../touchid.js';
8
+ import { printTxResult } from '../formatters.js';
7
9
  export const transferCommand = new Command('transfer')
8
10
  .description('Transfer tokens to another address')
9
11
  .option('-c, --chain <chain>', 'Blockchain')
10
- .option('-t, --token <address>', 'Token contract address')
12
+ .option('-t, --token <address|ticker>', 'Token contract address or ticker symbol')
11
13
  .option('-a, --amount <amount>', 'Token amount to send')
12
14
  .option('--to <address>', 'Recipient address')
13
15
  .option('-y, --yes', 'Skip confirmation')
@@ -16,10 +18,11 @@ export const transferCommand = new Command('transfer')
16
18
  // ── 1. Chain ─────────────────────────────────────────────────────────
17
19
  const chain = opts.chain ?? await selectChain();
18
20
  // ── 2. Token ─────────────────────────────────────────────────────────
19
- const tokenAddress = opts.token ?? await input({
20
- message: 'Token contract address (native token = 0x0…0):',
21
- validate: (v) => (v.length > 0 ? true : 'Address is required'),
21
+ const tokenInput = opts.token ?? await input({
22
+ message: 'Token (address or ticker, native = 0x0…0):',
23
+ validate: (v) => (v.length > 0 ? true : 'Token is required'),
22
24
  });
25
+ const tokenInfo = await lookupToken(tokenInput);
23
26
  // ── 3. Amount ────────────────────────────────────────────────────────
24
27
  const amount = opts.amount ?? await input({
25
28
  message: 'Amount to send:',
@@ -33,11 +36,12 @@ export const transferCommand = new Command('transfer')
33
36
  message: 'Recipient address:',
34
37
  validate: (v) => (v.length > 5 ? true : 'Enter a valid address'),
35
38
  });
36
- // ── 5. Summary & confirm ─────────────────────────────────────────────
39
+ // ── 5. Summary ───────────────────────────────────────────────────────
37
40
  console.log('');
38
41
  console.log(chalk.bold.red('⚠ Transfer Summary:'));
39
42
  console.log(` Chain : ${chalk.cyan(chain)}`);
40
- console.log(` Token : ${chalk.yellow(tokenAddress)}`);
43
+ console.log(` Token : ${formatTokenLabel(tokenInfo)}`);
44
+ console.log(` Address : ${chalk.yellow(tokenInfo.address)}`);
41
45
  console.log(` Amount : ${chalk.bold(amount)}`);
42
46
  console.log(` To : ${chalk.yellow(recipient)}`);
43
47
  console.log('');
@@ -49,12 +53,14 @@ export const transferCommand = new Command('transfer')
49
53
  return;
50
54
  }
51
55
  }
52
- // ── 6. Execute ───────────────────────────────────────────────────────
56
+ // ── 6. Transaction confirmation & Touch ID ────────────────────────────
57
+ await requireTransactionConfirmation(`Transfer ${amount} tokens → ${recipient} · ${chain}`, tokenInfo);
58
+ await requireTouchId();
59
+ // ── 7. Execute ───────────────────────────────────────────────────────
53
60
  const spin = spinner('Processing transfer…');
54
- const res = await transfer(creds.accessToken, { chain, tokenAddress, tokenAmount: amount, recipient });
61
+ const res = await transfer(creds.accessToken, { chain, tokenAddress: tokenInfo.address, tokenAmount: amount, recipient });
55
62
  spin.stop();
56
63
  assertApiOk(res, 'Transfer failed');
57
64
  success('Transfer submitted!');
58
- if (res.data)
59
- console.log(JSON.stringify(res.data, null, 2));
65
+ printTxResult(res.data);
60
66
  }));
@@ -3,11 +3,13 @@ import { input, confirm } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
4
  import { transfer, getAssets } from '../api/crosschain.js';
5
5
  import { requireAuth } from '../config.js';
6
- import { success, warn, spinner, assertApiOk, selectChain, wrapAction } from '../utils.js';
6
+ import { success, warn, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, formatTokenLabel } from '../utils.js';
7
+ import { requireTouchId } from '../touchid.js';
8
+ import { printTxResult } from '../formatters.js';
7
9
  export const withdrawCommand = new Command('withdraw')
8
10
  .description('Withdraw tokens from your Minara wallet to an external address')
9
11
  .option('-c, --chain <chain>', 'Blockchain network')
10
- .option('-t, --token <address>', 'Token contract address')
12
+ .option('-t, --token <address|ticker>', 'Token contract address or ticker symbol')
11
13
  .option('-a, --amount <amount>', 'Amount to withdraw')
12
14
  .option('--to <address>', 'Destination address')
13
15
  .option('-y, --yes', 'Skip confirmation')
@@ -39,10 +41,11 @@ export const withdrawCommand = new Command('withdraw')
39
41
  // ── 2. Chain ─────────────────────────────────────────────────────────
40
42
  const chain = opts.chain ?? await selectChain('Withdraw on which blockchain?');
41
43
  // ── 3. Token ─────────────────────────────────────────────────────────
42
- const tokenAddress = opts.token ?? await input({
43
- message: `Token contract address on ${chalk.cyan(chain)}:\n (native gas token = ${'0x' + '0'.repeat(40)})\n Address:`,
44
- validate: (v) => (v.length > 0 ? true : 'Token address is required'),
44
+ const tokenInput = opts.token ?? await input({
45
+ message: `Token on ${chalk.cyan(chain)} (address or ticker, native = ${'0x' + '0'.repeat(40)}):`,
46
+ validate: (v) => (v.length > 0 ? true : 'Token is required'),
45
47
  });
48
+ const tokenInfo = await lookupToken(tokenInput);
46
49
  // ── 4. Amount ────────────────────────────────────────────────────────
47
50
  const amount = opts.amount ?? await input({
48
51
  message: 'Amount to withdraw:',
@@ -56,11 +59,12 @@ export const withdrawCommand = new Command('withdraw')
56
59
  message: 'Destination address (your external wallet):',
57
60
  validate: (v) => (v.length > 5 ? true : 'Enter a valid address'),
58
61
  });
59
- // ── 6. Summary & double-confirm ──────────────────────────────────────
62
+ // ── 6. Summary ───────────────────────────────────────────────────────
60
63
  console.log('');
61
64
  console.log(chalk.bold.red('⚠ Withdrawal Summary'));
62
65
  console.log(` Chain : ${chalk.cyan(chain)}`);
63
- console.log(` Token : ${chalk.yellow(tokenAddress)}`);
66
+ console.log(` Token : ${formatTokenLabel(tokenInfo)}`);
67
+ console.log(` Address : ${chalk.yellow(tokenInfo.address)}`);
64
68
  console.log(` Amount : ${chalk.bold(amount)}`);
65
69
  console.log(` Destination : ${chalk.yellow(recipient)}`);
66
70
  console.log('');
@@ -77,13 +81,15 @@ export const withdrawCommand = new Command('withdraw')
77
81
  return;
78
82
  }
79
83
  }
80
- // ── 7. Execute ───────────────────────────────────────────────────────
84
+ // ── 7. Transaction confirmation & Touch ID ────────────────────────────
85
+ await requireTransactionConfirmation(`Withdraw ${amount} tokens → ${recipient} · ${chain}`, tokenInfo);
86
+ await requireTouchId();
87
+ // ── 8. Execute ───────────────────────────────────────────────────────
81
88
  const spin = spinner('Processing withdrawal…');
82
- const res = await transfer(creds.accessToken, { chain, tokenAddress, tokenAmount: amount, recipient });
89
+ const res = await transfer(creds.accessToken, { chain, tokenAddress: tokenInfo.address, tokenAmount: amount, recipient });
83
90
  spin.stop();
84
91
  assertApiOk(res, 'Withdrawal failed');
85
92
  success('Withdrawal submitted!');
86
- if (res.data)
87
- console.log(JSON.stringify(res.data, null, 2));
93
+ printTxResult(res.data);
88
94
  console.log(chalk.dim('\nIt may take a few minutes for the transaction to be confirmed on-chain.'));
89
95
  }));
package/dist/config.d.ts CHANGED
@@ -5,6 +5,8 @@ export declare function clearCredentials(): void;
5
5
  export declare function requireAuth(): Credentials;
6
6
  export interface AppConfig {
7
7
  baseUrl: string;
8
+ touchId?: boolean;
9
+ confirmBeforeTransaction?: boolean;
8
10
  }
9
11
  export declare function loadConfig(): AppConfig;
10
12
  export declare function saveConfig(config: Partial<AppConfig>): void;
package/dist/config.js CHANGED
@@ -48,6 +48,7 @@ export function requireAuth() {
48
48
  }
49
49
  const DEFAULT_CONFIG = {
50
50
  baseUrl: 'https://api.minara.ai',
51
+ confirmBeforeTransaction: true,
51
52
  };
52
53
  export function loadConfig() {
53
54
  if (!existsSync(CONFIG_FILE))
@@ -0,0 +1,56 @@
1
+ /** Enable raw JSON output (all formatters fall back to JSON.stringify). */
2
+ export declare function setRawJson(enabled: boolean): void;
3
+ /** Check if raw JSON output mode is active. */
4
+ export declare function isRawJson(): boolean;
5
+ /** Convert camelCase / snake_case key to "Title Case" label. */
6
+ export declare function formatLabel(key: string): string;
7
+ /** Smart-format a single value for terminal display. */
8
+ export declare function formatValue(value: unknown, key?: string): string;
9
+ /**
10
+ * Print an object as aligned key-value pairs.
11
+ *
12
+ * ```
13
+ * Transaction Id : 0x1234…abcd
14
+ * Status : pending
15
+ * ```
16
+ */
17
+ export declare function printKV(data: Record<string, unknown> | object, indent?: number): void;
18
+ export interface ColumnDef {
19
+ /** Object key (supports nested: "a.b") */
20
+ key: string;
21
+ /** Column header label (defaults to formatLabel(key)) */
22
+ label?: string;
23
+ /** Custom formatter for cell value */
24
+ format?: (value: unknown, row: Record<string, unknown>) => string;
25
+ /** Max column width (content will be truncated) */
26
+ maxWidth?: number;
27
+ }
28
+ /**
29
+ * Print an array of objects as a CLI table.
30
+ *
31
+ * If `columns` is omitted, columns are auto-detected from the data.
32
+ */
33
+ export declare function printTable(data: Record<string, unknown>[] | object[], columns?: ColumnDef[]): void;
34
+ /**
35
+ * Pretty-print a transaction result (deposit, withdraw, swap, transfer, order, etc.)
36
+ * Falls back silently if data is empty/undefined.
37
+ */
38
+ export declare function printTxResult(data: unknown): void;
39
+ /** Spot wallet assets (WalletAsset[]) */
40
+ export declare const ASSET_COLUMNS: ColumnDef[];
41
+ /** Perps positions (PerpsPosition[]) */
42
+ export declare const POSITION_COLUMNS: ColumnDef[];
43
+ /** Limit orders (LimitOrderInfo[]) */
44
+ export declare const LIMIT_ORDER_COLUMNS: ColumnDef[];
45
+ /** Copy trades (CopyTradeInfo[]) */
46
+ export declare const COPY_TRADE_COLUMNS: ColumnDef[];
47
+ /** Trending / search tokens (TokenInfo[]) */
48
+ export declare const TOKEN_COLUMNS: ColumnDef[];
49
+ /**
50
+ * Pretty-print Fear & Greed Index — hides redundant `timestamp` and `price`.
51
+ */
52
+ export declare function printFearGreed(data: Record<string, unknown>): void;
53
+ /**
54
+ * Pretty-print BTC/crypto metrics — flatten currentQuote, skip ohlcvQuotes.
55
+ */
56
+ export declare function printCryptoMetrics(data: Record<string, unknown>): void;
@@ -0,0 +1,376 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // CLI-friendly data formatters
3
+ //
4
+ // Replaces raw JSON.stringify output with tables and key-value displays.
5
+ // ═══════════════════════════════════════════════════════════════════════════
6
+ import chalk from 'chalk';
7
+ import Table from 'cli-table3';
8
+ // ─── Raw JSON mode ───────────────────────────────────────────────────────
9
+ let _rawJson = false;
10
+ /** Enable raw JSON output (all formatters fall back to JSON.stringify). */
11
+ export function setRawJson(enabled) { _rawJson = enabled; }
12
+ /** Check if raw JSON output mode is active. */
13
+ export function isRawJson() { return _rawJson; }
14
+ // ─── Hidden keys (internal IDs, metadata noise) ─────────────────────────
15
+ /**
16
+ * Keys matching this pattern are hidden from printKV / auto-detected table
17
+ * columns. They are still included in --json raw output.
18
+ */
19
+ const HIDDEN_KEYS = /^_?id$|^_id$|^__v$/i;
20
+ // ─── Per-context key exclusions (non-regex, for specific commands) ───────
21
+ /** Keys to skip from Fear & Greed display (redundant with pointDate). */
22
+ const FEAR_GREED_HIDDEN = new Set(['timestamp', 'price']);
23
+ // ─── Key / value helpers ─────────────────────────────────────────────────
24
+ /** Convert camelCase / snake_case key to "Title Case" label. */
25
+ export function formatLabel(key) {
26
+ return key
27
+ .replace(/([a-z0-9])([A-Z])/g, '$1 $2') // camelCase → camel Case
28
+ .replace(/[_-]/g, ' ') // snake_case / kebab → spaces
29
+ .replace(/\b\w/g, (c) => c.toUpperCase()); // Capitalise words
30
+ }
31
+ /** Smart-format a single value for terminal display. */
32
+ export function formatValue(value, key) {
33
+ if (value === null || value === undefined || value === '')
34
+ return chalk.dim('—');
35
+ if (typeof value === 'boolean')
36
+ return value ? chalk.green('Yes') : chalk.dim('No');
37
+ if (typeof value === 'number') {
38
+ // Percentage-like keys
39
+ if (key && /percent|change|pnl|roi|rate/i.test(key)) {
40
+ const sign = value >= 0 ? '+' : '';
41
+ const color = value >= 0 ? chalk.green : chalk.red;
42
+ return color(`${sign}${value.toLocaleString('en-US', { maximumFractionDigits: 4 })}%`);
43
+ }
44
+ // Price / USD-like keys
45
+ if (key && /price|value|usd|amount|equity|margin|balance|fee|cost|cap/i.test(key)) {
46
+ return `$${value.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 6 })}`;
47
+ }
48
+ // Unix timestamps – ms (13 digits ~2001–2100) or seconds (10 digits ~2001–2100)
49
+ if (key && /timestamp|time|date|createdAt|updatedAt|expir/i.test(key)) {
50
+ const ms = value > 1e12 ? value : value * 1000;
51
+ if (ms > 946684800000 && ms < 4102444800000) { // 2000-01-01 to 2100-01-01
52
+ return new Date(ms).toLocaleString();
53
+ }
54
+ }
55
+ return value.toLocaleString('en-US', { maximumFractionDigits: 6 });
56
+ }
57
+ if (typeof value === 'string') {
58
+ // Numeric string that looks like a price / amount
59
+ const num = Number(value);
60
+ if (!isNaN(num) && value.trim() !== '') {
61
+ return formatValue(num, key);
62
+ }
63
+ // Timestamps (ISO / unix seconds)
64
+ if (/^\d{4}-\d{2}-\d{2}T/.test(value)) {
65
+ return new Date(value).toLocaleString();
66
+ }
67
+ // Hex addresses
68
+ if (/^0x[0-9a-fA-F]{20,}$/.test(value))
69
+ return chalk.yellow(value);
70
+ // URLs
71
+ if (value.startsWith('http'))
72
+ return chalk.cyan.underline(value);
73
+ // Status-like
74
+ if (/^(success|completed|filled|running|active)$/i.test(value))
75
+ return chalk.green(value);
76
+ if (/^(failed|error|rejected|cancelled|canceled)$/i.test(value))
77
+ return chalk.red(value);
78
+ if (/^(pending|open|processing|paused)$/i.test(value))
79
+ return chalk.yellow(value);
80
+ return value;
81
+ }
82
+ if (Array.isArray(value)) {
83
+ if (value.length === 0)
84
+ return chalk.dim('—');
85
+ if (value.every((v) => typeof v === 'string' || typeof v === 'number')) {
86
+ return value.join(', ');
87
+ }
88
+ return chalk.dim(`[${value.length} items]`);
89
+ }
90
+ if (typeof value === 'object') {
91
+ // Shallow nested — show as inline key=value
92
+ const entries = Object.entries(value).filter(([, v]) => v !== null && v !== undefined);
93
+ if (entries.length <= 3) {
94
+ return entries.map(([k, v]) => `${k}=${typeof v === 'string' ? v : JSON.stringify(v)}`).join(' ');
95
+ }
96
+ return chalk.dim(JSON.stringify(value));
97
+ }
98
+ return String(value);
99
+ }
100
+ // ─── printKV ─────────────────────────────────────────────────────────────
101
+ /**
102
+ * Print an object as aligned key-value pairs.
103
+ *
104
+ * ```
105
+ * Transaction Id : 0x1234…abcd
106
+ * Status : pending
107
+ * ```
108
+ */
109
+ export function printKV(data, indent = 2) {
110
+ if (_rawJson) {
111
+ console.log(JSON.stringify(data, null, 2));
112
+ return;
113
+ }
114
+ const entries = Object.entries(data).filter(([k, v]) => v !== undefined && v !== null && v !== '' && !HIDDEN_KEYS.test(k));
115
+ if (entries.length === 0) {
116
+ console.log(chalk.dim(`${' '.repeat(indent)}No data.`));
117
+ return;
118
+ }
119
+ const labels = entries.map(([k]) => formatLabel(k));
120
+ const maxLen = Math.max(...labels.map((l) => l.length));
121
+ const pad = ' '.repeat(indent);
122
+ for (let i = 0; i < entries.length; i++) {
123
+ const [key, value] = entries[i];
124
+ const label = labels[i].padEnd(maxLen);
125
+ console.log(`${pad}${chalk.dim(label)} : ${formatValue(value, key)}`);
126
+ }
127
+ }
128
+ // ─── printTable ──────────────────────────────────────────────────────────
129
+ function getNestedValue(obj, path) {
130
+ return path.split('.').reduce((o, k) => {
131
+ if (o && typeof o === 'object' && k in o) {
132
+ return o[k];
133
+ }
134
+ return undefined;
135
+ }, obj);
136
+ }
137
+ /**
138
+ * Auto-detect columns from array data.
139
+ * Picks keys that appear in most rows and are not deeply nested.
140
+ */
141
+ function autoColumns(data) {
142
+ const keyCounts = new Map();
143
+ for (const row of data) {
144
+ for (const k of Object.keys(row)) {
145
+ keyCounts.set(k, (keyCounts.get(k) ?? 0) + 1);
146
+ }
147
+ }
148
+ // Keep keys present in at least half the rows, exclude hidden keys & complex nested objects
149
+ return [...keyCounts.entries()]
150
+ .filter(([k, count]) => count >= data.length / 2 && !HIDDEN_KEYS.test(k))
151
+ .filter(([k]) => {
152
+ const sample = data.find((r) => r[k] !== undefined)?.[k];
153
+ // Skip deeply nested objects / large arrays
154
+ if (Array.isArray(sample) && sample.length > 3)
155
+ return false;
156
+ if (sample && typeof sample === 'object' && !Array.isArray(sample)) {
157
+ const keys = Object.keys(sample);
158
+ return keys.length <= 3;
159
+ }
160
+ return true;
161
+ })
162
+ .map(([key]) => ({ key }));
163
+ }
164
+ /**
165
+ * Print an array of objects as a CLI table.
166
+ *
167
+ * If `columns` is omitted, columns are auto-detected from the data.
168
+ */
169
+ export function printTable(data, columns) {
170
+ if (_rawJson) {
171
+ console.log(JSON.stringify(data, null, 2));
172
+ return;
173
+ }
174
+ if (!data || data.length === 0) {
175
+ console.log(chalk.dim(' No data.'));
176
+ return;
177
+ }
178
+ const rows = data;
179
+ const cols = columns ?? autoColumns(rows);
180
+ if (cols.length === 0) {
181
+ // fallback — just printKV for each item
182
+ for (const row of data) {
183
+ printKV(row);
184
+ console.log('');
185
+ }
186
+ return;
187
+ }
188
+ const table = new Table({
189
+ head: cols.map((c) => chalk.white.bold(c.label ?? formatLabel(c.key))),
190
+ style: { head: [], border: ['dim'] },
191
+ wordWrap: true,
192
+ });
193
+ for (const row of rows) {
194
+ table.push(cols.map((c) => {
195
+ const raw = getNestedValue(row, c.key);
196
+ if (c.format)
197
+ return c.format(raw, row);
198
+ const str = formatValue(raw, c.key);
199
+ if (c.maxWidth && str.length > c.maxWidth) {
200
+ return str.slice(0, c.maxWidth - 1) + '…';
201
+ }
202
+ return str;
203
+ }));
204
+ }
205
+ console.log(table.toString());
206
+ }
207
+ // ─── printTxResult ───────────────────────────────────────────────────────
208
+ /**
209
+ * Pretty-print a transaction result (deposit, withdraw, swap, transfer, order, etc.)
210
+ * Falls back silently if data is empty/undefined.
211
+ */
212
+ export function printTxResult(data) {
213
+ if (!data)
214
+ return;
215
+ if (_rawJson) {
216
+ console.log(JSON.stringify(data, null, 2));
217
+ return;
218
+ }
219
+ if (typeof data !== 'object') {
220
+ console.log(chalk.dim(` ${data}`));
221
+ return;
222
+ }
223
+ const obj = data;
224
+ console.log('');
225
+ printKV(obj);
226
+ }
227
+ // ═══════════════════════════════════════════════════════════════════════════
228
+ // Pre-built column configs for known data types
229
+ // ═══════════════════════════════════════════════════════════════════════════
230
+ /** Spot wallet assets (WalletAsset[]) */
231
+ export const ASSET_COLUMNS = [
232
+ { key: 'symbol', label: 'Symbol', format: (v, row) => chalk.bold(String(v ?? row.tokenSymbol ?? '—')) },
233
+ { key: 'balance', label: 'Balance', format: (v, row) => String(v ?? row.amount ?? '—') },
234
+ { key: 'chain', label: 'Chain', format: (v, row) => chalk.cyan(String(v ?? row.chainName ?? '—')) },
235
+ { key: 'usdValue', label: 'USD Value', format: (v) => v ? `$${Number(v).toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}` : chalk.dim('—') },
236
+ { key: 'tokenAddress', label: 'Token Address', format: (v) => v ? chalk.dim(truncate(String(v), 16)) : chalk.dim('—') },
237
+ ];
238
+ /** Perps positions (PerpsPosition[]) */
239
+ export const POSITION_COLUMNS = [
240
+ { key: 'symbol', label: 'Symbol', format: (v) => chalk.bold(String(v ?? '—')) },
241
+ { key: 'side', label: 'Side', format: (v) => {
242
+ const s = String(v ?? '').toLowerCase();
243
+ return s === 'long' || s === 'buy' ? chalk.green.bold(String(v)) : chalk.red.bold(String(v));
244
+ } },
245
+ { key: 'size', label: 'Size' },
246
+ { key: 'entryPrice', label: 'Entry', format: (v) => formatValue(v, 'price') },
247
+ { key: 'markPrice', label: 'Mark', format: (v) => formatValue(v, 'price') },
248
+ { key: 'pnl', label: 'PnL', format: (v) => {
249
+ if (!v && v !== 0)
250
+ return chalk.dim('—');
251
+ const n = Number(v);
252
+ const color = n >= 0 ? chalk.green : chalk.red;
253
+ return color(`${n >= 0 ? '+' : ''}$${n.toLocaleString('en-US', { minimumFractionDigits: 2, maximumFractionDigits: 2 })}`);
254
+ } },
255
+ { key: 'leverage', label: 'Lev', format: (v) => v ? `${v}x` : chalk.dim('—') },
256
+ ];
257
+ /** Limit orders (LimitOrderInfo[]) */
258
+ export const LIMIT_ORDER_COLUMNS = [
259
+ { key: 'id', label: 'ID', format: (v) => chalk.dim(truncate(String(v ?? ''), 12)) },
260
+ { key: 'side', label: 'Side', format: (v) => {
261
+ const s = String(v ?? '').toLowerCase();
262
+ return s === 'buy' ? chalk.green.bold('BUY') : chalk.red.bold('SELL');
263
+ } },
264
+ { key: 'chain', label: 'Chain', format: (v) => chalk.cyan(String(v ?? '—')) },
265
+ { key: 'targetTokenCA', label: 'Token', format: (v) => v ? chalk.yellow(truncate(String(v), 14)) : chalk.dim('—') },
266
+ { key: 'priceCondition', label: 'Condition', format: (v, row) => `${v ?? '?'} $${row.targetPrice ?? '?'}` },
267
+ { key: 'amount', label: 'Amount', format: (v) => v ? `$${v}` : chalk.dim('—') },
268
+ { key: 'status', label: 'Status', format: (v) => formatValue(v, 'status') },
269
+ ];
270
+ /** Copy trades (CopyTradeInfo[]) */
271
+ export const COPY_TRADE_COLUMNS = [
272
+ { key: 'id', label: 'ID', format: (v) => chalk.dim(truncate(String(v ?? ''), 12)) },
273
+ { key: 'name', label: 'Name', format: (v) => String(v ?? chalk.dim('—')) },
274
+ { key: 'chain', label: 'Chain', format: (v) => chalk.cyan(String(v ?? '—')) },
275
+ { key: 'targetAddress', label: 'Target', format: (v) => v ? chalk.yellow(truncate(String(v), 14)) : chalk.dim('—') },
276
+ { key: 'fixedAmount', label: 'Amount', format: (v) => v ? `$${v}` : chalk.dim('—') },
277
+ { key: 'copySell', label: 'Copy Sell', format: (v) => v ? chalk.green('Yes') : chalk.dim('No') },
278
+ { key: 'status', label: 'Status', format: (v) => formatValue(v, 'status') },
279
+ ];
280
+ /** Format large numbers as $1.23B / $456.78M / $12.3K */
281
+ function compactUsd(v) {
282
+ if (!v && v !== 0)
283
+ return chalk.dim('—');
284
+ const n = Number(v);
285
+ if (isNaN(n))
286
+ return chalk.dim('—');
287
+ if (n >= 1e9)
288
+ return `$${(n / 1e9).toFixed(2)}B`;
289
+ if (n >= 1e6)
290
+ return `$${(n / 1e6).toFixed(2)}M`;
291
+ if (n >= 1e3)
292
+ return `$${(n / 1e3).toFixed(1)}K`;
293
+ return `$${n.toFixed(2)}`;
294
+ }
295
+ /** Trending / search tokens (TokenInfo[]) */
296
+ export const TOKEN_COLUMNS = [
297
+ { key: 'symbol', label: 'Symbol', format: (v) => chalk.bold(String(v ?? '—')) },
298
+ { key: 'chain', label: 'Chain', format: (v) => v ? chalk.cyan(String(v)) : chalk.dim('—') },
299
+ { key: 'price', label: 'Price', format: (v) => formatValue(v, 'price') },
300
+ { key: 'priceChange24H', label: '24h %', format: (v) => formatValue(v, 'change') },
301
+ { key: 'volume24H', label: 'Volume 24h', format: compactUsd },
302
+ { key: 'marketCap', label: 'Market Cap', format: compactUsd },
303
+ ];
304
+ // ═══════════════════════════════════════════════════════════════════════════
305
+ // Specialised display helpers for discover commands
306
+ // ═══════════════════════════════════════════════════════════════════════════
307
+ /**
308
+ * Pretty-print Fear & Greed Index — hides redundant `timestamp` and `price`.
309
+ */
310
+ export function printFearGreed(data) {
311
+ if (_rawJson) {
312
+ console.log(JSON.stringify(data, null, 2));
313
+ return;
314
+ }
315
+ const entries = Object.entries(data).filter(([k, v]) => v !== undefined && v !== null && v !== '' && !HIDDEN_KEYS.test(k) && !FEAR_GREED_HIDDEN.has(k));
316
+ if (entries.length === 0) {
317
+ console.log(chalk.dim(' No data.'));
318
+ return;
319
+ }
320
+ const labels = entries.map(([k]) => formatLabel(k));
321
+ const maxLen = Math.max(...labels.map((l) => l.length));
322
+ for (let i = 0; i < entries.length; i++) {
323
+ const [key, value] = entries[i];
324
+ const label = labels[i].padEnd(maxLen);
325
+ console.log(` ${chalk.dim(label)} : ${formatValue(value, key)}`);
326
+ }
327
+ }
328
+ /**
329
+ * Pretty-print BTC/crypto metrics — flatten currentQuote, skip ohlcvQuotes.
330
+ */
331
+ export function printCryptoMetrics(data) {
332
+ if (_rawJson) {
333
+ console.log(JSON.stringify(data, null, 2));
334
+ return;
335
+ }
336
+ const quote = data.currentQuote;
337
+ const change24h = data.priceChange24h;
338
+ // Build a flat display object from the quote
339
+ const display = [];
340
+ if (quote) {
341
+ if (quote.close !== undefined)
342
+ display.push(['Current Price', formatValue(quote.close, 'price')]);
343
+ if (quote.open !== undefined)
344
+ display.push(['Open (24h)', formatValue(quote.open, 'price')]);
345
+ if (quote.high !== undefined)
346
+ display.push(['High (24h)', formatValue(quote.high, 'price')]);
347
+ if (quote.low !== undefined)
348
+ display.push(['Low (24h)', formatValue(quote.low, 'price')]);
349
+ if (quote.high_timestamp)
350
+ display.push(['High At', formatValue(quote.high_timestamp, 'timestamp')]);
351
+ if (quote.low_timestamp)
352
+ display.push(['Low At', formatValue(quote.low_timestamp, 'timestamp')]);
353
+ }
354
+ if (change24h !== undefined) {
355
+ display.push(['Price Change 24h', formatValue(change24h, 'change')]);
356
+ }
357
+ else if (quote?.percent_change !== undefined) {
358
+ display.push(['Price Change 24h', formatValue(quote.percent_change, 'change')]);
359
+ }
360
+ if (quote?.price_change !== undefined) {
361
+ display.push(['Δ Price (USD)', formatValue(quote.price_change, 'price')]);
362
+ }
363
+ if (display.length === 0) {
364
+ // fallback to generic printKV
365
+ printKV(data);
366
+ return;
367
+ }
368
+ const maxLen = Math.max(...display.map(([l]) => l.length));
369
+ for (const [label, val] of display) {
370
+ console.log(` ${chalk.dim(label.padEnd(maxLen))} : ${val}`);
371
+ }
372
+ }
373
+ // ─── Helpers ─────────────────────────────────────────────────────────────
374
+ function truncate(str, len) {
375
+ return str.length > len ? str.slice(0, len - 1) + '…' : str;
376
+ }
package/dist/index.js CHANGED
@@ -18,13 +18,21 @@ import { copyTradeCommand } from './commands/copy-trade.js';
18
18
  import { chatCommand } from './commands/chat.js';
19
19
  import { discoverCommand } from './commands/discover.js';
20
20
  import { configCommand } from './commands/config.js';
21
+ import { premiumCommand } from './commands/premium.js';
22
+ import { setRawJson } from './formatters.js';
21
23
  const program = new Command();
22
24
  program
23
25
  .name('minara')
24
26
  .version(version)
27
+ .option('--json', 'Output raw JSON instead of formatted tables')
25
28
  .description(chalk.bold('Minara CLI') +
26
29
  ' — Your AI-powered digital finance assistant in the terminal.\n\n' +
27
- ' Login, swap, trade perps, copy-trade, and chat with Minara AI.');
30
+ ' Login, swap, trade perps, copy-trade, and chat with Minara AI.')
31
+ .hook('preAction', (thisCommand) => {
32
+ const opts = thisCommand.optsWithGlobals();
33
+ if (opts.json)
34
+ setRawJson(true);
35
+ });
28
36
  // ── Auth & Account ───────────────────────────────────────────────────────
29
37
  program.addCommand(loginCommand);
30
38
  program.addCommand(logoutCommand);
@@ -45,6 +53,8 @@ program.addCommand(copyTradeCommand);
45
53
  program.addCommand(chatCommand);
46
54
  // ── Market ───────────────────────────────────────────────────────────────
47
55
  program.addCommand(discoverCommand);
56
+ // ── Premium ─────────────────────────────────────────────────────────────
57
+ program.addCommand(premiumCommand);
48
58
  // ── Config ───────────────────────────────────────────────────────────────
49
59
  program.addCommand(configCommand);
50
60
  // Default: show help
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Check whether Touch ID hardware is available on this machine.
3
+ * Returns `false` on non-macOS or when hardware is absent / not enrolled.
4
+ */
5
+ export declare function isTouchIdAvailable(): boolean;
6
+ /**
7
+ * Perform a Touch ID verification.
8
+ * Resolves on success, throws on failure or cancellation.
9
+ */
10
+ export declare function verifyTouchId(reason?: string): void;
11
+ /**
12
+ * If Touch ID is enabled in config, prompt the user for biometric
13
+ * verification. Exits the process on failure.
14
+ *
15
+ * Call this before any sensitive financial operation.
16
+ * On non-macOS platforms a warning is shown and execution continues.
17
+ */
18
+ export declare function requireTouchId(): Promise<void>;