stigmergy 1.2.6 → 1.2.8

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.
Files changed (59) hide show
  1. package/README.md +32 -17
  2. package/STIGMERGY.md +16 -7
  3. package/docs/MULTI_USER_WIKI_COLLABORATION_SYSTEM.md +523 -0
  4. package/docs/PROMPT_BASED_SKILLS_SYSTEM_DESIGN.md +458 -0
  5. package/docs/SKILL_IMPLEMENTATION_CONSTRAINTS_AND_ALIGNMENT.md +423 -0
  6. package/docs/TECHNICAL_FEASIBILITY_ANALYSIS.md +308 -0
  7. package/examples/multilingual-hook-demo.js +125 -0
  8. package/package.json +14 -17
  9. package/scripts/dependency-analyzer.js +101 -0
  10. package/scripts/generate-cli-docs.js +64 -0
  11. package/scripts/postuninstall.js +46 -0
  12. package/scripts/preuninstall.js +75 -0
  13. package/scripts/run-layered-tests.js +3 -3
  14. package/src/adapters/claude/install_claude_integration.js +17 -17
  15. package/src/adapters/codebuddy/install_codebuddy_integration.js +13 -13
  16. package/src/adapters/codex/install_codex_integration.js +27 -27
  17. package/src/adapters/copilot/install_copilot_integration.js +46 -46
  18. package/src/adapters/gemini/install_gemini_integration.js +10 -10
  19. package/src/adapters/iflow/install_iflow_integration.js +7 -7
  20. package/src/adapters/qoder/install_qoder_integration.js +12 -12
  21. package/src/adapters/qwen/install_qwen_integration.js +17 -17
  22. package/src/auth.js +173 -173
  23. package/src/auth_command.js +208 -208
  24. package/src/calculator.js +313 -313
  25. package/src/cli/router.js +151 -7
  26. package/src/core/cache_cleaner.js +767 -767
  27. package/src/core/cli_help_analyzer.js +680 -680
  28. package/src/core/cli_parameter_handler.js +132 -132
  29. package/src/core/cli_tools.js +89 -89
  30. package/src/core/coordination/index.js +16 -16
  31. package/src/core/coordination/nodejs/AdapterManager.js +102 -102
  32. package/src/core/coordination/nodejs/CLCommunication.js +132 -132
  33. package/src/core/coordination/nodejs/CLIIntegrationManager.js +272 -272
  34. package/src/core/coordination/nodejs/HealthChecker.js +76 -76
  35. package/src/core/coordination/nodejs/HookDeploymentManager.js +463 -274
  36. package/src/core/coordination/nodejs/StatisticsCollector.js +71 -71
  37. package/src/core/coordination/nodejs/index.js +90 -90
  38. package/src/core/coordination/nodejs/utils/Logger.js +29 -29
  39. package/src/core/enhanced_installer.js +479 -479
  40. package/src/core/enhanced_uninstaller.js +638 -638
  41. package/src/core/error_handler.js +406 -406
  42. package/src/core/installer.js +32 -32
  43. package/src/core/memory_manager.js +83 -83
  44. package/src/core/multilingual/language-pattern-manager.js +172 -0
  45. package/src/core/rest_client.js +160 -160
  46. package/src/core/smart_router.js +261 -249
  47. package/src/core/upgrade_manager.js +48 -20
  48. package/src/data_encryption.js +143 -143
  49. package/src/data_structures.js +440 -440
  50. package/src/deploy.js +55 -55
  51. package/src/index.js +30 -30
  52. package/src/test/cli-availability-checker.js +194 -194
  53. package/src/test/test-environment.js +289 -289
  54. package/src/utils/helpers.js +35 -35
  55. package/src/utils.js +921 -921
  56. package/src/weatherProcessor.js +228 -228
  57. package/test/multilingual/hook-deployment.test.js +91 -0
  58. package/test/multilingual/language-pattern-manager.test.js +140 -0
  59. package/test/multilingual/system-test.js +85 -0
@@ -1,680 +1,680 @@
1
- // Enhanced CLI Help Analyzer with better pattern extraction
2
- const { spawnSync } = require('child_process');
3
- const fs = require('fs/promises');
4
- const path = require('path');
5
- const os = require('os');
6
- const { CLI_TOOLS } = require('./cli_tools');
7
- const { errorHandler } = require('./error_handler');
8
-
9
- class CLIHelpAnalyzer {
10
- constructor() {
11
- this.configDir = path.join(os.homedir(), '.stigmergy', 'cli-patterns');
12
- this.persistentConfig = path.join(this.configDir, 'cli-patterns.json');
13
- this.lastAnalysisFile = path.join(this.configDir, 'last-analysis.json');
14
- this.cliTools = CLI_TOOLS;
15
-
16
- // Pattern recognition rules for different CLI types
17
- this.patternRules = {
18
- // OpenAI style CLI (codex, chatgpt)
19
- openai: {
20
- commandPattern: /^(\w+)(?:\s+--?[\w-]+(?:\s+[\w-]+)?)?\s*(.*)$/,
21
- optionPattern: /--?[\w-]+(?:\s+[\w-]+)?/g,
22
- examplePattern:
23
- /(?:example|usage|Usage)[::]\s*\n([\s\S]*?)(?=\n\n|\n[A-Z]|\n$)/gi,
24
- subcommandPattern: /^\s{2,4}(\w+)\s+.+$/gm,
25
- },
26
- // Anthropic style CLI (claude)
27
- anthropic: {
28
- commandPattern: /^(\w+)(?:\s+(?:--?\w+|\[\w+\]))*\s*(.*)$/,
29
- optionPattern: /--?\w+(?:\s+<\w+>)?/g,
30
- examplePattern:
31
- /(?:Examples?|例子)[::]\s*\n([\s\S]*?)(?=\n\n|\n[A-Z]|\n$)/gi,
32
- subcommandPattern: /^\s{2}(\w+)\s{2,}.+$/gm,
33
- },
34
- // Google style CLI (gemini)
35
- google: {
36
- commandPattern: /^(\w+)(?:\s+(?:--?\w+(?:=\w+)?|<\w+>))*\s*(.*)$/,
37
- optionPattern: /--?\w+(?:=\w+)?/g,
38
- examplePattern:
39
- /(?:Examples?|SAMPLE)[::]\s*\n([\s\S]*?)(?=\n\n|\n[A-Z]|\n$)/gi,
40
- subcommandPattern: /^\s{2,4}(\w+)\s{2,}.+$/gm,
41
- },
42
- // Generic CLI pattern
43
- generic: {
44
- commandPattern: /^(\w+)(?:\s+.*)?$/,
45
- optionPattern: /--?[a-zA-Z][\w-]*/g,
46
- examplePattern:
47
- /(?:example|usage|用法|使用)[::]\s*\n([\s\S]*?)(?=\n\n|\n[A-Z]|\n$)/gi,
48
- subcommandPattern: /^\s{2,6}([a-z][a-z0-9_-]+)\s+.+$/gm,
49
- },
50
- };
51
- }
52
-
53
- async initialize() {
54
- try {
55
- await fs.mkdir(this.configDir, { recursive: true });
56
- // Initialize persistent config if not exists
57
- const configExists = await this.fileExists(this.persistentConfig);
58
- if (!configExists) {
59
- const initialConfig = {
60
- version: '1.0.0',
61
- lastUpdated: new Date().toISOString(),
62
- cliPatterns: {},
63
- failedAttempts: {},
64
- };
65
- // Suppress error if file creation fails initially
66
- try {
67
- await this.savePersistentConfig(initialConfig);
68
- } catch (createError) {
69
- // Only show error if it's not a permissions issue or similar expected issue
70
- if (
71
- !createError.message.includes('EACCES') &&
72
- !createError.message.includes('EPERM')
73
- ) {
74
- await errorHandler.logError(
75
- createError,
76
- 'ERROR',
77
- 'CLIHelpAnalyzer.initialize',
78
- );
79
- }
80
- }
81
- }
82
- return true;
83
- } catch (error) {
84
- // Don't spam error messages on initialization issues
85
- // These are often expected on first run
86
- if (process.env.DEBUG === 'true') {
87
- await errorHandler.logError(
88
- error,
89
- 'ERROR',
90
- 'CLIHelpAnalyzer.initialize',
91
- );
92
- }
93
- return false;
94
- }
95
- }
96
-
97
- /**
98
- * Analyze all configured CLI tools with optimized error handling
99
- */
100
- async analyzeAllCLI() {
101
- const results = {};
102
- for (const [cliName, _] of Object.entries(this.cliTools)) {
103
- try {
104
- if (process.env.DEBUG === 'true') {
105
- console.log(`Analyzing ${cliName}...`);
106
- }
107
- results[cliName] = await this.analyzeCLI(cliName);
108
- } catch (error) {
109
- // Only log important errors, suppress expected file not found errors
110
- if (
111
- !error.message.includes('ENOENT') &&
112
- !error.message.includes('no such file or directory') &&
113
- !error.message.includes(
114
- 'not recognized as an internal or external command',
115
- )
116
- ) {
117
- await errorHandler.logError(
118
- error,
119
- 'WARN',
120
- `CLIHelpAnalyzer.analyzeAllCLI.${cliName}`,
121
- );
122
- }
123
- results[cliName] = { success: false, error: error.message };
124
- }
125
- }
126
- return results;
127
- }
128
-
129
- /**
130
- * Analyze specific CLI tool
131
- */
132
- async analyzeCLI(cliName) {
133
- const cliConfig = this.cliTools[cliName];
134
- if (!cliConfig) {
135
- throw new Error(`CLI tool ${cliName} not found in configuration`);
136
- }
137
- try {
138
- // Get help information
139
- const helpInfo = await this.getHelpInfo(cliName, cliConfig);
140
- // Detect CLI type
141
- const cliType = this.detectCLIType(helpInfo.rawHelp, cliName);
142
- // Extract patterns
143
- const patterns = this.extractPatterns(helpInfo.rawHelp, cliType, cliName);
144
- // Analyze command structure
145
- const commandStructure = this.analyzeCommandStructure(patterns);
146
- // Extract usage examples
147
- const examples = this.extractUsageExamples(helpInfo.rawHelp, cliType);
148
- // Determine interaction mode
149
- const interactionMode = this.determineInteractionMode(helpInfo, patterns);
150
- const analysis = {
151
- success: true,
152
- cliName,
153
- cliType,
154
- version: helpInfo.version,
155
- helpMethod: helpInfo.method,
156
- patterns,
157
- commandStructure,
158
- examples,
159
- interactionMode,
160
- timestamp: new Date().toISOString(),
161
- };
162
- // Cache the analysis
163
- await this.cacheAnalysis(cliName, analysis);
164
- return analysis;
165
- } catch (error) {
166
- // Record failed attempt but suppress error if it's an expected issue
167
- await this.recordFailedAttempt(cliName, error);
168
- // Only throw if it's not an expected "tool not installed" error
169
- if (
170
- !error.message.includes(
171
- 'not recognized as an internal or external command',
172
- ) &&
173
- !error.message.includes('command not found')
174
- ) {
175
- throw error;
176
- }
177
- // For tool not found errors, return minimal analysis instead of throwing
178
- return {
179
- success: false,
180
- cliName,
181
- error: error.message,
182
- timestamp: new Date().toISOString(),
183
- };
184
- }
185
- }
186
-
187
- /**
188
- * Get help information using multiple methods
189
- */
190
- async getHelpInfo(cliName, cliConfig) {
191
- // Special handling for codex to avoid opening files or executing commands
192
- if (cliName === 'codex') {
193
- return await this.getCodexHelpInfo(cliConfig);
194
- }
195
-
196
- const helpMethods = [
197
- ['--help'],
198
- ['-h'],
199
- ['help'],
200
- ['--usage'],
201
- [''],
202
- ['version'],
203
- ['--version'],
204
- ['-v'],
205
- ];
206
- let rawHelp = '';
207
- let version = 'unknown';
208
- let method = 'unknown';
209
- // Try different help commands
210
- for (const helpArgs of helpMethods) {
211
- try {
212
- const result = spawnSync(cliName, helpArgs, {
213
- encoding: 'utf8',
214
- timeout: 15000,
215
- shell: true,
216
- });
217
- if (result.status === 0 && result.stdout) {
218
- rawHelp = result.stdout;
219
- method = `${cliName} ${helpArgs.join(' ')}`;
220
- break;
221
- } else if (result.stderr) {
222
- rawHelp = result.stderr;
223
- method = `${cliName} ${helpArgs.join(' ')} (stderr)`;
224
- break;
225
- }
226
- } catch (error) {
227
- // Try next method
228
- continue;
229
- }
230
- }
231
- // Try to get version separately
232
- if (cliConfig.version) {
233
- try {
234
- const versionCmd = cliConfig.version.split(' ');
235
- const versionResult = spawnSync(versionCmd[0], versionCmd.slice(1), {
236
- encoding: 'utf8',
237
- timeout: 10000,
238
- shell: true,
239
- });
240
- if (versionResult.status === 0) {
241
- version = versionResult.stdout.trim() || versionResult.stderr.trim();
242
- }
243
- } catch (error) {
244
- // Use default version
245
- }
246
- }
247
- if (!rawHelp) {
248
- throw new Error(`Unable to get help information for ${cliName}`);
249
- }
250
- return { rawHelp, version, method };
251
- }
252
-
253
- /**
254
- * Get Codex help information by checking file paths instead of executing commands
255
- */
256
- async getCodexHelpInfo(cliConfig) {
257
- try {
258
- // Check if codex is likely installed by checking common installation paths
259
- const os = require('os');
260
- const path = require('path');
261
- const fs = require('fs');
262
-
263
- // Common Codex installation paths
264
- const possiblePaths = [
265
- path.join(os.homedir(), '.codex'),
266
- path.join(os.homedir(), '.config', 'codex'),
267
- path.join(
268
- os.homedir(),
269
- 'AppData',
270
- 'Roaming',
271
- 'npm',
272
- 'node_modules',
273
- 'openai-codex-cli',
274
- ),
275
- path.join('/usr/local/lib/node_modules/openai-codex-cli'),
276
- path.join('/usr/lib/node_modules/openai-codex-cli'),
277
- ];
278
-
279
- // Check if any of these paths exist
280
- let codexInstalled = false;
281
- let configPath = '';
282
-
283
- for (const possiblePath of possiblePaths) {
284
- if (fs.existsSync(possiblePath)) {
285
- codexInstalled = true;
286
- configPath = possiblePath;
287
- break;
288
- }
289
- }
290
-
291
- // If Codex is not installed, throw an error without trying to execute commands
292
- if (!codexInstalled) {
293
- throw new Error('Codex CLI not found in standard installation paths');
294
- }
295
-
296
- // If Codex appears to be installed, return minimal help information
297
- // without actually executing any commands
298
- return {
299
- rawHelp:
300
- 'Codex CLI - OpenAI Codex Command Line Interface\nUsage: codex [options] [prompt]\n\nOptions:\n --help, -h Show help\n --version Show version\n --model Specify model to use\n --temperature Set temperature for generation',
301
- version: 'unknown',
302
- method: 'path-detection',
303
- };
304
- } catch (error) {
305
- throw new Error(
306
- `Unable to get help information for codex: ${error.message}`,
307
- );
308
- }
309
- }
310
-
311
- /**
312
- * Detect CLI type based on help content and naming
313
- */
314
- detectCLIType(helpText, cliName) {
315
- const text = helpText.toLowerCase();
316
- const name = cliName.toLowerCase();
317
- // Detect based on CLI name
318
- if (name.includes('claude') || name.includes('anthropic')) {
319
- return 'anthropic';
320
- }
321
- if (name.includes('gemini') || name.includes('google')) {
322
- return 'google';
323
- }
324
- if (
325
- name.includes('codex') ||
326
- name.includes('openai') ||
327
- name.includes('chatgpt')
328
- ) {
329
- return 'openai';
330
- }
331
- // Detect based on help content patterns
332
- if (text.includes('anthropic') || text.includes('claude')) {
333
- return 'anthropic';
334
- }
335
- if (
336
- text.includes('google') ||
337
- text.includes('gemini') ||
338
- text.includes('vertex')
339
- ) {
340
- return 'google';
341
- }
342
- if (
343
- text.includes('openai') ||
344
- text.includes('gpt') ||
345
- text.includes('codex')
346
- ) {
347
- return 'openai';
348
- }
349
- if (
350
- text.includes('qwen') ||
351
- text.includes('alibaba') ||
352
- text.includes('tongyi')
353
- ) {
354
- return 'alibaba';
355
- }
356
- if (text.includes('iflow') || text.includes('intelligent')) {
357
- return 'iflow';
358
- }
359
- if (text.includes('copilot') || text.includes('github')) {
360
- return 'github';
361
- }
362
- if (text.includes('codebuddy') || text.includes('buddy')) {
363
- return 'codebuddy';
364
- }
365
- if (text.includes('qoder') || text.includes('code')) {
366
- return 'qoder';
367
- }
368
- // Default to generic
369
- return 'generic';
370
- }
371
-
372
- /**
373
- * Extract patterns from help text
374
- */
375
- extractPatterns(helpText, cliType, cliName) {
376
- const rules = this.patternRules[cliType] || this.patternRules.generic;
377
- const patterns = {
378
- commands: [],
379
- options: [],
380
- subcommands: [],
381
- arguments: [],
382
- flags: [],
383
- nonInteractiveFlag: null,
384
- promptFlag: null,
385
- requiredFlags: [],
386
- commonPatterns: [],
387
- };
388
-
389
- // Extract options/flags
390
- const optionMatches = helpText.match(rules.optionPattern) || [];
391
- patterns.options = [...new Set(optionMatches)];
392
-
393
- // Extract subcommands
394
- const subcommandMatches = helpText.match(rules.subcommandPattern) || [];
395
- patterns.subcommands = subcommandMatches.map((match) => {
396
- const subcommand = match.trim().split(/\s+/)[0];
397
- return { name: subcommand, description: match.trim() };
398
- });
399
-
400
- // Try to identify non-interactive flag
401
- const nonInteractiveFlags = patterns.options.filter(
402
- (option) =>
403
- option.includes('non-interactive') ||
404
- option.includes('batch') ||
405
- option.includes('no-input') ||
406
- option.includes('stdin'),
407
- );
408
- patterns.nonInteractiveFlag =
409
- nonInteractiveFlags.length > 0 ? nonInteractiveFlags[0] : null;
410
-
411
- // Try to identify prompt flag
412
- const promptFlags = patterns.options.filter(
413
- (option) =>
414
- option.includes('prompt') ||
415
- option.includes('input') ||
416
- option.includes('query') ||
417
- option.includes('question'),
418
- );
419
- patterns.promptFlag = promptFlags.length > 0 ? promptFlags[0] : null;
420
-
421
- // Identify required flags
422
- patterns.requiredFlags = patterns.options.filter(
423
- (option) =>
424
- option.includes('<') || // Angle brackets indicate required parameters
425
- option.includes('*'), // Asterisk indicates required
426
- );
427
-
428
- return patterns;
429
- }
430
-
431
- /**
432
- * Analyze command structure
433
- */
434
- analyzeCommandStructure(patterns) {
435
- const structure = {
436
- primaryCommand: '',
437
- commandFormat: 'cli [args]',
438
- argumentStyle: '',
439
- optionStyle: '',
440
- interactiveMode: false,
441
- hasSubcommands: patterns.subcommands.length > 0,
442
- complexity: 'simple',
443
- nonInteractiveSupport: !!patterns.nonInteractiveFlag,
444
- promptStyle: patterns.promptFlag ? 'flag' : 'argument',
445
- executionPattern: 'interactive-default',
446
- nonInteractiveFlag: patterns.nonInteractiveFlag,
447
- promptFlag: patterns.promptFlag,
448
- requiredFlags: patterns.requiredFlags,
449
- commonPatterns: patterns.commonPatterns,
450
- };
451
-
452
- // Determine complexity
453
- if (patterns.subcommands.length > 5) {
454
- structure.complexity = 'complex';
455
- } else if (patterns.subcommands.length > 0) {
456
- structure.complexity = 'moderate';
457
- }
458
-
459
- // Determine execution pattern
460
- if (patterns.nonInteractiveFlag) {
461
- structure.executionPattern = 'flag-based';
462
- } else if (patterns.promptFlag) {
463
- structure.executionPattern = 'prompt-flag';
464
- }
465
-
466
- return structure;
467
- }
468
-
469
- /**
470
- * Extract usage examples
471
- */
472
- extractUsageExamples(helpText, cliType) {
473
- const rules = this.patternRules[cliType] || this.patternRules.generic;
474
- const examples = [];
475
- let match;
476
-
477
- while ((match = rules.examplePattern.exec(helpText)) !== null) {
478
- examples.push(match[1].trim());
479
- }
480
-
481
- return examples;
482
- }
483
-
484
- /**
485
- * Determine interaction mode
486
- */
487
- determineInteractionMode(helpInfo, patterns) {
488
- const helpText = helpInfo.rawHelp.toLowerCase();
489
-
490
- // Check for explicit non-interactive support
491
- if (patterns.nonInteractiveFlag) {
492
- return 'non-interactive';
493
- }
494
-
495
- // Check for stdin support
496
- if (helpText.includes('stdin') || helpText.includes('pipe')) {
497
- return 'stdin-support';
498
- }
499
-
500
- // Check for batch mode
501
- if (helpText.includes('batch') || helpText.includes('script')) {
502
- return 'batch-mode';
503
- }
504
-
505
- // Default to interactive
506
- return 'interactive';
507
- }
508
-
509
- /**
510
- * Cache analysis results
511
- */
512
- async cacheAnalysis(cliName, analysis) {
513
- try {
514
- const config = await this.loadPersistentConfig();
515
- config.cliPatterns[cliName] = analysis;
516
- config.lastUpdated = new Date().toISOString();
517
- await this.savePersistentConfig(config);
518
- } catch (error) {
519
- // Don't spam errors for cache issues
520
- if (process.env.DEBUG === 'true') {
521
- await errorHandler.logError(
522
- error,
523
- 'WARN',
524
- 'CLIHelpAnalyzer.cacheAnalysis',
525
- );
526
- }
527
- }
528
- }
529
-
530
- /**
531
- * Get cached analysis
532
- */
533
- async getCachedAnalysis(cliName) {
534
- try {
535
- const config = await this.loadPersistentConfig();
536
- return config.cliPatterns[cliName] || null;
537
- } catch (error) {
538
- return null;
539
- }
540
- }
541
-
542
- /**
543
- * Get CLI pattern (wrapper for getCachedAnalysis)
544
- */
545
- async getCLIPattern(cliName) {
546
- return await this.getCachedAnalysis(cliName);
547
- }
548
-
549
- /**
550
- * Check if cache is expired (1 day)
551
- */
552
- isCacheExpired(timestamp) {
553
- if (!timestamp) return true;
554
- const cacheTime = new Date(timestamp);
555
- const now = new Date();
556
- const diffHours = (now - cacheTime) / (1000 * 60 * 60);
557
- return diffHours > 24; // Expire after 24 hours
558
- }
559
-
560
- /**
561
- * Load persistent configuration
562
- */
563
- async loadPersistentConfig() {
564
- try {
565
- const configExists = await this.fileExists(this.persistentConfig);
566
- if (!configExists) {
567
- return {
568
- version: '1.0.0',
569
- lastUpdated: new Date().toISOString(),
570
- cliPatterns: {},
571
- failedAttempts: {},
572
- };
573
- }
574
- const configData = await fs.readFile(this.persistentConfig, 'utf8');
575
- return JSON.parse(configData);
576
- } catch (error) {
577
- // Return default config if loading fails
578
- return {
579
- version: '1.0.0',
580
- lastUpdated: new Date().toISOString(),
581
- cliPatterns: {},
582
- failedAttempts: {},
583
- };
584
- }
585
- }
586
-
587
- /**
588
- * Save persistent configuration
589
- */
590
- async savePersistentConfig(config) {
591
- try {
592
- await fs.writeFile(
593
- this.persistentConfig,
594
- JSON.stringify(config, null, 2),
595
- );
596
- } catch (error) {
597
- throw error;
598
- }
599
- }
600
-
601
- /**
602
- * Record failed attempt
603
- */
604
- async recordFailedAttempt(cliName, error) {
605
- try {
606
- const config = await this.loadPersistentConfig();
607
- config.failedAttempts[cliName] = {
608
- error: error.message,
609
- timestamp: new Date().toISOString(),
610
- attempts: (config.failedAttempts[cliName]?.attempts || 0) + 1,
611
- };
612
- await this.savePersistentConfig(config);
613
- } catch (saveError) {
614
- // Don't spam errors for failed attempt recording
615
- if (process.env.DEBUG === 'true') {
616
- await errorHandler.logError(
617
- saveError,
618
- 'WARN',
619
- 'CLIHelpAnalyzer.recordFailedAttempt',
620
- );
621
- }
622
- }
623
- }
624
-
625
- /**
626
- * Update CLI pattern when call fails
627
- */
628
- async updatePatternOnFailure(cliName, error, attemptedCommand) {
629
- // Only log in debug mode to reduce console noise
630
- if (process.env.DEBUG === 'true') {
631
- console.log(
632
- `Updating pattern for ${cliName} due to failure:`,
633
- error.message,
634
- );
635
- }
636
- try {
637
- // Re-analyze the CLI
638
- const newAnalysis = await this.analyzeCLI(cliName);
639
- // Add failure context
640
- newAnalysis.lastFailure = {
641
- error: error.message,
642
- attemptedCommand,
643
- timestamp: new Date().toISOString(),
644
- };
645
- // Update the cached analysis
646
- await this.cacheAnalysis(cliName, newAnalysis);
647
- return newAnalysis;
648
- } catch (analysisError) {
649
- // Only log analysis errors in debug mode
650
- if (process.env.DEBUG === 'true') {
651
- console.error(
652
- `Failed to re-analyze ${cliName}:`,
653
- analysisError.message,
654
- );
655
- }
656
- return null;
657
- }
658
- }
659
-
660
- /**
661
- * Check if file exists
662
- */
663
- async fileExists(filePath) {
664
- try {
665
- await fs.access(filePath);
666
- return true;
667
- } catch (error) {
668
- return false;
669
- }
670
- }
671
-
672
- /**
673
- * Set CLI tools
674
- */
675
- setCLITools(tools) {
676
- this.cliTools = tools;
677
- }
678
- }
679
-
680
- module.exports = CLIHelpAnalyzer;
1
+ // Enhanced CLI Help Analyzer with better pattern extraction
2
+ const { spawnSync } = require('child_process');
3
+ const fs = require('fs/promises');
4
+ const path = require('path');
5
+ const os = require('os');
6
+ const { CLI_TOOLS } = require('./cli_tools');
7
+ const { errorHandler } = require('./error_handler');
8
+
9
+ class CLIHelpAnalyzer {
10
+ constructor() {
11
+ this.configDir = path.join(os.homedir(), '.stigmergy', 'cli-patterns');
12
+ this.persistentConfig = path.join(this.configDir, 'cli-patterns.json');
13
+ this.lastAnalysisFile = path.join(this.configDir, 'last-analysis.json');
14
+ this.cliTools = CLI_TOOLS;
15
+
16
+ // Pattern recognition rules for different CLI types
17
+ this.patternRules = {
18
+ // OpenAI style CLI (codex, chatgpt)
19
+ openai: {
20
+ commandPattern: /^(\w+)(?:\s+--?[\w-]+(?:\s+[\w-]+)?)?\s*(.*)$/,
21
+ optionPattern: /--?[\w-]+(?:\s+[\w-]+)?/g,
22
+ examplePattern:
23
+ /(?:example|usage|Usage)[::]\s*\n([\s\S]*?)(?=\n\n|\n[A-Z]|\n$)/gi,
24
+ subcommandPattern: /^\s{2,4}(\w+)\s+.+$/gm,
25
+ },
26
+ // Anthropic style CLI (claude)
27
+ anthropic: {
28
+ commandPattern: /^(\w+)(?:\s+(?:--?\w+|\[\w+\]))*\s*(.*)$/,
29
+ optionPattern: /--?\w+(?:\s+<\w+>)?/g,
30
+ examplePattern:
31
+ /(?:Examples?|例子)[::]\s*\n([\s\S]*?)(?=\n\n|\n[A-Z]|\n$)/gi,
32
+ subcommandPattern: /^\s{2}(\w+)\s{2,}.+$/gm,
33
+ },
34
+ // Google style CLI (gemini)
35
+ google: {
36
+ commandPattern: /^(\w+)(?:\s+(?:--?\w+(?:=\w+)?|<\w+>))*\s*(.*)$/,
37
+ optionPattern: /--?\w+(?:=\w+)?/g,
38
+ examplePattern:
39
+ /(?:Examples?|SAMPLE)[::]\s*\n([\s\S]*?)(?=\n\n|\n[A-Z]|\n$)/gi,
40
+ subcommandPattern: /^\s{2,4}(\w+)\s{2,}.+$/gm,
41
+ },
42
+ // Generic CLI pattern
43
+ generic: {
44
+ commandPattern: /^(\w+)(?:\s+.*)?$/,
45
+ optionPattern: /--?[a-zA-Z][\w-]*/g,
46
+ examplePattern:
47
+ /(?:example|usage|用法|使用)[::]\s*\n([\s\S]*?)(?=\n\n|\n[A-Z]|\n$)/gi,
48
+ subcommandPattern: /^\s{2,6}([a-z][a-z0-9_-]+)\s+.+$/gm,
49
+ },
50
+ };
51
+ }
52
+
53
+ async initialize() {
54
+ try {
55
+ await fs.mkdir(this.configDir, { recursive: true });
56
+ // Initialize persistent config if not exists
57
+ const configExists = await this.fileExists(this.persistentConfig);
58
+ if (!configExists) {
59
+ const initialConfig = {
60
+ version: '1.0.0',
61
+ lastUpdated: new Date().toISOString(),
62
+ cliPatterns: {},
63
+ failedAttempts: {},
64
+ };
65
+ // Suppress error if file creation fails initially
66
+ try {
67
+ await this.savePersistentConfig(initialConfig);
68
+ } catch (createError) {
69
+ // Only show error if it's not a permissions issue or similar expected issue
70
+ if (
71
+ !createError.message.includes('EACCES') &&
72
+ !createError.message.includes('EPERM')
73
+ ) {
74
+ await errorHandler.logError(
75
+ createError,
76
+ 'ERROR',
77
+ 'CLIHelpAnalyzer.initialize',
78
+ );
79
+ }
80
+ }
81
+ }
82
+ return true;
83
+ } catch (error) {
84
+ // Don't spam error messages on initialization issues
85
+ // These are often expected on first run
86
+ if (process.env.DEBUG === 'true') {
87
+ await errorHandler.logError(
88
+ error,
89
+ 'ERROR',
90
+ 'CLIHelpAnalyzer.initialize',
91
+ );
92
+ }
93
+ return false;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Analyze all configured CLI tools with optimized error handling
99
+ */
100
+ async analyzeAllCLI() {
101
+ const results = {};
102
+ for (const [cliName, _] of Object.entries(this.cliTools)) {
103
+ try {
104
+ if (process.env.DEBUG === 'true') {
105
+ console.log(`Analyzing ${cliName}...`);
106
+ }
107
+ results[cliName] = await this.analyzeCLI(cliName);
108
+ } catch (error) {
109
+ // Only log important errors, suppress expected file not found errors
110
+ if (
111
+ !error.message.includes('ENOENT') &&
112
+ !error.message.includes('no such file or directory') &&
113
+ !error.message.includes(
114
+ 'not recognized as an internal or external command',
115
+ )
116
+ ) {
117
+ await errorHandler.logError(
118
+ error,
119
+ 'WARN',
120
+ `CLIHelpAnalyzer.analyzeAllCLI.${cliName}`,
121
+ );
122
+ }
123
+ results[cliName] = { success: false, error: error.message };
124
+ }
125
+ }
126
+ return results;
127
+ }
128
+
129
+ /**
130
+ * Analyze specific CLI tool
131
+ */
132
+ async analyzeCLI(cliName) {
133
+ const cliConfig = this.cliTools[cliName];
134
+ if (!cliConfig) {
135
+ throw new Error(`CLI tool ${cliName} not found in configuration`);
136
+ }
137
+ try {
138
+ // Get help information
139
+ const helpInfo = await this.getHelpInfo(cliName, cliConfig);
140
+ // Detect CLI type
141
+ const cliType = this.detectCLIType(helpInfo.rawHelp, cliName);
142
+ // Extract patterns
143
+ const patterns = this.extractPatterns(helpInfo.rawHelp, cliType, cliName);
144
+ // Analyze command structure
145
+ const commandStructure = this.analyzeCommandStructure(patterns);
146
+ // Extract usage examples
147
+ const examples = this.extractUsageExamples(helpInfo.rawHelp, cliType);
148
+ // Determine interaction mode
149
+ const interactionMode = this.determineInteractionMode(helpInfo, patterns);
150
+ const analysis = {
151
+ success: true,
152
+ cliName,
153
+ cliType,
154
+ version: helpInfo.version,
155
+ helpMethod: helpInfo.method,
156
+ patterns,
157
+ commandStructure,
158
+ examples,
159
+ interactionMode,
160
+ timestamp: new Date().toISOString(),
161
+ };
162
+ // Cache the analysis
163
+ await this.cacheAnalysis(cliName, analysis);
164
+ return analysis;
165
+ } catch (error) {
166
+ // Record failed attempt but suppress error if it's an expected issue
167
+ await this.recordFailedAttempt(cliName, error);
168
+ // Only throw if it's not an expected "tool not installed" error
169
+ if (
170
+ !error.message.includes(
171
+ 'not recognized as an internal or external command',
172
+ ) &&
173
+ !error.message.includes('command not found')
174
+ ) {
175
+ throw error;
176
+ }
177
+ // For tool not found errors, return minimal analysis instead of throwing
178
+ return {
179
+ success: false,
180
+ cliName,
181
+ error: error.message,
182
+ timestamp: new Date().toISOString(),
183
+ };
184
+ }
185
+ }
186
+
187
+ /**
188
+ * Get help information using multiple methods
189
+ */
190
+ async getHelpInfo(cliName, cliConfig) {
191
+ // Special handling for codex to avoid opening files or executing commands
192
+ if (cliName === 'codex') {
193
+ return await this.getCodexHelpInfo(cliConfig);
194
+ }
195
+
196
+ const helpMethods = [
197
+ ['--help'],
198
+ ['-h'],
199
+ ['help'],
200
+ ['--usage'],
201
+ [''],
202
+ ['version'],
203
+ ['--version'],
204
+ ['-v'],
205
+ ];
206
+ let rawHelp = '';
207
+ let version = 'unknown';
208
+ let method = 'unknown';
209
+ // Try different help commands
210
+ for (const helpArgs of helpMethods) {
211
+ try {
212
+ const result = spawnSync(cliName, helpArgs, {
213
+ encoding: 'utf8',
214
+ timeout: 15000,
215
+ shell: true,
216
+ });
217
+ if (result.status === 0 && result.stdout) {
218
+ rawHelp = result.stdout;
219
+ method = `${cliName} ${helpArgs.join(' ')}`;
220
+ break;
221
+ } else if (result.stderr) {
222
+ rawHelp = result.stderr;
223
+ method = `${cliName} ${helpArgs.join(' ')} (stderr)`;
224
+ break;
225
+ }
226
+ } catch (error) {
227
+ // Try next method
228
+ continue;
229
+ }
230
+ }
231
+ // Try to get version separately
232
+ if (cliConfig.version) {
233
+ try {
234
+ const versionCmd = cliConfig.version.split(' ');
235
+ const versionResult = spawnSync(versionCmd[0], versionCmd.slice(1), {
236
+ encoding: 'utf8',
237
+ timeout: 10000,
238
+ shell: true,
239
+ });
240
+ if (versionResult.status === 0) {
241
+ version = versionResult.stdout.trim() || versionResult.stderr.trim();
242
+ }
243
+ } catch (error) {
244
+ // Use default version
245
+ }
246
+ }
247
+ if (!rawHelp) {
248
+ throw new Error(`Unable to get help information for ${cliName}`);
249
+ }
250
+ return { rawHelp, version, method };
251
+ }
252
+
253
+ /**
254
+ * Get Codex help information by checking file paths instead of executing commands
255
+ */
256
+ async getCodexHelpInfo(cliConfig) {
257
+ try {
258
+ // Check if codex is likely installed by checking common installation paths
259
+ const os = require('os');
260
+ const path = require('path');
261
+ const fs = require('fs');
262
+
263
+ // Common Codex installation paths
264
+ const possiblePaths = [
265
+ path.join(os.homedir(), '.codex'),
266
+ path.join(os.homedir(), '.config', 'codex'),
267
+ path.join(
268
+ os.homedir(),
269
+ 'AppData',
270
+ 'Roaming',
271
+ 'npm',
272
+ 'node_modules',
273
+ '@openai/codex',
274
+ ),
275
+ path.join('/usr/local/lib/node_modules/@openai/codex'),
276
+ path.join('/usr/lib/node_modules/@openai/codex'),
277
+ ];
278
+
279
+ // Check if any of these paths exist
280
+ let codexInstalled = false;
281
+ let configPath = '';
282
+
283
+ for (const possiblePath of possiblePaths) {
284
+ if (fs.existsSync(possiblePath)) {
285
+ codexInstalled = true;
286
+ configPath = possiblePath;
287
+ break;
288
+ }
289
+ }
290
+
291
+ // If Codex is not installed, throw an error without trying to execute commands
292
+ if (!codexInstalled) {
293
+ throw new Error('Codex CLI not found in standard installation paths');
294
+ }
295
+
296
+ // If Codex appears to be installed, return minimal help information
297
+ // without actually executing any commands
298
+ return {
299
+ rawHelp:
300
+ 'Codex CLI - OpenAI Codex Command Line Interface\nUsage: codex [options] [prompt]\n\nOptions:\n --help, -h Show help\n --version Show version\n --model Specify model to use\n --temperature Set temperature for generation',
301
+ version: 'unknown',
302
+ method: 'path-detection',
303
+ };
304
+ } catch (error) {
305
+ throw new Error(
306
+ `Unable to get help information for codex: ${error.message}`,
307
+ );
308
+ }
309
+ }
310
+
311
+ /**
312
+ * Detect CLI type based on help content and naming
313
+ */
314
+ detectCLIType(helpText, cliName) {
315
+ const text = helpText.toLowerCase();
316
+ const name = cliName.toLowerCase();
317
+ // Detect based on CLI name
318
+ if (name.includes('claude') || name.includes('anthropic')) {
319
+ return 'anthropic';
320
+ }
321
+ if (name.includes('gemini') || name.includes('google')) {
322
+ return 'google';
323
+ }
324
+ if (
325
+ name.includes('codex') ||
326
+ name.includes('openai') ||
327
+ name.includes('chatgpt')
328
+ ) {
329
+ return 'openai';
330
+ }
331
+ // Detect based on help content patterns
332
+ if (text.includes('anthropic') || text.includes('claude')) {
333
+ return 'anthropic';
334
+ }
335
+ if (
336
+ text.includes('google') ||
337
+ text.includes('gemini') ||
338
+ text.includes('vertex')
339
+ ) {
340
+ return 'google';
341
+ }
342
+ if (
343
+ text.includes('openai') ||
344
+ text.includes('gpt') ||
345
+ text.includes('codex')
346
+ ) {
347
+ return 'openai';
348
+ }
349
+ if (
350
+ text.includes('qwen') ||
351
+ text.includes('alibaba') ||
352
+ text.includes('tongyi')
353
+ ) {
354
+ return 'alibaba';
355
+ }
356
+ if (text.includes('iflow') || text.includes('intelligent')) {
357
+ return 'iflow';
358
+ }
359
+ if (text.includes('copilot') || text.includes('github')) {
360
+ return 'github';
361
+ }
362
+ if (text.includes('codebuddy') || text.includes('buddy')) {
363
+ return 'codebuddy';
364
+ }
365
+ if (text.includes('qoder') || text.includes('code')) {
366
+ return 'qoder';
367
+ }
368
+ // Default to generic
369
+ return 'generic';
370
+ }
371
+
372
+ /**
373
+ * Extract patterns from help text
374
+ */
375
+ extractPatterns(helpText, cliType, cliName) {
376
+ const rules = this.patternRules[cliType] || this.patternRules.generic;
377
+ const patterns = {
378
+ commands: [],
379
+ options: [],
380
+ subcommands: [],
381
+ arguments: [],
382
+ flags: [],
383
+ nonInteractiveFlag: null,
384
+ promptFlag: null,
385
+ requiredFlags: [],
386
+ commonPatterns: [],
387
+ };
388
+
389
+ // Extract options/flags
390
+ const optionMatches = helpText.match(rules.optionPattern) || [];
391
+ patterns.options = [...new Set(optionMatches)];
392
+
393
+ // Extract subcommands
394
+ const subcommandMatches = helpText.match(rules.subcommandPattern) || [];
395
+ patterns.subcommands = subcommandMatches.map((match) => {
396
+ const subcommand = match.trim().split(/\s+/)[0];
397
+ return { name: subcommand, description: match.trim() };
398
+ });
399
+
400
+ // Try to identify non-interactive flag
401
+ const nonInteractiveFlags = patterns.options.filter(
402
+ (option) =>
403
+ option.includes('non-interactive') ||
404
+ option.includes('batch') ||
405
+ option.includes('no-input') ||
406
+ option.includes('stdin'),
407
+ );
408
+ patterns.nonInteractiveFlag =
409
+ nonInteractiveFlags.length > 0 ? nonInteractiveFlags[0] : null;
410
+
411
+ // Try to identify prompt flag
412
+ const promptFlags = patterns.options.filter(
413
+ (option) =>
414
+ option.includes('prompt') ||
415
+ option.includes('input') ||
416
+ option.includes('query') ||
417
+ option.includes('question'),
418
+ );
419
+ patterns.promptFlag = promptFlags.length > 0 ? promptFlags[0] : null;
420
+
421
+ // Identify required flags
422
+ patterns.requiredFlags = patterns.options.filter(
423
+ (option) =>
424
+ option.includes('<') || // Angle brackets indicate required parameters
425
+ option.includes('*'), // Asterisk indicates required
426
+ );
427
+
428
+ return patterns;
429
+ }
430
+
431
+ /**
432
+ * Analyze command structure
433
+ */
434
+ analyzeCommandStructure(patterns) {
435
+ const structure = {
436
+ primaryCommand: '',
437
+ commandFormat: 'cli [args]',
438
+ argumentStyle: '',
439
+ optionStyle: '',
440
+ interactiveMode: false,
441
+ hasSubcommands: patterns.subcommands.length > 0,
442
+ complexity: 'simple',
443
+ nonInteractiveSupport: !!patterns.nonInteractiveFlag,
444
+ promptStyle: patterns.promptFlag ? 'flag' : 'argument',
445
+ executionPattern: 'interactive-default',
446
+ nonInteractiveFlag: patterns.nonInteractiveFlag,
447
+ promptFlag: patterns.promptFlag,
448
+ requiredFlags: patterns.requiredFlags,
449
+ commonPatterns: patterns.commonPatterns,
450
+ };
451
+
452
+ // Determine complexity
453
+ if (patterns.subcommands.length > 5) {
454
+ structure.complexity = 'complex';
455
+ } else if (patterns.subcommands.length > 0) {
456
+ structure.complexity = 'moderate';
457
+ }
458
+
459
+ // Determine execution pattern
460
+ if (patterns.nonInteractiveFlag) {
461
+ structure.executionPattern = 'flag-based';
462
+ } else if (patterns.promptFlag) {
463
+ structure.executionPattern = 'prompt-flag';
464
+ }
465
+
466
+ return structure;
467
+ }
468
+
469
+ /**
470
+ * Extract usage examples
471
+ */
472
+ extractUsageExamples(helpText, cliType) {
473
+ const rules = this.patternRules[cliType] || this.patternRules.generic;
474
+ const examples = [];
475
+ let match;
476
+
477
+ while ((match = rules.examplePattern.exec(helpText)) !== null) {
478
+ examples.push(match[1].trim());
479
+ }
480
+
481
+ return examples;
482
+ }
483
+
484
+ /**
485
+ * Determine interaction mode
486
+ */
487
+ determineInteractionMode(helpInfo, patterns) {
488
+ const helpText = helpInfo.rawHelp.toLowerCase();
489
+
490
+ // Check for explicit non-interactive support
491
+ if (patterns.nonInteractiveFlag) {
492
+ return 'non-interactive';
493
+ }
494
+
495
+ // Check for stdin support
496
+ if (helpText.includes('stdin') || helpText.includes('pipe')) {
497
+ return 'stdin-support';
498
+ }
499
+
500
+ // Check for batch mode
501
+ if (helpText.includes('batch') || helpText.includes('script')) {
502
+ return 'batch-mode';
503
+ }
504
+
505
+ // Default to interactive
506
+ return 'interactive';
507
+ }
508
+
509
+ /**
510
+ * Cache analysis results
511
+ */
512
+ async cacheAnalysis(cliName, analysis) {
513
+ try {
514
+ const config = await this.loadPersistentConfig();
515
+ config.cliPatterns[cliName] = analysis;
516
+ config.lastUpdated = new Date().toISOString();
517
+ await this.savePersistentConfig(config);
518
+ } catch (error) {
519
+ // Don't spam errors for cache issues
520
+ if (process.env.DEBUG === 'true') {
521
+ await errorHandler.logError(
522
+ error,
523
+ 'WARN',
524
+ 'CLIHelpAnalyzer.cacheAnalysis',
525
+ );
526
+ }
527
+ }
528
+ }
529
+
530
+ /**
531
+ * Get cached analysis
532
+ */
533
+ async getCachedAnalysis(cliName) {
534
+ try {
535
+ const config = await this.loadPersistentConfig();
536
+ return config.cliPatterns[cliName] || null;
537
+ } catch (error) {
538
+ return null;
539
+ }
540
+ }
541
+
542
+ /**
543
+ * Get CLI pattern (wrapper for getCachedAnalysis)
544
+ */
545
+ async getCLIPattern(cliName) {
546
+ return await this.getCachedAnalysis(cliName);
547
+ }
548
+
549
+ /**
550
+ * Check if cache is expired (1 day)
551
+ */
552
+ isCacheExpired(timestamp) {
553
+ if (!timestamp) return true;
554
+ const cacheTime = new Date(timestamp);
555
+ const now = new Date();
556
+ const diffHours = (now - cacheTime) / (1000 * 60 * 60);
557
+ return diffHours > 24; // Expire after 24 hours
558
+ }
559
+
560
+ /**
561
+ * Load persistent configuration
562
+ */
563
+ async loadPersistentConfig() {
564
+ try {
565
+ const configExists = await this.fileExists(this.persistentConfig);
566
+ if (!configExists) {
567
+ return {
568
+ version: '1.0.0',
569
+ lastUpdated: new Date().toISOString(),
570
+ cliPatterns: {},
571
+ failedAttempts: {},
572
+ };
573
+ }
574
+ const configData = await fs.readFile(this.persistentConfig, 'utf8');
575
+ return JSON.parse(configData);
576
+ } catch (error) {
577
+ // Return default config if loading fails
578
+ return {
579
+ version: '1.0.0',
580
+ lastUpdated: new Date().toISOString(),
581
+ cliPatterns: {},
582
+ failedAttempts: {},
583
+ };
584
+ }
585
+ }
586
+
587
+ /**
588
+ * Save persistent configuration
589
+ */
590
+ async savePersistentConfig(config) {
591
+ try {
592
+ await fs.writeFile(
593
+ this.persistentConfig,
594
+ JSON.stringify(config, null, 2),
595
+ );
596
+ } catch (error) {
597
+ throw error;
598
+ }
599
+ }
600
+
601
+ /**
602
+ * Record failed attempt
603
+ */
604
+ async recordFailedAttempt(cliName, error) {
605
+ try {
606
+ const config = await this.loadPersistentConfig();
607
+ config.failedAttempts[cliName] = {
608
+ error: error.message,
609
+ timestamp: new Date().toISOString(),
610
+ attempts: (config.failedAttempts[cliName]?.attempts || 0) + 1,
611
+ };
612
+ await this.savePersistentConfig(config);
613
+ } catch (saveError) {
614
+ // Don't spam errors for failed attempt recording
615
+ if (process.env.DEBUG === 'true') {
616
+ await errorHandler.logError(
617
+ saveError,
618
+ 'WARN',
619
+ 'CLIHelpAnalyzer.recordFailedAttempt',
620
+ );
621
+ }
622
+ }
623
+ }
624
+
625
+ /**
626
+ * Update CLI pattern when call fails
627
+ */
628
+ async updatePatternOnFailure(cliName, error, attemptedCommand) {
629
+ // Only log in debug mode to reduce console noise
630
+ if (process.env.DEBUG === 'true') {
631
+ console.log(
632
+ `Updating pattern for ${cliName} due to failure:`,
633
+ error.message,
634
+ );
635
+ }
636
+ try {
637
+ // Re-analyze the CLI
638
+ const newAnalysis = await this.analyzeCLI(cliName);
639
+ // Add failure context
640
+ newAnalysis.lastFailure = {
641
+ error: error.message,
642
+ attemptedCommand,
643
+ timestamp: new Date().toISOString(),
644
+ };
645
+ // Update the cached analysis
646
+ await this.cacheAnalysis(cliName, newAnalysis);
647
+ return newAnalysis;
648
+ } catch (analysisError) {
649
+ // Only log analysis errors in debug mode
650
+ if (process.env.DEBUG === 'true') {
651
+ console.error(
652
+ `Failed to re-analyze ${cliName}:`,
653
+ analysisError.message,
654
+ );
655
+ }
656
+ return null;
657
+ }
658
+ }
659
+
660
+ /**
661
+ * Check if file exists
662
+ */
663
+ async fileExists(filePath) {
664
+ try {
665
+ await fs.access(filePath);
666
+ return true;
667
+ } catch (error) {
668
+ return false;
669
+ }
670
+ }
671
+
672
+ /**
673
+ * Set CLI tools
674
+ */
675
+ setCLITools(tools) {
676
+ this.cliTools = tools;
677
+ }
678
+ }
679
+
680
+ module.exports = CLIHelpAnalyzer;