@emblemvault/agentwallet 1.3.0 → 3.0.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,754 @@
1
+ /**
2
+ * commands.js — Slash command router for Emblem Enhanced TUI
3
+ *
4
+ * Handles all /commands in both TUI and simple-readline modes.
5
+ * Each command receives a context object with the full runtime state.
6
+ */
7
+
8
+ import chalk from 'chalk';
9
+
10
+ // ============================================================================
11
+ // Command Registry (for /help display)
12
+ // ============================================================================
13
+
14
+ export const COMMANDS = [
15
+ { cmd: '/help', desc: 'Show all commands' },
16
+ { cmd: '/plugins', desc: 'List all plugins with status' },
17
+ { cmd: '/plugin <name> on|off', desc: 'Toggle plugin' },
18
+ { cmd: '/tools', desc: 'List available tools' },
19
+ { cmd: '/tools add|remove <id>', desc: 'Manage tools' },
20
+ { cmd: '/tools clear', desc: 'Enable auto-tools mode' },
21
+ { cmd: '/auth', desc: 'Authentication menu' },
22
+ { cmd: '/wallet', desc: 'Show wallet info' },
23
+ { cmd: '/portfolio', desc: 'Show portfolio' },
24
+ { cmd: '/settings', desc: 'Show current settings' },
25
+ { cmd: '/model <id>', desc: 'Set model (or "clear")' },
26
+ { cmd: '/stream on|off', desc: 'Toggle streaming' },
27
+ { cmd: '/debug on|off', desc: 'Toggle debug mode' },
28
+ { cmd: '/history on|off', desc: 'Toggle history' },
29
+ { cmd: '/payment', desc: 'PAYG billing status' },
30
+ { cmd: '/payment enable|disable', desc: 'Toggle PAYG' },
31
+ { cmd: '/payment token <T>', desc: 'Set payment token' },
32
+ { cmd: '/payment mode <M>', desc: 'Set payment mode' },
33
+ { cmd: '/secrets', desc: 'Manage encrypted plugin secrets' },
34
+ { cmd: '/glow on|off', desc: 'Toggle markdown rendering' },
35
+ { cmd: '/log on|off', desc: 'Toggle stream logging to file' },
36
+ { cmd: '/reset', desc: 'Clear conversation' },
37
+ { cmd: '/exit', desc: 'Exit' },
38
+ ];
39
+
40
+ // ============================================================================
41
+ // Command Handlers
42
+ // ============================================================================
43
+
44
+ function cmdHelp(ctx) {
45
+ const maxCmd = Math.max(...COMMANDS.map(c => c.cmd.length));
46
+ const lines = COMMANDS.map(c => {
47
+ const padded = c.cmd.padEnd(maxCmd + 2);
48
+ return ` ${chalk.cyan(padded)} ${chalk.dim(c.desc)}`;
49
+ });
50
+ const header = chalk.bold.white('Available Commands');
51
+ const text = `\n${header}\n${chalk.dim('─'.repeat(maxCmd + 30))}\n${lines.join('\n')}\n`;
52
+ ctx.appendMessage('system', text);
53
+ return { handled: true };
54
+ }
55
+
56
+ function cmdPlugins(ctx) {
57
+ const plugins = ctx.pluginManager.list();
58
+ if (!plugins || plugins.length === 0) {
59
+ ctx.appendMessage('system', chalk.dim('No plugins registered.'));
60
+ return { handled: true };
61
+ }
62
+ const lines = plugins.map(p => {
63
+ const icon = p.enabled ? chalk.green('\u2611') : chalk.dim('\u2610');
64
+ const name = p.enabled ? chalk.white(p.name) : chalk.dim(p.name);
65
+ const ver = chalk.dim(`v${p.version}`);
66
+ const tools = chalk.dim(`(${p.toolCount} tools)`);
67
+ return ` ${icon} ${name} ${ver} ${tools}`;
68
+ });
69
+ const header = chalk.bold.white('Plugins');
70
+ ctx.appendMessage('system', `\n${header}\n${lines.join('\n')}\n`);
71
+ return { handled: true };
72
+ }
73
+
74
+ function cmdPlugin(args, ctx) {
75
+ const parts = args.trim().split(/\s+/);
76
+ if (parts.length < 2) {
77
+ ctx.appendMessage('system', chalk.yellow('Usage: /plugin <name> on|off'));
78
+ return { handled: true };
79
+ }
80
+ const name = parts[0];
81
+ const action = parts[1].toLowerCase();
82
+
83
+ if (action === 'on') {
84
+ const ok = ctx.pluginManager.enable(name);
85
+ if (ok) {
86
+ ctx.appendMessage('system', chalk.green(`Plugin "${name}" enabled.`));
87
+ ctx.addLog('plugin', `Enabled ${name}`);
88
+ } else {
89
+ ctx.appendMessage('system', chalk.red(`Plugin "${name}" not found.`));
90
+ }
91
+ } else if (action === 'off') {
92
+ const ok = ctx.pluginManager.disable(name);
93
+ if (ok) {
94
+ ctx.appendMessage('system', chalk.yellow(`Plugin "${name}" disabled.`));
95
+ ctx.addLog('plugin', `Disabled ${name}`);
96
+ } else {
97
+ ctx.appendMessage('system', chalk.red(`Plugin "${name}" not found.`));
98
+ }
99
+ } else {
100
+ ctx.appendMessage('system', chalk.yellow('Usage: /plugin <name> on|off'));
101
+ return { handled: true };
102
+ }
103
+
104
+ ctx.updateSidebar();
105
+ return { handled: true };
106
+ }
107
+
108
+ async function cmdTools(args, ctx) {
109
+ const parts = args.trim().split(/\s+/).filter(Boolean);
110
+
111
+ // /tools — list all
112
+ if (parts.length === 0) {
113
+ try {
114
+ let tools;
115
+ if (ctx.client && typeof ctx.client.getTools === 'function') {
116
+ tools = await ctx.client.getTools();
117
+ } else {
118
+ tools = ctx.pluginManager.getTools();
119
+ }
120
+ if (!tools || tools.length === 0) {
121
+ ctx.appendMessage('system', chalk.dim('No tools available.'));
122
+ return { handled: true };
123
+ }
124
+ const lines = tools.map(t => {
125
+ const id = t.id || t.name || 'unknown';
126
+ const isSelected = ctx.settings.selectedTools.includes(id);
127
+ const icon = isSelected ? chalk.green('[x]') : chalk.dim('[ ]');
128
+ const title = t.title || t.name || id;
129
+ const desc = t.description ? chalk.dim(` — ${t.description}`) : '';
130
+ return ` ${icon} ${chalk.white(title)} ${chalk.dim(`(${id})`)}${desc}`;
131
+ });
132
+ const mode = ctx.settings.selectedTools.length > 0
133
+ ? chalk.cyan(ctx.settings.selectedTools.join(', '))
134
+ : chalk.dim('Auto-tools mode');
135
+ const header = chalk.bold.white('Tools');
136
+ ctx.appendMessage('system', `\n${header}\n${lines.join('\n')}\n\n ${chalk.dim('Selected:')} ${mode}\n`);
137
+ } catch (err) {
138
+ ctx.appendMessage('system', chalk.red(`Error fetching tools: ${err.message}`));
139
+ }
140
+ return { handled: true };
141
+ }
142
+
143
+ const sub = parts[0];
144
+ const toolId = parts[1];
145
+
146
+ // /tools clear
147
+ if (sub === 'clear') {
148
+ ctx.settings.selectedTools = [];
149
+ ctx.appendMessage('system', chalk.green('Auto-tools mode enabled.'));
150
+ ctx.addLog('tools', 'Cleared tool selection');
151
+ return { handled: true };
152
+ }
153
+
154
+ // /tools add <id>
155
+ if (sub === 'add' && toolId) {
156
+ if (!ctx.settings.selectedTools.includes(toolId)) {
157
+ ctx.settings.selectedTools.push(toolId);
158
+ ctx.appendMessage('system', chalk.green(`Added tool: ${toolId}`));
159
+ ctx.addLog('tools', `Added ${toolId}`);
160
+ } else {
161
+ ctx.appendMessage('system', chalk.yellow(`Already selected: ${toolId}`));
162
+ }
163
+ return { handled: true };
164
+ }
165
+
166
+ // /tools remove <id>
167
+ if (sub === 'remove' && toolId) {
168
+ const idx = ctx.settings.selectedTools.indexOf(toolId);
169
+ if (idx > -1) {
170
+ ctx.settings.selectedTools.splice(idx, 1);
171
+ ctx.appendMessage('system', chalk.green(`Removed tool: ${toolId}`));
172
+ ctx.addLog('tools', `Removed ${toolId}`);
173
+ } else {
174
+ ctx.appendMessage('system', chalk.yellow(`Not found: ${toolId}`));
175
+ }
176
+ return { handled: true };
177
+ }
178
+
179
+ ctx.appendMessage('system', chalk.yellow('Usage: /tools [add|remove <id>|clear]'));
180
+ return { handled: true };
181
+ }
182
+
183
+ async function cmdAuth(ctx) {
184
+ // In TUI mode, show auth summary in chat panel
185
+ if (ctx.tui) {
186
+ try {
187
+ const sess = ctx.authSdk.getSession();
188
+ const lines = [
189
+ chalk.bold.white('Authentication Info'),
190
+ '',
191
+ ` ${chalk.dim('Vault ID:')} ${sess?.user?.vaultId || 'N/A'}`,
192
+ ` ${chalk.dim('Auth Type:')} ${sess?.authType || 'Password'}`,
193
+ ` ${chalk.dim('App ID:')} ${sess?.appId || 'emblem-agent-wallet'}`,
194
+ ` ${chalk.dim('Expires:')} ${sess?.expiresAt ? new Date(sess.expiresAt).toISOString() : 'N/A'}`,
195
+ '',
196
+ chalk.dim('Use /wallet for address info'),
197
+ ];
198
+ ctx.appendMessage('system', lines.join('\n'));
199
+ } catch (err) {
200
+ ctx.appendMessage('system', chalk.red(`Auth error: ${err.message}`));
201
+ }
202
+ return { handled: true };
203
+ }
204
+
205
+ // Simple mode — import and run the interactive auth menu
206
+ try {
207
+ const { authMenu } = await import('./auth.js');
208
+ const result = await authMenu(ctx.authSdk, ctx.promptText);
209
+ if (result === 'logout') {
210
+ return { handled: true, logout: true };
211
+ }
212
+ } catch (err) {
213
+ ctx.appendMessage('system', chalk.red(`Auth menu error: ${err.message}`));
214
+ }
215
+ return { handled: true };
216
+ }
217
+
218
+ async function cmdWallet(ctx) {
219
+ try {
220
+ const vaultInfo = await ctx.authSdk.getVaultInfo();
221
+ const lines = [
222
+ chalk.bold.white('Wallet Info'),
223
+ '',
224
+ ` ${chalk.dim('Vault ID:')} ${vaultInfo.vaultId || 'N/A'}`,
225
+ ` ${chalk.dim('Token ID:')} ${vaultInfo.tokenId || vaultInfo.vaultId || 'N/A'}`,
226
+ ];
227
+ if (vaultInfo.evmAddress) {
228
+ lines.push(` ${chalk.dim('EVM Address:')} ${chalk.white(vaultInfo.evmAddress)}`);
229
+ }
230
+ const solAddr = vaultInfo.solanaAddress || vaultInfo.address;
231
+ if (solAddr) {
232
+ lines.push(` ${chalk.dim('Solana Address:')} ${chalk.white(solAddr)}`);
233
+ }
234
+ if (vaultInfo.hederaAccountId) {
235
+ lines.push(` ${chalk.dim('Hedera Account:')} ${chalk.white(vaultInfo.hederaAccountId)}`);
236
+ }
237
+ if (vaultInfo.btcAddresses) {
238
+ lines.push(` ${chalk.dim('BTC Addresses:')}`);
239
+ if (vaultInfo.btcAddresses.p2pkh)
240
+ lines.push(` ${chalk.dim('P2PKH:')} ${vaultInfo.btcAddresses.p2pkh}`);
241
+ if (vaultInfo.btcAddresses.p2wpkh)
242
+ lines.push(` ${chalk.dim('P2WPKH:')} ${vaultInfo.btcAddresses.p2wpkh}`);
243
+ if (vaultInfo.btcAddresses.p2tr)
244
+ lines.push(` ${chalk.dim('P2TR:')} ${vaultInfo.btcAddresses.p2tr}`);
245
+ }
246
+ if (vaultInfo.createdAt) {
247
+ lines.push(` ${chalk.dim('Created:')} ${vaultInfo.createdAt}`);
248
+ }
249
+ lines.push('');
250
+ ctx.appendMessage('system', lines.join('\n'));
251
+ ctx.addLog('wallet', 'Displayed wallet info');
252
+ } catch (err) {
253
+ ctx.appendMessage('system', chalk.red(`Wallet error: ${err.message}`));
254
+ }
255
+ return { handled: true };
256
+ }
257
+
258
+ function cmdPortfolio() {
259
+ // Return handled:false so the main loop sends "show my portfolio" as a chat message
260
+ return { handled: false };
261
+ }
262
+
263
+ function cmdSettings(ctx) {
264
+ const sess = ctx.authSdk.getSession();
265
+ const lines = [
266
+ chalk.bold.white('Current Settings'),
267
+ '',
268
+ ` ${chalk.dim('App ID:')} emblem-agent-wallet`,
269
+ ` ${chalk.dim('Vault ID:')} ${sess?.user?.vaultId || 'N/A'}`,
270
+ ` ${chalk.dim('Auth Mode:')} Password (headless)`,
271
+ ` ${chalk.dim('Model:')} ${ctx.settings.model || chalk.dim('API default')}`,
272
+ ` ${chalk.dim('Streaming:')} ${ctx.settings.stream ? chalk.green('ON') : chalk.red('OFF')}`,
273
+ ` ${chalk.dim('Debug:')} ${ctx.settings.debug ? chalk.green('ON') : chalk.red('OFF')}`,
274
+ ` ${chalk.dim('History:')} ${ctx.settings.retainHistory ? chalk.green('ON') : chalk.red('OFF')}`,
275
+ ` ${chalk.dim('Glow:')} ${ctx.settings.glowEnabled ? chalk.green('ON') : chalk.dim('OFF')}`,
276
+ ` ${chalk.dim('Logging:')} ${ctx.settings.log ? chalk.green('ON') + chalk.dim(` → ${ctx.LOG_FILE}`) : chalk.dim('OFF')}`,
277
+ ` ${chalk.dim('Messages:')} ${ctx.history.messages.length}`,
278
+ ` ${chalk.dim('Tools:')} ${ctx.settings.selectedTools.length > 0 ? chalk.cyan(ctx.settings.selectedTools.join(', ')) : chalk.dim('Auto-tools mode')}`,
279
+ '',
280
+ ];
281
+ ctx.appendMessage('system', lines.join('\n'));
282
+ return { handled: true };
283
+ }
284
+
285
+ function cmdModel(args, ctx) {
286
+ const modelArg = args.trim();
287
+
288
+ // /model — show current
289
+ if (!modelArg) {
290
+ ctx.appendMessage('system', `${chalk.dim('Current model:')} ${ctx.settings.model || chalk.dim('API default')}`);
291
+ return { handled: true };
292
+ }
293
+
294
+ // /model clear
295
+ if (modelArg === 'clear') {
296
+ ctx.settings.model = null;
297
+ ctx.appendMessage('system', chalk.green('Model selection cleared. Using API default.'));
298
+ ctx.addLog('model', 'Cleared model');
299
+ return { handled: true };
300
+ }
301
+
302
+ // /model <id>
303
+ ctx.settings.model = modelArg;
304
+ ctx.appendMessage('system', chalk.green(`Model set to: ${modelArg}`));
305
+ ctx.addLog('model', `Set to ${modelArg}`);
306
+ return { handled: true };
307
+ }
308
+
309
+ function cmdStream(args, ctx) {
310
+ const val = args.trim().toLowerCase();
311
+ if (val === 'on') {
312
+ ctx.settings.stream = true;
313
+ ctx.appendMessage('system', chalk.green('Streaming enabled.'));
314
+ } else if (val === 'off') {
315
+ ctx.settings.stream = false;
316
+ ctx.appendMessage('system', chalk.yellow('Streaming disabled.'));
317
+ } else {
318
+ ctx.appendMessage('system', `${chalk.dim('Streaming:')} ${ctx.settings.stream ? chalk.green('ON') : chalk.red('OFF')}`);
319
+ }
320
+ return { handled: true };
321
+ }
322
+
323
+ function cmdDebug(args, ctx) {
324
+ const val = args.trim().toLowerCase();
325
+ if (val === 'on') {
326
+ ctx.settings.debug = true;
327
+ ctx.appendMessage('system', chalk.green('Debug mode enabled.'));
328
+ } else if (val === 'off') {
329
+ ctx.settings.debug = false;
330
+ ctx.appendMessage('system', chalk.yellow('Debug mode disabled.'));
331
+ } else {
332
+ ctx.appendMessage('system', `${chalk.dim('Debug:')} ${ctx.settings.debug ? chalk.green('ON') : chalk.red('OFF')}`);
333
+ }
334
+ return { handled: true };
335
+ }
336
+
337
+ function cmdHistory(args, ctx) {
338
+ const val = args.trim().toLowerCase();
339
+ if (val === 'on') {
340
+ ctx.settings.retainHistory = true;
341
+ ctx.appendMessage('system', chalk.green('History retention enabled.'));
342
+ } else if (val === 'off') {
343
+ ctx.settings.retainHistory = false;
344
+ ctx.appendMessage('system', chalk.yellow('History retention disabled.'));
345
+ } else {
346
+ const count = ctx.history.messages.length;
347
+ const lines = [
348
+ `${chalk.dim('History:')} ${ctx.settings.retainHistory ? chalk.green('ON') : chalk.red('OFF')}`,
349
+ `${chalk.dim('Messages:')} ${count}`,
350
+ ];
351
+ if (count > 0) {
352
+ lines.push('', chalk.dim('Recent:'));
353
+ ctx.history.messages.slice(-4).forEach(m => {
354
+ const preview = m.content.substring(0, 60) + (m.content.length > 60 ? '...' : '');
355
+ lines.push(` ${chalk.cyan(m.role)}: ${preview}`);
356
+ });
357
+ }
358
+ ctx.appendMessage('system', lines.join('\n'));
359
+ }
360
+ return { handled: true };
361
+ }
362
+
363
+ async function cmdPayment(args, ctx) {
364
+ const parts = args.trim().split(/\s+/).filter(Boolean);
365
+
366
+ // /payment — show status
367
+ if (parts.length === 0) {
368
+ try {
369
+ const status = await ctx.client.getPaygStatus();
370
+ const lines = [
371
+ chalk.bold.white('PAYG Billing Status'),
372
+ '',
373
+ ` ${chalk.dim('Enabled:')} ${status.enabled ? chalk.green('YES') : chalk.red('NO')}`,
374
+ ` ${chalk.dim('Mode:')} ${status.mode || 'N/A'}`,
375
+ ` ${chalk.dim('Payment Token:')} ${status.payment_token || 'N/A'}`,
376
+ ` ${chalk.dim('Payment Chain:')} ${status.payment_chain || 'N/A'}`,
377
+ ` ${chalk.dim('Blocked:')} ${status.is_blocked ? chalk.red('YES') : chalk.green('NO')}`,
378
+ ` ${chalk.dim('Total Debt:')} $${(status.total_debt_usd || 0).toFixed(4)}`,
379
+ ` ${chalk.dim('Total Paid:')} $${(status.total_paid_usd || 0).toFixed(4)}`,
380
+ ` ${chalk.dim('Debt Ceiling:')} $${(status.debt_ceiling_usd || 0).toFixed(2)}`,
381
+ ` ${chalk.dim('Pending Charges:')} ${status.pending_charges || 0}`,
382
+ ];
383
+ if (status.available_tokens && status.available_tokens.length > 0) {
384
+ lines.push('', ` ${chalk.dim('Available Tokens:')} ${status.available_tokens.join(', ')}`);
385
+ }
386
+ lines.push('');
387
+ ctx.appendMessage('system', lines.join('\n'));
388
+ } catch (err) {
389
+ ctx.appendMessage('system', chalk.red(`PAYG error: ${err.message}`));
390
+ }
391
+ return { handled: true };
392
+ }
393
+
394
+ const sub = parts[0];
395
+
396
+ // /payment enable
397
+ if (sub === 'enable') {
398
+ try {
399
+ const result = await ctx.client.configurePayg({ enabled: true });
400
+ ctx.appendMessage('system', result.success
401
+ ? chalk.green('PAYG billing enabled.')
402
+ : chalk.red('Failed to enable PAYG.'));
403
+ ctx.addLog('payment', 'Enabled PAYG');
404
+ } catch (err) {
405
+ ctx.appendMessage('system', chalk.red(`Error: ${err.message}`));
406
+ }
407
+ return { handled: true };
408
+ }
409
+
410
+ // /payment disable
411
+ if (sub === 'disable') {
412
+ try {
413
+ const result = await ctx.client.configurePayg({ enabled: false });
414
+ ctx.appendMessage('system', result.success
415
+ ? chalk.yellow('PAYG billing disabled.')
416
+ : chalk.red('Failed to disable PAYG.'));
417
+ ctx.addLog('payment', 'Disabled PAYG');
418
+ } catch (err) {
419
+ ctx.appendMessage('system', chalk.red(`Error: ${err.message}`));
420
+ }
421
+ return { handled: true };
422
+ }
423
+
424
+ // /payment token <T>
425
+ if (sub === 'token' && parts[1]) {
426
+ const token = parts[1].toUpperCase();
427
+ try {
428
+ const result = await ctx.client.configurePayg({ payment_token: token });
429
+ ctx.appendMessage('system', result.success
430
+ ? chalk.green(`Payment token set to: ${token}`)
431
+ : chalk.red('Failed to set payment token.'));
432
+ ctx.addLog('payment', `Token set to ${token}`);
433
+ } catch (err) {
434
+ ctx.appendMessage('system', chalk.red(`Error: ${err.message}`));
435
+ }
436
+ return { handled: true };
437
+ }
438
+
439
+ // /payment mode <M>
440
+ if (sub === 'mode' && parts[1]) {
441
+ const mode = parts[1];
442
+ if (mode !== 'pay_per_request' && mode !== 'debt_accumulation') {
443
+ ctx.appendMessage('system', chalk.yellow('Invalid mode. Use: pay_per_request or debt_accumulation'));
444
+ return { handled: true };
445
+ }
446
+ try {
447
+ const result = await ctx.client.configurePayg({ mode });
448
+ ctx.appendMessage('system', result.success
449
+ ? chalk.green(`Payment mode set to: ${mode}`)
450
+ : chalk.red('Failed to set payment mode.'));
451
+ ctx.addLog('payment', `Mode set to ${mode}`);
452
+ } catch (err) {
453
+ ctx.appendMessage('system', chalk.red(`Error: ${err.message}`));
454
+ }
455
+ return { handled: true };
456
+ }
457
+
458
+ ctx.appendMessage('system', chalk.yellow('Usage: /payment [enable|disable|token <T>|mode <M>]'));
459
+ return { handled: true };
460
+ }
461
+
462
+ async function cmdSecrets(args, ctx) {
463
+ const allSecrets = ctx.pluginManager.getPluginSecrets();
464
+ if (allSecrets.length === 0) {
465
+ ctx.appendMessage('system', chalk.dim('No plugins declare secrets.'));
466
+ return { handled: true };
467
+ }
468
+
469
+ if (!ctx.promptText) {
470
+ ctx.appendMessage('system', chalk.red('Interactive prompts not available in this mode.'));
471
+ return { handled: true };
472
+ }
473
+
474
+ const { readPluginSecrets, writePluginSecrets } = await import('./auth.js');
475
+
476
+ // Show menu
477
+ const secrets = readPluginSecrets();
478
+ const secretStatus = allSecrets.map(s => ({
479
+ ...s,
480
+ isSet: !!secrets[s.name]?.ciphertext,
481
+ }));
482
+
483
+ ctx.appendMessage('system', [
484
+ '',
485
+ chalk.bold.white(' Plugin Secrets'),
486
+ chalk.dim(' ' + '\u2500'.repeat(30)),
487
+ '',
488
+ ...secretStatus.map(s => {
489
+ const icon = s.isSet ? chalk.green('\u2713 set') : chalk.dim('\u2717 not set');
490
+ return ` ${chalk.white(s.name)} ${chalk.dim(`(${s.label})`)} ${chalk.dim('\u2014')} ${chalk.dim(s.plugin)} ${chalk.dim('\u2014')} ${icon}`;
491
+ }),
492
+ '',
493
+ ` ${chalk.cyan('1.')} Set a secret`,
494
+ ` ${chalk.cyan('2.')} Remove a secret`,
495
+ ` ${chalk.cyan('3.')} Back`,
496
+ '',
497
+ ].join('\n'));
498
+
499
+ const choice = (await ctx.promptText(chalk.cyan(' Select (1-3): '))).trim();
500
+
501
+ // ── Set a secret ──
502
+ if (choice === '1') {
503
+ ctx.appendMessage('system', [
504
+ '',
505
+ chalk.bold.white(' Select secret to set:'),
506
+ ...allSecrets.map((s, i) => {
507
+ const icon = secretStatus[i].isSet ? chalk.green('\u2713') : chalk.dim('\u2717');
508
+ return ` ${chalk.cyan(`${i + 1}.`)} ${s.label} ${chalk.dim(`(${s.name})`)} ${icon}`;
509
+ }),
510
+ '',
511
+ ].join('\n'));
512
+
513
+ const pick = (await ctx.promptText(chalk.cyan(` Select (1-${allSecrets.length}): `))).trim();
514
+ const idx = parseInt(pick, 10) - 1;
515
+ if (isNaN(idx) || idx < 0 || idx >= allSecrets.length) {
516
+ ctx.appendMessage('system', chalk.yellow(' Cancelled.'));
517
+ return { handled: true };
518
+ }
519
+
520
+ const decl = allSecrets[idx];
521
+
522
+ if (!ctx.authSdk) {
523
+ ctx.appendMessage('system', chalk.red(' Not authenticated — cannot encrypt secrets.'));
524
+ return { handled: true };
525
+ }
526
+
527
+ const value = await ctx.promptPassword(` Enter ${decl.label}: `);
528
+ if (!value) {
529
+ ctx.appendMessage('system', chalk.yellow(' No value entered. Cancelled.'));
530
+ return { handled: true };
531
+ }
532
+
533
+ try {
534
+ const { encrypt } = await import('@emblemvault/auth-sdk/crypto');
535
+ const encrypted = await encrypt(value, { config: { sdk: ctx.authSdk } });
536
+ const freshSecrets = readPluginSecrets();
537
+ freshSecrets[decl.name] = {
538
+ ciphertext: encrypted.ciphertext,
539
+ dataToEncryptHash: encrypted.dataToEncryptHash,
540
+ };
541
+ writePluginSecrets(freshSecrets);
542
+
543
+ // Hot-reload the plugin with the new secret (no restart needed)
544
+ const reloaded = await ctx.pluginManager.reloadPluginWithSecret(decl.plugin, decl.name, value);
545
+ if (reloaded) {
546
+ ctx.appendMessage('system', '\n' + chalk.green(` Secret "${decl.name}" encrypted, stored, and applied.`));
547
+ } else {
548
+ ctx.appendMessage('system', '\n' + chalk.green(` Secret "${decl.name}" encrypted and stored.`) + chalk.dim(' Restart to apply.'));
549
+ }
550
+ ctx.addLog('secrets', `Set ${decl.name}`);
551
+ } catch (err) {
552
+ ctx.appendMessage('system', chalk.red(` Encryption failed: ${err.message}`));
553
+ }
554
+ return { handled: true };
555
+ }
556
+
557
+ // ── Remove a secret ──
558
+ if (choice === '2') {
559
+ const setSecrets = secretStatus.filter(s => s.isSet);
560
+ if (setSecrets.length === 0) {
561
+ ctx.appendMessage('system', chalk.dim(' No secrets are currently stored.'));
562
+ return { handled: true };
563
+ }
564
+
565
+ ctx.appendMessage('system', [
566
+ '',
567
+ chalk.bold.white(' Select secret to remove:'),
568
+ ...setSecrets.map((s, i) =>
569
+ ` ${chalk.cyan(`${i + 1}.`)} ${s.label} ${chalk.dim(`(${s.name})`)} ${chalk.dim('\u2014')} ${chalk.dim(s.plugin)}`
570
+ ),
571
+ '',
572
+ ].join('\n'));
573
+
574
+ const pick = (await ctx.promptText(chalk.cyan(` Select (1-${setSecrets.length}): `))).trim();
575
+ const idx = parseInt(pick, 10) - 1;
576
+ if (isNaN(idx) || idx < 0 || idx >= setSecrets.length) {
577
+ ctx.appendMessage('system', chalk.yellow(' Cancelled.'));
578
+ return { handled: true };
579
+ }
580
+
581
+ const target = setSecrets[idx];
582
+ const freshSecrets = readPluginSecrets();
583
+ if (freshSecrets[target.name]) {
584
+ delete freshSecrets[target.name];
585
+ writePluginSecrets(freshSecrets);
586
+ ctx.appendMessage('system', chalk.green(` Secret "${target.name}" removed.`));
587
+ ctx.addLog('secrets', `Removed ${target.name}`);
588
+ }
589
+ return { handled: true };
590
+ }
591
+
592
+ // ── Back ──
593
+ return { handled: true };
594
+ }
595
+
596
+ function cmdGlow(args, ctx) {
597
+ const val = args.trim().toLowerCase();
598
+ if (val === 'on') {
599
+ const info = ctx.glow.detectGlow();
600
+ if (!info.installed) {
601
+ ctx.appendMessage('system', chalk.yellow('glow is not installed. Install from: https://github.com/charmbracelet/glow'));
602
+ return { handled: true };
603
+ }
604
+ ctx.settings.glowEnabled = true;
605
+ ctx.appendMessage('system', chalk.green('Markdown rendering enabled (via glow).'));
606
+ } else if (val === 'off') {
607
+ ctx.settings.glowEnabled = false;
608
+ ctx.appendMessage('system', chalk.yellow('Markdown rendering disabled.'));
609
+ } else {
610
+ const info = ctx.glow.detectGlow();
611
+ ctx.appendMessage('system', [
612
+ `${chalk.dim('Glow:')} ${ctx.settings.glowEnabled ? chalk.green('ON') : chalk.red('OFF')}`,
613
+ `${chalk.dim('Installed:')} ${info.installed ? chalk.green('YES') : chalk.red('NO')}`,
614
+ info.version ? `${chalk.dim('Version:')} ${info.version}` : '',
615
+ ].filter(Boolean).join('\n'));
616
+ }
617
+ return { handled: true };
618
+ }
619
+
620
+ function cmdLog(args, ctx) {
621
+ const val = args.trim().toLowerCase();
622
+ if (val === 'on') {
623
+ ctx.settings.log = true;
624
+ ctx.logOpen();
625
+ ctx.appendMessage('system', chalk.green(`Logging enabled → ${ctx.LOG_FILE}`));
626
+ } else if (val === 'off') {
627
+ ctx.logClose();
628
+ ctx.settings.log = false;
629
+ ctx.appendMessage('system', chalk.yellow('Logging disabled.'));
630
+ } else {
631
+ const lines = [
632
+ `${chalk.dim('Logging:')} ${ctx.settings.log ? chalk.green('ON') : chalk.red('OFF')}`,
633
+ `${chalk.dim('File:')} ${ctx.LOG_FILE}`,
634
+ '',
635
+ chalk.dim('Usage: /log on|off'),
636
+ chalk.dim('CLI: --log --log-file <path>'),
637
+ ];
638
+ ctx.appendMessage('system', lines.join('\n'));
639
+ }
640
+ return { handled: true };
641
+ }
642
+
643
+ function cmdReset(ctx) {
644
+ ctx.history.messages = [];
645
+ ctx.history.created = new Date().toISOString();
646
+ ctx.history.lastUpdated = new Date().toISOString();
647
+
648
+ // Persist the cleared state to disk
649
+ if (typeof ctx.saveHistory === 'function') ctx.saveHistory(ctx.history);
650
+
651
+ // Clear chat panel in TUI mode
652
+ if (ctx.tui && ctx.tui.panels && ctx.tui.panels.chat) {
653
+ ctx.tui.panels.chat.setContent('');
654
+ ctx.tui.screen.render();
655
+ }
656
+
657
+ ctx.appendMessage('system', chalk.green('Conversation cleared.'));
658
+ ctx.addLog('reset', 'Conversation reset');
659
+ return { handled: true };
660
+ }
661
+
662
+ function cmdExit(ctx) {
663
+ if (ctx.tui) {
664
+ ctx.tui.destroy();
665
+ }
666
+ process.exit(0);
667
+ }
668
+
669
+ // ============================================================================
670
+ // Main Router
671
+ // ============================================================================
672
+
673
+ /**
674
+ * Process a slash command.
675
+ *
676
+ * @param {string} input - The raw user input (e.g. "/tools add foo")
677
+ * @param {object} ctx - Runtime context
678
+ * @returns {Promise<{ handled: boolean }>}
679
+ */
680
+ export async function processCommand(input, ctx) {
681
+ const trimmed = input.trim();
682
+ if (!trimmed.startsWith('/')) {
683
+ return { handled: false };
684
+ }
685
+
686
+ // Split into command and arguments
687
+ const spaceIdx = trimmed.indexOf(' ');
688
+ const cmd = spaceIdx === -1 ? trimmed.toLowerCase() : trimmed.slice(0, spaceIdx).toLowerCase();
689
+ const args = spaceIdx === -1 ? '' : trimmed.slice(spaceIdx + 1);
690
+
691
+ switch (cmd) {
692
+ case '/help':
693
+ return cmdHelp(ctx);
694
+
695
+ case '/plugins':
696
+ return cmdPlugins(ctx);
697
+
698
+ case '/plugin':
699
+ return cmdPlugin(args, ctx);
700
+
701
+ case '/tools':
702
+ return cmdTools(args, ctx);
703
+
704
+ case '/auth':
705
+ return cmdAuth(ctx);
706
+
707
+ case '/wallet':
708
+ return cmdWallet(ctx);
709
+
710
+ case '/portfolio':
711
+ return cmdPortfolio();
712
+
713
+ case '/settings':
714
+ return cmdSettings(ctx);
715
+
716
+ case '/model':
717
+ return cmdModel(args, ctx);
718
+
719
+ case '/stream':
720
+ return cmdStream(args, ctx);
721
+
722
+ case '/debug':
723
+ return cmdDebug(args, ctx);
724
+
725
+ case '/history':
726
+ return cmdHistory(args, ctx);
727
+
728
+ case '/payment':
729
+ return cmdPayment(args, ctx);
730
+
731
+ case '/secrets':
732
+ return cmdSecrets(args, ctx);
733
+
734
+ case '/glow':
735
+ return cmdGlow(args, ctx);
736
+
737
+ case '/log':
738
+ return cmdLog(args, ctx);
739
+
740
+ case '/reset':
741
+ case '/clear':
742
+ return cmdReset(ctx);
743
+
744
+ case '/exit':
745
+ case '/quit':
746
+ return cmdExit(ctx);
747
+
748
+ default:
749
+ ctx.appendMessage('system', chalk.yellow(`Unknown command: ${cmd}`) + chalk.dim(' Type /help for available commands.'));
750
+ return { handled: true };
751
+ }
752
+ }
753
+
754
+ export default { processCommand, COMMANDS };