minara 0.1.5 → 0.2.1

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.
@@ -0,0 +1,181 @@
1
+ // ═══════════════════════════════════════════════════════════════════════════
2
+ // Touch ID (macOS) — biometric verification for sensitive operations
3
+ // ═══════════════════════════════════════════════════════════════════════════
4
+ import { execFileSync } from 'node:child_process';
5
+ import { existsSync, writeFileSync, unlinkSync, chmodSync, readFileSync } from 'node:fs';
6
+ import { join } from 'node:path';
7
+ import { platform } from 'node:os';
8
+ import chalk from 'chalk';
9
+ import { loadConfig, getMinaraDir } from './config.js';
10
+ // Bump this when SWIFT_SOURCE changes to force recompilation
11
+ const TOUCHID_BINARY_VERSION = '1';
12
+ const SWIFT_SOURCE = `
13
+ import Foundation
14
+ import LocalAuthentication
15
+
16
+ // Mode: "check" = just check availability, "auth" = perform authentication
17
+ let mode = CommandLine.arguments.count > 1 ? CommandLine.arguments[1] : "auth"
18
+ let context = LAContext()
19
+ var error: NSError?
20
+
21
+ guard context.canEvaluatePolicy(.deviceOwnerAuthenticationWithBiometrics, error: &error) else {
22
+ let msg = error?.localizedDescription ?? "Touch ID not available"
23
+ fputs("unavailable:\\(msg)\\n", stderr)
24
+ exit(2)
25
+ }
26
+
27
+ if mode == "check" {
28
+ print("available")
29
+ exit(0)
30
+ }
31
+
32
+ // Auth mode — prompt for Touch ID
33
+ let reason = CommandLine.arguments.count > 2
34
+ ? CommandLine.arguments[2]
35
+ : "Minara CLI: Authorize transaction"
36
+
37
+ let semaphore = DispatchSemaphore(value: 0)
38
+ var authSuccess = false
39
+
40
+ context.evaluatePolicy(
41
+ .deviceOwnerAuthenticationWithBiometrics,
42
+ localizedReason: reason
43
+ ) { result, authError in
44
+ authSuccess = result
45
+ if !result {
46
+ let msg = authError?.localizedDescription ?? "Authentication failed"
47
+ fputs("failed:\\(msg)\\n", stderr)
48
+ }
49
+ semaphore.signal()
50
+ }
51
+
52
+ semaphore.wait()
53
+ exit(authSuccess ? 0 : 1)
54
+ `;
55
+ // ─── Binary management ───────────────────────────────────────────────────
56
+ const BINARY_NAME = 'touchid_auth';
57
+ const VERSION_FILE = 'touchid_auth.version';
58
+ function getBinaryPath() {
59
+ return join(getMinaraDir(), BINARY_NAME);
60
+ }
61
+ function getVersionPath() {
62
+ return join(getMinaraDir(), VERSION_FILE);
63
+ }
64
+ function isBinaryUpToDate() {
65
+ const binaryPath = getBinaryPath();
66
+ const versionPath = getVersionPath();
67
+ if (!existsSync(binaryPath) || !existsSync(versionPath))
68
+ return false;
69
+ try {
70
+ const ver = readFileSync(versionPath, 'utf-8').trim();
71
+ return ver === TOUCHID_BINARY_VERSION;
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ }
77
+ /**
78
+ * Compile the Touch ID Swift helper binary (cached in ~/.minara/).
79
+ * Returns the path to the compiled binary.
80
+ */
81
+ function ensureBinary() {
82
+ const binaryPath = getBinaryPath();
83
+ if (isBinaryUpToDate())
84
+ return binaryPath;
85
+ const dir = getMinaraDir();
86
+ const sourcePath = join(dir, 'touchid_auth.swift');
87
+ writeFileSync(sourcePath, SWIFT_SOURCE, 'utf-8');
88
+ try {
89
+ execFileSync('/usr/bin/swiftc', [
90
+ '-O', // optimized build
91
+ '-o', binaryPath,
92
+ sourcePath,
93
+ ], {
94
+ timeout: 120_000, // 2 min timeout for first compile
95
+ stdio: 'pipe',
96
+ });
97
+ chmodSync(binaryPath, 0o700);
98
+ writeFileSync(getVersionPath(), TOUCHID_BINARY_VERSION, 'utf-8');
99
+ }
100
+ catch (err) {
101
+ // Clean up on failure
102
+ if (existsSync(binaryPath))
103
+ unlinkSync(binaryPath);
104
+ const msg = err instanceof Error ? err.message : String(err);
105
+ throw new Error(`Failed to compile Touch ID helper: ${msg}`);
106
+ }
107
+ finally {
108
+ if (existsSync(sourcePath))
109
+ unlinkSync(sourcePath);
110
+ }
111
+ return binaryPath;
112
+ }
113
+ // ─── Public API ──────────────────────────────────────────────────────────
114
+ /**
115
+ * Check whether Touch ID hardware is available on this machine.
116
+ * Returns `false` on non-macOS or when hardware is absent / not enrolled.
117
+ */
118
+ export function isTouchIdAvailable() {
119
+ if (platform() !== 'darwin')
120
+ return false;
121
+ try {
122
+ const binary = ensureBinary();
123
+ execFileSync(binary, ['check'], { timeout: 10_000, stdio: 'pipe' });
124
+ return true;
125
+ }
126
+ catch {
127
+ return false;
128
+ }
129
+ }
130
+ /**
131
+ * Perform a Touch ID verification.
132
+ * Resolves on success, throws on failure or cancellation.
133
+ */
134
+ export function verifyTouchId(reason) {
135
+ const binary = ensureBinary();
136
+ const args = ['auth'];
137
+ if (reason)
138
+ args.push(reason);
139
+ execFileSync(binary, args, { timeout: 60_000, stdio: ['pipe', 'pipe', 'pipe'] });
140
+ }
141
+ /**
142
+ * If Touch ID is enabled in config, prompt the user for biometric
143
+ * verification. Exits the process on failure.
144
+ *
145
+ * Call this before any sensitive financial operation.
146
+ * On non-macOS platforms a warning is shown and execution continues.
147
+ */
148
+ export async function requireTouchId() {
149
+ const config = loadConfig();
150
+ if (!config.touchId)
151
+ return; // Touch ID not enabled — no-op
152
+ if (platform() !== 'darwin') {
153
+ console.log(chalk.yellow('⚠'), 'Touch ID is only available on macOS. Skipping biometric check.');
154
+ return;
155
+ }
156
+ console.log('');
157
+ console.log(chalk.blue('🔐'), chalk.bold('Touch ID verification required'));
158
+ try {
159
+ verifyTouchId();
160
+ console.log(chalk.green('✔'), 'Touch ID verified');
161
+ console.log('');
162
+ }
163
+ catch (err) {
164
+ // Parse stderr for details
165
+ const stderr = (err && typeof err === 'object' && 'stderr' in err)
166
+ ? err.stderr?.toString().trim()
167
+ : '';
168
+ if (stderr.startsWith('unavailable:')) {
169
+ console.error(chalk.red('✖'), 'Touch ID is not available on this device.');
170
+ console.error(chalk.dim(' Tip: Run `minara config` to disable Touch ID protection.'));
171
+ }
172
+ else {
173
+ console.error(chalk.red('✖'), 'Touch ID verification failed. Operation cancelled.');
174
+ if (stderr) {
175
+ const detail = stderr.startsWith('failed:') ? stderr.slice(7) : stderr;
176
+ console.error(chalk.dim(` ${detail}`));
177
+ }
178
+ }
179
+ process.exit(1);
180
+ }
181
+ }
package/dist/types.d.ts CHANGED
@@ -32,6 +32,17 @@ export interface AuthUser {
32
32
  accounts?: Record<string, unknown>;
33
33
  invitationCode?: string;
34
34
  mfaSettings?: Record<string, unknown>;
35
+ subscription?: {
36
+ planId?: number;
37
+ planName?: string;
38
+ status?: string;
39
+ interval?: string;
40
+ currentPeriodEnd?: string;
41
+ cancelAtPeriodEnd?: boolean;
42
+ [key: string]: unknown;
43
+ };
44
+ plan?: Record<string, unknown>;
45
+ [key: string]: unknown;
35
46
  }
36
47
  export interface DeviceAuthStartResponse {
37
48
  device_code: string;
@@ -68,7 +79,6 @@ export interface ChatRequestDTO {
68
79
  chatId?: string;
69
80
  parentMessageId?: string;
70
81
  thinking?: boolean;
71
- deepresearch?: boolean;
72
82
  workMode?: string;
73
83
  platform?: string;
74
84
  message: {
@@ -246,41 +256,6 @@ export interface UpdateLimitOrderDto {
246
256
  targetPrice?: number;
247
257
  expiredAt?: number;
248
258
  }
249
- export interface CreateCopyTradeDto {
250
- targetAddress: string;
251
- chain: string;
252
- name?: string;
253
- mode?: 'fixedAmount';
254
- copySell?: boolean;
255
- copySellSamePercentage?: boolean;
256
- copySellQuitPercentage?: number;
257
- fixedAmount?: number;
258
- status?: 'running' | 'paused';
259
- expiredAt?: number;
260
- }
261
- export interface CopyTradeInfo {
262
- id: string;
263
- name?: string;
264
- targetAddress: string;
265
- chain: string;
266
- mode?: string;
267
- fixedAmount?: number;
268
- copySell?: boolean;
269
- status?: string;
270
- createdAt?: string;
271
- }
272
- export interface UpdateCopyTradeDto {
273
- chain: string;
274
- name?: string;
275
- mode?: 'fixedAmount';
276
- copySell?: boolean;
277
- copySellSamePercentage?: boolean;
278
- copySellQuitPercentage?: number;
279
- fixedAmount?: number;
280
- targetAddress?: string;
281
- status?: 'running' | 'paused';
282
- expiredAt?: number;
283
- }
284
259
  export interface UserTradeConfig {
285
260
  slippage?: string;
286
261
  priorityFee?: string;
@@ -292,9 +267,15 @@ export interface TokenInfo {
292
267
  name?: string;
293
268
  address?: string;
294
269
  chain?: string;
270
+ logo?: string;
271
+ description?: string;
295
272
  price?: number;
296
- change24h?: number;
273
+ priceChange24H?: number;
274
+ volume?: number;
275
+ volume24H?: number;
297
276
  marketCap?: number;
277
+ fdv?: number;
278
+ socialUrls?: Record<string, string>;
298
279
  }
299
280
  export interface DiscoverEvent {
300
281
  id: string;
@@ -304,11 +285,44 @@ export interface DiscoverEvent {
304
285
  createdAt?: string;
305
286
  }
306
287
  export interface PaymentPlan {
307
- id: string;
308
- name?: string;
309
- price?: number;
288
+ _id: string;
289
+ id: number;
290
+ name: string;
291
+ price: number;
292
+ status: string;
293
+ interval: 'month' | 'year';
294
+ rules: {
295
+ limitDegenMode?: number;
296
+ limitDeepresearch?: number;
297
+ limitCredit?: string;
298
+ limitWorkflows?: number;
299
+ };
300
+ stripePriceId?: string;
301
+ inviteCount?: number;
302
+ }
303
+ export interface CreditPackage {
304
+ _id: string;
305
+ id: number;
306
+ amount: number;
307
+ degenMode: number;
308
+ stripePriceId: string;
309
+ credit: string;
310
+ }
311
+ export interface PlansResponse {
312
+ plans: PaymentPlan[];
313
+ packages: CreditPackage[];
314
+ }
315
+ export interface CheckoutSession {
316
+ url?: string;
317
+ sessionId?: string;
318
+ [key: string]: unknown;
319
+ }
320
+ export interface CryptoCheckout {
321
+ url?: string;
322
+ address?: string;
323
+ amount?: number;
310
324
  currency?: string;
311
- interval?: string;
325
+ [key: string]: unknown;
312
326
  }
313
327
  export interface GasFeeInfo {
314
328
  chain?: string;
package/dist/utils.d.ts CHANGED
@@ -30,5 +30,49 @@ export declare function selectChain(message?: string, compact?: boolean): Promis
30
30
  * prints a clean error instead of a raw stack trace.
31
31
  */
32
32
  export declare function wrapAction<A extends unknown[]>(fn: (...args: A) => Promise<void>): (...args: A) => Promise<void>;
33
+ export interface TokenDisplayInfo {
34
+ symbol?: string;
35
+ name?: string;
36
+ address: string;
37
+ chain?: string;
38
+ }
39
+ /**
40
+ * Normalize a chain identifier from the token search API to a supported
41
+ * `Chain` value used by the swap / transfer APIs.
42
+ */
43
+ export declare function normalizeChain(raw?: string): Chain | undefined;
44
+ /**
45
+ * Look up token metadata by address, ticker, or name.
46
+ *
47
+ * - Input starting with `$` (e.g. `$BONK`) is treated as an exact ticker
48
+ * search. Results are filtered to those whose symbol matches (case-insensitive).
49
+ * - Otherwise the raw input is sent to the search API and may match by
50
+ * address, ticker, or name.
51
+ *
52
+ * When multiple candidates remain the user is prompted to disambiguate.
53
+ * The returned `address` is always the resolved contract address.
54
+ */
55
+ export declare function lookupToken(tokenInput: string): Promise<TokenDisplayInfo>;
56
+ /**
57
+ * Format token info for display: "$BONK — Bonk" or just the address when
58
+ * symbol/name are unavailable.
59
+ */
60
+ export declare function formatTokenLabel(token: TokenDisplayInfo): string;
61
+ /**
62
+ * Prompt the user for a mandatory second confirmation before executing a
63
+ * fund-related operation. Controlled by `confirmBeforeTransaction` in config
64
+ * (default: enabled). This confirmation is independent of the `-y` flag and
65
+ * Touch ID — it serves as an extra safety net.
66
+ *
67
+ * @param description Short one-line summary of the operation.
68
+ * @param token Optional token metadata to highlight in the prompt.
69
+ *
70
+ * Exits the process if the user declines.
71
+ */
72
+ export declare function requireTransactionConfirmation(description: string, token?: TokenDisplayInfo, details?: {
73
+ chain?: string;
74
+ side?: string;
75
+ amount?: string;
76
+ }): Promise<void>;
33
77
  /** Open a URL in the user's default browser (cross-platform). */
34
78
  export declare function openBrowser(url: string): void;
package/dist/utils.js CHANGED
@@ -2,8 +2,9 @@ import chalk from 'chalk';
2
2
  import ora from 'ora';
3
3
  import { exec } from 'node:child_process';
4
4
  import { platform } from 'node:os';
5
- import { select } from '@inquirer/prompts';
5
+ import { select, confirm } from '@inquirer/prompts';
6
6
  import { SUPPORTED_CHAINS } from './types.js';
7
+ import { loadConfig } from './config.js';
7
8
  // ─── Logging helpers ─────────────────────────────────────────────────────────
8
9
  export function success(msg) {
9
10
  console.log(chalk.green('✔'), msg);
@@ -114,6 +115,228 @@ export function wrapAction(fn) {
114
115
  }
115
116
  };
116
117
  }
118
+ const NATIVE_TOKEN_ADDRESS = {
119
+ sol: 'So11111111111111111111111111111111111111112',
120
+ solana: 'So11111111111111111111111111111111111111112',
121
+ };
122
+ const EVM_NATIVE = '0x' + '0'.repeat(40);
123
+ function resolveNativeAddress(chain) {
124
+ if (!chain)
125
+ return EVM_NATIVE;
126
+ return NATIVE_TOKEN_ADDRESS[chain.toLowerCase()] ?? EVM_NATIVE;
127
+ }
128
+ const CHAIN_ALIAS = {
129
+ sol: 'solana',
130
+ eth: 'ethereum',
131
+ arb: 'arbitrum',
132
+ op: 'optimism',
133
+ matic: 'polygon',
134
+ poly: 'polygon',
135
+ avax: 'avalanche',
136
+ bnb: 'bsc',
137
+ bera: 'berachain',
138
+ // Numeric chain IDs returned by the token search API
139
+ '101': 'solana',
140
+ '1': 'ethereum',
141
+ '8453': 'base',
142
+ '42161': 'arbitrum',
143
+ '10': 'optimism',
144
+ '56': 'bsc',
145
+ '137': 'polygon',
146
+ '43114': 'avalanche',
147
+ '81457': 'blast',
148
+ '169': 'manta',
149
+ '34443': 'mode',
150
+ '146': 'sonic',
151
+ '80094': 'berachain',
152
+ '196': 'xlayer',
153
+ '4200': 'merlin',
154
+ };
155
+ /**
156
+ * Normalize a chain identifier from the token search API to a supported
157
+ * `Chain` value used by the swap / transfer APIs.
158
+ */
159
+ export function normalizeChain(raw) {
160
+ if (!raw)
161
+ return undefined;
162
+ const lower = raw.toLowerCase();
163
+ if (SUPPORTED_CHAINS.includes(lower))
164
+ return lower;
165
+ return CHAIN_ALIAS[lower];
166
+ }
167
+ /** Capitalize chain name for display (e.g. "solana" → "Solana", "bsc" → "BSC"). */
168
+ function displayChain(raw) {
169
+ const name = normalizeChain(raw) ?? raw ?? 'unknown';
170
+ if (name === 'bsc')
171
+ return 'BSC';
172
+ return name.charAt(0).toUpperCase() + name.slice(1);
173
+ }
174
+ /** Lower = cheaper gas. Used to sort chain choices so the cheapest is first. */
175
+ const CHAIN_GAS_RANK = {
176
+ sol: 1, solana: 1, '101': 1,
177
+ base: 2, '8453': 2,
178
+ arbitrum: 3, arb: 3, '42161': 3,
179
+ optimism: 4, op: 4, '10': 4,
180
+ bsc: 5, bnb: 5, '56': 5,
181
+ polygon: 6, matic: 6, poly: 6, '137': 6,
182
+ sonic: 7, '146': 7,
183
+ avalanche: 8, avax: 8, '43114': 8,
184
+ berachain: 9, bera: 9, '80094': 9,
185
+ blast: 10, '81457': 10,
186
+ manta: 11, '169': 11,
187
+ mode: 12, '34443': 12,
188
+ ethereum: 50, eth: 50, '1': 50,
189
+ };
190
+ function chainGasRank(chain) {
191
+ if (!chain)
192
+ return 99;
193
+ return CHAIN_GAS_RANK[chain.toLowerCase()] ?? 30;
194
+ }
195
+ /**
196
+ * Look up token metadata by address, ticker, or name.
197
+ *
198
+ * - Input starting with `$` (e.g. `$BONK`) is treated as an exact ticker
199
+ * search. Results are filtered to those whose symbol matches (case-insensitive).
200
+ * - Otherwise the raw input is sent to the search API and may match by
201
+ * address, ticker, or name.
202
+ *
203
+ * When multiple candidates remain the user is prompted to disambiguate.
204
+ * The returned `address` is always the resolved contract address.
205
+ */
206
+ export async function lookupToken(tokenInput) {
207
+ const isTicker = tokenInput.startsWith('$');
208
+ const keyword = isTicker ? tokenInput.slice(1) : tokenInput;
209
+ const spin = spinner('Looking up token…');
210
+ try {
211
+ const { searchTokens } = await import('./api/tokens.js');
212
+ const res = await searchTokens(keyword);
213
+ spin.stop();
214
+ if (!res.success || !res.data || res.data.length === 0) {
215
+ if (isTicker) {
216
+ warn(`No token found for ticker $${keyword}`);
217
+ }
218
+ return { address: tokenInput };
219
+ }
220
+ let tokens = res.data;
221
+ if (isTicker) {
222
+ const filtered = tokens.filter((t) => t.symbol?.toLowerCase() === keyword.toLowerCase());
223
+ if (filtered.length > 0)
224
+ tokens = filtered;
225
+ }
226
+ if (!isTicker) {
227
+ const exact = tokens.find((t) => t.address?.toLowerCase() === keyword.toLowerCase());
228
+ if (exact) {
229
+ return { symbol: exact.symbol, name: exact.name ?? displayChain(exact.chain), address: exact.address ?? resolveNativeAddress(exact.chain), chain: exact.chain };
230
+ }
231
+ }
232
+ if (tokens.length === 1) {
233
+ const t = tokens[0];
234
+ return { symbol: t.symbol, name: t.name ?? displayChain(t.chain), address: t.address ?? resolveNativeAddress(t.chain), chain: t.chain };
235
+ }
236
+ // Check if all results share the same symbol → multi-chain scenario
237
+ const uniqueSymbols = new Set(tokens.map((t) => t.symbol?.toLowerCase()));
238
+ if (uniqueSymbols.size === 1) {
239
+ const sorted = [...tokens].sort((a, b) => chainGasRank(a.chain) - chainGasRank(b.chain));
240
+ info(`$${sorted[0].symbol} is available on ${sorted.length} chains`);
241
+ const selected = await select({
242
+ message: 'Select chain:',
243
+ choices: sorted.map((t, i) => {
244
+ const chainName = displayChain(t.chain);
245
+ const addr = t.address
246
+ ? chalk.dim(` · ${t.address.slice(0, 10)}…${t.address.slice(-6)}`)
247
+ : chalk.dim(' · native token');
248
+ const tag = i === 0 ? chalk.green(' (lowest gas)') : '';
249
+ return { name: `${chalk.cyan(chainName)}${tag}${addr}`, value: t };
250
+ }),
251
+ });
252
+ return {
253
+ symbol: selected.symbol,
254
+ name: selected.name ?? displayChain(selected.chain),
255
+ address: selected.address ?? resolveNativeAddress(selected.chain),
256
+ chain: selected.chain,
257
+ };
258
+ }
259
+ info(`Found ${tokens.length} tokens matching "${tokenInput}"`);
260
+ const selected = await select({
261
+ message: 'Select the correct token:',
262
+ choices: tokens.map((t) => {
263
+ const sym = t.symbol ? chalk.bold('$' + t.symbol) : '?';
264
+ const chainName = displayChain(t.chain);
265
+ const label = t.name || chainName;
266
+ const desc = label ? ` — ${label}` : '';
267
+ const chainTag = chainName && chainName !== label ? chalk.dim(` [${chainName}]`) : '';
268
+ const addr = t.address ? `\n ${chalk.yellow(t.address)}` : chalk.dim('\n (native token)');
269
+ return { name: `${sym}${desc}${chainTag}${addr}`, value: t };
270
+ }),
271
+ });
272
+ return {
273
+ symbol: selected.symbol,
274
+ name: selected.name ?? displayChain(selected.chain),
275
+ address: selected.address ?? resolveNativeAddress(selected.chain),
276
+ chain: selected.chain,
277
+ };
278
+ }
279
+ catch {
280
+ spin.stop();
281
+ return { address: tokenInput };
282
+ }
283
+ }
284
+ /**
285
+ * Format token info for display: "$BONK — Bonk" or just the address when
286
+ * symbol/name are unavailable.
287
+ */
288
+ export function formatTokenLabel(token) {
289
+ if (!token.symbol)
290
+ return chalk.yellow(token.address);
291
+ const ticker = chalk.bold('$' + token.symbol);
292
+ return token.name ? `${ticker} ${chalk.dim('—')} ${token.name}` : ticker;
293
+ }
294
+ // ─── Transaction confirmation ─────────────────────────────────────────────────
295
+ /**
296
+ * Prompt the user for a mandatory second confirmation before executing a
297
+ * fund-related operation. Controlled by `confirmBeforeTransaction` in config
298
+ * (default: enabled). This confirmation is independent of the `-y` flag and
299
+ * Touch ID — it serves as an extra safety net.
300
+ *
301
+ * @param description Short one-line summary of the operation.
302
+ * @param token Optional token metadata to highlight in the prompt.
303
+ *
304
+ * Exits the process if the user declines.
305
+ */
306
+ export async function requireTransactionConfirmation(description, token, details) {
307
+ const config = loadConfig();
308
+ if (config.confirmBeforeTransaction === false)
309
+ return;
310
+ console.log('');
311
+ console.log(chalk.yellow('⚠'), chalk.bold('Transaction confirmation'));
312
+ if (details?.chain) {
313
+ console.log(chalk.dim(' Chain : ') + chalk.cyan(details.chain));
314
+ }
315
+ if (token) {
316
+ const ticker = token.symbol ? '$' + token.symbol : undefined;
317
+ const label = [ticker, token.name].filter(Boolean).join(' — ');
318
+ console.log(chalk.dim(' Token : ') + (label ? chalk.bold(label) : chalk.dim('Unknown token')));
319
+ console.log(chalk.dim(' Address : ') + chalk.yellow(token.address));
320
+ }
321
+ if (details?.side) {
322
+ const s = details.side.toLowerCase();
323
+ const colored = s === 'buy' ? chalk.green.bold(details.side.toUpperCase()) : chalk.red.bold(details.side.toUpperCase());
324
+ console.log(chalk.dim(' Side : ') + colored);
325
+ }
326
+ if (details?.amount) {
327
+ console.log(chalk.dim(' Amount : ') + chalk.bold(details.amount));
328
+ }
329
+ console.log(chalk.dim(` Action : ${description}`));
330
+ console.log('');
331
+ const ok = await confirm({
332
+ message: 'Are you sure you want to proceed?',
333
+ default: false,
334
+ });
335
+ if (!ok) {
336
+ console.log(chalk.dim('Operation cancelled.'));
337
+ process.exit(0);
338
+ }
339
+ }
117
340
  // ─── Browser ──────────────────────────────────────────────────────────────────
118
341
  /** Open a URL in the user's default browser (cross-platform). */
119
342
  export function openBrowser(url) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "minara",
3
- "version": "0.1.5",
3
+ "version": "0.2.1",
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",