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.
- package/LICENSE +21 -0
- package/README.md +855 -0
- package/README_CN.md +854 -0
- package/bin/flowmind.js +464 -0
- package/core/adapters/api-doc-adapter.js +71 -0
- package/core/adapters/base-adapter.js +80 -0
- package/core/adapters/database-manager-adapter.js +60 -0
- package/core/adapters/database-query-adapter.js +51 -0
- package/core/adapters/knowledge-base-adapter.js +75 -0
- package/core/adapters/log-service-adapter.js +41 -0
- package/core/adapters/mcp-adapter.js +65 -0
- package/core/adapters/report-adapter.js +60 -0
- package/core/adapters/workflow-adapter.js +62 -0
- package/core/component-registry.js +281 -0
- package/core/component-types.js +63 -0
- package/core/config-manager.js +360 -0
- package/core/index.js +223 -0
- package/core/learning-engine.js +588 -0
- package/core/mcp-compatibility.js +150 -0
- package/core/providers/aliyun/dms-adapter.js +98 -0
- package/core/providers/aliyun/redis-adapter.js +88 -0
- package/core/providers/aliyun/sls-adapter.js +86 -0
- package/core/providers/friday/flow-adapter.js +85 -0
- package/core/providers/friday/report-adapter.js +83 -0
- package/core/providers/yapi/yapi-adapter.js +79 -0
- package/core/providers/yuque/yuque-adapter.js +90 -0
- package/core/scene-matcher.js +326 -0
- package/core/skill-loader.js +291 -0
- package/package.json +67 -0
- package/scripts/migrate-config.js +153 -0
- package/skills/api-sync/SKILL.md +203 -0
- package/skills/archive-change/SKILL.md +172 -0
- package/skills/auto-flow/SKILL.md +277 -0
- package/skills/code-review/SKILL.md +206 -0
- package/skills/code-review-audit/SKILL.md +150 -0
- package/skills/data-logic-validation/SKILL.md +162 -0
- package/skills/data-validation/SKILL.md +210 -0
- package/skills/git-review/SKILL.md +190 -0
- package/skills/learning-engine/SKILL.md +352 -0
- package/skills/learning-feedback/SKILL.md +174 -0
- package/skills/log-audit/SKILL.md +226 -0
- package/skills/project-review/SKILL.md +196 -0
- package/skills/requirement-analyst/SKILL.md +275 -0
- package/skills/resource-bind/SKILL.md +222 -0
- package/skills/sls-log-audit/SKILL.md +223 -0
- package/skills/yapi-sync-interface/SKILL.md +145 -0
- 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;
|