@lanonasis/cli 1.2.0 → 1.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,812 @@
1
+ import { Command } from 'commander';
2
+ import inquirer from 'inquirer';
3
+ import chalk from 'chalk';
4
+ import Table from 'cli-table3';
5
+ import { apiClient } from '../utils/api.js';
6
+ import { formatDate, truncateText } from '../utils/formatting.js';
7
+ // Enhanced VPS-style color scheme
8
+ const colors = {
9
+ primary: chalk.blue.bold,
10
+ success: chalk.green,
11
+ warning: chalk.yellow,
12
+ error: chalk.red,
13
+ info: chalk.cyan,
14
+ accent: chalk.magenta,
15
+ muted: chalk.gray,
16
+ highlight: chalk.white.bold
17
+ };
18
+ const apiKeysCommand = new Command('api-keys')
19
+ .alias('keys')
20
+ .description(colors.info('🔐 Manage API keys securely with enterprise-grade encryption'));
21
+ // ============================================================================
22
+ // PROJECT COMMANDS
23
+ // ============================================================================
24
+ const projectsCommand = new Command('projects')
25
+ .description(colors.accent('📁 Manage API key projects and organization'));
26
+ projectsCommand
27
+ .command('create')
28
+ .description('Create a new API key project')
29
+ .option('-n, --name <name>', 'Project name')
30
+ .option('-d, --description [description]', 'Project description')
31
+ .option('-o, --organization-id <id>', 'Organization ID')
32
+ .option('--interactive', 'Interactive mode')
33
+ .action(async (options) => {
34
+ try {
35
+ let projectData = {
36
+ name: options.name,
37
+ description: options.description,
38
+ organizationId: options.organizationId
39
+ };
40
+ if (options.interactive || !projectData.name || !projectData.organizationId) {
41
+ const answers = await inquirer.prompt([
42
+ {
43
+ type: 'input',
44
+ name: 'name',
45
+ message: 'Project name:',
46
+ when: !projectData.name,
47
+ validate: (input) => input.length > 0 || 'Project name is required'
48
+ },
49
+ {
50
+ type: 'input',
51
+ name: 'description',
52
+ message: 'Project description (optional):',
53
+ when: !projectData.description
54
+ },
55
+ {
56
+ type: 'input',
57
+ name: 'organizationId',
58
+ message: 'Organization ID:',
59
+ when: !projectData.organizationId,
60
+ validate: (input) => {
61
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
62
+ return uuidRegex.test(input) || 'Please enter a valid UUID';
63
+ }
64
+ }
65
+ ]);
66
+ projectData = { ...projectData, ...answers };
67
+ }
68
+ const project = await apiClient.post('/api-keys/projects', projectData);
69
+ console.log(chalk.green('✅ Project created successfully!'));
70
+ console.log(chalk.blue(`Project ID: ${project.id}`));
71
+ console.log(chalk.blue(`Name: ${project.name}`));
72
+ if (project.description) {
73
+ console.log(chalk.blue(`Description: ${project.description}`));
74
+ }
75
+ }
76
+ catch (error) {
77
+ console.error(chalk.red('❌ Failed to create project:'), error.message);
78
+ process.exit(1);
79
+ }
80
+ });
81
+ projectsCommand
82
+ .command('list')
83
+ .alias('ls')
84
+ .description('List all API key projects')
85
+ .option('--json', 'Output as JSON')
86
+ .action(async (options) => {
87
+ try {
88
+ const projects = await apiClient.get('/api-keys/projects');
89
+ if (options.json) {
90
+ console.log(JSON.stringify(projects, null, 2));
91
+ return;
92
+ }
93
+ if (projects.length === 0) {
94
+ console.log(chalk.yellow('No projects found'));
95
+ return;
96
+ }
97
+ const table = new Table({
98
+ head: ['ID', 'Name', 'Description', 'Owner', 'Created'].map(h => chalk.cyan(h)),
99
+ style: { head: [], border: [] }
100
+ });
101
+ projects.forEach((project) => {
102
+ table.push([
103
+ truncateText(project.id, 20),
104
+ project.name,
105
+ truncateText(project.description || '-', 30),
106
+ truncateText(project.ownerId, 20),
107
+ formatDate(project.createdAt)
108
+ ]);
109
+ });
110
+ console.log(table.toString());
111
+ console.log(chalk.gray(`Total: ${projects.length} projects`));
112
+ }
113
+ catch (error) {
114
+ console.error(chalk.red('❌ Failed to list projects:'), error.message);
115
+ process.exit(1);
116
+ }
117
+ });
118
+ // ============================================================================
119
+ // API KEY COMMANDS
120
+ // ============================================================================
121
+ apiKeysCommand
122
+ .command('create')
123
+ .description('Create a new API key')
124
+ .option('-n, --name <name>', 'API key name')
125
+ .option('-v, --value <value>', 'API key value')
126
+ .option('-t, --type <type>', 'Key type (api_key, database_url, oauth_token, etc.)')
127
+ .option('-e, --environment <env>', 'Environment (development, staging, production)')
128
+ .option('-p, --project-id <id>', 'Project ID')
129
+ .option('--access-level <level>', 'Access level (public, authenticated, team, admin, enterprise)')
130
+ .option('--tags <tags>', 'Comma-separated tags')
131
+ .option('--expires-at <date>', 'Expiration date (ISO format)')
132
+ .option('--rotation-frequency <days>', 'Rotation frequency in days', '90')
133
+ .option('--interactive', 'Interactive mode')
134
+ .action(async (options) => {
135
+ try {
136
+ let keyData = {
137
+ name: options.name,
138
+ value: options.value,
139
+ keyType: options.type,
140
+ environment: options.environment || 'development',
141
+ projectId: options.projectId,
142
+ accessLevel: options.accessLevel || 'team',
143
+ tags: options.tags ? options.tags.split(',').map((tag) => tag.trim()) : [],
144
+ expiresAt: options.expiresAt,
145
+ rotationFrequency: parseInt(options.rotationFrequency)
146
+ };
147
+ if (options.interactive || !keyData.name || !keyData.value || !keyData.projectId) {
148
+ const projects = await apiClient.get('/api-keys/projects');
149
+ const answers = await inquirer.prompt([
150
+ {
151
+ type: 'input',
152
+ name: 'name',
153
+ message: 'API key name:',
154
+ when: !keyData.name,
155
+ validate: (input) => input.length > 0 || 'Name is required'
156
+ },
157
+ {
158
+ type: 'password',
159
+ name: 'value',
160
+ message: 'API key value:',
161
+ when: !keyData.value,
162
+ validate: (input) => input.length > 0 || 'Value is required'
163
+ },
164
+ {
165
+ type: 'list',
166
+ name: 'keyType',
167
+ message: 'Key type:',
168
+ when: !keyData.keyType,
169
+ choices: [
170
+ 'api_key',
171
+ 'database_url',
172
+ 'oauth_token',
173
+ 'certificate',
174
+ 'ssh_key',
175
+ 'webhook_secret',
176
+ 'encryption_key'
177
+ ]
178
+ },
179
+ {
180
+ type: 'list',
181
+ name: 'environment',
182
+ message: 'Environment:',
183
+ choices: ['development', 'staging', 'production'],
184
+ default: 'development'
185
+ },
186
+ {
187
+ type: 'list',
188
+ name: 'projectId',
189
+ message: 'Select project:',
190
+ when: !keyData.projectId && projects.length > 0,
191
+ choices: projects.map((p) => ({ name: `${p.name} (${p.id})`, value: p.id }))
192
+ },
193
+ {
194
+ type: 'list',
195
+ name: 'accessLevel',
196
+ message: 'Access level:',
197
+ choices: ['public', 'authenticated', 'team', 'admin', 'enterprise'],
198
+ default: 'team'
199
+ },
200
+ {
201
+ type: 'input',
202
+ name: 'tags',
203
+ message: 'Tags (comma-separated, optional):',
204
+ filter: (input) => input ? input.split(',').map((tag) => tag.trim()) : []
205
+ },
206
+ {
207
+ type: 'input',
208
+ name: 'expiresAt',
209
+ message: 'Expiration date (YYYY-MM-DD, optional):',
210
+ validate: (input) => {
211
+ if (!input)
212
+ return true;
213
+ const date = new Date(input);
214
+ return !isNaN(date.getTime()) || 'Please enter a valid date';
215
+ },
216
+ filter: (input) => input ? new Date(input).toISOString() : undefined
217
+ },
218
+ {
219
+ type: 'number',
220
+ name: 'rotationFrequency',
221
+ message: 'Rotation frequency (days):',
222
+ default: 90,
223
+ validate: (input) => input > 0 && input <= 365 || 'Must be between 1 and 365 days'
224
+ }
225
+ ]);
226
+ keyData = { ...keyData, ...answers };
227
+ }
228
+ const apiKey = await apiClient.post('/api-keys', keyData);
229
+ console.log(colors.success('🔐 API key created successfully!'));
230
+ console.log(colors.info('━'.repeat(50)));
231
+ console.log(`${colors.highlight('Key ID:')} ${colors.primary(apiKey.id)}`);
232
+ console.log(`${colors.highlight('Name:')} ${colors.accent(apiKey.name)}`);
233
+ console.log(`${colors.highlight('Type:')} ${colors.info(apiKey.keyType)}`);
234
+ console.log(`${colors.highlight('Environment:')} ${colors.accent(apiKey.environment)}`);
235
+ console.log(colors.info('━'.repeat(50)));
236
+ console.log(colors.warning('⚠️ The key value is securely encrypted and cannot be retrieved later.'));
237
+ }
238
+ catch (error) {
239
+ console.error(colors.error('✖ Failed to create API key:'), colors.muted(error.message));
240
+ process.exit(1);
241
+ }
242
+ });
243
+ apiKeysCommand
244
+ .command('list')
245
+ .alias('ls')
246
+ .description('List API keys')
247
+ .option('-p, --project-id <id>', 'Filter by project ID')
248
+ .option('--json', 'Output as JSON')
249
+ .action(async (options) => {
250
+ try {
251
+ let url = '/api-keys';
252
+ if (options.projectId) {
253
+ url += `?projectId=${options.projectId}`;
254
+ }
255
+ const apiKeys = await apiClient.get(url);
256
+ if (options.json) {
257
+ console.log(JSON.stringify(apiKeys, null, 2));
258
+ return;
259
+ }
260
+ if (apiKeys.length === 0) {
261
+ console.log(colors.warning('⚠️ No API keys found'));
262
+ console.log(colors.muted('Run: lanonasis api-keys create'));
263
+ return;
264
+ }
265
+ console.log(colors.primary('🔐 API Key Management'));
266
+ console.log(colors.info('═'.repeat(80)));
267
+ const table = new Table({
268
+ head: ['ID', 'Name', 'Type', 'Environment', 'Status', 'Usage', 'Last Rotated'].map(h => colors.accent(h)),
269
+ style: { head: [], border: [] }
270
+ });
271
+ apiKeys.forEach((key) => {
272
+ const statusColor = key.status === 'active' ? colors.success :
273
+ key.status === 'rotating' ? colors.warning : colors.error;
274
+ table.push([
275
+ truncateText(key.id, 20),
276
+ key.name,
277
+ key.keyType,
278
+ key.environment,
279
+ statusColor(key.status),
280
+ colors.highlight(key.usageCount.toString()),
281
+ formatDate(key.lastRotated)
282
+ ]);
283
+ });
284
+ console.log(table.toString());
285
+ console.log(colors.info('═'.repeat(80)));
286
+ console.log(colors.muted(`🔢 Total: ${colors.highlight(apiKeys.length)} API keys`));
287
+ }
288
+ catch (error) {
289
+ console.error(colors.error('✖ Failed to list API keys:'), colors.muted(error.message));
290
+ process.exit(1);
291
+ }
292
+ });
293
+ apiKeysCommand
294
+ .command('get')
295
+ .description('Get details of a specific API key')
296
+ .argument('<keyId>', 'API key ID')
297
+ .option('--json', 'Output as JSON')
298
+ .action(async (keyId, options) => {
299
+ try {
300
+ const apiKey = await apiClient.get(`/api-keys/${keyId}`);
301
+ if (options.json) {
302
+ console.log(JSON.stringify(apiKey, null, 2));
303
+ return;
304
+ }
305
+ console.log(colors.primary('🔍 API Key Details'));
306
+ console.log(colors.info('═'.repeat(60)));
307
+ console.log(`${colors.highlight('ID:')} ${colors.primary(apiKey.id)}`);
308
+ console.log(`${colors.highlight('Name:')} ${colors.accent(apiKey.name)}`);
309
+ console.log(`${colors.highlight('Type:')} ${colors.info(apiKey.keyType)}`);
310
+ console.log(`${colors.highlight('Environment:')} ${colors.accent(apiKey.environment)}`);
311
+ console.log(`${colors.highlight('Project ID:')} ${colors.muted(apiKey.projectId)}`);
312
+ console.log(`${colors.highlight('Access Level:')} ${colors.warning(apiKey.accessLevel)}`);
313
+ const statusColor = apiKey.status === 'active' ? colors.success :
314
+ apiKey.status === 'rotating' ? colors.warning : colors.error;
315
+ console.log(`${colors.highlight('Status:')} ${statusColor(apiKey.status)}`);
316
+ console.log(`${colors.highlight('Usage Count:')} ${colors.accent(apiKey.usageCount)}`);
317
+ console.log(`${colors.highlight('Tags:')} ${colors.muted(apiKey.tags.join(', ') || 'None')}`);
318
+ console.log(`${colors.highlight('Rotation Frequency:')} ${colors.info(apiKey.rotationFrequency)} days`);
319
+ console.log(`${colors.highlight('Last Rotated:')} ${colors.muted(formatDate(apiKey.lastRotated))}`);
320
+ console.log(`${colors.highlight('Created:')} ${colors.muted(formatDate(apiKey.createdAt))}`);
321
+ console.log(`${colors.highlight('Updated:')} ${colors.muted(formatDate(apiKey.updatedAt))}`);
322
+ if (apiKey.expiresAt) {
323
+ console.log(`${colors.highlight('Expires:')} ${colors.warning(formatDate(apiKey.expiresAt))}`);
324
+ }
325
+ console.log(colors.info('═'.repeat(60)));
326
+ }
327
+ catch (error) {
328
+ console.error(colors.error('✖ Failed to get API key:'), colors.muted(error.message));
329
+ process.exit(1);
330
+ }
331
+ });
332
+ apiKeysCommand
333
+ .command('update')
334
+ .description('Update an API key')
335
+ .argument('<keyId>', 'API key ID')
336
+ .option('-n, --name <name>', 'New name')
337
+ .option('-v, --value <value>', 'New value')
338
+ .option('--tags <tags>', 'Comma-separated tags')
339
+ .option('--rotation-frequency <days>', 'Rotation frequency in days')
340
+ .option('--interactive', 'Interactive mode')
341
+ .action(async (keyId, options) => {
342
+ try {
343
+ let updateData = {};
344
+ if (options.name)
345
+ updateData.name = options.name;
346
+ if (options.value)
347
+ updateData.value = options.value;
348
+ if (options.tags)
349
+ updateData.tags = options.tags.split(',').map((tag) => tag.trim());
350
+ if (options.rotationFrequency)
351
+ updateData.rotationFrequency = parseInt(options.rotationFrequency);
352
+ if (options.interactive || Object.keys(updateData).length === 0) {
353
+ const current = await apiClient.get(`/api-keys/${keyId}`);
354
+ const answers = await inquirer.prompt([
355
+ {
356
+ type: 'input',
357
+ name: 'name',
358
+ message: 'Name:',
359
+ default: current.name,
360
+ when: !updateData.name
361
+ },
362
+ {
363
+ type: 'confirm',
364
+ name: 'updateValue',
365
+ message: 'Update the key value?',
366
+ default: false,
367
+ when: !updateData.value
368
+ },
369
+ {
370
+ type: 'password',
371
+ name: 'value',
372
+ message: 'New key value:',
373
+ when: (answers) => answers.updateValue
374
+ },
375
+ {
376
+ type: 'input',
377
+ name: 'tags',
378
+ message: 'Tags (comma-separated):',
379
+ default: current.tags.join(', '),
380
+ filter: (input) => input.split(',').map((tag) => tag.trim()),
381
+ when: !updateData.tags
382
+ },
383
+ {
384
+ type: 'number',
385
+ name: 'rotationFrequency',
386
+ message: 'Rotation frequency (days):',
387
+ default: current.rotationFrequency,
388
+ validate: (input) => input > 0 && input <= 365 || 'Must be between 1 and 365 days',
389
+ when: !updateData.rotationFrequency
390
+ }
391
+ ]);
392
+ updateData = { ...updateData, ...answers };
393
+ delete updateData.updateValue;
394
+ }
395
+ const updatedKey = await apiClient.put(`/api-keys/${keyId}`, updateData);
396
+ console.log(colors.success('🔄 API key updated successfully!'));
397
+ console.log(colors.info('━'.repeat(40)));
398
+ console.log(`${colors.highlight('Name:')} ${colors.accent(updatedKey.name)}`);
399
+ console.log(`${colors.highlight('Status:')} ${colors.success(updatedKey.status)}`);
400
+ if (updateData.value) {
401
+ console.log(colors.warning('⚠️ The key value has been updated and re-encrypted.'));
402
+ }
403
+ console.log(colors.info('━'.repeat(40)));
404
+ }
405
+ catch (error) {
406
+ console.error(colors.error('✖ Failed to update API key:'), colors.muted(error.message));
407
+ process.exit(1);
408
+ }
409
+ });
410
+ apiKeysCommand
411
+ .command('delete')
412
+ .alias('rm')
413
+ .description('Delete an API key')
414
+ .argument('<keyId>', 'API key ID')
415
+ .option('-f, --force', 'Skip confirmation')
416
+ .action(async (keyId, options) => {
417
+ try {
418
+ if (!options.force) {
419
+ const apiKey = await apiClient.get(`/api-keys/${keyId}`);
420
+ const { confirm } = await inquirer.prompt([
421
+ {
422
+ type: 'confirm',
423
+ name: 'confirm',
424
+ message: `Are you sure you want to delete "${apiKey.name}"? This action cannot be undone.`,
425
+ default: false
426
+ }
427
+ ]);
428
+ if (!confirm) {
429
+ console.log(colors.warning('🚫 Operation cancelled'));
430
+ return;
431
+ }
432
+ }
433
+ await apiClient.delete(`/api-keys/${keyId}`);
434
+ console.log(colors.success('🗑️ API key deleted successfully!'));
435
+ }
436
+ catch (error) {
437
+ console.error(colors.error('✖ Failed to delete API key:'), colors.muted(error.message));
438
+ process.exit(1);
439
+ }
440
+ });
441
+ // ============================================================================
442
+ // MCP COMMANDS
443
+ // ============================================================================
444
+ const mcpCommand = new Command('mcp')
445
+ .description(colors.accent('🤖 Model Context Protocol (MCP) - Secure AI agent access'));
446
+ mcpCommand
447
+ .command('register-tool')
448
+ .description('Register a new MCP tool')
449
+ .option('--tool-id <id>', 'Tool ID')
450
+ .option('--tool-name <name>', 'Tool name')
451
+ .option('--organization-id <id>', 'Organization ID')
452
+ .option('--keys <keys>', 'Comma-separated list of accessible key names')
453
+ .option('--environments <envs>', 'Comma-separated list of environments')
454
+ .option('--max-sessions <num>', 'Maximum concurrent sessions', '3')
455
+ .option('--max-duration <seconds>', 'Maximum session duration in seconds', '900')
456
+ .option('--webhook-url <url>', 'Webhook URL for notifications')
457
+ .option('--auto-approve', 'Enable auto-approval for low-risk requests')
458
+ .option('--risk-level <level>', 'Risk level (low, medium, high, critical)', 'medium')
459
+ .option('--interactive', 'Interactive mode')
460
+ .action(async (options) => {
461
+ try {
462
+ let toolData = {
463
+ toolId: options.toolId,
464
+ toolName: options.toolName,
465
+ organizationId: options.organizationId,
466
+ permissions: {
467
+ keys: options.keys ? options.keys.split(',').map((k) => k.trim()) : [],
468
+ environments: options.environments ? options.environments.split(',').map((e) => e.trim()) : ['development'],
469
+ maxConcurrentSessions: parseInt(options.maxSessions),
470
+ maxSessionDuration: parseInt(options.maxDuration)
471
+ },
472
+ webhookUrl: options.webhookUrl,
473
+ autoApprove: options.autoApprove || false,
474
+ riskLevel: options.riskLevel
475
+ };
476
+ if (options.interactive || !toolData.toolId || !toolData.toolName || !toolData.organizationId) {
477
+ const answers = await inquirer.prompt([
478
+ {
479
+ type: 'input',
480
+ name: 'toolId',
481
+ message: 'Tool ID:',
482
+ when: !toolData.toolId,
483
+ validate: (input) => input.length > 0 || 'Tool ID is required'
484
+ },
485
+ {
486
+ type: 'input',
487
+ name: 'toolName',
488
+ message: 'Tool name:',
489
+ when: !toolData.toolName,
490
+ validate: (input) => input.length > 0 || 'Tool name is required'
491
+ },
492
+ {
493
+ type: 'input',
494
+ name: 'organizationId',
495
+ message: 'Organization ID:',
496
+ when: !toolData.organizationId,
497
+ validate: (input) => {
498
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
499
+ return uuidRegex.test(input) || 'Please enter a valid UUID';
500
+ }
501
+ },
502
+ {
503
+ type: 'input',
504
+ name: 'keys',
505
+ message: 'Accessible key names (comma-separated):',
506
+ filter: (input) => input.split(',').map((k) => k.trim()),
507
+ when: toolData.permissions.keys.length === 0
508
+ },
509
+ {
510
+ type: 'checkbox',
511
+ name: 'environments',
512
+ message: 'Accessible environments:',
513
+ choices: ['development', 'staging', 'production'],
514
+ default: ['development']
515
+ },
516
+ {
517
+ type: 'number',
518
+ name: 'maxConcurrentSessions',
519
+ message: 'Maximum concurrent sessions:',
520
+ default: 3,
521
+ validate: (input) => input > 0 && input <= 10 || 'Must be between 1 and 10'
522
+ },
523
+ {
524
+ type: 'number',
525
+ name: 'maxSessionDuration',
526
+ message: 'Maximum session duration (seconds):',
527
+ default: 900,
528
+ validate: (input) => input >= 60 && input <= 3600 || 'Must be between 60 and 3600 seconds'
529
+ },
530
+ {
531
+ type: 'input',
532
+ name: 'webhookUrl',
533
+ message: 'Webhook URL (optional):'
534
+ },
535
+ {
536
+ type: 'confirm',
537
+ name: 'autoApprove',
538
+ message: 'Enable auto-approval?',
539
+ default: false
540
+ },
541
+ {
542
+ type: 'list',
543
+ name: 'riskLevel',
544
+ message: 'Risk level:',
545
+ choices: ['low', 'medium', 'high', 'critical'],
546
+ default: 'medium'
547
+ }
548
+ ]);
549
+ if (answers.keys)
550
+ toolData.permissions.keys = answers.keys;
551
+ if (answers.environments)
552
+ toolData.permissions.environments = answers.environments;
553
+ if (answers.maxConcurrentSessions)
554
+ toolData.permissions.maxConcurrentSessions = answers.maxConcurrentSessions;
555
+ if (answers.maxSessionDuration)
556
+ toolData.permissions.maxSessionDuration = answers.maxSessionDuration;
557
+ toolData = { ...toolData, ...answers };
558
+ delete toolData.keys;
559
+ delete toolData.environments;
560
+ delete toolData.maxConcurrentSessions;
561
+ delete toolData.maxSessionDuration;
562
+ }
563
+ const tool = await apiClient.post('/api-keys/mcp/tools', toolData);
564
+ console.log(colors.success('🤖 MCP tool registered successfully!'));
565
+ console.log(colors.info('━'.repeat(50)));
566
+ console.log(`${colors.highlight('Tool ID:')} ${colors.primary(tool.toolId)}`);
567
+ console.log(`${colors.highlight('Name:')} ${colors.accent(tool.toolName)}`);
568
+ console.log(`${colors.highlight('Risk Level:')} ${colors.warning(tool.riskLevel)}`);
569
+ console.log(`${colors.highlight('Auto Approve:')} ${tool.autoApprove ? colors.success('Yes') : colors.error('No')}`);
570
+ console.log(colors.info('━'.repeat(50)));
571
+ }
572
+ catch (error) {
573
+ console.error(colors.error('✖ Failed to register MCP tool:'), colors.muted(error.message));
574
+ process.exit(1);
575
+ }
576
+ });
577
+ mcpCommand
578
+ .command('list-tools')
579
+ .description('List registered MCP tools')
580
+ .option('--json', 'Output as JSON')
581
+ .action(async (options) => {
582
+ try {
583
+ const tools = await apiClient.get('/api-keys/mcp/tools');
584
+ if (options.json) {
585
+ console.log(JSON.stringify(tools, null, 2));
586
+ return;
587
+ }
588
+ if (tools.length === 0) {
589
+ console.log(colors.warning('⚠️ No MCP tools found'));
590
+ console.log(colors.muted('Run: lanonasis api-keys mcp register-tool'));
591
+ return;
592
+ }
593
+ console.log(colors.primary('🤖 Registered MCP Tools'));
594
+ console.log(colors.info('═'.repeat(80)));
595
+ const table = new Table({
596
+ head: ['Tool ID', 'Name', 'Risk Level', 'Status', 'Auto Approve', 'Created'].map(h => colors.accent(h)),
597
+ style: { head: [], border: [] }
598
+ });
599
+ tools.forEach((tool) => {
600
+ const statusColor = tool.status === 'active' ? colors.success :
601
+ tool.status === 'suspended' ? colors.error : colors.warning;
602
+ table.push([
603
+ tool.toolId,
604
+ tool.toolName,
605
+ tool.riskLevel,
606
+ statusColor(tool.status),
607
+ tool.autoApprove ? colors.success('Yes') : colors.error('No'),
608
+ formatDate(tool.createdAt)
609
+ ]);
610
+ });
611
+ console.log(table.toString());
612
+ console.log(colors.info('═'.repeat(80)));
613
+ console.log(colors.muted(`🤖 Total: ${colors.highlight(tools.length)} MCP tools`));
614
+ }
615
+ catch (error) {
616
+ console.error(colors.error('✖ Failed to list MCP tools:'), colors.muted(error.message));
617
+ process.exit(1);
618
+ }
619
+ });
620
+ mcpCommand
621
+ .command('request-access')
622
+ .description('Request access to API keys via MCP')
623
+ .option('--tool-id <id>', 'Tool ID')
624
+ .option('--organization-id <id>', 'Organization ID')
625
+ .option('--keys <keys>', 'Comma-separated list of key names')
626
+ .option('--environment <env>', 'Environment (development, staging, production)')
627
+ .option('--justification <text>', 'Justification for access')
628
+ .option('--duration <seconds>', 'Estimated duration in seconds', '900')
629
+ .option('--interactive', 'Interactive mode')
630
+ .action(async (options) => {
631
+ try {
632
+ let requestData = {
633
+ toolId: options.toolId,
634
+ organizationId: options.organizationId,
635
+ keyNames: options.keys ? options.keys.split(',').map((k) => k.trim()) : [],
636
+ environment: options.environment,
637
+ justification: options.justification,
638
+ estimatedDuration: parseInt(options.duration),
639
+ context: {}
640
+ };
641
+ if (options.interactive || !requestData.toolId || !requestData.organizationId ||
642
+ requestData.keyNames.length === 0 || !requestData.environment || !requestData.justification) {
643
+ const tools = await apiClient.get('/api-keys/mcp/tools');
644
+ const answers = await inquirer.prompt([
645
+ {
646
+ type: 'list',
647
+ name: 'toolId',
648
+ message: 'Select MCP tool:',
649
+ when: !requestData.toolId && tools.length > 0,
650
+ choices: tools.map((t) => ({ name: `${t.toolName} (${t.toolId})`, value: t.toolId }))
651
+ },
652
+ {
653
+ type: 'input',
654
+ name: 'organizationId',
655
+ message: 'Organization ID:',
656
+ when: !requestData.organizationId,
657
+ validate: (input) => {
658
+ const uuidRegex = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
659
+ return uuidRegex.test(input) || 'Please enter a valid UUID';
660
+ }
661
+ },
662
+ {
663
+ type: 'input',
664
+ name: 'keyNames',
665
+ message: 'Key names to access (comma-separated):',
666
+ when: requestData.keyNames.length === 0,
667
+ filter: (input) => input.split(',').map((k) => k.trim()),
668
+ validate: (input) => input.length > 0 || 'At least one key name is required'
669
+ },
670
+ {
671
+ type: 'list',
672
+ name: 'environment',
673
+ message: 'Environment:',
674
+ when: !requestData.environment,
675
+ choices: ['development', 'staging', 'production']
676
+ },
677
+ {
678
+ type: 'input',
679
+ name: 'justification',
680
+ message: 'Justification for access:',
681
+ when: !requestData.justification,
682
+ validate: (input) => input.length > 0 || 'Justification is required'
683
+ },
684
+ {
685
+ type: 'number',
686
+ name: 'estimatedDuration',
687
+ message: 'Estimated duration (seconds):',
688
+ default: 900,
689
+ validate: (input) => input >= 60 && input <= 3600 || 'Must be between 60 and 3600 seconds'
690
+ }
691
+ ]);
692
+ requestData = { ...requestData, ...answers };
693
+ }
694
+ const response = await apiClient.post('/api-keys/mcp/request-access', requestData);
695
+ console.log(colors.success('🔐 Access request created successfully!'));
696
+ console.log(colors.info('━'.repeat(50)));
697
+ console.log(`${colors.highlight('Request ID:')} ${colors.primary(response.requestId)}`);
698
+ console.log(`${colors.highlight('Status:')} ${colors.accent(response.status)}`);
699
+ console.log(colors.info('━'.repeat(50)));
700
+ console.log(colors.warning('💡 Check the status with: lanonasis api-keys analytics usage'));
701
+ }
702
+ catch (error) {
703
+ console.error(colors.error('✖ Failed to create access request:'), colors.muted(error.message));
704
+ process.exit(1);
705
+ }
706
+ });
707
+ // ============================================================================
708
+ // ANALYTICS COMMANDS
709
+ // ============================================================================
710
+ const analyticsCommand = new Command('analytics')
711
+ .description('View API key usage analytics and security events');
712
+ analyticsCommand
713
+ .command('usage')
714
+ .description('View usage analytics')
715
+ .option('--key-id <id>', 'Filter by specific API key')
716
+ .option('--days <days>', 'Number of days to look back', '30')
717
+ .option('--json', 'Output as JSON')
718
+ .action(async (options) => {
719
+ try {
720
+ let url = '/api-keys/analytics/usage';
721
+ const params = new URLSearchParams();
722
+ if (options.keyId)
723
+ params.append('keyId', options.keyId);
724
+ if (options.days)
725
+ params.append('days', options.days);
726
+ if (params.toString()) {
727
+ url += `?${params.toString()}`;
728
+ }
729
+ const analytics = await apiClient.get(url);
730
+ if (options.json) {
731
+ console.log(JSON.stringify(analytics, null, 2));
732
+ return;
733
+ }
734
+ if (analytics.length === 0) {
735
+ console.log(chalk.yellow('No usage data found'));
736
+ return;
737
+ }
738
+ const table = new Table({
739
+ head: ['Key ID', 'Operation', 'Tool ID', 'Success', 'Timestamp'].map(h => chalk.cyan(h)),
740
+ style: { head: [], border: [] }
741
+ });
742
+ analytics.forEach((entry) => {
743
+ const successColor = entry.success ? colors.success('✓') : colors.error('✗');
744
+ table.push([
745
+ truncateText(entry.keyId || '-', 20),
746
+ entry.operation,
747
+ truncateText(entry.toolId || '-', 15),
748
+ successColor,
749
+ formatDate(entry.timestamp)
750
+ ]);
751
+ });
752
+ console.log(table.toString());
753
+ console.log(colors.info('═'.repeat(80)));
754
+ console.log(colors.muted(`📈 Total: ${colors.highlight(analytics.length)} events`));
755
+ }
756
+ catch (error) {
757
+ console.error(colors.error('✖ Failed to get usage analytics:'), colors.muted(error.message));
758
+ process.exit(1);
759
+ }
760
+ });
761
+ analyticsCommand
762
+ .command('security-events')
763
+ .description('View security events')
764
+ .option('--severity <level>', 'Filter by severity (low, medium, high, critical)')
765
+ .option('--json', 'Output as JSON')
766
+ .action(async (options) => {
767
+ try {
768
+ let url = '/api-keys/analytics/security-events';
769
+ if (options.severity) {
770
+ url += `?severity=${options.severity}`;
771
+ }
772
+ const events = await apiClient.get(url);
773
+ if (options.json) {
774
+ console.log(JSON.stringify(events, null, 2));
775
+ return;
776
+ }
777
+ if (events.length === 0) {
778
+ console.log(colors.success('✅ No security events found'));
779
+ return;
780
+ }
781
+ console.log(colors.primary('🛡️ Security Events Monitor'));
782
+ console.log(colors.info('═'.repeat(80)));
783
+ const table = new Table({
784
+ head: ['Event Type', 'Severity', 'Description', 'Resolved', 'Timestamp'].map(h => colors.accent(h)),
785
+ style: { head: [], border: [] }
786
+ });
787
+ events.forEach((event) => {
788
+ const severityColor = event.severity === 'critical' ? colors.error :
789
+ event.severity === 'high' ? colors.accent :
790
+ event.severity === 'medium' ? colors.warning : colors.success;
791
+ table.push([
792
+ event.eventType,
793
+ severityColor(event.severity.toUpperCase()),
794
+ truncateText(event.description, 40),
795
+ event.resolved ? colors.success('✓') : colors.warning('Pending'),
796
+ formatDate(event.timestamp)
797
+ ]);
798
+ });
799
+ console.log(table.toString());
800
+ console.log(colors.info('═'.repeat(80)));
801
+ console.log(colors.muted(`🛡️ Total: ${colors.highlight(events.length)} security events`));
802
+ }
803
+ catch (error) {
804
+ console.error(colors.error('✖ Failed to get security events:'), colors.muted(error.message));
805
+ process.exit(1);
806
+ }
807
+ });
808
+ // Add subcommands
809
+ apiKeysCommand.addCommand(projectsCommand);
810
+ apiKeysCommand.addCommand(mcpCommand);
811
+ apiKeysCommand.addCommand(analyticsCommand);
812
+ export default apiKeysCommand;