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.
- package/README.md +39 -3
- package/STIGMERGY.md +3 -0
- package/config/builtin-skills.json +43 -0
- package/config/enhanced-cli-config.json +438 -0
- package/docs/CLI_TOOLS_AGENT_SKILL_ANALYSIS.md +463 -0
- package/docs/DESIGN_CLI_HELP_ANALYZER_REFACTOR.md +726 -0
- package/docs/ENHANCED_CLI_AGENT_SKILL_CONFIG.md +285 -0
- package/docs/IMPLEMENTATION_CHECKLIST_CLI_HELP_ANALYZER_REFACTOR.md +1268 -0
- package/docs/INSTALLER_ARCHITECTURE.md +257 -0
- package/docs/LESSONS_LEARNED.md +252 -0
- package/docs/SPECS_CLI_HELP_ANALYZER_REFACTOR.md +287 -0
- package/docs/SUDO_PROBLEM_AND_SOLUTION.md +529 -0
- package/docs/correct-skillsio-implementation.md +368 -0
- package/docs/development_guidelines.md +276 -0
- package/docs/independent-resume-implementation.md +198 -0
- package/docs/resumesession-final-implementation.md +195 -0
- package/docs/resumesession-usage.md +87 -0
- package/package.json +146 -136
- package/scripts/analyze-router.js +168 -0
- package/scripts/run-comprehensive-tests.js +230 -0
- package/scripts/run-quick-tests.js +90 -0
- package/scripts/test-runner.js +344 -0
- package/skills/resumesession/INDEPENDENT_SKILL.md +403 -0
- package/skills/resumesession/README.md +381 -0
- package/skills/resumesession/SKILL.md +211 -0
- package/skills/resumesession/__init__.py +33 -0
- package/skills/resumesession/implementations/simple-resume.js +13 -0
- package/skills/resumesession/independent-resume.js +750 -0
- package/skills/resumesession/package.json +1 -0
- package/skills/resumesession/skill.json +1 -0
- package/src/adapters/claude/install_claude_integration.js +9 -1
- package/src/adapters/codebuddy/install_codebuddy_integration.js +3 -1
- package/src/adapters/codex/install_codex_integration.js +15 -5
- package/src/adapters/gemini/install_gemini_integration.js +3 -1
- package/src/adapters/qwen/install_qwen_integration.js +3 -1
- package/src/cli/commands/autoinstall.js +65 -0
- package/src/cli/commands/errors.js +190 -0
- package/src/cli/commands/independent-resume.js +395 -0
- package/src/cli/commands/install.js +179 -0
- package/src/cli/commands/permissions.js +108 -0
- package/src/cli/commands/project.js +485 -0
- package/src/cli/commands/scan.js +97 -0
- package/src/cli/commands/simple-resume.js +377 -0
- package/src/cli/commands/skills.js +158 -0
- package/src/cli/commands/status.js +113 -0
- package/src/cli/commands/stigmergy-resume.js +775 -0
- package/src/cli/commands/system.js +301 -0
- package/src/cli/commands/universal-resume.js +394 -0
- package/src/cli/router-beta.js +471 -0
- package/src/cli/utils/environment.js +75 -0
- package/src/cli/utils/formatters.js +47 -0
- package/src/cli/utils/skills_cache.js +92 -0
- package/src/core/cache_cleaner.js +1 -0
- package/src/core/cli_adapters.js +345 -0
- package/src/core/cli_help_analyzer.js +1236 -680
- package/src/core/cli_path_detector.js +702 -709
- package/src/core/cli_tools.js +515 -160
- package/src/core/coordination/nodejs/CLIIntegrationManager.js +18 -0
- package/src/core/coordination/nodejs/HookDeploymentManager.js +242 -412
- package/src/core/coordination/nodejs/HookDeploymentManager.refactored.js +323 -0
- package/src/core/coordination/nodejs/generators/CLIAdapterGenerator.js +363 -0
- package/src/core/coordination/nodejs/generators/ResumeSessionGenerator.js +932 -0
- package/src/core/coordination/nodejs/generators/SkillsIntegrationGenerator.js +1395 -0
- package/src/core/coordination/nodejs/generators/index.js +12 -0
- package/src/core/enhanced_cli_installer.js +1208 -608
- package/src/core/enhanced_cli_parameter_handler.js +402 -0
- package/src/core/execution_mode_detector.js +222 -0
- package/src/core/installer.js +151 -106
- package/src/core/local_skill_scanner.js +732 -0
- package/src/core/multilingual/language-pattern-manager.js +1 -1
- package/src/core/skills/BuiltinSkillsDeployer.js +188 -0
- package/src/core/skills/StigmergySkillManager.js +123 -16
- package/src/core/skills/embedded-openskills/SkillParser.js +7 -3
- package/src/core/smart_router.js +550 -261
- package/src/index.js +10 -4
- package/src/utils.js +66 -7
- package/test/cli-integration.test.js +304 -0
- package/test/direct_smart_router_test.js +88 -0
- package/test/enhanced-cli-agent-skill-test.js +485 -0
- package/test/simple_test.js +82 -0
- package/test/smart_router_test_runner.js +123 -0
- package/test/smart_routing_edge_cases.test.js +284 -0
- package/test/smart_routing_simple_verification.js +139 -0
- package/test/smart_routing_verification.test.js +346 -0
- package/test/specific-cli-agent-skill-analysis.js +385 -0
- package/test/unit/smart_router.test.js +295 -0
- package/test/very_simple_test.js +54 -0
- 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;
|