flowmind 1.0.1 → 1.2.2

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 (39) hide show
  1. package/README_CN.md +248 -0
  2. package/bin/flowmind.js +638 -2
  3. package/config/ai-config.example.json +64 -0
  4. package/config/claude-mcp-config.example.json +12 -0
  5. package/core/ai/base-model.js +70 -0
  6. package/core/ai/index.js +29 -0
  7. package/core/ai/model-manager.js +320 -0
  8. package/core/ai/prompts/extraction.js +38 -0
  9. package/core/ai/prompts/index.js +11 -0
  10. package/core/ai/prompts/intent.js +43 -0
  11. package/core/ai/prompts/learning.js +46 -0
  12. package/core/ai/prompts/selection.js +38 -0
  13. package/core/ai/prompts/summary.js +35 -0
  14. package/core/ai/providers/anthropic.js +93 -0
  15. package/core/ai/providers/deepseek.js +80 -0
  16. package/core/ai/providers/ernie.js +111 -0
  17. package/core/ai/providers/glm.js +80 -0
  18. package/core/ai/providers/mimo.js +80 -0
  19. package/core/ai/providers/ollama.js +147 -0
  20. package/core/ai/providers/openai.js +82 -0
  21. package/core/ai/providers/qwen.js +80 -0
  22. package/core/event-bus.js +17 -0
  23. package/core/honor-engine.js +255 -0
  24. package/core/index.js +115 -13
  25. package/core/learning-engine.js +29 -1
  26. package/core/skill-loader.js +31 -9
  27. package/dashboard/app.jsx +29 -0
  28. package/dashboard/components/ActivityFeed.jsx +86 -0
  29. package/dashboard/components/DragonPanel.jsx +67 -0
  30. package/dashboard/components/McpStatusBar.jsx +43 -0
  31. package/dashboard/components/StatsRow.jsx +65 -0
  32. package/mcp/server.js +328 -0
  33. package/package.json +19 -7
  34. package/tui/app.jsx +69 -0
  35. package/tui/components/ChatPanel.jsx +72 -0
  36. package/tui/components/DragonTotem.jsx +108 -0
  37. package/tui/components/ResultPanel.jsx +33 -0
  38. package/tui/components/Sidebar.jsx +70 -0
  39. package/tui/components/StatusBar.jsx +49 -0
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Qwen Provider - 阿里云通义千问模型接入
3
+ * 支持 Qwen-Turbo、Qwen-Plus、Qwen-Max 等模型
4
+ */
5
+
6
+ const BaseModel = require('../base-model');
7
+
8
+ class QwenProvider extends BaseModel {
9
+ constructor(config = {}) {
10
+ super('qwen', config);
11
+ this.apiKey = config.apiKey || process.env.DASHSCOPE_API_KEY;
12
+ this.model = config.model || 'qwen-turbo';
13
+ this.baseUrl = config.baseUrl || 'https://dashscope.aliyuncs.com/compatible-mode/v1';
14
+ this.temperature = config.temperature ?? 0.3;
15
+ this.maxTokens = config.maxTokens ?? 2000;
16
+ }
17
+
18
+ async init() {
19
+ if (!this.apiKey) {
20
+ throw new Error('DashScope API key is required. Set DASHSCOPE_API_KEY environment variable or provide in config.');
21
+ }
22
+ this.initialized = true;
23
+ }
24
+
25
+ validateConfig() {
26
+ return !!this.apiKey;
27
+ }
28
+
29
+ async chat(messages, options = {}) {
30
+ if (!this.initialized) {
31
+ await this.init();
32
+ }
33
+
34
+ const response = await fetch(`${this.baseUrl}/chat/completions`, {
35
+ method: 'POST',
36
+ headers: {
37
+ 'Content-Type': 'application/json',
38
+ 'Authorization': `Bearer ${this.apiKey}`
39
+ },
40
+ body: JSON.stringify({
41
+ model: options.model || this.model,
42
+ messages: messages,
43
+ temperature: options.temperature ?? this.temperature,
44
+ max_tokens: options.maxTokens ?? this.maxTokens
45
+ })
46
+ });
47
+
48
+ if (!response.ok) {
49
+ const error = await response.text();
50
+ throw new Error(`Qwen API error: ${response.status} - ${error}`);
51
+ }
52
+
53
+ const data = await response.json();
54
+ return data.choices[0].message.content;
55
+ }
56
+
57
+ async complete(prompt, options = {}) {
58
+ return this.chat([{ role: 'user', content: prompt }], options);
59
+ }
60
+
61
+ async isAvailable() {
62
+ try {
63
+ if (!this.apiKey) return false;
64
+ return true;
65
+ } catch {
66
+ return false;
67
+ }
68
+ }
69
+
70
+ getInfo() {
71
+ return {
72
+ ...super.getInfo(),
73
+ model: this.model,
74
+ baseUrl: this.baseUrl,
75
+ provider: 'Alibaba Cloud'
76
+ };
77
+ }
78
+ }
79
+
80
+ module.exports = QwenProvider;
@@ -0,0 +1,17 @@
1
+ /**
2
+ * FlowMind Event Bus
3
+ * Shared singleton EventEmitter for cross-module event communication.
4
+ * Enables real-time monitoring (dashboard) and event-driven TUI updates.
5
+ */
6
+
7
+ const EventEmitter = require('events');
8
+
9
+ class FlowMindEventBus extends EventEmitter {
10
+ constructor() {
11
+ super();
12
+ this.setMaxListeners(50);
13
+ }
14
+ }
15
+
16
+ // Singleton — shared across all modules
17
+ module.exports = new FlowMindEventBus();
@@ -0,0 +1,255 @@
1
+ /**
2
+ * FlowMind Honor Engine
3
+ * Tracks honor points earned through usage and manages dragon totem levels
4
+ */
5
+
6
+ const fs = require('fs-extra');
7
+ const path = require('path');
8
+ const eventBus = require('./event-bus');
9
+
10
+ const HONOR_POINTS = {
11
+ init: 1,
12
+ skill_use: 1,
13
+ new_skill: 2,
14
+ learning: 3
15
+ };
16
+
17
+ const DRAGON_LEVELS = [
18
+ { level: 0, minPoints: 0, name: 'Egg', state: 'dormant' },
19
+ { level: 1, minPoints: 1, name: 'Hatchling', state: 'awakening' },
20
+ { level: 2, minPoints: 10, name: 'Juvenile', state: 'growing' },
21
+ { level: 3, minPoints: 30, name: 'Adult', state: 'soaring' },
22
+ { level: 4, minPoints: 60, name: 'Elder', state: 'wise' },
23
+ { level: 5, minPoints: 100, name: 'Ascended', state: 'transcendent' }
24
+ ];
25
+
26
+ class HonorEngine {
27
+ constructor(config) {
28
+ this.config = config;
29
+ this.honorPath = path.join(
30
+ config.get('storagePath') || path.join(process.env.HOME || process.env.USERPROFILE, '.flowmind'),
31
+ 'honor.json'
32
+ );
33
+ this.skillsPath = config.get('skills.path', path.join(__dirname, '..', 'skills'));
34
+ this.data = null;
35
+ this.initialized = false;
36
+ }
37
+
38
+ /**
39
+ * Initialize honor engine
40
+ */
41
+ async init() {
42
+ try {
43
+ if (await fs.pathExists(this.honorPath)) {
44
+ this.data = await fs.readJson(this.honorPath);
45
+ } else {
46
+ this.data = this.createDefaultData();
47
+ // Seed knownSkills by scanning skills/ directory
48
+ await this.seedKnownSkills();
49
+ await this.save();
50
+ }
51
+ this.initialized = true;
52
+ } catch (error) {
53
+ // Non-blocking: create default data in memory
54
+ this.data = this.createDefaultData();
55
+ this.initialized = true;
56
+ }
57
+ }
58
+
59
+ /**
60
+ * Create default honor data structure
61
+ */
62
+ createDefaultData() {
63
+ return {
64
+ version: '1.0',
65
+ points: 0,
66
+ level: 0,
67
+ history: [],
68
+ knownSkills: [],
69
+ stats: {
70
+ initCount: 0,
71
+ skillUseCount: 0,
72
+ newSkillCount: 0,
73
+ learningCount: 0
74
+ },
75
+ lastUpdated: new Date().toISOString()
76
+ };
77
+ }
78
+
79
+ /**
80
+ * Seed knownSkills by scanning skills/ directory
81
+ */
82
+ async seedKnownSkills() {
83
+ try {
84
+ if (await fs.pathExists(this.skillsPath)) {
85
+ const entries = await fs.readdir(this.skillsPath);
86
+ for (const entry of entries) {
87
+ const fullPath = path.join(this.skillsPath, entry);
88
+ const stat = await fs.stat(fullPath);
89
+ if (stat.isDirectory()) {
90
+ this.data.knownSkills.push(entry);
91
+ }
92
+ }
93
+ }
94
+ } catch (error) {
95
+ // Non-blocking
96
+ }
97
+ }
98
+
99
+ /**
100
+ * Award honor points for an action
101
+ */
102
+ async award(action, description) {
103
+ if (!this.initialized) await this.init();
104
+
105
+ try {
106
+ const points = HONOR_POINTS[action] || 0;
107
+ if (points === 0) return;
108
+
109
+ // Check init action: only award once
110
+ if (action === 'init' && this.data.stats.initCount > 0) {
111
+ return;
112
+ }
113
+
114
+ // Award points
115
+ this.data.points += points;
116
+ this.data.level = this.getLevel(this.data.points);
117
+
118
+ // Update stats
119
+ const statKey = `${action}Count`;
120
+ if (this.data.stats[statKey] !== undefined) {
121
+ this.data.stats[statKey]++;
122
+ }
123
+
124
+ // Add to history
125
+ this.data.history.push({
126
+ action,
127
+ points,
128
+ description,
129
+ timestamp: new Date().toISOString()
130
+ });
131
+
132
+ // Keep history to last 100 entries
133
+ if (this.data.history.length > 100) {
134
+ this.data.history = this.data.history.slice(-100);
135
+ }
136
+
137
+ this.data.lastUpdated = new Date().toISOString();
138
+ await this.save();
139
+
140
+ // Emit honor event for TUI/dashboard monitoring
141
+ eventBus.emit('honor:awarded', {
142
+ action,
143
+ points,
144
+ description,
145
+ level: this.data.level,
146
+ levelName: this.getLevelName(this.data.level),
147
+ total: this.data.points,
148
+ timestamp: this.data.lastUpdated
149
+ });
150
+ } catch (error) {
151
+ // Non-blocking
152
+ }
153
+ }
154
+
155
+ /**
156
+ * Check if a skill name is already known
157
+ */
158
+ isKnownSkill(name) {
159
+ return this.data && this.data.knownSkills.includes(name);
160
+ }
161
+
162
+ /**
163
+ * Add a skill to the known list
164
+ */
165
+ async addKnownSkill(name) {
166
+ if (!this.initialized) await this.init();
167
+ try {
168
+ if (!this.data.knownSkills.includes(name)) {
169
+ this.data.knownSkills.push(name);
170
+ await this.save();
171
+ }
172
+ } catch (error) {
173
+ // Non-blocking
174
+ }
175
+ }
176
+
177
+ /**
178
+ * Get the honor data
179
+ */
180
+ getData() {
181
+ return this.data || this.createDefaultData();
182
+ }
183
+
184
+ /**
185
+ * Get level for a given point total
186
+ */
187
+ getLevel(points) {
188
+ let level = 0;
189
+ for (const tier of DRAGON_LEVELS) {
190
+ if (points >= tier.minPoints) {
191
+ level = tier.level;
192
+ }
193
+ }
194
+ return level;
195
+ }
196
+
197
+ /**
198
+ * Get level name for a given level number
199
+ */
200
+ getLevelName(level) {
201
+ const tier = DRAGON_LEVELS.find(t => t.level === level);
202
+ return tier ? tier.name : 'Unknown';
203
+ }
204
+
205
+ /**
206
+ * Get level info including next level hint
207
+ */
208
+ getLevelInfo(points) {
209
+ const level = this.getLevel(points);
210
+ const currentTier = DRAGON_LEVELS.find(t => t.level === level);
211
+ const nextTier = DRAGON_LEVELS.find(t => t.level === level + 1);
212
+
213
+ return {
214
+ level,
215
+ name: currentTier.name,
216
+ state: currentTier.state,
217
+ points,
218
+ nextLevel: nextTier ? nextTier.level : null,
219
+ nextLevelName: nextTier ? nextTier.name : null,
220
+ pointsToNext: nextTier ? nextTier.minPoints - points : 0
221
+ };
222
+ }
223
+
224
+ /**
225
+ * Export honor data for publishing
226
+ */
227
+ exportForPublish() {
228
+ const info = this.getLevelInfo(this.data.points);
229
+ return {
230
+ version: this.data.version,
231
+ points: this.data.points,
232
+ level: info.level,
233
+ levelName: info.name,
234
+ state: info.state,
235
+ stats: this.data.stats,
236
+ knownSkillsCount: this.data.knownSkills.length,
237
+ recentHistory: this.data.history.slice(-20),
238
+ exportedAt: new Date().toISOString()
239
+ };
240
+ }
241
+
242
+ /**
243
+ * Save honor data to disk
244
+ */
245
+ async save() {
246
+ try {
247
+ await fs.ensureDir(path.dirname(this.honorPath));
248
+ await fs.writeJson(this.honorPath, this.data, { spaces: 2 });
249
+ } catch (error) {
250
+ // Non-blocking
251
+ }
252
+ }
253
+ }
254
+
255
+ module.exports = HonorEngine;
package/core/index.js CHANGED
@@ -5,17 +5,22 @@
5
5
 
6
6
  const SkillLoader = require('./skill-loader');
7
7
  const LearningEngine = require('./learning-engine');
8
+ const HonorEngine = require('./honor-engine');
8
9
  const SceneMatcher = require('./scene-matcher');
9
10
  const ConfigManager = require('./config-manager');
10
11
  const ComponentRegistry = require('./component-registry');
12
+ const ModelManager = require('./ai/model-manager');
13
+ const eventBus = require('./event-bus');
11
14
 
12
15
  class FlowMind {
13
16
  constructor(options = {}) {
14
17
  this.config = new ConfigManager(options.configPath);
15
- this.learning = new LearningEngine(this.config);
18
+ this.honor = new HonorEngine(this.config);
19
+ this.learning = new LearningEngine(this.config, this.honor);
16
20
  this.matcher = new SceneMatcher(this.config, this.learning);
17
21
  this.components = new ComponentRegistry(this.config);
18
- this.skills = new SkillLoader(this.config, this.learning, this.components);
22
+ this.skills = new SkillLoader(this.config, this.learning, this.components, this.honor);
23
+ this.ai = new ModelManager(options.ai || {});
19
24
  this.initialized = false;
20
25
  }
21
26
 
@@ -28,10 +33,18 @@ class FlowMind {
28
33
  await this.config.load();
29
34
  await this.components.init();
30
35
  await this.components.initAll();
36
+ await this.honor.init();
31
37
  await this.learning.init();
32
38
  await this.skills.loadAll();
33
39
  await this.matcher.loadScenes();
34
40
 
41
+ // Initialize AI model manager
42
+ try {
43
+ await this.ai.init();
44
+ } catch (error) {
45
+ console.warn(`AI model initialization failed: ${error.message}. Falling back to rule-based engine.`);
46
+ }
47
+
35
48
  this.initialized = true;
36
49
  return this;
37
50
  }
@@ -46,36 +59,87 @@ class FlowMind {
46
59
 
47
60
  const startTime = Date.now();
48
61
 
62
+ eventBus.emit('process:start', { input, timestamp: new Date().toISOString() });
63
+
49
64
  try {
50
- // 1. Check for learning patterns (corrections, feedback)
51
- const learningResult = await this.learning.detectLearning(input, context);
65
+ // 1. AI Intent Understanding (if available)
66
+ const intent = await this.ai.understandIntent(input, context);
67
+
68
+ // 2. Check for learning patterns (corrections, feedback)
69
+ // Use AI to analyze learning feedback if available
70
+ const aiLearningResult = await this.ai.analyzeLearningFeedback(input, context);
71
+ const learningResult = aiLearningResult?.isLearning
72
+ ? aiLearningResult
73
+ : await this.learning.detectLearning(input, context);
52
74
  if (learningResult) {
53
75
  return this.formatLearningResponse(learningResult);
54
76
  }
55
77
 
56
- // 2. Check scene mappings
57
- const sceneMatch = await this.matcher.match(input);
78
+ // 3. Check scene mappings (with AI intent if available)
79
+ const sceneMatch = await this.matcher.match(input, intent);
58
80
  if (sceneMatch && sceneMatch.confidence >= 0.7) {
59
81
  return this.executeSceneWorkflow(sceneMatch, input, context);
60
82
  }
61
83
 
62
- // 3. Select and execute skill
63
- const skill = await this.skills.select(input, context);
84
+ // 4. Select skill (AI-assisted if available)
85
+ let skill = null;
86
+ const candidates = await this.skills.getCandidates(input, context);
87
+
88
+ if (candidates.length > 0) {
89
+ // Use AI to select skill if available
90
+ const aiSelection = await this.ai.selectSkill(input, candidates);
91
+ if (aiSelection && aiSelection.selectedSkill) {
92
+ skill = this.skills.get(aiSelection.selectedSkill);
93
+ }
94
+ }
95
+
96
+ // Fallback to rule-based selection
97
+ if (!skill) {
98
+ skill = await this.skills.select(input, context);
99
+ }
100
+
64
101
  if (!skill) {
65
102
  return this.formatError('No matching skill found', input);
66
103
  }
67
104
 
68
- // 4. Execute with learning applied
69
- const result = await this.executeWithLearning(skill, input, context);
105
+ // 5. Extract parameters using AI (if available)
106
+ const extractedParams = await this.ai.extractParameters(input, skill.name);
107
+
108
+ // 6. Execute with learning applied
109
+ const enhancedContext = {
110
+ ...context,
111
+ ...extractedParams,
112
+ intent: intent
113
+ };
114
+ const result = await this.executeWithLearning(skill, input, enhancedContext);
115
+
116
+ // 7. Generate AI summary (if available)
117
+ const summary = await this.ai.summarizeResult(result, {
118
+ skill: skill.name,
119
+ intent: intent
120
+ });
70
121
 
71
- // 5. Format and return
72
- return this.formatResult(result, {
122
+ // 8. Format and return
123
+ const formatted = this.formatResult(summary || result, {
73
124
  skill: skill.name,
74
125
  duration: Date.now() - startTime,
75
- sceneMatch: sceneMatch
126
+ sceneMatch: sceneMatch,
127
+ intent: intent,
128
+ aiEnhanced: !!summary
129
+ });
130
+
131
+ eventBus.emit('process:complete', {
132
+ input,
133
+ skill: skill.name,
134
+ duration: formatted.metadata.duration,
135
+ success: true,
136
+ timestamp: formatted.timestamp
76
137
  });
77
138
 
139
+ return formatted;
140
+
78
141
  } catch (error) {
142
+ eventBus.emit('process:error', { input, error: error.message, timestamp: new Date().toISOString() });
79
143
  return this.formatError(error.message, input);
80
144
  }
81
145
  }
@@ -84,6 +148,8 @@ class FlowMind {
84
148
  * Execute skill with learning applied
85
149
  */
86
150
  async executeWithLearning(skill, input, context) {
151
+ const skillStartTime = Date.now();
152
+
87
153
  // Get learning rules for this skill
88
154
  const learnings = await this.learning.getSkillLearnings(skill.name);
89
155
 
@@ -97,6 +163,19 @@ class FlowMind {
97
163
  // Execute skill
98
164
  const result = await skill.execute(input, enhancedContext);
99
165
 
166
+ const duration = Date.now() - skillStartTime;
167
+
168
+ // Emit skill execution event
169
+ eventBus.emit('skill:executed', {
170
+ name: skill.name,
171
+ duration,
172
+ success: true,
173
+ timestamp: new Date().toISOString()
174
+ });
175
+
176
+ // Award honor point for skill use
177
+ await this.honor.award('skill_use', `Used skill: ${skill.name}`);
178
+
100
179
  return result;
101
180
  }
102
181
 
@@ -180,6 +259,13 @@ class FlowMind {
180
259
  return this.learning.getStats();
181
260
  }
182
261
 
262
+ /**
263
+ * Get honor data
264
+ */
265
+ getHonorData() {
266
+ return this.honor.getData();
267
+ }
268
+
183
269
  /**
184
270
  * Get the component registry
185
271
  * @returns {ComponentRegistry}
@@ -205,6 +291,22 @@ class FlowMind {
205
291
  return this.components.getStatus();
206
292
  }
207
293
 
294
+ /**
295
+ * Get AI model status
296
+ * @returns {object}
297
+ */
298
+ getAIStatus() {
299
+ return this.ai.getStatus();
300
+ }
301
+
302
+ /**
303
+ * Check if AI is available
304
+ * @returns {boolean}
305
+ */
306
+ hasAI() {
307
+ return this.ai.hasAvailableProvider();
308
+ }
309
+
208
310
  /**
209
311
  * Export learnings
210
312
  */
@@ -6,10 +6,12 @@
6
6
  const fs = require('fs-extra');
7
7
  const path = require('path');
8
8
  const { v4: uuidv4 } = require('uuid');
9
+ const eventBus = require('./event-bus');
9
10
 
10
11
  class LearningEngine {
11
- constructor(config) {
12
+ constructor(config, honorEngine = null) {
12
13
  this.config = config;
14
+ this.honorEngine = honorEngine;
13
15
  this.learningPath = config.get('learning.storagePath', '~/.flowmind/learning');
14
16
  this.records = {};
15
17
  this.skillBindings = {};
@@ -170,6 +172,13 @@ class LearningEngine {
170
172
  // Update stats
171
173
  await this.updateStats('correction', record.skill);
172
174
 
175
+ eventBus.emit('learning:recorded', {
176
+ type: 'correction',
177
+ skill: record.skill,
178
+ recordId: record.id,
179
+ severity: record.severity
180
+ });
181
+
173
182
  return {
174
183
  type: 'correction',
175
184
  record: record,
@@ -204,6 +213,13 @@ class LearningEngine {
204
213
  // Update stats
205
214
  await this.updateStats('scene_mapping', 'global');
206
215
 
216
+ eventBus.emit('learning:recorded', {
217
+ type: 'scene_mapping',
218
+ skill: 'global',
219
+ recordId: record.id,
220
+ keywords: record.keywords
221
+ });
222
+
207
223
  return {
208
224
  type: 'scene_mapping',
209
225
  record: record,
@@ -232,6 +248,13 @@ class LearningEngine {
232
248
  // Update stats
233
249
  await this.updateStats('preference', record.skill);
234
250
 
251
+ eventBus.emit('learning:recorded', {
252
+ type: 'preference',
253
+ skill: record.skill,
254
+ recordId: record.id,
255
+ preferenceType: record.preferenceType
256
+ });
257
+
235
258
  return {
236
259
  type: 'preference',
237
260
  record: record,
@@ -398,6 +421,11 @@ class LearningEngine {
398
421
 
399
422
  const statsPath = path.join(this.expandPath(this.learningPath), 'stats.json');
400
423
  await fs.writeJson(statsPath, this.stats, { spaces: 2 });
424
+
425
+ // Award honor points for learning
426
+ if (this.honorEngine) {
427
+ await this.honorEngine.award('learning', `Learned: ${type} for ${skill}`);
428
+ }
401
429
  }
402
430
 
403
431
  /**
@@ -7,10 +7,11 @@ const fs = require('fs-extra');
7
7
  const path = require('path');
8
8
 
9
9
  class SkillLoader {
10
- constructor(config, learning, componentRegistry = null) {
10
+ constructor(config, learning, componentRegistry = null, honorEngine = null) {
11
11
  this.config = config;
12
12
  this.learning = learning;
13
13
  this.componentRegistry = componentRegistry;
14
+ this.honorEngine = honorEngine;
14
15
  this.skills = new Map();
15
16
  this.skillPath = config.get('skills.path', path.join(__dirname, '..', 'skills'));
16
17
  }
@@ -68,6 +69,13 @@ class SkillLoader {
68
69
  };
69
70
 
70
71
  this.skills.set(name, skill);
72
+
73
+ // Award honor points for new (previously unknown) skills
74
+ if (this.honorEngine && !this.honorEngine.isKnownSkill(name)) {
75
+ await this.honorEngine.award('new_skill', `New skill loaded: ${name}`);
76
+ await this.honorEngine.addKnownSkill(name);
77
+ }
78
+
71
79
  return skill;
72
80
  } catch (error) {
73
81
  console.error(`Failed to load skill ${name}:`, error.message);
@@ -225,6 +233,23 @@ class SkillLoader {
225
233
  * Select best skill for input
226
234
  */
227
235
  async select(input, context = {}) {
236
+ const candidates = await this.getCandidates(input, context);
237
+
238
+ if (candidates.length === 0) {
239
+ return null;
240
+ }
241
+
242
+ // Sort by score (highest first)
243
+ candidates.sort((a, b) => b.score - a.score);
244
+
245
+ return candidates[0].skill;
246
+ }
247
+
248
+ /**
249
+ * Get candidate skills for input
250
+ * Returns skills that can handle the input with their scores
251
+ */
252
+ async getCandidates(input, context = {}) {
228
253
  const candidates = [];
229
254
 
230
255
  for (const [name, skill] of this.skills) {
@@ -232,7 +257,11 @@ class SkillLoader {
232
257
  const canHandle = await skill.canHandle(input, context);
233
258
  if (canHandle) {
234
259
  candidates.push({
260
+ name: skill.name,
235
261
  skill: skill,
262
+ description: skill.definition?.description || '',
263
+ triggers: skill.definition?.triggers || [],
264
+ category: skill.definition?.category || skill.definition?.metadata?.category || '',
236
265
  score: this.calculateSkillScore(skill, input, context)
237
266
  });
238
267
  }
@@ -241,14 +270,7 @@ class SkillLoader {
241
270
  }
242
271
  }
243
272
 
244
- if (candidates.length === 0) {
245
- return null;
246
- }
247
-
248
- // Sort by score (highest first)
249
- candidates.sort((a, b) => b.score - a.score);
250
-
251
- return candidates[0].skill;
273
+ return candidates;
252
274
  }
253
275
 
254
276
  /**