stigmergy 1.2.13 → 1.3.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.
Files changed (88) hide show
  1. package/README.md +39 -3
  2. package/STIGMERGY.md +3 -0
  3. package/config/builtin-skills.json +43 -0
  4. package/config/enhanced-cli-config.json +438 -0
  5. package/docs/CLI_TOOLS_AGENT_SKILL_ANALYSIS.md +463 -0
  6. package/docs/DESIGN_CLI_HELP_ANALYZER_REFACTOR.md +726 -0
  7. package/docs/ENHANCED_CLI_AGENT_SKILL_CONFIG.md +285 -0
  8. package/docs/IMPLEMENTATION_CHECKLIST_CLI_HELP_ANALYZER_REFACTOR.md +1268 -0
  9. package/docs/INSTALLER_ARCHITECTURE.md +257 -0
  10. package/docs/LESSONS_LEARNED.md +252 -0
  11. package/docs/SPECS_CLI_HELP_ANALYZER_REFACTOR.md +287 -0
  12. package/docs/SUDO_PROBLEM_AND_SOLUTION.md +529 -0
  13. package/docs/correct-skillsio-implementation.md +368 -0
  14. package/docs/development_guidelines.md +276 -0
  15. package/docs/independent-resume-implementation.md +198 -0
  16. package/docs/resumesession-final-implementation.md +195 -0
  17. package/docs/resumesession-usage.md +87 -0
  18. package/package.json +146 -136
  19. package/scripts/analyze-router.js +168 -0
  20. package/scripts/run-comprehensive-tests.js +230 -0
  21. package/scripts/run-quick-tests.js +90 -0
  22. package/scripts/test-runner.js +344 -0
  23. package/skills/resumesession/INDEPENDENT_SKILL.md +403 -0
  24. package/skills/resumesession/README.md +381 -0
  25. package/skills/resumesession/SKILL.md +211 -0
  26. package/skills/resumesession/__init__.py +33 -0
  27. package/skills/resumesession/implementations/simple-resume.js +13 -0
  28. package/skills/resumesession/independent-resume.js +750 -0
  29. package/skills/resumesession/package.json +1 -0
  30. package/skills/resumesession/skill.json +1 -0
  31. package/src/adapters/claude/install_claude_integration.js +9 -1
  32. package/src/adapters/codebuddy/install_codebuddy_integration.js +3 -1
  33. package/src/adapters/codex/install_codex_integration.js +15 -5
  34. package/src/adapters/gemini/install_gemini_integration.js +3 -1
  35. package/src/adapters/qwen/install_qwen_integration.js +3 -1
  36. package/src/cli/commands/autoinstall.js +65 -0
  37. package/src/cli/commands/errors.js +190 -0
  38. package/src/cli/commands/independent-resume.js +395 -0
  39. package/src/cli/commands/install.js +179 -0
  40. package/src/cli/commands/permissions.js +108 -0
  41. package/src/cli/commands/project.js +485 -0
  42. package/src/cli/commands/scan.js +97 -0
  43. package/src/cli/commands/simple-resume.js +377 -0
  44. package/src/cli/commands/skills.js +158 -0
  45. package/src/cli/commands/status.js +113 -0
  46. package/src/cli/commands/stigmergy-resume.js +775 -0
  47. package/src/cli/commands/system.js +301 -0
  48. package/src/cli/commands/universal-resume.js +394 -0
  49. package/src/cli/router-beta.js +471 -0
  50. package/src/cli/utils/environment.js +75 -0
  51. package/src/cli/utils/formatters.js +47 -0
  52. package/src/cli/utils/skills_cache.js +92 -0
  53. package/src/core/cache_cleaner.js +1 -0
  54. package/src/core/cli_adapters.js +345 -0
  55. package/src/core/cli_help_analyzer.js +1236 -680
  56. package/src/core/cli_path_detector.js +702 -709
  57. package/src/core/cli_tools.js +515 -160
  58. package/src/core/coordination/nodejs/CLIIntegrationManager.js +18 -0
  59. package/src/core/coordination/nodejs/HookDeploymentManager.js +242 -412
  60. package/src/core/coordination/nodejs/HookDeploymentManager.refactored.js +323 -0
  61. package/src/core/coordination/nodejs/generators/CLIAdapterGenerator.js +363 -0
  62. package/src/core/coordination/nodejs/generators/ResumeSessionGenerator.js +932 -0
  63. package/src/core/coordination/nodejs/generators/SkillsIntegrationGenerator.js +1395 -0
  64. package/src/core/coordination/nodejs/generators/index.js +12 -0
  65. package/src/core/enhanced_cli_installer.js +1208 -608
  66. package/src/core/enhanced_cli_parameter_handler.js +402 -0
  67. package/src/core/execution_mode_detector.js +222 -0
  68. package/src/core/installer.js +151 -106
  69. package/src/core/local_skill_scanner.js +732 -0
  70. package/src/core/multilingual/language-pattern-manager.js +1 -1
  71. package/src/core/skills/BuiltinSkillsDeployer.js +188 -0
  72. package/src/core/skills/StigmergySkillManager.js +123 -16
  73. package/src/core/skills/embedded-openskills/SkillParser.js +7 -3
  74. package/src/core/smart_router.js +550 -261
  75. package/src/index.js +10 -4
  76. package/src/utils.js +66 -7
  77. package/test/cli-integration.test.js +304 -0
  78. package/test/direct_smart_router_test.js +88 -0
  79. package/test/enhanced-cli-agent-skill-test.js +485 -0
  80. package/test/simple_test.js +82 -0
  81. package/test/smart_router_test_runner.js +123 -0
  82. package/test/smart_routing_edge_cases.test.js +284 -0
  83. package/test/smart_routing_simple_verification.js +139 -0
  84. package/test/smart_routing_verification.test.js +346 -0
  85. package/test/specific-cli-agent-skill-analysis.js +385 -0
  86. package/test/unit/smart_router.test.js +295 -0
  87. package/test/very_simple_test.js +54 -0
  88. package/src/cli/router.js +0 -1783
@@ -0,0 +1,732 @@
1
+ /**
2
+ * Local Skills and Agents Scanner
3
+ *
4
+ * This module scans local directories for skills and agents configuration,
5
+ * and provides them to the smart routing system for intelligent parameter configuration.
6
+ *
7
+ * FEATURES:
8
+ * - Persistent caching with NO expiration (cache is valid until file changes)
9
+ * - Incremental updates based on file modification time
10
+ * - Trigger-based scanning (only when necessary)
11
+ * - Fast in-memory access for cached data
12
+ * - Automatic cache generation during setup/install/scan commands
13
+ */
14
+
15
+ const fs = require('fs/promises');
16
+ const fsSync = require('fs');
17
+ const path = require('path');
18
+ const os = require('os');
19
+ const crypto = require('crypto');
20
+
21
+ class LocalSkillScanner {
22
+ constructor() {
23
+ this.cacheDir = path.join(os.homedir(), '.stigmergy', 'cache');
24
+ this.cacheFile = path.join(this.cacheDir, 'skills-agents-cache.json');
25
+ this.scanResults = null;
26
+ this.initialized = false;
27
+ }
28
+
29
+ /**
30
+ * Initialize the scanner (lazy loading with cache)
31
+ * - Loads from cache if available (no expiration)
32
+ * - Only scans if cache doesn't exist or forceRefresh=true
33
+ * - Uses persistent cache for fast subsequent access
34
+ */
35
+ async initialize(forceRefresh = false) {
36
+ if (this.initialized && !forceRefresh) {
37
+ return this.scanResults;
38
+ }
39
+
40
+ // Ensure cache directory exists
41
+ try {
42
+ await fs.mkdir(this.cacheDir, { recursive: true });
43
+ } catch (error) {
44
+ // Ignore error
45
+ }
46
+
47
+ // Try to load from cache (no expiration check - cache is valid until file changes)
48
+ if (!forceRefresh) {
49
+ const cached = await this.loadFromCache();
50
+ if (cached) {
51
+ this.scanResults = cached;
52
+ this.initialized = true;
53
+ if (process.env.DEBUG === 'true') {
54
+ console.log('[SKILL-SCANNER] Loaded skills/agents from cache');
55
+ }
56
+ return this.scanResults;
57
+ }
58
+ }
59
+
60
+ // Cache miss, perform scan
61
+ const results = await this.scanAll();
62
+ await this.saveToCache(results);
63
+ this.scanResults = results;
64
+ this.initialized = true;
65
+
66
+ if (process.env.DEBUG === 'true') {
67
+ console.log('[SKILL-SCANNER] Performed fresh scan and cached results');
68
+ }
69
+
70
+ return this.scanResults;
71
+ }
72
+
73
+ /**
74
+ * Check if cache file exists
75
+ */
76
+ async hasCache() {
77
+ try {
78
+ await fs.access(this.cacheFile);
79
+ return true;
80
+ } catch {
81
+ return false;
82
+ }
83
+ }
84
+
85
+ /**
86
+ * Get cache file timestamp
87
+ */
88
+ async getCacheTimestamp() {
89
+ try {
90
+ const stats = await fs.stat(this.cacheFile);
91
+ return stats.mtime;
92
+ } catch {
93
+ return null;
94
+ }
95
+ }
96
+
97
+ /**
98
+ * Load scan results from persistent cache
99
+ */
100
+ async loadFromCache() {
101
+ try {
102
+ const data = await fs.readFile(this.cacheFile, 'utf8');
103
+ return JSON.parse(data);
104
+ } catch (error) {
105
+ return null;
106
+ }
107
+ }
108
+
109
+ /**
110
+ * Save scan results to persistent cache
111
+ */
112
+ async saveToCache(results) {
113
+ try {
114
+ await fs.writeFile(this.cacheFile, JSON.stringify(results, null, 2), 'utf8');
115
+ } catch (error) {
116
+ // Cache save is not critical, ignore errors
117
+ if (process.env.DEBUG === 'true') {
118
+ console.log('[SKILL-SCANNER] Failed to save cache:', error.message);
119
+ }
120
+ }
121
+ }
122
+
123
+ /**
124
+ * Perform incremental scan (only scan directories that changed)
125
+ * Compares file modification times to determine if re-scanning is needed
126
+ * - Cache is valid indefinitely until files change
127
+ */
128
+ async scanIncremental() {
129
+ const cached = await this.loadFromCache();
130
+
131
+ if (!cached) {
132
+ return await this.initialize();
133
+ }
134
+
135
+ const cliTools = ['claude', 'gemini', 'qwen', 'iflow', 'codebuddy', 'qodercli', 'copilot', 'codex'];
136
+ const results = {
137
+ skills: { ...cached.skills },
138
+ agents: { ...cached.agents },
139
+ timestamp: new Date().toISOString()
140
+ };
141
+
142
+ let hasChanges = false;
143
+
144
+ for (const cli of cliTools) {
145
+ const cliConfigPath = path.join(os.homedir(), `.${cli}`);
146
+ const skillsPath = path.join(cliConfigPath, 'skills');
147
+ const agentsPath = path.join(cliConfigPath, 'agents');
148
+
149
+ // Check if skills directory needs re-scanning (based on file modification time)
150
+ if (await this.directoryChanged(skillsPath, cached.timestamp)) {
151
+ results.skills[cli] = await this.scanDirectory(skillsPath, 'skill');
152
+ hasChanges = true;
153
+ if (process.env.DEBUG === 'true') {
154
+ console.log(`[SKILL-SCANNER] Updated skills cache for ${cli}`);
155
+ }
156
+ }
157
+
158
+ // Check if agents directory needs re-scanning
159
+ if (await this.directoryChanged(agentsPath, cached.timestamp)) {
160
+ results.agents[cli] = await this.scanDirectory(agentsPath, 'agent');
161
+ hasChanges = true;
162
+ if (process.env.DEBUG === 'true') {
163
+ console.log(`[SKILL-SCANNER] Updated agents cache for ${cli}`);
164
+ }
165
+ }
166
+ }
167
+
168
+ if (hasChanges) {
169
+ await this.saveToCache(results);
170
+ if (process.env.DEBUG === 'true') {
171
+ console.log('[SKILL-SCANNER] Saved updated cache');
172
+ }
173
+ } else {
174
+ if (process.env.DEBUG === 'true') {
175
+ console.log('[SKILL-SCANNER] No changes detected, cache is up-to-date');
176
+ }
177
+ }
178
+
179
+ this.scanResults = results;
180
+ return results;
181
+ }
182
+
183
+ /**
184
+ * Check if a directory has changed since a given timestamp
185
+ */
186
+ async directoryChanged(dirPath, sinceTimestamp) {
187
+ try {
188
+ const stats = await fs.stat(dirPath);
189
+ const sinceTime = new Date(sinceTimestamp);
190
+
191
+ // Check directory modification time
192
+ if (stats.mtime > sinceTime) {
193
+ return true;
194
+ }
195
+
196
+ // Check if any files in the directory are newer
197
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
198
+ for (const entry of entries) {
199
+ if (entry.isDirectory()) {
200
+ const itemPath = path.join(dirPath, entry.name);
201
+ try {
202
+ const itemStats = await fs.stat(itemPath);
203
+ if (itemStats.mtime > sinceTime) {
204
+ return true;
205
+ }
206
+ } catch {
207
+ // Ignore errors
208
+ }
209
+ }
210
+ }
211
+
212
+ return false;
213
+ } catch (error) {
214
+ // Directory doesn't exist or other error
215
+ return false;
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Scan all CLI tool directories for skills and agents
221
+ * This is expensive and should only be called when necessary
222
+ */
223
+ async scanAll() {
224
+ const cliTools = ['claude', 'gemini', 'qwen', 'iflow', 'codebuddy', 'qodercli', 'copilot', 'codex'];
225
+ const results = {
226
+ skills: {},
227
+ agents: {},
228
+ timestamp: new Date().toISOString()
229
+ };
230
+
231
+ for (const cli of cliTools) {
232
+ const cliConfigPath = path.join(os.homedir(), `.${cli}`);
233
+ const skillsPath = path.join(cliConfigPath, 'skills');
234
+ const agentsPath = path.join(cliConfigPath, 'agents');
235
+
236
+ results.skills[cli] = await this.scanDirectory(skillsPath, 'skill');
237
+ results.agents[cli] = await this.scanDirectory(agentsPath, 'agent');
238
+ }
239
+
240
+ return results;
241
+ }
242
+
243
+ /**
244
+ * Scan a directory for skills or agents
245
+ */
246
+ async scanDirectory(dirPath, type) {
247
+ const items = [];
248
+
249
+ try {
250
+ const stats = await fs.stat(dirPath);
251
+ if (!stats.isDirectory()) {
252
+ return [];
253
+ }
254
+ } catch (error) {
255
+ // Directory doesn't exist
256
+ return [];
257
+ }
258
+
259
+ try {
260
+ const entries = await fs.readdir(dirPath, { withFileTypes: true });
261
+
262
+ for (const entry of entries) {
263
+ if (entry.isDirectory()) {
264
+ const itemPath = path.join(dirPath, entry.name);
265
+
266
+ // Try to find SKILL.md or AGENT.md
267
+ const skillMdPath = path.join(itemPath, 'SKILL.md');
268
+ const agentMdPath = path.join(itemPath, 'AGENT.md');
269
+ const readmePath = path.join(itemPath, 'README.md');
270
+
271
+ let metadata = null;
272
+
273
+ // Try to read metadata from markdown files
274
+ if (type === 'skill') {
275
+ metadata = await this.readSkillMetadata(itemPath, skillMdPath, readmePath);
276
+ } else {
277
+ metadata = await this.readAgentMetadata(itemPath, agentMdPath, readmePath);
278
+ }
279
+
280
+ if (metadata) {
281
+ items.push({
282
+ name: entry.name,
283
+ path: itemPath,
284
+ type,
285
+ metadata,
286
+ keywords: this.extractKeywords(metadata)
287
+ });
288
+ }
289
+ }
290
+ }
291
+ } catch (error) {
292
+ // Ignore errors
293
+ }
294
+
295
+ return items;
296
+ }
297
+
298
+ /**
299
+ * Read skill metadata from markdown files
300
+ */
301
+ async readSkillMetadata(itemPath, skillMdPath, readmePath) {
302
+ try {
303
+ let content = '';
304
+ let sourceFile = '';
305
+
306
+ // Try SKILL.md first
307
+ try {
308
+ content = await fs.readFile(skillMdPath, 'utf8');
309
+ sourceFile = 'SKILL.md';
310
+ } catch {
311
+ // Try README.md
312
+ try {
313
+ content = await fs.readFile(readmePath, 'utf8');
314
+ sourceFile = 'README.md';
315
+ } catch {
316
+ return null;
317
+ }
318
+ }
319
+
320
+ // Extract metadata
321
+ const metadata = {
322
+ name: this.extractName(content, path.basename(itemPath)),
323
+ description: this.extractDescription(content),
324
+ capabilities: this.extractCapabilities(content),
325
+ usage: this.extractUsage(content),
326
+ sourceFile
327
+ };
328
+
329
+ return metadata;
330
+ } catch (error) {
331
+ return null;
332
+ }
333
+ }
334
+
335
+ /**
336
+ * Read agent metadata from markdown files
337
+ */
338
+ async readAgentMetadata(itemPath, agentMdPath, readmePath) {
339
+ try {
340
+ let content = '';
341
+ let sourceFile = '';
342
+
343
+ // Try AGENT.md first
344
+ try {
345
+ content = await fs.readFile(agentMdPath, 'utf8');
346
+ sourceFile = 'AGENT.md';
347
+ } catch {
348
+ // Try README.md
349
+ try {
350
+ content = await fs.readFile(readmePath, 'utf8');
351
+ sourceFile = 'README.md';
352
+ } catch {
353
+ return null;
354
+ }
355
+ }
356
+
357
+ // Extract metadata
358
+ const metadata = {
359
+ name: this.extractName(content, path.basename(itemPath)),
360
+ description: this.extractDescription(content),
361
+ capabilities: this.extractCapabilities(content),
362
+ usage: this.extractUsage(content),
363
+ sourceFile
364
+ };
365
+
366
+ return metadata;
367
+ } catch (error) {
368
+ return null;
369
+ }
370
+ }
371
+
372
+ /**
373
+ * Extract name from markdown content
374
+ */
375
+ extractName(content, fallbackName) {
376
+ // Try to find name in frontmatter or heading
377
+ const nameMatch = content.match(/name:\s*"([^"]+)"/i) ||
378
+ content.match(/^#\s+(.+)$/m) ||
379
+ content.match(/name:\s*([^#\n]+)/i);
380
+
381
+ if (nameMatch) {
382
+ return nameMatch[1].trim();
383
+ }
384
+
385
+ return fallbackName;
386
+ }
387
+
388
+ /**
389
+ * Extract description from markdown content
390
+ */
391
+ extractDescription(content) {
392
+ // Try to find description in frontmatter
393
+ const descMatch = content.match(/description:\s*"([^"]+)"/i);
394
+ if (descMatch) {
395
+ return descMatch[1].trim();
396
+ }
397
+
398
+ // Try to find first paragraph after heading
399
+ const firstParagraph = content.match(/^#\s+.+?\n+(.+?)(?:\n\n|\n#|$)/s);
400
+ if (firstParagraph) {
401
+ return firstParagraph[1].trim().substring(0, 200);
402
+ }
403
+
404
+ return '';
405
+ }
406
+
407
+ /**
408
+ * Extract capabilities from markdown content
409
+ */
410
+ extractCapabilities(content) {
411
+ const capabilities = [];
412
+
413
+ // Look for lists in the content
414
+ const listMatches = content.matchAll(/^[-*]\s+(.+?)$/gm);
415
+ for (const match of listMatches) {
416
+ const capability = match[1].trim();
417
+ if (capability.length > 0 && capability.length < 200) {
418
+ capabilities.push(capability);
419
+ }
420
+ }
421
+
422
+ return capabilities.slice(0, 10); // Limit to 10 capabilities
423
+ }
424
+
425
+ /**
426
+ * Extract usage examples from markdown content
427
+ */
428
+ extractUsage(content) {
429
+ const usage = [];
430
+
431
+ // Look for code blocks
432
+ const codeBlockMatches = content.matchAll(/```[\w]*\n([\s\S]+?)```/g);
433
+ for (const match of codeBlockMatches) {
434
+ const example = match[1].trim();
435
+ if (example.length > 0 && example.length < 500) {
436
+ usage.push(example);
437
+ }
438
+ }
439
+
440
+ return usage.slice(0, 5); // Limit to 5 usage examples
441
+ }
442
+
443
+ /**
444
+ * Extract keywords from metadata
445
+ */
446
+ extractKeywords(metadata) {
447
+ const keywords = new Set();
448
+
449
+ // Add name and description words
450
+ if (metadata.name) {
451
+ metadata.name.split(/\s+/).forEach(word => {
452
+ if (word.length > 2) keywords.add(word.toLowerCase());
453
+ });
454
+ }
455
+
456
+ if (metadata.description) {
457
+ metadata.description.split(/\s+/).forEach(word => {
458
+ if (word.length > 3) keywords.add(word.toLowerCase());
459
+ });
460
+ }
461
+
462
+ return Array.from(keywords);
463
+ }
464
+
465
+ /**
466
+ * Get skills for a specific CLI
467
+ */
468
+ getSkillsForCLI(cliName) {
469
+ if (!this.scanResults) {
470
+ return [];
471
+ }
472
+ return this.scanResults.skills[cliName] || [];
473
+ }
474
+
475
+ /**
476
+ * Get agents for a specific CLI
477
+ */
478
+ getAgentsForCLI(cliName) {
479
+ if (!this.scanResults) {
480
+ return [];
481
+ }
482
+ return this.scanResults.agents[cliName] || [];
483
+ }
484
+
485
+ /**
486
+ * Quick pre-check for agent/skill mentions (Stage 1)
487
+ * This is a FAST in-memory check that avoids cache I/O if no keywords are found
488
+ * Only if this returns true should we proceed to load cache and do detailed matching
489
+ *
490
+ * @param {string} userInput - User's input text
491
+ * @returns {Object} Quick detection result
492
+ */
493
+ quickDetectMention(userInput) {
494
+ const inputLower = userInput.toLowerCase();
495
+
496
+ // Quick keyword check (no cache needed, just string matching)
497
+ const quickAgentKeywords = [
498
+ 'agent', '智能体', '专家', 'expert', 'specialist',
499
+ '使用.*agent', '调用.*agent', '用.*agent'
500
+ ];
501
+
502
+ const quickSkillKeywords = [
503
+ 'skill', '技能', '能力', 'method', 'tool',
504
+ '使用.*skill', '调用.*skill', '用.*skill'
505
+ ];
506
+
507
+ // Stage 1: Fast regex check (no I/O, <1ms)
508
+ const hasAgentKeyword = quickAgentKeywords.some(keyword =>
509
+ new RegExp(keyword, 'i').test(userInput)
510
+ );
511
+
512
+ const hasSkillKeyword = quickSkillKeywords.some(keyword =>
513
+ new RegExp(keyword, 'i').test(userInput)
514
+ );
515
+
516
+ // Early exit if no keywords detected
517
+ if (!hasAgentKeyword && !hasSkillKeyword) {
518
+ return {
519
+ hasMention: false,
520
+ hasAgentMention: false,
521
+ hasSkillMention: false,
522
+ shouldLoadCache: false
523
+ };
524
+ }
525
+
526
+ // Keywords detected, but we still need detailed matching (Stage 2)
527
+ return {
528
+ hasMention: true,
529
+ hasAgentKeyword,
530
+ hasSkillKeyword,
531
+ shouldLoadCache: true // Proceed to Stage 2: load cache and match
532
+ };
533
+ }
534
+
535
+ /**
536
+ * Match user input to available skills (Stage 2 - Detailed)
537
+ * Only called after quickDetectMention returns true
538
+ * Requires cache to be loaded first
539
+ */
540
+ matchSkills(userInput, cliName) {
541
+ const skills = this.getSkillsForCLI(cliName);
542
+ const matches = [];
543
+
544
+ const inputLower = userInput.toLowerCase();
545
+
546
+ for (const skill of skills) {
547
+ let score = 0;
548
+ const reasons = [];
549
+
550
+ // Check name match
551
+ if (inputLower.includes(skill.name.toLowerCase())) {
552
+ score += 0.5;
553
+ reasons.push('Skill name mentioned');
554
+ }
555
+
556
+ // Check keyword matches
557
+ for (const keyword of skill.keywords) {
558
+ if (inputLower.includes(keyword.toLowerCase())) {
559
+ score += 0.1;
560
+ reasons.push(`Keyword match: ${keyword}`);
561
+ }
562
+ }
563
+
564
+ // Check description matches
565
+ if (skill.metadata.description) {
566
+ const descWords = skill.metadata.description.toLowerCase().split(/\s+/);
567
+ for (const word of descWords) {
568
+ if (word.length > 4 && inputLower.includes(word)) {
569
+ score += 0.05;
570
+ }
571
+ }
572
+ }
573
+
574
+ if (score > 0.3) {
575
+ matches.push({
576
+ skill,
577
+ score: Math.min(score, 1.0),
578
+ reasons
579
+ });
580
+ }
581
+ }
582
+
583
+ // Sort by score descending
584
+ return matches.sort((a, b) => b.score - a.score);
585
+ }
586
+
587
+ /**
588
+ * Match user input to available agents (Stage 2 - Detailed)
589
+ * Only called after quickDetectMention returns true
590
+ * Requires cache to be loaded first
591
+ */
592
+ matchAgents(userInput, cliName) {
593
+ const agents = this.getAgentsForCLI(cliName);
594
+ const matches = [];
595
+
596
+ const inputLower = userInput.toLowerCase();
597
+
598
+ for (const agent of agents) {
599
+ let score = 0;
600
+ const reasons = [];
601
+
602
+ // Check name match
603
+ if (inputLower.includes(agent.name.toLowerCase())) {
604
+ score += 0.5;
605
+ reasons.push('Agent name mentioned');
606
+ }
607
+
608
+ // Check keyword matches
609
+ for (const keyword of agent.keywords) {
610
+ if (inputLower.includes(keyword.toLowerCase())) {
611
+ score += 0.1;
612
+ reasons.push(`Keyword match: ${keyword}`);
613
+ }
614
+ }
615
+
616
+ // Check description matches
617
+ if (agent.metadata.description) {
618
+ const descWords = agent.metadata.description.toLowerCase().split(/\s+/);
619
+ for (const word of descWords) {
620
+ if (word.length > 4 && inputLower.includes(word)) {
621
+ score += 0.05;
622
+ }
623
+ }
624
+ }
625
+
626
+ if (score > 0.3) {
627
+ matches.push({
628
+ agent,
629
+ score: Math.min(score, 1.0),
630
+ reasons
631
+ });
632
+ }
633
+ }
634
+
635
+ // Sort by score descending
636
+ return matches.sort((a, b) => b.score - a.score);
637
+ }
638
+
639
+ /**
640
+ * Full explicit mention detection (Stage 2)
641
+ * Only called after quickDetectMention returns true
642
+ * This loads cache and performs detailed matching
643
+ *
644
+ * IMPORTANT: This should only be called after quickDetectMention() returns true
645
+ */
646
+ detectExplicitMention(userInput, cliName) {
647
+ const inputLower = userInput.toLowerCase();
648
+
649
+ const agentMatches = this.matchAgents(userInput, cliName);
650
+ const skillMatches = this.matchSkills(userInput, cliName);
651
+
652
+ // Check for explicit keywords
653
+ const explicitAgentKeywords = [
654
+ 'agent', '智能体', '专家', 'expert', 'specialist',
655
+ '使用.*agent', '调用.*agent', '用.*agent'
656
+ ];
657
+
658
+ const explicitSkillKeywords = [
659
+ 'skill', '技能', '能力', 'method', 'tool',
660
+ '使用.*skill', '调用.*skill', '用.*skill'
661
+ ];
662
+
663
+ const hasExplicitAgentMention = explicitAgentKeywords.some(keyword =>
664
+ new RegExp(keyword, 'i').test(userInput)
665
+ ) || agentMatches.some(m => m.score > 0.5);
666
+
667
+ const hasExplicitSkillMention = explicitSkillKeywords.some(keyword =>
668
+ new RegExp(keyword, 'i').test(userInput)
669
+ ) || skillMatches.some(m => m.score > 0.5);
670
+
671
+ return {
672
+ hasExplicitAgentMention,
673
+ hasExplicitSkillMention,
674
+ agentMatches: hasExplicitAgentMention ? agentMatches : [],
675
+ skillMatches: hasExplicitSkillMention ? skillMatches : []
676
+ };
677
+ }
678
+
679
+ /**
680
+ * Generate CLI-specific parameter pattern for a skill
681
+ */
682
+ generateSkillParameterPattern(skillName, cliName) {
683
+ const patterns = {
684
+ 'claude': `Bash("stigmergy skill read ${skillName}")`,
685
+ 'gemini': `--skill ${skillName}`,
686
+ 'qwen': `使用${skillName}技能`,
687
+ 'codebuddy': `-p "skill:${skillName}"`,
688
+ 'iflow': `-p "请使用${skillName}技能"`,
689
+ 'copilot': `--skill ${skillName}`,
690
+ 'codex': `--skill ${skillName}`,
691
+ 'qodercli': `-p "请使用${skillName}技能"`
692
+ };
693
+
694
+ return patterns[cliName] || `-p "${skillName}"`;
695
+ }
696
+
697
+ /**
698
+ * Generate CLI-specific parameter pattern for an agent
699
+ */
700
+ generateAgentParameterPattern(agentName, cliName) {
701
+ const patterns = {
702
+ 'claude': `Bash("stigmergy use ${agentName} agent")`,
703
+ 'gemini': `--agent ${agentName}`,
704
+ 'qwen': `使用${agentName}智能体`,
705
+ 'codebuddy': `-p "agent:${agentName}"`,
706
+ 'iflow': `-p "请使用${agentName}智能体"`,
707
+ 'copilot': `--agent ${agentName}`,
708
+ 'codex': `--agent ${agentName}`,
709
+ 'qodercli': `-p "请使用${agentName}智能体"`
710
+ };
711
+
712
+ return patterns[cliName] || `-p "${agentName}"`;
713
+ }
714
+
715
+ /**
716
+ * Get cached scan results
717
+ */
718
+ getScanResults() {
719
+ return this.scanResults;
720
+ }
721
+
722
+ /**
723
+ * Refresh the scan
724
+ */
725
+ async refresh() {
726
+ this.skillCache.clear();
727
+ this.agentCache.clear();
728
+ return await this.scanAll();
729
+ }
730
+ }
731
+
732
+ module.exports = LocalSkillScanner;