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.
@@ -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: {
@@ -292,9 +302,15 @@ export interface TokenInfo {
292
302
  name?: string;
293
303
  address?: string;
294
304
  chain?: string;
305
+ logo?: string;
306
+ description?: string;
295
307
  price?: number;
296
- change24h?: number;
308
+ priceChange24H?: number;
309
+ volume?: number;
310
+ volume24H?: number;
297
311
  marketCap?: number;
312
+ fdv?: number;
313
+ socialUrls?: Record<string, string>;
298
314
  }
299
315
  export interface DiscoverEvent {
300
316
  id: string;
@@ -304,11 +320,44 @@ export interface DiscoverEvent {
304
320
  createdAt?: string;
305
321
  }
306
322
  export interface PaymentPlan {
307
- id: string;
308
- name?: string;
309
- price?: number;
323
+ _id: string;
324
+ id: number;
325
+ name: string;
326
+ price: number;
327
+ status: string;
328
+ interval: 'month' | 'year';
329
+ rules: {
330
+ limitDegenMode?: number;
331
+ limitDeepresearch?: number;
332
+ limitCredit?: string;
333
+ limitWorkflows?: number;
334
+ };
335
+ stripePriceId?: string;
336
+ inviteCount?: number;
337
+ }
338
+ export interface CreditPackage {
339
+ _id: string;
340
+ id: number;
341
+ amount: number;
342
+ degenMode: number;
343
+ stripePriceId: string;
344
+ credit: string;
345
+ }
346
+ export interface PlansResponse {
347
+ plans: PaymentPlan[];
348
+ packages: CreditPackage[];
349
+ }
350
+ export interface CheckoutSession {
351
+ url?: string;
352
+ sessionId?: string;
353
+ [key: string]: unknown;
354
+ }
355
+ export interface CryptoCheckout {
356
+ url?: string;
357
+ address?: string;
358
+ amount?: number;
310
359
  currency?: string;
311
- interval?: string;
360
+ [key: string]: unknown;
312
361
  }
313
362
  export interface GasFeeInfo {
314
363
  chain?: string;
package/dist/utils.d.ts CHANGED
@@ -30,5 +30,39 @@ 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
+ }
38
+ /**
39
+ * Look up token metadata by address, ticker, or name.
40
+ *
41
+ * - Input starting with `$` (e.g. `$BONK`) is treated as an exact ticker
42
+ * search. Results are filtered to those whose symbol matches (case-insensitive).
43
+ * - Otherwise the raw input is sent to the search API and may match by
44
+ * address, ticker, or name.
45
+ *
46
+ * When multiple candidates remain the user is prompted to disambiguate.
47
+ * The returned `address` is always the resolved contract address.
48
+ */
49
+ export declare function lookupToken(tokenInput: string): Promise<TokenDisplayInfo>;
50
+ /**
51
+ * Format token info for display: "$BONK — Bonk" or just the address when
52
+ * symbol/name are unavailable.
53
+ */
54
+ export declare function formatTokenLabel(token: TokenDisplayInfo): string;
55
+ /**
56
+ * Prompt the user for a mandatory second confirmation before executing a
57
+ * fund-related operation. Controlled by `confirmBeforeTransaction` in config
58
+ * (default: enabled). This confirmation is independent of the `-y` flag and
59
+ * Touch ID — it serves as an extra safety net.
60
+ *
61
+ * @param description Short one-line summary of the operation.
62
+ * @param token Optional token metadata to highlight in the prompt.
63
+ *
64
+ * Exits the process if the user declines.
65
+ */
66
+ export declare function requireTransactionConfirmation(description: string, token?: TokenDisplayInfo): Promise<void>;
33
67
  /** Open a URL in the user's default browser (cross-platform). */
34
68
  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,111 @@ export function wrapAction(fn) {
114
115
  }
115
116
  };
116
117
  }
118
+ /**
119
+ * Look up token metadata by address, ticker, or name.
120
+ *
121
+ * - Input starting with `$` (e.g. `$BONK`) is treated as an exact ticker
122
+ * search. Results are filtered to those whose symbol matches (case-insensitive).
123
+ * - Otherwise the raw input is sent to the search API and may match by
124
+ * address, ticker, or name.
125
+ *
126
+ * When multiple candidates remain the user is prompted to disambiguate.
127
+ * The returned `address` is always the resolved contract address.
128
+ */
129
+ export async function lookupToken(tokenInput) {
130
+ const isTicker = tokenInput.startsWith('$');
131
+ const keyword = isTicker ? tokenInput.slice(1) : tokenInput;
132
+ const spin = spinner('Looking up token…');
133
+ try {
134
+ const { searchTokens } = await import('./api/tokens.js');
135
+ const res = await searchTokens(keyword);
136
+ spin.stop();
137
+ if (!res.success || !res.data || res.data.length === 0) {
138
+ if (isTicker) {
139
+ warn(`No token found for ticker $${keyword}`);
140
+ }
141
+ return { address: tokenInput };
142
+ }
143
+ let tokens = res.data;
144
+ if (isTicker) {
145
+ const filtered = tokens.filter((t) => t.symbol?.toLowerCase() === keyword.toLowerCase());
146
+ if (filtered.length > 0)
147
+ tokens = filtered;
148
+ }
149
+ if (!isTicker) {
150
+ const exact = tokens.find((t) => t.address?.toLowerCase() === keyword.toLowerCase());
151
+ if (exact) {
152
+ return { symbol: exact.symbol, name: exact.name, address: exact.address ?? tokenInput };
153
+ }
154
+ }
155
+ if (tokens.length === 1) {
156
+ const t = tokens[0];
157
+ return { symbol: t.symbol, name: t.name, address: t.address ?? tokenInput };
158
+ }
159
+ info(`Found ${tokens.length} tokens matching "${tokenInput}"`);
160
+ const selected = await select({
161
+ message: 'Select the correct token:',
162
+ choices: tokens.map((t) => ({
163
+ name: `${t.symbol ? chalk.bold('$' + t.symbol) : '?'} — ${t.name ?? 'Unknown'}\n ${chalk.yellow(t.address ?? '')}`,
164
+ value: t,
165
+ })),
166
+ });
167
+ return {
168
+ symbol: selected.symbol,
169
+ name: selected.name,
170
+ address: selected.address ?? tokenInput,
171
+ };
172
+ }
173
+ catch {
174
+ spin.stop();
175
+ return { address: tokenInput };
176
+ }
177
+ }
178
+ /**
179
+ * Format token info for display: "$BONK — Bonk" or just the address when
180
+ * symbol/name are unavailable.
181
+ */
182
+ export function formatTokenLabel(token) {
183
+ if (!token.symbol)
184
+ return chalk.yellow(token.address);
185
+ const ticker = chalk.bold('$' + token.symbol);
186
+ return token.name ? `${ticker} ${chalk.dim('—')} ${token.name}` : ticker;
187
+ }
188
+ // ─── Transaction confirmation ─────────────────────────────────────────────────
189
+ /**
190
+ * Prompt the user for a mandatory second confirmation before executing a
191
+ * fund-related operation. Controlled by `confirmBeforeTransaction` in config
192
+ * (default: enabled). This confirmation is independent of the `-y` flag and
193
+ * Touch ID — it serves as an extra safety net.
194
+ *
195
+ * @param description Short one-line summary of the operation.
196
+ * @param token Optional token metadata to highlight in the prompt.
197
+ *
198
+ * Exits the process if the user declines.
199
+ */
200
+ export async function requireTransactionConfirmation(description, token) {
201
+ const config = loadConfig();
202
+ if (config.confirmBeforeTransaction === false)
203
+ return;
204
+ console.log('');
205
+ console.log(chalk.yellow('⚠'), chalk.bold('Transaction confirmation'));
206
+ if (token) {
207
+ const ticker = token.symbol ? '$' + token.symbol : undefined;
208
+ const label = [ticker, token.name].filter(Boolean).join(' — ');
209
+ console.log(chalk.dim(' Token : ') + (label ? chalk.bold(label) : chalk.dim('Unknown token')));
210
+ console.log(chalk.dim(' Address : ') + chalk.yellow(token.address));
211
+ }
212
+ console.log(chalk.dim(` Action : ${description}`));
213
+ console.log('');
214
+ const ok = await confirm({
215
+ message: 'Are you sure you want to proceed?',
216
+ default: false,
217
+ });
218
+ if (!ok) {
219
+ console.log(chalk.dim('Operation cancelled.'));
220
+ process.exit(0);
221
+ }
222
+ }
117
223
  // ─── Browser ──────────────────────────────────────────────────────────────────
118
224
  /** Open a URL in the user's default browser (cross-platform). */
119
225
  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.0",
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",