@jishankai/solid-cli 1.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.
package/src/index.js ADDED
@@ -0,0 +1,629 @@
1
+ #!/usr/bin/env node
2
+
3
+ import inquirer from 'inquirer';
4
+ import chalk from 'chalk';
5
+ import { Orchestrator } from './Orchestrator.js';
6
+ import { ReportManager } from './report/ReportManager.js';
7
+ import { LLMAnalyzer } from './llm/LLMAnalyzer.js';
8
+ import { log } from './logging/Logger.js';
9
+ import { getConfig } from './config/ConfigManager.js';
10
+ import ora from 'ora';
11
+
12
+ /**
13
+ * Main CLI Application
14
+ */
15
+ class SecurityAnalysisCLI {
16
+ constructor() {
17
+ this.results = null;
18
+ this.llmAnalysis = null;
19
+ log.userInteraction('cli_start', { timestamp: new Date().toISOString() });
20
+ }
21
+
22
+ /**
23
+ * Display welcome banner
24
+ */
25
+ displayBanner() {
26
+ console.clear();
27
+ console.log(chalk.cyan.bold('\n╔═══════════════════════════════════════════════════════════╗'));
28
+ console.log(chalk.cyan.bold('║ MacOS System & Security Analysis Agent CLI ║'));
29
+ console.log(chalk.cyan.bold('║ Powered by AI - Local-First Security Analysis ║'));
30
+ console.log(chalk.cyan.bold('╚═══════════════════════════════════════════════════════════╝\n'));
31
+ }
32
+
33
+
34
+ /**
35
+ * Auto-detect LLM provider from available API keys
36
+ */
37
+ detectLLMProvider() {
38
+ const hasOpenAI = process.env.OPENAI_API_KEY && process.env.OPENAI_API_KEY !== 'your_openai_api_key_here';
39
+ const hasClaude = process.env.ANTHROPIC_API_KEY && process.env.ANTHROPIC_API_KEY !== 'your_anthropic_api_key_here';
40
+
41
+ if (hasClaude) {
42
+ console.log(chalk.green('✅ Claude (Anthropic) - API key detected'));
43
+ return 'claude';
44
+ } else if (hasOpenAI) {
45
+ console.log(chalk.green('✅ OpenAI (GPT-4) - API key detected'));
46
+ return 'openai';
47
+ } else {
48
+ console.log(chalk.yellow('⚠️ No LLM API keys found - generating report only'));
49
+ console.log(chalk.gray(' Set OPENAI_API_KEY or ANTHROPIC_API_KEY in .env file'));
50
+ return 'none';
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Let user choose whether to use LLM analysis
56
+ */
57
+ async chooseLLMAnalysis(detectedProvider) {
58
+ if (detectedProvider === 'none') {
59
+ return 'none';
60
+ }
61
+
62
+ const { useLLM } = await inquirer.prompt([
63
+ {
64
+ type: 'list',
65
+ name: 'useLLM',
66
+ message: '🤖 AI Analysis Option:',
67
+ choices: [
68
+ {
69
+ name: `🧠 Use AI Analysis (${detectedProvider.toUpperCase()}) - Enhanced insights & recommendations`,
70
+ value: detectedProvider
71
+ },
72
+ {
73
+ name: '📋 Generate Security Report Only - Maximum privacy protection',
74
+ value: 'none'
75
+ }
76
+ ],
77
+ default: detectedProvider
78
+ }
79
+ ]);
80
+
81
+ // Provide privacy notice if user chooses LLM
82
+ if (useLLM !== 'none') {
83
+ console.log(chalk.yellow('\n🔒 Privacy Protection Active:'));
84
+ console.log(chalk.gray(' • All private keys, addresses, and sensitive data are automatically redacted'));
85
+ console.log(chalk.gray(' • Sensitive data is blocked from being sent to LLM'));
86
+ console.log(chalk.gray(' • You will be prompted if any sensitive patterns are detected\n'));
87
+ }
88
+
89
+ return useLLM;
90
+ }
91
+
92
+ /**
93
+ * Select report format
94
+ */
95
+ async selectReportFormat() {
96
+ const defaultFormats = getConfig('reports.defaultFormats', ['markdown', 'pdf']);
97
+ const defaultFormat = defaultFormats[0] || 'pdf';
98
+
99
+ const { format } = await inquirer.prompt([
100
+ {
101
+ type: 'list',
102
+ name: 'format',
103
+ message: 'Select report format:',
104
+ choices: [
105
+ { name: 'PDF (.pdf)', value: 'pdf' },
106
+ { name: 'Markdown (.md)', value: 'markdown' }
107
+ ],
108
+ default: defaultFormat
109
+ }
110
+ ]);
111
+
112
+ // Keep downstream API the same (array of formats)
113
+ return [format];
114
+ }
115
+
116
+ /**
117
+ * Get geo lookup setting
118
+ */
119
+ getGeoLookupSetting() {
120
+ const geoLookupEnabled = getConfig('security.enableGeoLookup', true);
121
+ if (geoLookupEnabled) {
122
+ console.log(chalk.green('✅ IP geolocation enabled'));
123
+ } else {
124
+ console.log(chalk.gray('⚫ IP geolocation disabled'));
125
+ }
126
+ return geoLookupEnabled;
127
+ }
128
+
129
+ /**
130
+ * Confirm to proceed
131
+ */
132
+ async confirmProceed(llmProvider, reportFormats, geoLookupEnabled) {
133
+ console.log(chalk.yellow('\n📋 Configuration Summary (auto-continue):'));
134
+ console.log(chalk.gray(' Scan Mode: Comprehensive (automatic)'));
135
+ console.log(chalk.gray(' Analysis: Unified Adaptive Analysis'));
136
+ console.log(chalk.gray(` LLM Provider: ${llmProvider === 'none' ? 'None (report only)' : llmProvider.toUpperCase()}`));
137
+ console.log(chalk.gray(` Report Format: ${reportFormats.join(', ')}`));
138
+ console.log(chalk.gray(` IP Geolocation: ${geoLookupEnabled ? 'enabled' : 'disabled'}`));
139
+ console.log(chalk.gray(' Confirmation: skipped (auto start)'));
140
+ console.log();
141
+
142
+ // Auto-continue without user confirmation
143
+ return true;
144
+ }
145
+
146
+ /**
147
+ * Run unified analysis
148
+ */
149
+ async runAnalysis(options = {}) {
150
+ console.log(chalk.cyan('\n🔍 Starting unified adaptive analysis...\n'));
151
+
152
+ const analysisDepth = 'comprehensive';
153
+ const startTime = Date.now();
154
+
155
+ log.analysisStart({
156
+ mode: 'unified',
157
+ depth: analysisDepth,
158
+ geoLookup: options.geoLookupEnabled
159
+ });
160
+
161
+ const orchestrator = new Orchestrator({
162
+ analysisDepth,
163
+ enableGeoLookup: options.geoLookupEnabled,
164
+ parallelExecution: getConfig('analysis.parallelExecution', true),
165
+ maxParallelAgents: getConfig('analysis.maxParallelAgents', 3)
166
+ });
167
+
168
+ try {
169
+ this.results = await orchestrator.runAnalysis();
170
+
171
+ // Validate results before proceeding
172
+ if (!this.results) {
173
+ throw new Error('Analysis completed but no results were returned');
174
+ }
175
+
176
+ const duration = Date.now() - startTime;
177
+
178
+ log.analysisComplete(this.results, duration);
179
+ console.log(chalk.green('\n✅ Analysis completed!\n'));
180
+
181
+ // Display summary
182
+ this.displaySummary();
183
+ } catch (error) {
184
+ const duration = Date.now() - startTime;
185
+ log.error('Analysis failed', {
186
+ duration,
187
+ error: error.message,
188
+ depth: analysisDepth,
189
+ geoLookup: options.geoLookupEnabled
190
+ });
191
+ console.error(chalk.red(`\n❌ Analysis failed: ${error.message}`));
192
+ if (process.env.DEBUG) {
193
+ console.error(error.stack);
194
+ }
195
+ throw error;
196
+ }
197
+ }
198
+
199
+
200
+
201
+
202
+
203
+
204
+
205
+
206
+
207
+
208
+ /**
209
+ * Display analysis summary
210
+ */
211
+ displaySummary() {
212
+ // Validate results and summary
213
+ if (!this.results) {
214
+ console.log(chalk.yellow('\n⚠️ No analysis results available to display'));
215
+ return;
216
+ }
217
+
218
+ const { summary, overallRisk } = this.results;
219
+
220
+ // Handle missing summary
221
+ if (!summary) {
222
+ console.log(chalk.yellow('\n⚠️ No summary data available'));
223
+ return;
224
+ }
225
+
226
+ console.log(chalk.bold('\n📊 Analysis Summary:'));
227
+ console.log(chalk.gray('─'.repeat(50)));
228
+
229
+ const normalizedRisk = (overallRisk || 'unknown').toLowerCase();
230
+ const riskColor = this.getRiskColor(normalizedRisk);
231
+ console.log(`Overall Risk: ${riskColor(normalizedRisk.toUpperCase())}`);
232
+ console.log(`Total Findings: ${summary.totalFindings || 0}`);
233
+
234
+ const highRisk = summary.highRiskFindings || 0;
235
+ const mediumRisk = summary.mediumRiskFindings || 0;
236
+ const lowRisk = summary.lowRiskFindings || 0;
237
+
238
+ if (highRisk > 0) {
239
+ console.log(chalk.red(` 🔴 High Risk: ${highRisk}`));
240
+ }
241
+ if (mediumRisk > 0) {
242
+ console.log(chalk.yellow(` 🟡 Medium Risk: ${mediumRisk}`));
243
+ }
244
+ if (lowRisk > 0) {
245
+ console.log(chalk.green(` 🟢 Low Risk: ${lowRisk}`));
246
+ }
247
+
248
+ console.log(chalk.gray('─'.repeat(50)));
249
+ }
250
+
251
+ isLikelySeedPhrase(candidate) {
252
+ if (!candidate) return false;
253
+ const words = candidate.trim().toLowerCase().split(/\s+/);
254
+ if (words.length < 12 || words.length > 24) return false;
255
+ if (words.some(word => !/^[a-z]+$/.test(word))) return false;
256
+
257
+ const stopwords = new Set([
258
+ 'the', 'and', 'that', 'with', 'from', 'this', 'have', 'will', 'your',
259
+ 'macos', 'analysis', 'system', 'security', 'process', 'service', 'launch',
260
+ 'agent', 'apple', 'icloud', 'profile'
261
+ ]);
262
+ const stopwordHits = words.filter(word => stopwords.has(word)).length;
263
+ const uniqueWords = new Set(words).size;
264
+
265
+ return stopwordHits <= 2 && uniqueWords >= words.length - 2;
266
+ }
267
+
268
+ isLikelyApiKey(candidate) {
269
+ if (!candidate) return false;
270
+ if (candidate.length < 24 || candidate.length > 120) return false;
271
+ if (candidate.includes('<key>') || candidate.includes('</key>')) return false;
272
+
273
+ const hasUpper = /[A-Z]/.test(candidate);
274
+ const hasLower = /[a-z]/.test(candidate);
275
+ const hasDigit = /\d/.test(candidate);
276
+ const uniqueRatio = new Set(candidate).size / candidate.length;
277
+
278
+ if (!(hasDigit && (hasUpper || hasLower))) return false;
279
+ if (uniqueRatio < 0.2) return false;
280
+ if (/^(.)\1{10,}$/.test(candidate)) return false;
281
+
282
+ return true;
283
+ }
284
+
285
+ detectSensitiveDataInResults(results) {
286
+ const patterns = [
287
+ { pattern: /0x[a-fA-F0-9]{40}/g, name: 'Ethereum address' },
288
+ { pattern: /[a-fA-F0-9]{64,}/g, name: 'Potential private key' },
289
+ { pattern: /(private|mnemonic|seed).*?[=:][a-zA-Z0-9+/]{8,}/gi, name: 'Private key phrase' },
290
+ { pattern: /[a-zA-Z0-9+/]{32,}={0,2}/g, name: 'API key or token' },
291
+ { pattern: /[6][a-km-zA-HJ-NP-Z1-9]{50,}/g, name: 'Wallet import format' },
292
+ { pattern: /\b([a-z]+(\s+[a-z]+){11,})\b/gi, name: 'Potential seed phrase' },
293
+ { pattern: /\b[13][a-km-zA-HJ-NP-Z1-9]{25,34}\b/g, name: 'Bitcoin address' },
294
+ { pattern: /\b[a-fA-F0-9]{16,}\b/g, name: 'Long hex string' }
295
+ ];
296
+
297
+ const locations = [];
298
+ if (!results?.agents) return locations;
299
+
300
+ Object.entries(results.agents).forEach(([agentKey, agentResult]) => {
301
+ const findings = Array.isArray(agentResult?.findings) ? agentResult.findings : [];
302
+ findings.forEach((finding, index) => {
303
+ const fields = [
304
+ { field: 'description', value: finding.description },
305
+ { field: 'command', value: finding.command },
306
+ { field: 'path', value: finding.path },
307
+ { field: 'program', value: finding.program },
308
+ { field: 'plist', value: finding.plist }
309
+ ];
310
+
311
+ fields.forEach(({ field, value }) => {
312
+ if (!value || typeof value !== 'string') return;
313
+ patterns.forEach(({ pattern, name }) => {
314
+ let matches = [];
315
+
316
+ if (name === 'Potential seed phrase') {
317
+ matches = [...value.matchAll(pattern)]
318
+ .map(match => match[0])
319
+ .filter(seq => this.isLikelySeedPhrase(seq));
320
+ } else if (name === 'API key or token') {
321
+ matches = [...value.matchAll(pattern)]
322
+ .map(match => match[0])
323
+ .filter(seq => this.isLikelyApiKey(seq));
324
+ } else {
325
+ matches = value.match(pattern) || [];
326
+ }
327
+
328
+ if (matches.length > 0) {
329
+ locations.push({
330
+ agent: agentResult.agent || agentKey,
331
+ findingIndex: index,
332
+ field,
333
+ path: finding.path || finding.plist || finding.command || 'N/A',
334
+ pattern: name,
335
+ count: matches.length,
336
+ samples: matches.slice(0, 2).map(m => (m.length > 20 ? `${m.slice(0, 20)}***` : m))
337
+ });
338
+ }
339
+ });
340
+ });
341
+ });
342
+ });
343
+
344
+ return locations;
345
+ }
346
+
347
+ /**
348
+ * Run LLM analysis (with user consent and privacy protection)
349
+ */
350
+ async runLLMAnalysis(provider) {
351
+ if (provider === 'none') {
352
+ console.log(chalk.gray('\n📋 Generating security report only (no AI analysis)\n'));
353
+ return null;
354
+ }
355
+
356
+ console.log(chalk.cyan('\n🤖 Preparing AI Analysis...\n'));
357
+ console.log(chalk.gray('🔒 Privacy Protection:'));
358
+ console.log(chalk.gray(' • All private keys will be redacted'));
359
+ console.log(chalk.gray(' • Sensitive data will be blocked from LLM'));
360
+ console.log(chalk.gray(' • Analysis will be aborted if sensitive patterns detected\n'));
361
+
362
+ const summary = this.results?.summary || {};
363
+ const highRiskCount = summary.highRiskFindings || 0;
364
+ const totalFindings = summary.totalFindings || 0;
365
+ const minHighRisk = getConfig('llm.minHighRiskFindings', 1);
366
+ const minTotalFindings = getConfig('llm.minTotalFindings', 5);
367
+ const skipBelowThreshold = getConfig('llm.skipWhenBelowThreshold', true);
368
+ const llmMode = getConfig('llm.mode', 'summary');
369
+
370
+ if (skipBelowThreshold && highRiskCount < minHighRisk && totalFindings < minTotalFindings) {
371
+ console.log(chalk.gray('⏩ Skipping AI analysis: findings below trigger thresholds'));
372
+ console.log(chalk.gray(` High risk: ${highRiskCount}/${minHighRisk}, Total: ${totalFindings}/${minTotalFindings}`));
373
+ console.log(chalk.gray(' Adjust llm.minHighRiskFindings/minTotalFindings in config to change behavior\n'));
374
+
375
+ this.llmAnalysis = {
376
+ provider,
377
+ skipped: true,
378
+ reason: 'LLM skipped: below trigger thresholds',
379
+ thresholds: {
380
+ minHighRiskFindings: minHighRisk,
381
+ minTotalFindings
382
+ },
383
+ summary: {
384
+ highRiskFindings: highRiskCount,
385
+ totalFindings
386
+ },
387
+ timestamp: new Date().toISOString()
388
+ };
389
+ return this.llmAnalysis;
390
+ }
391
+
392
+ // Build payload with privacy protection
393
+ const analyzer = new LLMAnalyzer(provider, null, {
394
+ enableLogging: true,
395
+ logDir: './logs/llm-requests',
396
+ mode: llmMode
397
+ });
398
+ const promptContent = analyzer.buildPrompt(this.results, { objective: 'unified', mode: llmMode });
399
+
400
+ // Final security check before proceeding
401
+ console.log(chalk.yellow('🔍 Performing security scan...'));
402
+ const securityCheck = analyzer.performSecurityCheck(promptContent);
403
+
404
+ if (securityCheck.hasSensitiveData) {
405
+ console.log(chalk.red('\n🚨 SECURITY ALERT:'));
406
+ console.log(chalk.red(' Sensitive data detected in analysis data!'));
407
+ console.log(chalk.red(' AI analysis aborted to protect your privacy.'));
408
+
409
+ console.log(chalk.yellow('\n📊 Detected Sensitive Patterns:'));
410
+ securityCheck.sensitivePatterns.forEach(pattern => {
411
+ console.log(chalk.yellow(` • ${pattern.name}: ${pattern.count} occurrence(s)`));
412
+ });
413
+
414
+ console.log(chalk.cyan('\n💡 Recommendation:'));
415
+ console.log(chalk.cyan(' 1. Remove sensitive data from your system'));
416
+ console.log(chalk.cyan(' 2. Try analysis again after cleanup'));
417
+ console.log(chalk.cyan(' 3. Or continue with report-only analysis\n'));
418
+
419
+ const detectedLocations = this.detectSensitiveDataInResults(this.results);
420
+
421
+ if (detectedLocations.length > 0) {
422
+ console.log(chalk.gray('📂 Location details (redacted for display):'));
423
+ detectedLocations.forEach(location => {
424
+ const sampleText = location.samples && location.samples.length > 0
425
+ ? ` | samples: ${location.samples.join(', ')}`
426
+ : '';
427
+ console.log(chalk.gray(` • [${location.pattern}] ${location.agent} → ${location.field} @ ${location.path}${sampleText}`));
428
+ });
429
+ console.log();
430
+ } else {
431
+ console.log(chalk.gray('ℹ️ No specific file or command path was flagged. Likely a formatting false positive (e.g., plist <key> tags or long sentences).\n'));
432
+ }
433
+
434
+ // Persist the security check into report data so users can triage
435
+ this.llmAnalysis = {
436
+ provider,
437
+ skipped: true,
438
+ reason: 'Sensitive data detected in analysis data',
439
+ securityCheck: {
440
+ ...securityCheck,
441
+ detectedLocations
442
+ },
443
+ timestamp: new Date().toISOString()
444
+ };
445
+ return this.llmAnalysis;
446
+ }
447
+
448
+ const spinner = ora('Running AI analysis with privacy protection...').start();
449
+
450
+ try {
451
+ this.llmAnalysis = await analyzer.analyze(this.results, {
452
+ objective: 'unified',
453
+ promptOverride: promptContent
454
+ });
455
+
456
+ spinner.succeed('AI analysis completed (details logged).');
457
+
458
+ console.log(chalk.green('\n✅ Privacy Protected AI analysis finished. Full details are stored in logs.'));
459
+ console.log(chalk.green('\n🔒 All sensitive data was automatically redacted before analysis\n'));
460
+
461
+ return this.llmAnalysis;
462
+ } catch (error) {
463
+ spinner.fail(`AI analysis failed: ${error.message}`);
464
+
465
+ if (error.message.includes('SECURITY: Sensitive data detected')) {
466
+ console.log(chalk.red('\n🚨 Privacy protection triggered!'));
467
+ console.log(chalk.red(' Analysis was aborted to prevent sensitive data leakage'));
468
+ console.log(chalk.cyan('\n💡 This is a safety feature to protect your private keys and sensitive information\n'));
469
+ return null;
470
+ }
471
+
472
+ // Ask if user wants to continue without LLM
473
+ const { continueWithout } = await inquirer.prompt([
474
+ {
475
+ type: 'confirm',
476
+ name: 'continueWithout',
477
+ message: 'Continue without AI analysis?',
478
+ default: true
479
+ }
480
+ ]);
481
+
482
+ if (!continueWithout) {
483
+ process.exit(1);
484
+ }
485
+
486
+ return null;
487
+ }
488
+ }
489
+
490
+ /**
491
+ * Generate and save reports
492
+ */
493
+ async generateReports(formats) {
494
+ // Validate inputs
495
+ if (!this.results) {
496
+ console.log(chalk.yellow('\n⚠️ No analysis results available. Skipping report generation.'));
497
+ return [];
498
+ }
499
+
500
+ const spinner = ora('Generating reports...').start();
501
+ const startTime = Date.now();
502
+
503
+ try {
504
+ const reportManager = new ReportManager(this.results, this.llmAnalysis, {
505
+ reportsDir: getConfig('reports.outputDir', './reports'),
506
+ retentionDays: getConfig('reports.retentionDays', 90),
507
+ defaultTemplate: getConfig('reports.defaultTemplate', 'executive'),
508
+ pdfOptions: getConfig('reports.pdfOptions', {})
509
+ });
510
+
511
+ const savedFiles = await reportManager.generateReports(formats);
512
+ const duration = Date.now() - startTime;
513
+
514
+ log.reportGeneration(formats, savedFiles.map(f => f.path), duration);
515
+
516
+ spinner.succeed('Reports generated!');
517
+
518
+ console.log(chalk.bold('\n📁 Saved Reports:'));
519
+ savedFiles.forEach(file => {
520
+ const typeEmoji = file.type === 'pdf' ? '📑' : '📄';
521
+ console.log(` ${typeEmoji} ${file.type.charAt(0).toUpperCase() + file.type.slice(1)}: ${file.path}`);
522
+ });
523
+
524
+ const reportsDir = getConfig('reports.outputDir', './reports');
525
+ console.log(chalk.cyan(`\n💡 All reports are organized in ${reportsDir}/ directory by year and month`));
526
+ } catch (error) {
527
+ log.error('Report generation failed', { error: error.message });
528
+ spinner.fail(`Report generation failed: ${error.message}`);
529
+ throw error;
530
+ }
531
+ }
532
+
533
+ /**
534
+ * Get color for risk level
535
+ */
536
+ getRiskColor(risk) {
537
+ const normalizedRisk = (risk || '').toLowerCase();
538
+ switch (normalizedRisk) {
539
+ case 'high':
540
+ return chalk.red.bold;
541
+ case 'medium':
542
+ return chalk.yellow.bold;
543
+ case 'low':
544
+ return chalk.green;
545
+ default:
546
+ return chalk.gray;
547
+ }
548
+ }
549
+
550
+ /**
551
+ * Main application flow
552
+ */
553
+ async run() {
554
+ try {
555
+ this.displayBanner();
556
+
557
+ // Step 1: Auto-detect LLM provider
558
+ const detectedProvider = this.detectLLMProvider();
559
+
560
+ // Step 2: Choose LLM analysis option
561
+ const llmProvider = await this.chooseLLMAnalysis(detectedProvider);
562
+
563
+ // Step 3: Select report format
564
+ const reportFormats = await this.selectReportFormat();
565
+
566
+ // Step 4: IP geolocation (always enabled)
567
+ const geoLookupEnabled = this.getGeoLookupSetting();
568
+
569
+ // Step 5: Confirm
570
+ const proceed = await this.confirmProceed(llmProvider, reportFormats, geoLookupEnabled);
571
+
572
+ if (!proceed) {
573
+ console.log(chalk.yellow('\n👋 Analysis cancelled.\n'));
574
+ process.exit(0);
575
+ }
576
+
577
+ // Step 6: Run unified analysis (comprehensive by default)
578
+ await this.runAnalysis({ geoLookupEnabled });
579
+
580
+ // Step 7: Run LLM analysis (if selected)
581
+ if (llmProvider !== 'none') {
582
+ await this.runLLMAnalysis(llmProvider);
583
+ }
584
+
585
+ // Step 8: Generate reports
586
+ await this.generateReports(reportFormats);
587
+
588
+ console.log(chalk.green.bold('\n✨ Analysis complete! Check your reports for details.\n'));
589
+ } catch (error) {
590
+ console.error(chalk.red(`\n❌ Error: ${error.message}\n`));
591
+ if (process.env.DEBUG) {
592
+ console.error(error.stack);
593
+ }
594
+ process.exit(1);
595
+ }
596
+ }
597
+ }
598
+
599
+ // Check for help flag
600
+ if (process.argv.includes('--help') || process.argv.includes('-h')) {
601
+ console.log(`
602
+ MacOS Security Analysis CLI v2.0.0
603
+
604
+ USAGE:
605
+ npm start # Run interactive analysis
606
+ npm start -- --help # Show this help message
607
+
608
+ EXAMPLES:
609
+ npm start # Interactive mode with prompts
610
+
611
+ FEATURES:
612
+ 🔍 Unified Adaptive Analysis - Automatically detects and analyzes relevant areas
613
+ 📑 Professional PDF Reports - Enterprise-quality reporting
614
+ 🔗 Smart Blockchain Detection - Only runs blockchain agents when needed
615
+ 📊 Structured Logging - Comprehensive audit trails
616
+ ⚙️ Configuration Management - Customizable behavior
617
+ 🔒 Privacy Protection - Advanced data sanitization
618
+
619
+ For more information, see the README.md file.
620
+ `);
621
+ process.exit(0);
622
+ }
623
+
624
+ // Run the CLI
625
+ const cli = new SecurityAnalysisCLI();
626
+ cli.run();
627
+
628
+ // Export for testing
629
+ export { SecurityAnalysisCLI };