flowmind 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +855 -0
  3. package/README_CN.md +854 -0
  4. package/bin/flowmind.js +464 -0
  5. package/core/adapters/api-doc-adapter.js +71 -0
  6. package/core/adapters/base-adapter.js +80 -0
  7. package/core/adapters/database-manager-adapter.js +60 -0
  8. package/core/adapters/database-query-adapter.js +51 -0
  9. package/core/adapters/knowledge-base-adapter.js +75 -0
  10. package/core/adapters/log-service-adapter.js +41 -0
  11. package/core/adapters/mcp-adapter.js +65 -0
  12. package/core/adapters/report-adapter.js +60 -0
  13. package/core/adapters/workflow-adapter.js +62 -0
  14. package/core/component-registry.js +281 -0
  15. package/core/component-types.js +63 -0
  16. package/core/config-manager.js +360 -0
  17. package/core/index.js +223 -0
  18. package/core/learning-engine.js +588 -0
  19. package/core/mcp-compatibility.js +150 -0
  20. package/core/providers/aliyun/dms-adapter.js +98 -0
  21. package/core/providers/aliyun/redis-adapter.js +88 -0
  22. package/core/providers/aliyun/sls-adapter.js +86 -0
  23. package/core/providers/friday/flow-adapter.js +85 -0
  24. package/core/providers/friday/report-adapter.js +83 -0
  25. package/core/providers/yapi/yapi-adapter.js +79 -0
  26. package/core/providers/yuque/yuque-adapter.js +90 -0
  27. package/core/scene-matcher.js +326 -0
  28. package/core/skill-loader.js +291 -0
  29. package/package.json +67 -0
  30. package/scripts/migrate-config.js +153 -0
  31. package/skills/api-sync/SKILL.md +203 -0
  32. package/skills/archive-change/SKILL.md +172 -0
  33. package/skills/auto-flow/SKILL.md +277 -0
  34. package/skills/code-review/SKILL.md +206 -0
  35. package/skills/code-review-audit/SKILL.md +150 -0
  36. package/skills/data-logic-validation/SKILL.md +162 -0
  37. package/skills/data-validation/SKILL.md +210 -0
  38. package/skills/git-review/SKILL.md +190 -0
  39. package/skills/learning-engine/SKILL.md +352 -0
  40. package/skills/learning-feedback/SKILL.md +174 -0
  41. package/skills/log-audit/SKILL.md +226 -0
  42. package/skills/project-review/SKILL.md +196 -0
  43. package/skills/requirement-analyst/SKILL.md +275 -0
  44. package/skills/resource-bind/SKILL.md +222 -0
  45. package/skills/sls-log-audit/SKILL.md +223 -0
  46. package/skills/yapi-sync-interface/SKILL.md +145 -0
  47. package/skills/yuque-sync-design/SKILL.md +157 -0
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Yuque Knowledge Base Adapter
3
+ * Wraps the aomi-yuque-mcp MCP server for Yuque platform management
4
+ */
5
+
6
+ const KnowledgeBaseAdapter = require('../../adapters/knowledge-base-adapter');
7
+
8
+ class YuqueAdapter extends KnowledgeBaseAdapter {
9
+ constructor(config = {}) {
10
+ super('yuque', config);
11
+
12
+ // Register MCP tool mappings
13
+ this.registerTool('getRepos', 'get_user_repos');
14
+ this.registerTool('getDocs', 'get_repo_docs');
15
+ this.registerTool('getDoc', 'get_doc');
16
+ this.registerTool('createDoc', 'create_doc');
17
+ this.registerTool('updateDoc', 'update_doc');
18
+ this.registerTool('search', 'search');
19
+ this.registerTool('getCurrentUser', 'get_current_user');
20
+ this.registerTool('getUserDocs', 'get_user_docs');
21
+ this.registerTool('getGroupStatistics', 'get_group_statistics');
22
+ this.registerTool('getGroupMemberStatistics', 'get_group_member_statistics');
23
+ }
24
+
25
+ get mcpServer() {
26
+ return 'aomi-yuque-mcp';
27
+ }
28
+
29
+ async getRepos(params) {
30
+ return {
31
+ mcpServer: this.mcpServer,
32
+ tool: this.resolveTool('getRepos'),
33
+ params
34
+ };
35
+ }
36
+
37
+ async getDocs(namespace, params = {}) {
38
+ return {
39
+ mcpServer: this.mcpServer,
40
+ tool: this.resolveTool('getDocs'),
41
+ params: { namespace, ...params }
42
+ };
43
+ }
44
+
45
+ async getDoc(namespace, slug) {
46
+ return {
47
+ mcpServer: this.mcpServer,
48
+ tool: this.resolveTool('getDoc'),
49
+ params: { namespace, slug }
50
+ };
51
+ }
52
+
53
+ async createDoc(namespace, docData) {
54
+ return {
55
+ mcpServer: this.mcpServer,
56
+ tool: this.resolveTool('createDoc'),
57
+ params: { namespace, ...docData }
58
+ };
59
+ }
60
+
61
+ async updateDoc(namespace, slug, docData) {
62
+ return {
63
+ mcpServer: this.mcpServer,
64
+ tool: this.resolveTool('updateDoc'),
65
+ params: { namespace, slug, ...docData }
66
+ };
67
+ }
68
+
69
+ async search(query, type = 'doc') {
70
+ return {
71
+ mcpServer: this.mcpServer,
72
+ tool: this.resolveTool('search'),
73
+ params: { q: query, type }
74
+ };
75
+ }
76
+
77
+ /**
78
+ * Get current authenticated user info.
79
+ * @returns {Promise<object>}
80
+ */
81
+ async getCurrentUser() {
82
+ return {
83
+ mcpServer: this.mcpServer,
84
+ tool: this.resolveTool('getCurrentUser'),
85
+ params: {}
86
+ };
87
+ }
88
+ }
89
+
90
+ module.exports = YuqueAdapter;
@@ -0,0 +1,326 @@
1
+ /**
2
+ * FlowMind Scene Matcher
3
+ * Matches user input to scene workflows
4
+ */
5
+
6
+ const fs = require('fs-extra');
7
+ const path = require('path');
8
+
9
+ class SceneMatcher {
10
+ constructor(config, learning) {
11
+ this.config = config;
12
+ this.learning = learning;
13
+ this.scenes = [];
14
+ this.initialized = false;
15
+ }
16
+
17
+ /**
18
+ * Load scenes from storage
19
+ */
20
+ async loadScenes() {
21
+ const learningPath = this.config.get('learning.storagePath', '~/.flowmind/learning');
22
+ const scenesPath = path.join(this.expandPath(learningPath), 'scenes.json');
23
+
24
+ if (await fs.pathExists(scenesPath)) {
25
+ const data = await fs.readJson(scenesPath);
26
+ this.scenes = data.mappings || [];
27
+ }
28
+
29
+ // Add built-in scenes
30
+ this.scenes.push(...this.getBuiltInScenes());
31
+
32
+ this.initialized = true;
33
+ return this.scenes;
34
+ }
35
+
36
+ /**
37
+ * Get built-in scene mappings
38
+ */
39
+ getBuiltInScenes() {
40
+ return [
41
+ {
42
+ id: 'builtin-log-trace',
43
+ name: 'Trace Log Query',
44
+ keywords: ['traceId', '链路', 'trace', '调用链', '日志'],
45
+ patterns: [
46
+ /查询.*traceId/i,
47
+ /查看.*链路/i,
48
+ /traceId.*分析/i,
49
+ /查询.*日志/i
50
+ ],
51
+ workflow: {
52
+ skills: [
53
+ { skill: 'log-audit', params: { action: 'query-trace' } }
54
+ ]
55
+ },
56
+ preferences: {
57
+ outputFormat: 'sequential-list'
58
+ },
59
+ stats: { useCount: 0, successRate: 1.0 }
60
+ },
61
+ {
62
+ id: 'builtin-log-error',
63
+ name: 'Error Log Query',
64
+ keywords: ['错误日志', '异常日志', 'error', 'exception'],
65
+ patterns: [
66
+ /查看.*错误日志/i,
67
+ /查询.*异常/i,
68
+ /error.*log/i
69
+ ],
70
+ workflow: {
71
+ skills: [
72
+ { skill: 'log-audit', params: { action: 'query-errors' } }
73
+ ]
74
+ },
75
+ preferences: {},
76
+ stats: { useCount: 0, successRate: 1.0 }
77
+ },
78
+ {
79
+ id: 'builtin-code-review',
80
+ name: 'Code Review',
81
+ keywords: ['代码审查', 'review', 'PR', '代码质量'],
82
+ patterns: [
83
+ /审查.*代码/i,
84
+ /review.*PR/i,
85
+ /代码.*检查/i
86
+ ],
87
+ workflow: {
88
+ skills: [
89
+ { skill: 'code-review', params: {} }
90
+ ]
91
+ },
92
+ preferences: {},
93
+ stats: { useCount: 0, successRate: 1.0 }
94
+ },
95
+ {
96
+ id: 'builtin-data-validation',
97
+ name: 'Data Validation',
98
+ keywords: ['数据验证', '数据检查', '验证', '逻辑验证'],
99
+ patterns: [
100
+ /验证.*数据/i,
101
+ /检查.*逻辑/i,
102
+ /数据.*正确/i
103
+ ],
104
+ workflow: {
105
+ skills: [
106
+ { skill: 'data-validation', params: {} }
107
+ ]
108
+ },
109
+ preferences: {},
110
+ stats: { useCount: 0, successRate: 1.0 }
111
+ }
112
+ ];
113
+ }
114
+
115
+ /**
116
+ * Match input to scenes
117
+ */
118
+ async match(input) {
119
+ if (!this.initialized) {
120
+ await this.loadScenes();
121
+ }
122
+
123
+ let bestMatch = null;
124
+ let bestScore = 0;
125
+
126
+ for (const scene of this.scenes) {
127
+ const score = this.calculateScore(input, scene);
128
+
129
+ if (score > bestScore) {
130
+ bestScore = score;
131
+ bestMatch = scene;
132
+ }
133
+ }
134
+
135
+ if (bestMatch && bestScore >= 0.5) {
136
+ return {
137
+ scene: bestMatch,
138
+ confidence: bestScore,
139
+ params: this.extractParams(input, bestMatch)
140
+ };
141
+ }
142
+
143
+ return null;
144
+ }
145
+
146
+ /**
147
+ * Calculate match score
148
+ */
149
+ calculateScore(input, scene) {
150
+ const weights = this.config.get('sceneMapping.weights', {
151
+ keywordMatch: 0.4,
152
+ patternMatch: 0.3,
153
+ historyScore: 0.2,
154
+ confidence: 0.1
155
+ });
156
+
157
+ // Keyword matching
158
+ const keywordScore = this.matchKeywords(input, scene.keywords) * weights.keywordMatch;
159
+
160
+ // Pattern matching
161
+ const patternScore = this.matchPatterns(input, scene.patterns) * weights.patternMatch;
162
+
163
+ // History score (based on usage)
164
+ const historyScore = Math.min((scene.stats?.useCount || 0) / 10, 1) * weights.historyScore;
165
+
166
+ // Confidence score (based on success rate)
167
+ const confidenceScore = (scene.stats?.successRate || 0.5) * weights.confidence;
168
+
169
+ return keywordScore + patternScore + historyScore + confidenceScore;
170
+ }
171
+
172
+ /**
173
+ * Match keywords
174
+ */
175
+ matchKeywords(input, keywords) {
176
+ if (!keywords || keywords.length === 0) return 0;
177
+
178
+ const inputLower = input.toLowerCase();
179
+ let matchCount = 0;
180
+
181
+ for (const keyword of keywords) {
182
+ if (inputLower.includes(keyword.toLowerCase())) {
183
+ matchCount++;
184
+ }
185
+ }
186
+
187
+ return matchCount / keywords.length;
188
+ }
189
+
190
+ /**
191
+ * Match patterns
192
+ */
193
+ matchPatterns(input, patterns) {
194
+ if (!patterns || patterns.length === 0) return 0;
195
+
196
+ let matchCount = 0;
197
+
198
+ for (const pattern of patterns) {
199
+ const regex = pattern instanceof RegExp ? pattern : new RegExp(pattern, 'i');
200
+ if (regex.test(input)) {
201
+ matchCount++;
202
+ }
203
+ }
204
+
205
+ return matchCount / patterns.length;
206
+ }
207
+
208
+ /**
209
+ * Extract parameters from input
210
+ */
211
+ extractParams(input, scene) {
212
+ const params = {};
213
+
214
+ // Extract trace ID
215
+ const traceMatch = input.match(/traceId\s*[::]\s*(\w+)|traceId\s+(\w+)/i);
216
+ if (traceMatch) {
217
+ params.traceId = traceMatch[1] || traceMatch[2];
218
+ }
219
+
220
+ // Extract service name
221
+ const serviceMatch = input.match(/service\s*[::]\s*(\w+)|(\w+)-service/i);
222
+ if (serviceMatch) {
223
+ params.service = serviceMatch[1] || serviceMatch[2];
224
+ }
225
+
226
+ // Extract time range
227
+ const timeMatch = input.match(/最近(\d+)(小时|天|分钟)/);
228
+ if (timeMatch) {
229
+ params.timeRange = {
230
+ value: parseInt(timeMatch[1]),
231
+ unit: timeMatch[2]
232
+ };
233
+ }
234
+
235
+ return params;
236
+ }
237
+
238
+ /**
239
+ * Update scene usage stats
240
+ */
241
+ async updateSceneStats(sceneId, success) {
242
+ const scene = this.scenes.find(s => s.id === sceneId);
243
+ if (!scene) return;
244
+
245
+ scene.stats.useCount++;
246
+ scene.stats.lastUsed = new Date().toISOString();
247
+
248
+ if (!success) {
249
+ const total = scene.stats.useCount;
250
+ const successes = total * scene.stats.successRate;
251
+ scene.stats.successRate = (successes) / total;
252
+ }
253
+
254
+ // Save updated scenes
255
+ await this.saveScenes();
256
+ }
257
+
258
+ /**
259
+ * Save scenes to storage
260
+ */
261
+ async saveScenes() {
262
+ const learningPath = this.config.get('learning.storagePath', '~/.flowmind/learning');
263
+ const scenesPath = path.join(this.expandPath(learningPath), 'scenes.json');
264
+
265
+ const data = {
266
+ version: '1.0',
267
+ lastUpdated: new Date().toISOString(),
268
+ mappings: this.scenes.filter(s => !s.id.startsWith('builtin-'))
269
+ };
270
+
271
+ await fs.writeJson(scenesPath, data, { spaces: 2 });
272
+ }
273
+
274
+ /**
275
+ * Add custom scene
276
+ */
277
+ async addScene(scene) {
278
+ const newScene = {
279
+ id: `scene-${Date.now()}`,
280
+ timestamp: new Date().toISOString(),
281
+ ...scene,
282
+ stats: {
283
+ useCount: 0,
284
+ lastUsed: null,
285
+ successRate: 1.0
286
+ }
287
+ };
288
+
289
+ this.scenes.push(newScene);
290
+ await this.saveScenes();
291
+
292
+ return newScene;
293
+ }
294
+
295
+ /**
296
+ * Remove scene
297
+ */
298
+ async removeScene(sceneId) {
299
+ this.scenes = this.scenes.filter(s => s.id !== sceneId);
300
+ await this.saveScenes();
301
+ }
302
+
303
+ /**
304
+ * List all scenes
305
+ */
306
+ listScenes() {
307
+ return this.scenes.map(s => ({
308
+ id: s.id,
309
+ name: s.name,
310
+ keywords: s.keywords,
311
+ useCount: s.stats?.useCount || 0
312
+ }));
313
+ }
314
+
315
+ /**
316
+ * Helper to expand path
317
+ */
318
+ expandPath(filePath) {
319
+ if (filePath.startsWith('~')) {
320
+ return path.join(process.env.HOME || process.env.USERPROFILE, filePath.slice(1));
321
+ }
322
+ return filePath;
323
+ }
324
+ }
325
+
326
+ module.exports = SceneMatcher;
@@ -0,0 +1,291 @@
1
+ /**
2
+ * FlowMind Skill Loader
3
+ * Manages skill discovery, loading, and execution
4
+ */
5
+
6
+ const fs = require('fs-extra');
7
+ const path = require('path');
8
+
9
+ class SkillLoader {
10
+ constructor(config, learning, componentRegistry = null) {
11
+ this.config = config;
12
+ this.learning = learning;
13
+ this.componentRegistry = componentRegistry;
14
+ this.skills = new Map();
15
+ this.skillPath = config.get('skills.path', path.join(__dirname, '..', 'skills'));
16
+ }
17
+
18
+ /**
19
+ * Load all skills
20
+ */
21
+ async loadAll() {
22
+ const skillsDir = this.skillPath;
23
+
24
+ if (!await fs.pathExists(skillsDir)) {
25
+ console.warn(`Skills directory not found: ${skillsDir}`);
26
+ return;
27
+ }
28
+
29
+ const entries = await fs.readdir(skillsDir);
30
+
31
+ for (const entry of entries) {
32
+ const skillDir = path.join(skillsDir, entry);
33
+ const stat = await fs.stat(skillDir);
34
+
35
+ if (stat.isDirectory()) {
36
+ await this.loadSkill(entry, skillDir);
37
+ }
38
+ }
39
+
40
+ return Array.from(this.skills.values());
41
+ }
42
+
43
+ /**
44
+ * Load a single skill
45
+ */
46
+ async loadSkill(name, skillDir) {
47
+ try {
48
+ // Load skill definition
49
+ const skillMdPath = path.join(skillDir, 'SKILL.md');
50
+ const definition = await this.loadSkillDefinition(skillMdPath);
51
+
52
+ // Load skill implementation
53
+ const indexPath = path.join(skillDir, 'index.js');
54
+ let implementation = {};
55
+
56
+ if (await fs.pathExists(indexPath)) {
57
+ implementation = require(indexPath);
58
+ }
59
+
60
+ // Create skill object
61
+ const skill = {
62
+ name: name,
63
+ path: skillDir,
64
+ definition: definition,
65
+ ...implementation,
66
+ canHandle: implementation.canHandle || this.createDefaultCanHandle(definition),
67
+ execute: implementation.execute || this.createDefaultExecute(definition)
68
+ };
69
+
70
+ this.skills.set(name, skill);
71
+ return skill;
72
+ } catch (error) {
73
+ console.error(`Failed to load skill ${name}:`, error.message);
74
+ return null;
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Load skill definition from SKILL.md
80
+ */
81
+ async loadSkillDefinition(filePath) {
82
+ if (!await fs.pathExists(filePath)) {
83
+ return {};
84
+ }
85
+
86
+ const content = await fs.readFile(filePath, 'utf-8');
87
+
88
+ // Parse frontmatter
89
+ const frontmatterMatch = content.match(/^---\n([\s\S]*?)\n---/);
90
+ if (!frontmatterMatch) {
91
+ return { raw: content };
92
+ }
93
+
94
+ const frontmatter = frontmatterMatch[1];
95
+ const definition = {};
96
+
97
+ // Parse YAML-like frontmatter
98
+ const lines = frontmatter.split('\n');
99
+ for (const line of lines) {
100
+ const match = line.match(/^(\w+):\s*(.+)$/);
101
+ if (match) {
102
+ definition[match[1]] = match[2].replace(/^["']|["']$/g, '');
103
+ }
104
+ }
105
+
106
+ // Parse YAML list fields (componentDependencies, etc.)
107
+ const listFieldMatch = frontmatter.match(/componentDependencies:\s*\n((?:\s+-\s+.+\n?)*)/);
108
+ if (listFieldMatch) {
109
+ definition.componentDependencies = listFieldMatch[1]
110
+ .split('\n')
111
+ .map(line => line.match(/^\s+-\s+(.+)$/))
112
+ .filter(Boolean)
113
+ .map(m => m[1].trim());
114
+ }
115
+
116
+ // Extract trigger patterns
117
+ const triggerMatch = content.match(/## Trigger Conditions\n([\s\S]*?)(?=\n##|$)/);
118
+ if (triggerMatch) {
119
+ definition.triggers = this.extractTriggers(triggerMatch[1]);
120
+ }
121
+
122
+ return definition;
123
+ }
124
+
125
+ /**
126
+ * Extract trigger patterns from markdown
127
+ */
128
+ extractTriggers(section) {
129
+ const triggers = [];
130
+ const lines = section.split('\n');
131
+
132
+ for (const line of lines) {
133
+ // Match quoted strings
134
+ const matches = line.match(/"([^"]+)"|'([^']+)'|`([^`]+)`/g);
135
+ if (matches) {
136
+ triggers.push(...matches.map(m => m.replace(/["'`]/g, '')));
137
+ }
138
+ }
139
+
140
+ return triggers;
141
+ }
142
+
143
+ /**
144
+ * Create default canHandle function from definition
145
+ */
146
+ createDefaultCanHandle(definition) {
147
+ const triggers = definition.triggers || [];
148
+
149
+ return (input, context) => {
150
+ if (!input) return false;
151
+
152
+ const inputLower = input.toLowerCase();
153
+
154
+ for (const trigger of triggers) {
155
+ if (inputLower.includes(trigger.toLowerCase())) {
156
+ return true;
157
+ }
158
+ }
159
+
160
+ return false;
161
+ };
162
+ }
163
+
164
+ /**
165
+ * Create default execute function from definition
166
+ */
167
+ createDefaultExecute(definition) {
168
+ const registry = this.componentRegistry;
169
+ return async (input, context) => {
170
+ // Enrich context with component registry info
171
+ const enrichedContext = { ...context };
172
+ if (registry) {
173
+ enrichedContext.componentRegistry = registry;
174
+ // Add resolved MCP server info for skills that declare component dependencies
175
+ const componentDeps = definition.componentDependencies || [];
176
+ enrichedContext.resolvedComponents = {};
177
+ for (const dep of componentDeps) {
178
+ const adapter = registry.getAdapter(dep);
179
+ if (adapter) {
180
+ enrichedContext.resolvedComponents[dep] = {
181
+ provider: registry.getActiveProvider(dep),
182
+ mcpServer: adapter.mcpServer,
183
+ tools: adapter.getProvidedTools()
184
+ };
185
+ }
186
+ }
187
+ }
188
+
189
+ return {
190
+ skill: definition.name,
191
+ input: input,
192
+ context: enrichedContext,
193
+ message: `Executed skill: ${definition.name || 'unknown'}`,
194
+ timestamp: new Date().toISOString()
195
+ };
196
+ };
197
+ }
198
+
199
+ /**
200
+ * Select best skill for input
201
+ */
202
+ async select(input, context = {}) {
203
+ const candidates = [];
204
+
205
+ for (const [name, skill] of this.skills) {
206
+ try {
207
+ const canHandle = await skill.canHandle(input, context);
208
+ if (canHandle) {
209
+ candidates.push({
210
+ skill: skill,
211
+ score: this.calculateSkillScore(skill, input, context)
212
+ });
213
+ }
214
+ } catch (error) {
215
+ // Skip skills that error on canHandle
216
+ }
217
+ }
218
+
219
+ if (candidates.length === 0) {
220
+ return null;
221
+ }
222
+
223
+ // Sort by score (highest first)
224
+ candidates.sort((a, b) => b.score - a.score);
225
+
226
+ return candidates[0].skill;
227
+ }
228
+
229
+ /**
230
+ * Calculate skill score for selection
231
+ */
232
+ calculateSkillScore(skill, input, context) {
233
+ let score = 0;
234
+
235
+ // Base score for matching
236
+ score += 10;
237
+
238
+ // Bonus for trigger specificity
239
+ const triggers = skill.definition?.triggers || [];
240
+ for (const trigger of triggers) {
241
+ if (input.toLowerCase().includes(trigger.toLowerCase())) {
242
+ score += 5;
243
+ }
244
+ }
245
+
246
+ // Bonus for recent successful use
247
+ const bindings = this.learning?.skillBindings?.bindings?.[skill.name];
248
+ if (bindings) {
249
+ score += Math.min(bindings.learningCount, 10);
250
+ }
251
+
252
+ return score;
253
+ }
254
+
255
+ /**
256
+ * Get skill by name
257
+ */
258
+ get(name) {
259
+ return this.skills.get(name);
260
+ }
261
+
262
+ /**
263
+ * List all loaded skills
264
+ */
265
+ list() {
266
+ return Array.from(this.skills.values()).map(s => ({
267
+ name: s.name,
268
+ description: s.definition?.description,
269
+ category: s.definition?.category
270
+ }));
271
+ }
272
+
273
+ /**
274
+ * Reload a skill
275
+ */
276
+ async reload(name) {
277
+ const skill = this.skills.get(name);
278
+ if (!skill) return null;
279
+
280
+ return await this.loadSkill(name, skill.path);
281
+ }
282
+
283
+ /**
284
+ * Check if skill exists
285
+ */
286
+ has(name) {
287
+ return this.skills.has(name);
288
+ }
289
+ }
290
+
291
+ module.exports = SkillLoader;