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.
@@ -25,15 +25,12 @@ async function* parseSSE(response) {
25
25
  if (!line)
26
26
  continue;
27
27
  // Handle AI SDK v5 streaming format: "type:value"
28
- // e.g., "0:text", "1:reasoning", "9:tool_call"
29
28
  const colonIndex = line.indexOf(':');
30
29
  if (colonIndex !== -1) {
31
30
  const type = line.slice(0, colonIndex);
32
31
  const data = line.slice(colonIndex + 1);
33
- // Type 0 is text content
34
32
  if (type === '0' && data) {
35
33
  try {
36
- // Data might be JSON-encoded string like "text" or actual JSON
37
34
  const parsed = JSON.parse(data);
38
35
  if (typeof parsed === 'string') {
39
36
  yield parsed;
@@ -46,12 +43,9 @@ async function* parseSSE(response) {
46
43
  }
47
44
  }
48
45
  catch {
49
- // If parsing fails, treat as raw text
50
46
  yield data;
51
47
  }
52
48
  }
53
- // Type 1 is reasoning (can be skipped or shown differently)
54
- // Type 9 is tool_call (can be skipped for now)
55
49
  continue;
56
50
  }
57
51
  // Handle standard SSE format: "data:json"
@@ -70,7 +64,6 @@ async function* parseSSE(response) {
70
64
  yield text;
71
65
  }
72
66
  catch {
73
- // Non-JSON data line — might be raw text
74
67
  if (data)
75
68
  yield data;
76
69
  }
@@ -83,13 +76,13 @@ async function* parseSSE(response) {
83
76
  }
84
77
  }
85
78
  export const chatCommand = new Command('chat')
86
- .description('Chat with Minara AI assistant')
87
- .argument('[message]', 'Send a single message (non-interactive)')
79
+ .description('Chat with Minara AI assistant (interactive REPL when no message given)')
80
+ .argument('[message]', 'Send a single message and exit')
88
81
  .option('-c, --chat-id <id>', 'Continue existing chat')
89
82
  .option('--list', 'List past chats')
90
83
  .option('--history <chatId>', 'Show chat history')
91
84
  .option('--thinking', 'Enable thinking/degen mode')
92
- .option('--deep-research', 'Enable deep research mode')
85
+ .option('--quality', 'Use quality mode instead of the default fast mode')
93
86
  .action(wrapAction(async (messageArg, opts) => {
94
87
  const creds = requireAuth();
95
88
  // ── List chats ───────────────────────────────────────────────────────
@@ -122,43 +115,16 @@ export const chatCommand = new Command('chat')
122
115
  }
123
116
  // ── Chat context ─────────────────────────────────────────────────────
124
117
  let chatId = opts?.chatId;
125
- if (!chatId && !messageArg) {
126
- const mode = await select({
127
- message: 'Chat mode:',
128
- choices: [
129
- { name: 'Start a new conversation', value: 'new' },
130
- { name: 'Continue existing conversation', value: 'continue' },
131
- ],
132
- });
133
- if (mode === 'continue') {
134
- const spin = spinner('Fetching chats…');
135
- const res = await listChats(creds.accessToken);
136
- spin.stop();
137
- const chats = res.data;
138
- if (chats && chats.length > 0) {
139
- chatId = await select({
140
- message: 'Select chat:',
141
- choices: chats.map((c) => ({
142
- name: `${(c.chatId).slice(0, 12)}… ${c.name ?? '(untitled)'}`,
143
- value: c.chatId,
144
- })),
145
- });
146
- }
147
- else {
148
- info('No existing chats. Starting new.');
149
- }
150
- }
151
- }
152
118
  if (!chatId)
153
119
  chatId = randomUUID();
154
- // ── Send single message ──────────────────────────────────────────────
120
+ // ── Stream a response and print to stdout ────────────────────────────
155
121
  async function sendAndPrint(msg) {
156
- process.stdout.write(`${chalk.green.bold('Minara')}: `);
122
+ process.stdout.write(chalk.green.bold('Minara') + chalk.dim(': '));
157
123
  const response = await sendChatStream(creds.accessToken, {
158
124
  chatId,
159
125
  message: { role: 'user', content: msg },
160
126
  thinking: opts?.thinking,
161
- deepresearch: opts?.deepResearch,
127
+ workMode: opts?.quality ? 'quality' : 'fast',
162
128
  chartOptions: { chartsCountRecommendedLimit: 0 },
163
129
  });
164
130
  if (!response.ok) {
@@ -195,35 +161,45 @@ export const chatCommand = new Command('chat')
195
161
  }
196
162
  console.log('\n');
197
163
  }
164
+ // ── Single-shot mode: minara chat "message" ──────────────────────────
198
165
  if (messageArg) {
199
166
  await sendAndPrint(messageArg);
200
167
  return;
201
168
  }
202
- // ── REPL ─────────────────────────────────────────────────────────────
169
+ // ── Interactive REPL mode ────────────────────────────────────────────
170
+ const modeFlags = [
171
+ opts?.quality ? chalk.cyan('quality') : chalk.green('fast'),
172
+ opts?.thinking && chalk.yellow('thinking'),
173
+ ].filter(Boolean);
174
+ const modeStr = modeFlags.length ? ` ${chalk.dim('[')}${modeFlags.join(chalk.dim(', '))}${chalk.dim(']')}` : '';
203
175
  console.log('');
204
- console.log(chalk.bold('Minara AI Chat'));
205
- console.log(chalk.dim('Type your message. "exit" to quit, "/new" for new chat, "/help" for commands.'));
176
+ console.log(chalk.green.bold('Minara AI Chat') +
177
+ chalk.dim(` session:${chatId.slice(0, 8)}`) +
178
+ modeStr);
179
+ console.log(chalk.dim('─'.repeat(50)));
180
+ console.log(chalk.dim('Type a message to chat. /help for commands, Ctrl+C to exit.'));
206
181
  console.log('');
207
182
  const rl = createInterface({ input: process.stdin, output: process.stdout });
208
- // Fix: Pause readline before streaming response to prevent prompt interference
209
183
  async function sendAndPrintWithPause(msg) {
210
- rl.pause(); // Pause readline to prevent prompt interference
184
+ rl.pause();
211
185
  try {
212
186
  await sendAndPrint(msg);
213
187
  }
214
188
  finally {
215
- rl.resume(); // Resume readline after streaming is complete
216
- process.stdout.write('\n'); // Ensure clean line before next prompt
189
+ rl.resume();
190
+ process.stdout.write('\n');
217
191
  }
218
192
  }
219
- const prompt = () => new Promise((resolve) => {
220
- rl.question(chalk.blue('You: '), resolve);
193
+ const ask = () => new Promise((resolve) => rl.question(chalk.blue.bold('>>> '), resolve));
194
+ rl.on('close', () => {
195
+ console.log(chalk.dim('\nGoodbye!'));
196
+ process.exit(0);
221
197
  });
222
- rl.on('close', () => { console.log(chalk.dim('\nGoodbye!')); process.exit(0); });
223
198
  while (true) {
224
- const userMsg = (await prompt()).trim();
199
+ const userMsg = (await ask()).trim();
225
200
  if (!userMsg)
226
201
  continue;
202
+ // ── REPL commands ──────────────────────────────────────────────────
227
203
  if (userMsg.toLowerCase() === 'exit' || userMsg.toLowerCase() === 'quit') {
228
204
  console.log(chalk.dim('Goodbye!'));
229
205
  rl.close();
@@ -231,15 +207,63 @@ export const chatCommand = new Command('chat')
231
207
  }
232
208
  if (userMsg === '/new') {
233
209
  chatId = randomUUID();
234
- info('New conversation started.');
210
+ info(`New conversation started ${chalk.dim(`(session:${chatId.slice(0, 8)})`)}`);
235
211
  continue;
236
212
  }
237
213
  if (userMsg === '/id') {
238
214
  console.log(chalk.dim(`Chat ID: ${chatId}`));
239
215
  continue;
240
216
  }
217
+ if (userMsg === '/continue') {
218
+ const spin = spinner('Fetching chats…');
219
+ const res = await listChats(creds.accessToken);
220
+ spin.stop();
221
+ const chats = res.data;
222
+ if (chats && chats.length > 0) {
223
+ const selected = await select({
224
+ message: 'Select a chat to continue:',
225
+ choices: chats.map((c) => ({
226
+ name: `${(c.chatId).slice(0, 12)}… ${c.name ?? '(untitled)'}`,
227
+ value: c.chatId,
228
+ })),
229
+ });
230
+ chatId = selected;
231
+ info(`Continuing chat ${chalk.dim(`(session:${chatId.slice(0, 8)})`)}`);
232
+ }
233
+ else {
234
+ info('No existing chats found.');
235
+ }
236
+ continue;
237
+ }
238
+ if (userMsg === '/list') {
239
+ const spin = spinner('Fetching chats…');
240
+ const res = await listChats(creds.accessToken);
241
+ spin.stop();
242
+ const chats = res.data;
243
+ if (chats && chats.length > 0) {
244
+ console.log('');
245
+ for (const c of chats) {
246
+ const id = chalk.dim(c.chatId.slice(0, 8));
247
+ const name = c.name ?? chalk.dim('(untitled)');
248
+ const time = c.updatedAt ? chalk.dim(` ${c.updatedAt}`) : '';
249
+ console.log(` ${id} ${name}${time}`);
250
+ }
251
+ console.log('');
252
+ }
253
+ else {
254
+ info('No chats yet.');
255
+ }
256
+ continue;
257
+ }
241
258
  if (userMsg === '/help') {
242
- console.log(chalk.dim(' /new — New conversation\n /id — Show chat ID\n exit — Quit'));
259
+ console.log('');
260
+ console.log(chalk.bold(' Commands:'));
261
+ console.log(chalk.dim(' /new ') + 'Start a new conversation');
262
+ console.log(chalk.dim(' /continue ') + 'Continue an existing conversation');
263
+ console.log(chalk.dim(' /list ') + 'List all historical chats');
264
+ console.log(chalk.dim(' /id ') + 'Show current chat ID');
265
+ console.log(chalk.dim(' exit ') + 'Quit the chat');
266
+ console.log('');
243
267
  continue;
244
268
  }
245
269
  await sendAndPrintWithPause(userMsg);
@@ -1,26 +1,38 @@
1
1
  import { Command } from 'commander';
2
- import { input, select } from '@inquirer/prompts';
2
+ import { input, select, confirm } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
4
  import { loadConfig, saveConfig, getMinaraDir } from '../config.js';
5
- import { success, info, wrapAction } from '../utils.js';
5
+ import { success, info, warn, wrapAction } from '../utils.js';
6
+ import { isTouchIdAvailable } from '../touchid.js';
6
7
  export const configCommand = new Command('config')
7
8
  .description('View or update CLI configuration')
8
9
  .action(wrapAction(async () => {
10
+ const config = loadConfig();
11
+ const confirmTx = config.confirmBeforeTransaction !== false;
9
12
  const action = await select({
10
13
  message: 'Configuration:',
11
14
  choices: [
12
15
  { name: 'Show current config', value: 'show' },
13
16
  { name: 'Set base URL', value: 'baseUrl' },
17
+ {
18
+ name: `Touch ID ${config.touchId ? chalk.green('[ON]') : chalk.dim('[OFF]')}`,
19
+ value: 'touchId',
20
+ },
21
+ {
22
+ name: `Transaction Confirmation ${confirmTx ? chalk.green('[ON]') : chalk.dim('[OFF]')}`,
23
+ value: 'confirmTx',
24
+ },
14
25
  { name: 'Show config directory path', value: 'path' },
15
26
  ],
16
27
  });
17
- const config = loadConfig();
18
28
  switch (action) {
19
29
  case 'show':
20
30
  console.log('');
21
31
  console.log(chalk.bold('Current Configuration:'));
22
- console.log(` Base URL : ${chalk.cyan(config.baseUrl)}`);
23
- console.log(` Config Dir : ${chalk.dim(getMinaraDir())}`);
32
+ console.log(` Base URL : ${chalk.cyan(config.baseUrl)}`);
33
+ console.log(` Touch ID : ${config.touchId ? chalk.green('Enabled') : chalk.dim('Disabled')}`);
34
+ console.log(` Confirm Tx : ${confirmTx ? chalk.green('Enabled') : chalk.dim('Disabled')}`);
35
+ console.log(` Config Dir : ${chalk.dim(getMinaraDir())}`);
24
36
  console.log('');
25
37
  break;
26
38
  case 'baseUrl': {
@@ -41,6 +53,71 @@ export const configCommand = new Command('config')
41
53
  success(`Base URL set to ${url}`);
42
54
  break;
43
55
  }
56
+ case 'touchId': {
57
+ if (config.touchId) {
58
+ // Currently enabled — offer to disable
59
+ const disable = await confirm({
60
+ message: 'Touch ID is currently enabled. Disable it?',
61
+ default: false,
62
+ });
63
+ if (disable) {
64
+ saveConfig({ touchId: false });
65
+ success('Touch ID protection disabled.');
66
+ }
67
+ }
68
+ else {
69
+ // Currently disabled — check availability then enable
70
+ console.log('');
71
+ info('Checking Touch ID availability…');
72
+ if (!isTouchIdAvailable()) {
73
+ console.log('');
74
+ warn('Touch ID is not available on this device.');
75
+ console.log(chalk.dim(' Make sure you are on a Mac with Touch ID and have enrolled at least one fingerprint.'));
76
+ console.log('');
77
+ break;
78
+ }
79
+ console.log(chalk.green('✔'), 'Touch ID hardware detected.');
80
+ console.log('');
81
+ console.log(chalk.dim(' When enabled, all fund-related operations (transfer, withdraw, swap, order, etc.)'));
82
+ console.log(chalk.dim(' will require Touch ID verification before execution.'));
83
+ console.log('');
84
+ const enable = await confirm({
85
+ message: 'Enable Touch ID protection?',
86
+ default: true,
87
+ });
88
+ if (enable) {
89
+ saveConfig({ touchId: true });
90
+ success('Touch ID protection enabled!');
91
+ console.log(chalk.dim(' All fund-related operations now require fingerprint verification.'));
92
+ }
93
+ }
94
+ break;
95
+ }
96
+ case 'confirmTx': {
97
+ if (confirmTx) {
98
+ const disable = await confirm({
99
+ message: 'Transaction confirmation is currently enabled. Disable it?',
100
+ default: false,
101
+ });
102
+ if (disable) {
103
+ saveConfig({ confirmBeforeTransaction: false });
104
+ success('Transaction confirmation disabled.');
105
+ warn('Fund-related operations will no longer require a second confirmation.');
106
+ }
107
+ }
108
+ else {
109
+ const enable = await confirm({
110
+ message: 'Enable transaction confirmation for fund-related operations?',
111
+ default: true,
112
+ });
113
+ if (enable) {
114
+ saveConfig({ confirmBeforeTransaction: true });
115
+ success('Transaction confirmation enabled!');
116
+ console.log(chalk.dim(' All fund-related operations now require a second confirmation before execution.'));
117
+ }
118
+ }
119
+ break;
120
+ }
44
121
  case 'path':
45
122
  info(`Config directory: ${getMinaraDir()}`);
46
123
  break;
@@ -3,7 +3,9 @@ import { input, select, confirm, number as numberPrompt } from '@inquirer/prompt
3
3
  import chalk from 'chalk';
4
4
  import * as ctApi from '../api/copytrade.js';
5
5
  import { requireAuth } from '../config.js';
6
- import { success, info, spinner, assertApiOk, selectChain, wrapAction } from '../utils.js';
6
+ import { success, info, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation } from '../utils.js';
7
+ import { requireTouchId } from '../touchid.js';
8
+ import { printTxResult, printTable, COPY_TRADE_COLUMNS } from '../formatters.js';
7
9
  // ─── create ──────────────────────────────────────────────────────────────
8
10
  const createCmd = new Command('create')
9
11
  .description('Create a copy trade bot')
@@ -48,6 +50,8 @@ const createCmd = new Command('create')
48
50
  if (!ok)
49
51
  return;
50
52
  }
53
+ await requireTransactionConfirmation(`Copy trade · $${fixedAmount}/trade · target ${targetAddress} · ${chain}`);
54
+ await requireTouchId();
51
55
  const spin = spinner('Creating copy trade…');
52
56
  const res = await ctApi.createCopyTrade(creds.accessToken, {
53
57
  chain, targetAddress, name,
@@ -60,8 +64,7 @@ const createCmd = new Command('create')
60
64
  spin.stop();
61
65
  assertApiOk(res, 'Failed to create copy trade');
62
66
  success('Copy trade created!');
63
- if (res.data)
64
- console.log(JSON.stringify(res.data, null, 2));
67
+ printTxResult(res.data);
65
68
  }));
66
69
  // ─── list ────────────────────────────────────────────────────────────────
67
70
  const listCmd = new Command('list')
@@ -78,7 +81,10 @@ const listCmd = new Command('list')
78
81
  console.log(chalk.dim('No copy trades.'));
79
82
  return;
80
83
  }
81
- console.log(JSON.stringify(data, null, 2));
84
+ console.log('');
85
+ console.log(chalk.bold('Copy Trades:'));
86
+ printTable(data, COPY_TRADE_COLUMNS);
87
+ console.log('');
82
88
  }));
83
89
  // ─── start / stop ────────────────────────────────────────────────────────
84
90
  async function pickCopyTrade(token) {
@@ -6,6 +6,7 @@ import { getCurrentUser } from '../api/auth.js';
6
6
  import { getAccount } from '../api/crosschain.js';
7
7
  import { requireAuth } from '../config.js';
8
8
  import { info, spinner, unwrapApi, wrapAction } from '../utils.js';
9
+ import { printKV } from '../formatters.js';
9
10
  /**
10
11
  * Map wallet type keys from /auth/me → human-readable chain info.
11
12
  */
@@ -73,7 +74,10 @@ export const depositCommand = new Command('deposit')
73
74
  default: false,
74
75
  });
75
76
  if (wantDetails) {
76
- console.log(JSON.stringify(accountRes.data, null, 2));
77
+ console.log('');
78
+ console.log(chalk.bold('Account Details:'));
79
+ printKV(accountRes.data);
80
+ console.log('');
77
81
  }
78
82
  }
79
83
  }));
@@ -1,7 +1,9 @@
1
1
  import { Command } from 'commander';
2
2
  import { input, select } from '@inquirer/prompts';
3
+ import chalk from 'chalk';
3
4
  import { searchTokens, getTrendingTokens, searchStocks, getFearGreedIndex, getBitcoinMetrics } from '../api/tokens.js';
4
5
  import { spinner, assertApiOk, wrapAction } from '../utils.js';
6
+ import { printKV, printTable, printFearGreed, printCryptoMetrics, TOKEN_COLUMNS } from '../formatters.js';
5
7
  // ─── trending ────────────────────────────────────────────────────────────
6
8
  const trendingCmd = new Command('trending')
7
9
  .description('View trending tokens')
@@ -10,7 +12,15 @@ const trendingCmd = new Command('trending')
10
12
  const res = await getTrendingTokens();
11
13
  spin.stop();
12
14
  assertApiOk(res, 'Failed to fetch trending tokens');
13
- console.log(JSON.stringify(res.data, null, 2));
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);
19
+ }
20
+ else if (res.data && typeof res.data === 'object') {
21
+ printKV(res.data);
22
+ }
23
+ console.log('');
14
24
  }));
15
25
  // ─── search ──────────────────────────────────────────────────────────────
16
26
  const searchCmd = new Command('search')
@@ -31,7 +41,18 @@ const searchCmd = new Command('search')
31
41
  : await searchStocks(keyword);
32
42
  spin.stop();
33
43
  assertApiOk(res, `Search for "${keyword}" failed`);
34
- console.log(JSON.stringify(res.data, null, 2));
44
+ console.log('');
45
+ console.log(chalk.bold(`Search Results for "${keyword}":`));
46
+ if (Array.isArray(res.data) && res.data.length > 0) {
47
+ printTable(res.data, category === 'tokens' ? TOKEN_COLUMNS : undefined);
48
+ }
49
+ else if (Array.isArray(res.data)) {
50
+ console.log(chalk.dim(' No results found.'));
51
+ }
52
+ else if (res.data && typeof res.data === 'object') {
53
+ printKV(res.data);
54
+ }
55
+ console.log('');
35
56
  }));
36
57
  // ─── fear-greed ──────────────────────────────────────────────────────────
37
58
  const fearGreedCmd = new Command('fear-greed')
@@ -41,7 +62,10 @@ const fearGreedCmd = new Command('fear-greed')
41
62
  const res = await getFearGreedIndex();
42
63
  spin.stop();
43
64
  assertApiOk(res, 'Failed to fetch Fear & Greed Index');
44
- console.log(JSON.stringify(res.data, null, 2));
65
+ console.log('');
66
+ console.log(chalk.bold('Fear & Greed Index:'));
67
+ printFearGreed(res.data);
68
+ console.log('');
45
69
  }));
46
70
  // ─── btc metrics ─────────────────────────────────────────────────────────
47
71
  const btcCmd = new Command('btc-metrics')
@@ -51,7 +75,10 @@ const btcCmd = new Command('btc-metrics')
51
75
  const res = await getBitcoinMetrics();
52
76
  spin.stop();
53
77
  assertApiOk(res, 'Failed to fetch Bitcoin metrics');
54
- console.log(JSON.stringify(res.data, null, 2));
78
+ console.log('');
79
+ console.log(chalk.bold('Bitcoin Metrics:'));
80
+ printCryptoMetrics(res.data);
81
+ console.log('');
55
82
  }));
56
83
  // ─── parent ──────────────────────────────────────────────────────────────
57
84
  export const discoverCommand = new Command('discover')
@@ -3,7 +3,9 @@ import { input, select, confirm, number as numberPrompt } from '@inquirer/prompt
3
3
  import chalk from 'chalk';
4
4
  import * as loApi from '../api/limitorder.js';
5
5
  import { requireAuth } from '../config.js';
6
- import { success, info, spinner, assertApiOk, selectChain, wrapAction } from '../utils.js';
6
+ import { success, info, spinner, assertApiOk, selectChain, wrapAction, requireTransactionConfirmation, lookupToken, formatTokenLabel } from '../utils.js';
7
+ import { requireTouchId } from '../touchid.js';
8
+ import { printTxResult, printTable, LIMIT_ORDER_COLUMNS } from '../formatters.js';
7
9
  // ─── create ──────────────────────────────────────────────────────────────
8
10
  const createCmd = new Command('create')
9
11
  .description('Create a limit order')
@@ -18,10 +20,11 @@ const createCmd = new Command('create')
18
20
  { name: 'Sell', value: 'sell' },
19
21
  ],
20
22
  });
21
- const targetTokenCA = await input({
22
- message: 'Target token contract address:',
23
+ const tokenInput = await input({
24
+ message: 'Target token (contract address or ticker):',
23
25
  validate: (v) => (v.length > 0 ? true : 'Required'),
24
26
  });
27
+ const tokenInfo = await lookupToken(tokenInput);
25
28
  const priceCondition = await select({
26
29
  message: 'Trigger when price is:',
27
30
  choices: [
@@ -40,7 +43,8 @@ const createCmd = new Command('create')
40
43
  console.log(chalk.bold('Limit Order:'));
41
44
  console.log(` Chain : ${chalk.cyan(chain)}`);
42
45
  console.log(` Side : ${side}`);
43
- console.log(` Token : ${chalk.yellow(targetTokenCA)}`);
46
+ console.log(` Token : ${formatTokenLabel(tokenInfo)}`);
47
+ console.log(` Address : ${chalk.yellow(tokenInfo.address)}`);
44
48
  console.log(` Condition : price ${priceCondition} $${targetPrice}`);
45
49
  console.log(` Amount : $${amount}`);
46
50
  console.log(` Expires : ${new Date(expiredAt * 1000).toLocaleString()}`);
@@ -50,16 +54,17 @@ const createCmd = new Command('create')
50
54
  if (!ok)
51
55
  return;
52
56
  }
57
+ await requireTransactionConfirmation(`Limit ${side} · $${amount} · price ${priceCondition} $${targetPrice} · ${chain}`, tokenInfo);
58
+ await requireTouchId();
53
59
  const spin = spinner('Creating limit order…');
54
60
  const res = await loApi.createLimitOrder(creds.accessToken, {
55
- chain, side, amount, targetTokenCA,
61
+ chain, side, amount, targetTokenCA: tokenInfo.address,
56
62
  priceCondition, targetPrice: targetPrice, expiredAt,
57
63
  });
58
64
  spin.stop();
59
65
  assertApiOk(res, 'Failed to create limit order');
60
66
  success('Limit order created!');
61
- if (res.data)
62
- console.log(JSON.stringify(res.data, null, 2));
67
+ printTxResult(res.data);
63
68
  }));
64
69
  // ─── list ────────────────────────────────────────────────────────────────
65
70
  const listCmd = new Command('list')
@@ -76,7 +81,10 @@ const listCmd = new Command('list')
76
81
  console.log(chalk.dim('No limit orders.'));
77
82
  return;
78
83
  }
79
- console.log(JSON.stringify(orders, null, 2));
84
+ console.log('');
85
+ console.log(chalk.bold('Limit Orders:'));
86
+ printTable(orders, LIMIT_ORDER_COLUMNS);
87
+ console.log('');
80
88
  }));
81
89
  // ─── cancel ──────────────────────────────────────────────────────────────
82
90
  const cancelCmd = new Command('cancel')
@@ -2,10 +2,11 @@ import { Command } from 'commander';
2
2
  import { input, select, confirm } from '@inquirer/prompts';
3
3
  import chalk from 'chalk';
4
4
  import { sendEmailCode, verifyEmailCode, getOAuthUrl, getCurrentUser, startDeviceAuth, getDeviceAuthStatus, } from '../api/auth.js';
5
- import { saveCredentials, loadCredentials } from '../config.js';
5
+ import { saveCredentials, loadCredentials, loadConfig, saveConfig } from '../config.js';
6
6
  import { success, error, info, warn, spinner, openBrowser, unwrapApi, wrapAction } from '../utils.js';
7
7
  import { startOAuthServer } from '../oauth-server.js';
8
8
  import { OAUTH_PROVIDERS } from '../types.js';
9
+ import { isTouchIdAvailable } from '../touchid.js';
9
10
  // ─── Email login flow ─────────────────────────────────────────────────────
10
11
  async function loginWithEmail(emailOpt) {
11
12
  const email = emailOpt ?? await input({
@@ -219,4 +220,19 @@ export const loginCommand = new Command('login')
219
220
  else {
220
221
  await loginWithOAuth(method);
221
222
  }
223
+ // ── Offer Touch ID setup (macOS only, if not already enabled) ────
224
+ const config = loadConfig();
225
+ if (!config.touchId && isTouchIdAvailable()) {
226
+ console.log('');
227
+ const enableTouchId = await confirm({
228
+ message: 'Enable Touch ID to protect fund operations (transfer, withdraw, swap, etc.)?',
229
+ default: true,
230
+ });
231
+ if (enableTouchId) {
232
+ saveConfig({ touchId: true });
233
+ success('Touch ID protection enabled!');
234
+ console.log(chalk.dim(' All fund-related operations now require fingerprint verification.'));
235
+ console.log(chalk.dim(` To disable, run: ${chalk.cyan('minara config')} → Touch ID`));
236
+ }
237
+ }
222
238
  }));